From 8bcd8ccc04ae959889364aaebb28259be1010309 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 28 Aug 2026 19:39:48 +0200 Subject: [PATCH 1/9] Port test-quality refactors from the lint-hardening work Establish the base of the lint-hardening PR stack: the test, fixture, helper, and assertion improvements, separated from the lint-config, formatting, and rename changes stacked above so each review stays scoped. - Fixtures and setup helpers become fallible and propagate errors; the test body performs the unwrap with a descriptive expect. rstest fixtures are consumed as `#[from(fixture)] fixture_res: Result<..>` so bindings keep their names. - Assertion blocks are decomposed into named helpers, pure queries, and single-purpose assertion macros; duplicated step logic is extracted (binstall value comparison, adjacency neighbour lookup, CLI default condition tables). - Test modules gain explanatory `//!` opening lines; self-named test module files move to `mod.rs` form. - `test_support` gains scoped environment helpers backed by `temp_env` alongside the existing guard; a new `whitaker_test_macros` crate provides `allow_fixture_expansion_lints` for fixture expansion noise. Symbols follow main's current API; renames and signature changes land in their own PRs above this one. --- Cargo.lock | 21 + Cargo.toml | 4 + common/Cargo.toml | 2 + .../brain_trait_metrics/diagnostic_tests.rs | 37 +- .../brain_trait_metrics/evaluation_tests.rs | 26 +- .../cognitive_complexity_tests.rs | 32 +- .../brain_type_metrics/diagnostic_tests.rs | 25 +- .../brain_type_metrics/evaluation_tests.rs | 24 +- common/src/decomposition_advice/note_tests.rs | 23 +- .../decomposition_advice/tests/adjacency.rs | 21 +- .../{tests.rs => tests/mod.rs} | 66 ++- .../decomposition_advice/tests/propagation.rs | 23 +- .../tests/test_fixtures.rs | 14 +- .../tests/vector_algebra.rs | 3 +- common/src/rstest/tests/fingerprint.rs | 3 +- common/src/rstest/tests/fingerprint_props.rs | 8 +- common/src/rstest/tests/mod.rs | 65 +-- common/src/test_support/decomposition.rs | 15 +- .../test_support/decomposition_adjacency.rs | 86 +++- .../decomposition_label_propagation.rs | 11 +- .../decomposition_vector_algebra.rs | 38 +- common/src/test_support/fixtures.rs | 64 ++- common/src/test_support/mod.rs | 176 ++++---- common/src/test_support/ui.rs | 114 +++-- .../tests/brain_trait_evaluation_behaviour.rs | 41 +- common/tests/brain_trait_metrics_behaviour.rs | 39 +- .../tests/brain_type_evaluation_behaviour.rs | 23 +- common/tests/brain_type_metrics_behaviour.rs | 11 +- .../tests/cognitive_complexity_behaviour.rs | 58 ++- common/tests/complexity_signal_behaviour.rs | 88 +++- common/tests/context_behaviour.rs | 20 +- common/tests/cosine_threshold_behaviour.rs | 14 +- .../decomposition_adjacency_behaviour.rs | 34 +- .../tests/decomposition_advice_behaviour.rs | 59 +-- ...ecomposition_diagnostic_notes_behaviour.rs | 87 ++-- ...composition_label_propagation_behaviour.rs | 6 +- .../decomposition_vector_algebra_behaviour.rs | 15 +- .../features/brain_trait_metrics.feature | 2 +- .../features/cognitive_complexity.feature | 16 +- .../tests/features/context_detection.feature | 16 +- .../features/decomposition_advice.feature | 2 +- .../tests/features/rstest_detection.feature | 12 +- common/tests/i18n_behaviour.rs | 71 +-- common/tests/i18n_packaging.rs | 105 ++--- .../tests/i18n_quality/ftl_smoke_behaviour.rs | 2 +- common/tests/i18n_quality/suite.rs | 8 +- common/tests/lcom4_behaviour.rs | 17 +- common/tests/localizer_helpers.rs | 143 +++--- common/tests/method_extraction_behaviour.rs | 5 +- common/tests/rstest_detection_behaviour.rs | 31 +- common/tests/rstest_fingerprint_behaviour.rs | 5 +- .../tests/rstest_span_recovery_behaviour.rs | 11 +- common/tests/support/i18n_helpers.rs | 13 +- crates/bumpy_road_function/Cargo.toml | 1 + .../tests/analysis_behaviour.rs | 59 ++- .../tests/features/bumpy_road.feature | 4 +- crates/bumpy_road_function/tests/ui.rs | 19 +- crates/conditional_max_n_branches/Cargo.toml | 1 + .../src/lib_ui_tests.rs | 19 +- .../src/tests/behaviour.rs | 7 +- crates/function_attrs_follow_docs/Cargo.toml | 1 + .../src/tests/localization.rs | 14 +- .../src/tests/order_detection.rs | 21 +- .../src/tests/ui.rs | 7 +- .../function_attrs_localization.feature | 12 +- crates/module_max_lines/src/lib_ui_tests.rs | 19 +- crates/module_must_have_inner_docs/Cargo.toml | 1 + .../src/tests/behaviour.rs | 7 +- .../src/tests/classifier.rs | 6 +- .../src/tests/span_to_snippet.rs | 10 +- .../src/tests/ui.rs | 11 +- crates/no_expect_outside_tests/Cargo.toml | 1 + .../src/dependency_rlib_tests.rs | 28 +- .../src/lib_ui_tests.rs | 41 +- .../src/tests/localization.rs | 59 +-- .../src/tests/receiver_type_edge_cases.rs | 3 +- .../tests/features/context_summary.feature | 4 +- .../tests/features/localization.feature | 16 +- .../no_std_fs_operations/src/config_tests.rs | 36 +- crates/no_std_fs_operations/src/tests/ui.rs | 7 +- .../tests/integration_exclusion.rs | 43 +- .../tests/no_default_features_build.rs | 26 +- .../tests/test_support/mod.rs | 80 ++-- crates/no_unwrap_or_else_panic/Cargo.toml | 1 + .../src/lib_ui_tests.rs | 29 +- .../src/tests/behaviour.rs | 13 +- .../src/collector_tests.rs | 21 +- .../src/driver_tests.rs | 22 +- .../tests/ui.rs | 56 ++- .../tests/ui/harness_lock.rs | 81 ++-- .../tests/ui/lock_model.rs | 22 +- .../src/lib_ui_tests.rs | 20 +- crates/whitaker_clones_core/Cargo.toml | 1 + .../src/ast/lowering_tests.rs | 73 ++-- .../tests/ast_boundary.rs | 48 +- .../tests/ast_feature_extraction_behaviour.rs | 66 ++- .../tests/build_script_integration.rs | 65 +-- .../tests/build_script_parsing.rs | 50 ++- .../tests/candidate_pair_behaviour.rs | 20 +- .../tests/min_hash_lsh_behaviour.rs | 55 ++- .../tests/run0_sarif_behaviour.rs | 122 ++++-- .../tests/token_pass_behaviour.rs | 51 ++- crates/whitaker_sarif/Cargo.toml | 1 + .../whitaker_sarif/tests/sarif_behaviour.rs | 101 +++-- crates/whitaker_test_macros/Cargo.toml | 14 + crates/whitaker_test_macros/src/lib.rs | 32 ++ installer/Cargo.toml | 1 + installer/src/artefact/manifest_tests.rs | 68 +-- installer/src/artefact/packaging_tests.rs | 138 ++++-- installer/src/binstall_metadata_tests.rs | 9 +- installer/src/cli_tests.rs | 66 ++- .../install/downloader_boundary_tests.rs | 210 ++++----- installer/src/dependency_packaging_tests.rs | 45 +- installer/src/deps/install_tests.rs | 3 +- installer/src/deps/path_tests.rs | 38 +- installer/src/install_metrics_tests.rs | 82 +++- installer/src/installer_packaging_tests.rs | 98 +++-- installer/src/pipeline_staging_tests.rs | 131 +++--- installer/src/pipeline_tests.rs | 23 +- installer/src/prebuilt_tests.rs | 103 +++-- .../test_utils/dependency_binary_helpers.rs | 82 ++-- .../dependency_binary_helpers_tests.rs | 30 +- .../src/{test_utils.rs => test_utils/mod.rs} | 87 ++-- installer/src/tests/fast_path.rs | 21 +- installer/src/{tests.rs => tests/mod.rs} | 93 ++-- .../mod.rs} | 44 +- installer/src/toolchain/tests/mod.rs | 73 ++-- installer/src/toolchain/tests/test_helpers.rs | 70 +-- installer/tests/behaviour_artefact.rs | 211 +++++---- .../tests/behaviour_artefact_packaging.rs | 412 +++++++++++------- installer/tests/behaviour_binstall.rs | 136 +++--- installer/tests/behaviour_cli.rs | 3 +- installer/tests/behaviour_cli/scenarios.rs | 3 +- installer/tests/behaviour_cli/support.rs | 252 +++-------- .../tests/behaviour_cli/support_assertions.rs | 203 +++++++++ installer/tests/behaviour_core.rs | 65 ++- .../tests/behaviour_dependency_binaries.rs | 251 ++++++----- installer/tests/behaviour_docs.rs | 224 +++++----- installer/tests/behaviour_install_metrics.rs | 222 ++++++---- .../tests/behaviour_installer_release.rs | 148 ++++--- installer/tests/behaviour_prebuilt.rs | 177 +++++--- installer/tests/behaviour_staging.rs | 118 +++-- installer/tests/behaviour_toolchain.rs | 10 +- installer/tests/behaviour_workflows.rs | 242 +++++----- installer/tests/doc_extraction/extraction.rs | 37 +- installer/tests/support/mod.rs | 150 ++++--- installer/tests/toolchain_steps/mod.rs | 363 ++++++--------- .../tests/toolchain_steps/scenario_setup.rs | 174 ++++++++ installer/tests/ui.rs | 9 +- suite/Cargo.toml | 1 + suite/tests/registration.rs | 31 +- 151 files changed, 5008 insertions(+), 3331 deletions(-) rename common/src/decomposition_advice/{tests.rs => tests/mod.rs} (88%) create mode 100644 crates/whitaker_test_macros/Cargo.toml create mode 100644 crates/whitaker_test_macros/src/lib.rs rename installer/src/{test_utils.rs => test_utils/mod.rs} (84%) rename installer/src/{tests.rs => tests/mod.rs} (83%) rename installer/src/toolchain/tests/{failure_mocks.rs => failure_mocks/mod.rs} (90%) create mode 100644 installer/tests/behaviour_cli/support_assertions.rs create mode 100644 installer/tests/toolchain_steps/scenario_setup.rs diff --git a/Cargo.lock b/Cargo.lock index 8700d93d1..63add6fad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,7 @@ dependencies = [ "toml 1.1.3+spec-1.1.0", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -461,6 +462,7 @@ dependencies = [ "tempfile", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1043,6 +1045,7 @@ dependencies = [ "serial_test", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1854,6 +1857,7 @@ dependencies = [ "serial_test", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1884,6 +1888,7 @@ dependencies = [ "tokio", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1936,6 +1941,7 @@ dependencies = [ "temp-env", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -3649,9 +3655,11 @@ dependencies = [ "rstest", "rstest-bdd", "rstest-bdd-macros", + "temp-env", "tempfile", "thiserror 2.0.20", "unic-langid", + "whitaker_test_macros", ] [[package]] @@ -3684,6 +3692,7 @@ dependencies = [ "wait-timeout", "whitaker-common", "whitaker-installer", + "whitaker_test_macros", "zip", "zstd", ] @@ -3708,6 +3717,7 @@ dependencies = [ "toml 1.1.3+spec-1.1.0", "tracing", "whitaker_sarif", + "whitaker_test_macros", ] [[package]] @@ -3722,6 +3732,7 @@ dependencies = [ "serde_json", "tempfile", "thiserror 2.0.20", + "whitaker_test_macros", ] [[package]] @@ -3746,6 +3757,16 @@ dependencies = [ "rustc_session", "rustc_span", "test_must_not_have_example", + "whitaker_test_macros", +] + +[[package]] +name = "whitaker_test_macros" +version = "0.2.7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7e79a2660..c22cf16d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,10 @@ rustc_lexer = "0.1.0" ra_ap_syntax = "=0.0.334" whitaker_clones_core = { path = "crates/whitaker_clones_core", version = "0.2.7" } whitaker_sarif = { path = "crates/whitaker_sarif", version = "0.2.7" } +# Path-only (no `version`): the crate is `publish = false`, and Cargo strips +# versionless dev-dependencies when packaging, so the published crates do not +# gain an unresolvable requirement. +whitaker_test_macros = { path = "crates/whitaker_test_macros" } temp-env = "0.3.6" tempfile = "3.19.1" wait-timeout = "0.2.1" diff --git a/common/Cargo.toml b/common/Cargo.toml index 76b52eb51..c04e06c4b 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -18,11 +18,13 @@ fluent-templates = { workspace = true } glob = "0.3.3" log = { workspace = true } once_cell = { workspace = true } +temp-env = { workspace = true } tempfile = "3.14.0" thiserror = { workspace = true } unic-langid = { workspace = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } # Testing: Behavioural specs use rstest-bdd 0.5.x (workspace pin). Capture any # regressions with `make test` and coordinate with the maintainers per # `docs/rstest-bdd-users-guide.md` before gating behaviour tests or backporting diff --git a/common/src/brain_trait_metrics/diagnostic_tests.rs b/common/src/brain_trait_metrics/diagnostic_tests.rs index e555fbd31..8ffd0bf64 100644 --- a/common/src/brain_trait_metrics/diagnostic_tests.rs +++ b/common/src/brain_trait_metrics/diagnostic_tests.rs @@ -1,12 +1,14 @@ //! Unit tests for brain trait diagnostic formatting. -use super::*; -use crate::brain_trait_metrics::TraitMetricsBuilder; -use crate::brain_trait_metrics::evaluation::BrainTraitDisposition; -use crate::decomposition_advice::SubjectKind; -use crate::test_support::decomposition::{decomposition_suggestions, transport_trait_fixture}; use rstest::rstest; +use super::*; +use crate::{ + brain_trait_metrics::{TraitMetricsBuilder, evaluation::BrainTraitDisposition}, + decomposition_advice::SubjectKind, + test_support::decomposition::{decomposition_suggestions, transport_trait_fixture}, +}; + // --------------------------------------------------------------------------- // Helper: build diagnostics for formatting tests // --------------------------------------------------------------------------- @@ -20,7 +22,7 @@ struct DiagnosticInput<'a> { } /// Builds a diagnostic for a trait with the given method breakdown. -fn build_diagnostic(input: DiagnosticInput<'_>) -> BrainTraitDiagnostic { +fn build_diagnostic(input: &DiagnosticInput<'_>) -> BrainTraitDiagnostic { let mut builder = TraitMetricsBuilder::new(input.name); for i in 0..input.required { builder.add_required_method(format!("req_{i}")); @@ -39,7 +41,7 @@ fn build_diagnostic(input: DiagnosticInput<'_>) -> BrainTraitDiagnostic { /// Builds a primary message for a trait with 15 required + 10 default /// methods, each default having CC=5 (CC sum=50). fn primary_message_with_defaults() -> String { - let diag = build_diagnostic(DiagnosticInput { + let diag = build_diagnostic(&DiagnosticInput { name: "Parser", required: 15, default: 10, @@ -55,9 +57,12 @@ fn primary_message_with_defaults() -> String { #[case("15 required", "required count")] #[case("10 default", "default count")] #[case("CC=50", "CC sum")] -fn primary_message_with_defaults_contains(#[case] fragment: &str, #[case] _description: &str) { +fn primary_message_with_defaults_contains(#[case] fragment: &str, #[case] description: &str) { let msg = primary_message_with_defaults(); - assert!(msg.contains(fragment), "missing fragment: {fragment}"); + assert!( + msg.contains(fragment), + "missing {description} fragment: {fragment}" + ); } // --------------------------------------------------------------------------- @@ -66,7 +71,7 @@ fn primary_message_with_defaults_contains(#[case] fragment: &str, #[case] _descr #[rstest] fn primary_message_omits_cc_when_zero() { - let diag = build_diagnostic(DiagnosticInput { + let diag = build_diagnostic(&DiagnosticInput { name: "Simple", required: 10, default: 0, @@ -87,7 +92,7 @@ fn primary_message_omits_cc_when_zero() { #[rstest] fn primary_message_with_only_default_methods() { - let diag = build_diagnostic(DiagnosticInput { + let diag = build_diagnostic(&DiagnosticInput { name: "AllDefault", required: 0, default: 5, @@ -115,7 +120,7 @@ fn note_contains_expected_fragment( #[case] fragment: &str, ) { let (required, default, cc_per_default) = shape; - let diag = build_diagnostic(DiagnosticInput { + let diag = build_diagnostic(&DiagnosticInput { name, required, default, @@ -130,7 +135,7 @@ fn note_contains_expected_fragment( #[rstest] fn note_omits_cc_when_no_default_methods() { - let diag = build_diagnostic(DiagnosticInput { + let diag = build_diagnostic(&DiagnosticInput { name: "Foo", required: 10, default: 0, @@ -149,7 +154,7 @@ fn note_omits_cc_when_no_default_methods() { #[rstest] fn decomposition_note_delegates_to_shared_renderer_for_traits() { - let diagnostic = build_diagnostic(DiagnosticInput { + let diagnostic = build_diagnostic(&DiagnosticInput { name: "Transport", required: 2, default: 2, @@ -191,7 +196,7 @@ fn help_suggestions( #[case] fragment: &str, ) { let (required, default, cc_per_default) = shape; - let diag = build_diagnostic(DiagnosticInput { + let diag = build_diagnostic(&DiagnosticInput { name, required, default, @@ -236,7 +241,7 @@ fn total_item_count_includes_associated_items() { /// Builds a diagnostic for accessor tests: trait "Qux" with 10 /// required, 5 default (CC=4 each), CC sum=20. fn accessor_diagnostic() -> BrainTraitDiagnostic { - build_diagnostic(DiagnosticInput { + build_diagnostic(&DiagnosticInput { name: "Qux", required: 10, default: 5, diff --git a/common/src/brain_trait_metrics/evaluation_tests.rs b/common/src/brain_trait_metrics/evaluation_tests.rs index 8cf779f4f..d591b3378 100644 --- a/common/src/brain_trait_metrics/evaluation_tests.rs +++ b/common/src/brain_trait_metrics/evaluation_tests.rs @@ -1,8 +1,9 @@ //! Unit tests for brain trait threshold evaluation. +use rstest::rstest; + use super::*; use crate::brain_trait_metrics::TraitMetricsBuilder; -use rstest::rstest; // --------------------------------------------------------------------------- // Helper: build TraitMetrics with the desired shape @@ -116,7 +117,7 @@ fn builder_default_trait_matches_new() { #[case("high CC but few methods", 3, 2, 10)] #[case("at methods_warn but CC below threshold", 15, 5, 1)] fn evaluate_pass_cases( - #[case] _label: &str, + #[case] label: &str, #[case] required: usize, #[case] default: usize, #[case] cc_per_default: usize, @@ -125,7 +126,8 @@ fn evaluate_pass_cases( let thresholds = BrainTraitThresholdsBuilder::new().build(); assert_eq!( evaluate_brain_trait(&metrics, &thresholds), - BrainTraitDisposition::Pass + BrainTraitDisposition::Pass, + "case: {label}" ); } @@ -149,7 +151,7 @@ fn pass_when_at_methods_warn_but_cc_one_below() { #[case("above warn below deny", 15, 10, 6)] #[case("just below methods_deny", 19, 10, 4)] fn evaluate_warn_cases( - #[case] _label: &str, + #[case] label: &str, #[case] required: usize, #[case] default: usize, #[case] cc_per_default: usize, @@ -158,7 +160,8 @@ fn evaluate_warn_cases( let thresholds = BrainTraitThresholdsBuilder::new().build(); assert_eq!( evaluate_brain_trait(&metrics, &thresholds), - BrainTraitDisposition::Warn + BrainTraitDisposition::Warn, + "case: {label}" ); } @@ -171,7 +174,7 @@ fn evaluate_warn_cases( #[case("method count above deny", 25, 10, 0)] #[case("deny supersedes warn", 20, 10, 5)] fn evaluate_deny_cases( - #[case] _label: &str, + #[case] label: &str, #[case] required: usize, #[case] default: usize, #[case] cc_per_default: usize, @@ -180,7 +183,8 @@ fn evaluate_deny_cases( let thresholds = BrainTraitThresholdsBuilder::new().build(); assert_eq!( evaluate_brain_trait(&metrics, &thresholds), - BrainTraitDisposition::Deny + BrainTraitDisposition::Deny, + "case: {label}" ); } @@ -208,14 +212,18 @@ fn evaluate_deny_cases( BrainTraitDisposition::Warn )] fn custom_threshold_overrides( - #[case] _label: &str, + #[case] label: &str, #[case] shape: (usize, usize, usize), #[case] thresholds: BrainTraitThresholds, #[case] expected: BrainTraitDisposition, ) { let (required, default, cc_per_default) = shape; let metrics = build_trait_metrics("Custom", required, default, cc_per_default); - assert_eq!(evaluate_brain_trait(&metrics, &thresholds), expected); + assert_eq!( + evaluate_brain_trait(&metrics, &thresholds), + expected, + "case: {label}" + ); } // --------------------------------------------------------------------------- diff --git a/common/src/brain_type_metrics/cognitive_complexity_tests.rs b/common/src/brain_type_metrics/cognitive_complexity_tests.rs index b9eb59c5e..9e8a32518 100644 --- a/common/src/brain_type_metrics/cognitive_complexity_tests.rs +++ b/common/src/brain_type_metrics/cognitive_complexity_tests.rs @@ -1,8 +1,9 @@ //! Unit tests for [`super::CognitiveComplexityBuilder`]. -use super::*; use rstest::rstest; +use super::*; + // --------------------------------------------------------------------------- // Individual increment types — non-expansion // --------------------------------------------------------------------------- @@ -151,10 +152,10 @@ fn mixed_expansion_nesting() { /// Parameterized composite scenarios. Each case simulates a code /// pattern and asserts the expected CC score. /// -/// Parameters: `(label, expected_score)` +/// Parameters: `(expected_score)` #[rstest] -#[case("simple_if", 1)] -fn composite_simple_if(#[case] _label: &str, #[case] expected: usize) { +#[case(1)] +fn composite_simple_if(#[case] expected: usize) { // if cond {} => structural +1, nesting +0 (depth 0) let mut cc = CognitiveComplexityBuilder::new(); cc.record_structural_increment(false); @@ -165,8 +166,8 @@ fn composite_simple_if(#[case] _label: &str, #[case] expected: usize) { } #[rstest] -#[case("nested_if_in_if", 3)] -fn composite_nested_if(#[case] _label: &str, #[case] expected: usize) { +#[case(3)] +fn composite_nested_if(#[case] expected: usize) { // if { if {} } // outer: struct +1, nest +0 (depth 0) // inner: struct +1, nest +1 (depth 1) @@ -183,8 +184,8 @@ fn composite_nested_if(#[case] _label: &str, #[case] expected: usize) { } #[rstest] -#[case("if_with_boolean_ops", 3)] -fn composite_if_with_boolean_ops(#[case] _label: &str, #[case] expected: usize) { +#[case(3)] +fn composite_if_with_boolean_ops(#[case] expected: usize) { // if a && b || c {} // structural +1, fundamental +1 (&&), fundamental +1 (||) let mut cc = CognitiveComplexityBuilder::new(); @@ -197,8 +198,8 @@ fn composite_if_with_boolean_ops(#[case] _label: &str, #[case] expected: usize) } #[rstest] -#[case("triple_nested_loop", 6)] -fn composite_triple_nested_loop(#[case] _label: &str, #[case] expected: usize) { +#[case(6)] +fn composite_triple_nested_loop(#[case] expected: usize) { // for { for { for {} } } // L1: struct +1, nest +0 (depth 0), push => depth 1 // L2: struct +1, nest +1 (depth 1), push => depth 2 @@ -220,8 +221,8 @@ fn composite_triple_nested_loop(#[case] _label: &str, #[case] expected: usize) { } #[rstest] -#[case("macro_if_inside_real_for", 1)] -fn composite_macro_if_inside_real_for(#[case] _label: &str, #[case] expected: usize) { +#[case(1)] +fn composite_macro_if_inside_real_for(#[case] expected: usize) { // for { MACRO_IF } // for: struct +1, nest +0, push(false) => depth 1 // macro if: struct(true) skipped, nest(true) skipped @@ -238,8 +239,8 @@ fn composite_macro_if_inside_real_for(#[case] _label: &str, #[case] expected: us } #[rstest] -#[case("real_if_inside_macro_for", 1)] -fn composite_real_if_inside_macro_for(#[case] _label: &str, #[case] expected: usize) { +#[case(1)] +fn composite_real_if_inside_macro_for(#[case] expected: usize) { // MACRO_FOR { if {} } // macro for: struct(true) skipped, push(true) => eff depth 0 // real if: struct(false) +1, nest(false) +0 (eff depth is 0) @@ -285,7 +286,8 @@ fn default_matches_new() { fn build_panics_on_unbalanced_stack() { let mut cc = CognitiveComplexityBuilder::new(); cc.push_nesting(false); - let _ = cc.build(); + // The build call should panic before yielding a score. + let _score = cc.build(); } #[rstest] diff --git a/common/src/brain_type_metrics/diagnostic_tests.rs b/common/src/brain_type_metrics/diagnostic_tests.rs index 12cfeae3e..3caf610f6 100644 --- a/common/src/brain_type_metrics/diagnostic_tests.rs +++ b/common/src/brain_type_metrics/diagnostic_tests.rs @@ -1,12 +1,14 @@ //! Unit tests for brain type diagnostic formatting. -use super::*; -use crate::brain_type_metrics::TypeMetricsBuilder; -use crate::brain_type_metrics::evaluation::BrainTypeDisposition; -use crate::decomposition_advice::SubjectKind; -use crate::test_support::decomposition::{decomposition_suggestions, parser_serde_fs_fixture}; use rstest::rstest; +use super::*; +use crate::{ + brain_type_metrics::{TypeMetricsBuilder, evaluation::BrainTypeDisposition}, + decomposition_advice::SubjectKind, + test_support::decomposition::{decomposition_suggestions, parser_serde_fs_fixture}, +}; + // --------------------------------------------------------------------------- // Diagnostic — primary message (one brain method) // --------------------------------------------------------------------------- @@ -33,10 +35,13 @@ fn one_brain_method_message() -> String { #[case("a brain method", "singular form")] fn one_brain_method_message_contains_expected_fragment( #[case] fragment: &str, - #[case] _description: &str, + #[case] description: &str, ) { let msg = one_brain_method_message(); - assert!(msg.contains(fragment), "missing fragment: {fragment}"); + assert!( + msg.contains(fragment), + "missing {description} fragment: {fragment}" + ); } // --------------------------------------------------------------------------- @@ -329,5 +334,9 @@ fn diagnostic_brain_methods_count_accessor() { #[rstest] fn diagnostic_brain_methods_name_accessor() { let diag = accessor_diagnostic(); - assert_eq!(diag.brain_methods()[0].name(), "big"); + let brain_method = diag + .brain_methods() + .first() + .expect("diagnostic should expose one brain method"); + assert_eq!(brain_method.name(), "big"); } diff --git a/common/src/brain_type_metrics/evaluation_tests.rs b/common/src/brain_type_metrics/evaluation_tests.rs index b94404713..8a4f249f5 100644 --- a/common/src/brain_type_metrics/evaluation_tests.rs +++ b/common/src/brain_type_metrics/evaluation_tests.rs @@ -1,8 +1,9 @@ //! Unit tests for brain type threshold evaluation. +use rstest::rstest; + use super::*; use crate::brain_type_metrics::TypeMetricsBuilder; -use rstest::rstest; // --------------------------------------------------------------------------- // Helper: build TypeMetrics with the desired shape @@ -114,7 +115,7 @@ fn builder_default_trait_matches_new() { #[case("brain method present but cohesive (LCOM4=1)", 80, 1, 1)] #[case("brain method and low cohesion but low WMC", 30, 1, 2)] fn evaluate_pass_cases( - #[case] _label: &str, + #[case] label: &str, #[case] wmc: usize, #[case] brain_count: usize, #[case] lcom4: usize, @@ -123,7 +124,8 @@ fn evaluate_pass_cases( let thresholds = BrainTypeThresholdsBuilder::new().build(); assert_eq!( evaluate_brain_type(&metrics, &thresholds), - BrainTypeDisposition::Pass + BrainTypeDisposition::Pass, + "case: {label}" ); } @@ -136,7 +138,7 @@ fn evaluate_pass_cases( #[case("above warn below deny", 80, 1, 2)] #[case("just below WMC deny", 99, 1, 2)] fn evaluate_warn_cases( - #[case] _label: &str, + #[case] label: &str, #[case] wmc: usize, #[case] brain_count: usize, #[case] lcom4: usize, @@ -145,7 +147,8 @@ fn evaluate_warn_cases( let thresholds = BrainTypeThresholdsBuilder::new().build(); assert_eq!( evaluate_brain_type(&metrics, &thresholds), - BrainTypeDisposition::Warn + BrainTypeDisposition::Warn, + "case: {label}" ); } @@ -160,7 +163,7 @@ fn evaluate_warn_cases( #[case("deny supersedes warn", 100, 1, 3)] #[case("all deny triggers active", 100, 2, 3)] fn evaluate_deny_cases( - #[case] _label: &str, + #[case] label: &str, #[case] wmc: usize, #[case] brain_count: usize, #[case] lcom4: usize, @@ -169,7 +172,8 @@ fn evaluate_deny_cases( let thresholds = BrainTypeThresholdsBuilder::new().build(); assert_eq!( evaluate_brain_type(&metrics, &thresholds), - BrainTypeDisposition::Deny + BrainTypeDisposition::Deny, + "case: {label}" ); } @@ -227,7 +231,7 @@ fn exact_brain_method_deny_count_triggers_deny() { #[case("below custom threshold", 2, false)] #[case("at custom threshold boundary", 3, true)] fn custom_brain_method_deny_count_boundary( - #[case] _label: &str, + #[case] label: &str, #[case] brain_count: usize, #[case] should_deny: bool, ) { @@ -240,8 +244,8 @@ fn custom_brain_method_deny_count_boundary( let disposition = evaluate_brain_type(&metrics, &thresholds); if should_deny { - assert_eq!(disposition, BrainTypeDisposition::Deny); + assert_eq!(disposition, BrainTypeDisposition::Deny, "case: {label}"); } else { - assert_ne!(disposition, BrainTypeDisposition::Deny); + assert_ne!(disposition, BrainTypeDisposition::Deny, "case: {label}"); } } diff --git a/common/src/decomposition_advice/note_tests.rs b/common/src/decomposition_advice/note_tests.rs index 7697d051a..bb6acd638 100644 --- a/common/src/decomposition_advice/note_tests.rs +++ b/common/src/decomposition_advice/note_tests.rs @@ -1,10 +1,12 @@ //! Unit tests for decomposition diagnostic-note rendering. use super::format_diagnostic_note; -use crate::decomposition_advice::{DecompositionContext, MethodProfile, SubjectKind}; -use crate::test_support::decomposition::{ - MethodInput, decomposition_suggestions, parser_serde_fs_fixture, profile, - transport_trait_fixture, +use crate::{ + decomposition_advice::{DecompositionContext, MethodProfile, SubjectKind}, + test_support::decomposition::{ + MethodInput, decomposition_suggestions, parser_serde_fs_fixture, profile, + transport_trait_fixture, + }, }; fn parser_serde_fs_suggestions() -> ( @@ -14,8 +16,8 @@ fn parser_serde_fs_suggestions() -> ( decomposition_suggestions("Foo", SubjectKind::Type, &parser_serde_fs_fixture()) } -fn render_note(subject: &str, kind: SubjectKind, methods: Vec) -> String { - let (context, suggestions) = decomposition_suggestions(subject, kind, &methods); +fn render_note(subject: &str, kind: SubjectKind, methods: &[MethodProfile]) -> String { + let (context, suggestions) = decomposition_suggestions(subject, kind, methods); format_diagnostic_note(&context, &suggestions).unwrap_or_default() } @@ -43,7 +45,7 @@ fn format_diagnostic_note_renders_type_suggestions() { #[test] fn format_diagnostic_note_renders_trait_sub_traits() { - let rendered = render_note("Transport", SubjectKind::Trait, transport_trait_fixture()); + let rendered = render_note("Transport", SubjectKind::Trait, &transport_trait_fixture()); assert!(rendered.contains("- [serde::json] sub-trait for `decode_request`, `encode_request`")); assert!(rendered.contains("- [std::io] sub-trait for `read_frame`, `write_frame`")); @@ -110,7 +112,7 @@ fn format_diagnostic_note_caps_rendered_suggestions() { }), ]; - let rendered = render_note("Coordinator", SubjectKind::Type, methods); + let rendered = render_note("Coordinator", SubjectKind::Type, &methods); assert!(rendered.contains("- [grammar] helper struct")); assert!(rendered.contains("- [serde::json] module")); @@ -173,9 +175,10 @@ fn format_diagnostic_note_caps_methods_per_suggestion() { }), ]; - let rendered = render_note("Reporter", SubjectKind::Type, methods); + let rendered = render_note("Reporter", SubjectKind::Type, &methods); assert!(rendered.contains( - "- [report] helper struct for `report_alpha`, `report_beta`, `report_delta`, +2 more methods" + "- [report] helper struct for `report_alpha`, `report_beta`, `report_delta`, +2 more \ + methods" )); } diff --git a/common/src/decomposition_advice/tests/adjacency.rs b/common/src/decomposition_advice/tests/adjacency.rs index 8219e2a1b..c69a72c18 100644 --- a/common/src/decomposition_advice/tests/adjacency.rs +++ b/common/src/decomposition_advice/tests/adjacency.rs @@ -27,9 +27,9 @@ fn single_edge_inserted_in_both_directions() { let adjacency = build_adjacency(3, &[edge(0, 2, 10)]); assert_eq!(adjacency.len(), 3); - assert_eq!(adjacency[0], vec![(2, 10)]); - assert!(adjacency[1].is_empty()); - assert_eq!(adjacency[2], vec![(0, 10)]); + assert_eq!(adjacency.first(), Some(&vec![(2, 10)])); + assert_eq!(adjacency.get(1), Some(&Vec::new())); + assert_eq!(adjacency.get(2), Some(&vec![(0, 10)])); } #[test] @@ -39,7 +39,7 @@ fn multiple_edges_produce_sorted_neighbour_lists() { let adjacency = build_adjacency(4, &edges); // Node 1's neighbours should be sorted by neighbour index. - assert_eq!(adjacency[1], vec![(0, 5), (2, 8), (3, 3)]); + assert_eq!(adjacency.get(1), Some(&vec![(0, 5), (2, 8), (3, 3)])); } #[test] @@ -48,10 +48,10 @@ fn sparse_graph_preserves_isolated_nodes() { let adjacency = build_adjacency(4, &[edge(0, 2, 7)]); assert_eq!(adjacency.len(), 4); - assert_eq!(adjacency[0], vec![(2, 7)]); - assert!(adjacency[1].is_empty()); - assert_eq!(adjacency[2], vec![(0, 7)]); - assert!(adjacency[3].is_empty()); + assert_eq!(adjacency.first(), Some(&vec![(2, 7)])); + assert_eq!(adjacency.get(1), Some(&Vec::new())); + assert_eq!(adjacency.get(2), Some(&vec![(0, 7)])); + assert_eq!(adjacency.get(3), Some(&Vec::new())); } #[test] @@ -62,8 +62,11 @@ fn multi_edge_graph_is_symmetric() { // Every (node -> neighbour, weight) pair has its mirror. for (node, bucket) in adjacency.iter().enumerate() { for &(neighbour, weight) in bucket { + let mirror_bucket = adjacency + .get(neighbour) + .expect("neighbour index should be within adjacency bounds"); assert!( - adjacency[neighbour] + mirror_bucket .iter() .any(|&(mirror, mirror_weight)| mirror == node && mirror_weight == weight), "missing mirror for ({node} -> {neighbour}, weight {weight})", diff --git a/common/src/decomposition_advice/tests.rs b/common/src/decomposition_advice/tests/mod.rs similarity index 88% rename from common/src/decomposition_advice/tests.rs rename to common/src/decomposition_advice/tests/mod.rs index 93fc40af0..e7f1cd8cd 100644 --- a/common/src/decomposition_advice/tests.rs +++ b/common/src/decomposition_advice/tests/mod.rs @@ -6,15 +6,18 @@ mod propagation; mod test_fixtures; mod vector_algebra; +use std::str::FromStr; + use self::test_fixtures::{ ExpectedSuggestion, MethodInput, assert_suggestion, assert_type_decomposition_is_empty, parser_serde_fs_fixture, profile, }; -use super::community::{build_similarity_edges, detect_communities}; -use super::profile::{DecompositionContext, SubjectKind}; -use super::suggestion::{SuggestedExtractionKind, suggest_decomposition}; -use super::vector::{build_feature_vector, dot_product, identifier_keywords}; -use std::str::FromStr; +use super::{ + community::{build_similarity_edges, detect_communities}, + profile::{DecompositionContext, SubjectKind}, + suggestion::{SuggestedExtractionKind, suggest_decomposition}, + vector::{build_feature_vector, dot_product, identifier_keywords}, +}; #[test] fn identifier_keywords_split_camel_case_and_remove_stop_words() { @@ -119,8 +122,11 @@ fn similarity_edges_include_related_methods_only() { let edges = build_similarity_edges(&vectors); assert_eq!(edges.len(), 1); - assert_eq!((edges[0].left(), edges[0].right()), (0, 1)); - assert!(edges[0].weight() > 0); + let edge = edges + .first() + .expect("similarity edges should contain the related parser pair"); + assert_eq!((edge.left(), edge.right()), (0, 1)); + assert!(edge.weight() > 0); } #[test] @@ -129,13 +135,19 @@ fn detect_communities_is_order_invariant() { let mut original_vectors: Vec<_> = fixture.iter().map(build_feature_vector).collect(); original_vectors.sort(); + let fixture_method = |index: usize| { + fixture + .get(index) + .cloned() + .expect("parser fixture should provide six methods") + }; let reordered_fixture = [ - fixture[4].clone(), - fixture[1].clone(), - fixture[5].clone(), - fixture[0].clone(), - fixture[3].clone(), - fixture[2].clone(), + fixture_method(4), + fixture_method(1), + fixture_method(5), + fixture_method(0), + fixture_method(3), + fixture_method(2), ]; let mut reordered_vectors: Vec<_> = reordered_fixture.iter().map(build_feature_vector).collect(); @@ -151,7 +163,7 @@ fn detect_communities_is_order_invariant() { fn suggest_decomposition_returns_empty_for_single_community() { assert_type_decomposition_is_empty( "Parser", - vec![ + &[ profile(MethodInput { name: "parse_tokens", fields: &["grammar"], @@ -190,8 +202,13 @@ fn suggest_decomposition_for_type_prefers_domain_module_and_field_helper_struct( let suggestions = suggest_decomposition(&context, &parser_serde_fs_fixture()); assert_eq!(suggestions.len(), 3); + let suggestion_at = |index: usize| { + suggestions + .get(index) + .expect("decomposition should yield three suggestions") + }; assert_suggestion( - &suggestions[0], + suggestion_at(0), ExpectedSuggestion { label: "grammar", extraction_kind: SuggestedExtractionKind::HelperStruct, @@ -199,7 +216,7 @@ fn suggest_decomposition_for_type_prefers_domain_module_and_field_helper_struct( }, ); assert_suggestion( - &suggestions[1], + suggestion_at(1), ExpectedSuggestion { label: "serde::json", extraction_kind: SuggestedExtractionKind::Module, @@ -207,7 +224,7 @@ fn suggest_decomposition_for_type_prefers_domain_module_and_field_helper_struct( }, ); assert_suggestion( - &suggestions[2], + suggestion_at(2), ExpectedSuggestion { label: "std::fs", extraction_kind: SuggestedExtractionKind::Module, @@ -294,12 +311,13 @@ fn suggest_decomposition_is_order_invariant_for_duplicate_method_names() { }), ]; - let reordered = vec![ - methods[2].clone(), - methods[0].clone(), - methods[3].clone(), - methods[1].clone(), - ]; + let method_at = |index: usize| { + methods + .get(index) + .cloned() + .expect("importer fixture should provide four methods") + }; + let reordered = vec![method_at(2), method_at(0), method_at(3), method_at(1)]; assert_eq!( suggest_decomposition(&context, &methods), @@ -333,7 +351,7 @@ fn suggestions_drop_singleton_noise_methods() { fn suggestions_skip_degenerate_groups_without_features() { assert_type_decomposition_is_empty( "Runner", - vec![ + &[ profile(MethodInput { name: "build", fields: &[], diff --git a/common/src/decomposition_advice/tests/propagation.rs b/common/src/decomposition_advice/tests/propagation.rs index 29f8e5f42..4240402c2 100644 --- a/common/src/decomposition_advice/tests/propagation.rs +++ b/common/src/decomposition_advice/tests/propagation.rs @@ -1,12 +1,13 @@ //! Unit tests for deterministic label propagation. use rstest::{fixture, rstest}; +use whitaker_test_macros::allow_fixture_expansion_lints; -use crate::decomposition_advice::community::{ - SimilarityEdge, build_adjacency, detect_communities, propagate_labels_report, +use crate::decomposition_advice::{ + community::{SimilarityEdge, build_adjacency, detect_communities, propagate_labels_report}, + minimal_feature_vector, + vector::{MethodFeatureVector, test_feature_vector}, }; -use crate::decomposition_advice::minimal_feature_vector; -use crate::decomposition_advice::vector::{MethodFeatureVector, test_feature_vector}; fn vectors(method_names: &[&str]) -> Vec { method_names @@ -19,6 +20,7 @@ fn edge(left: usize, right: usize, weight: u64) -> SimilarityEdge { SimilarityEdge::new(left, right, weight) } +#[allow_fixture_expansion_lints] #[fixture] fn connected_triplet_vectors() -> Vec { vectors(&["alpha", "beta", "gamma"]) @@ -39,16 +41,19 @@ fn linear_quartet_adjacency() -> Vec> { build_adjacency(4, &[edge(0, 1, 5), edge(1, 2, 5), edge(2, 3, 5)]) } +#[allow_fixture_expansion_lints] #[fixture] fn isolated_tail_vectors() -> Vec { vectors(&["alpha", "beta", "gamma"]) } +#[allow_fixture_expansion_lints] #[fixture] fn isolated_tail_adjacency() -> Vec> { build_adjacency(3, &[edge(0, 1, 5)]) } +#[allow_fixture_expansion_lints] #[fixture] fn lexical_tie_vectors() -> Vec { vectors(&["gamma", "alpha", "beta"]) @@ -122,7 +127,7 @@ fn propagate_labels_leaves_isolated_nodes_with_original_labels( ) { let report = propagate_labels_report(&isolated_tail_vectors, &isolated_tail_adjacency, 3); - assert_eq!(report.labels[2], 2); + assert_eq!(report.labels.get(2), Some(&2)); } #[rstest] @@ -158,7 +163,7 @@ fn propagate_labels_uses_lexical_tie_break_for_equal_scores( ) { let report = propagate_labels_report(&lexical_tie_vectors, &lexical_tie_adjacency, 1); - assert_eq!(report.labels[0], 1); + assert_eq!(report.labels.first(), Some(&1)); } #[rstest] @@ -168,8 +173,8 @@ fn propagate_labels_prefers_heavier_star_neighbour_when_counts_match() { let report = propagate_labels_report(&vectors, &adjacency, 1); - assert_eq!(report.labels[0], 1); - assert_eq!(report.labels[0], report.labels[1]); + assert_eq!(report.labels.first(), Some(&1)); + assert_eq!(report.labels.first(), report.labels.get(1)); } #[rstest] @@ -179,7 +184,7 @@ fn propagate_labels_prefers_triangle_weight_over_count_tie() { let report = propagate_labels_report(&vectors, &adjacency, 1); - assert_eq!(report.labels[0], 1); + assert_eq!(report.labels.first(), Some(&1)); } #[rstest] diff --git a/common/src/decomposition_advice/tests/test_fixtures.rs b/common/src/decomposition_advice/tests/test_fixtures.rs index 1fc256ddb..b09fba956 100644 --- a/common/src/decomposition_advice/tests/test_fixtures.rs +++ b/common/src/decomposition_advice/tests/test_fixtures.rs @@ -1,11 +1,17 @@ //! Shared fixture builders for decomposition-advice unit tests. -use super::super::profile::{DecompositionContext, MethodProfile, SubjectKind}; -use super::super::{DecompositionSuggestion, SuggestedExtractionKind, suggest_decomposition}; +use super::super::{ + DecompositionSuggestion, SuggestedExtractionKind, + profile::{DecompositionContext, MethodProfile, SubjectKind}, + suggest_decomposition, +}; pub(super) use crate::test_support::decomposition::{ MethodInput, parser_serde_fs_fixture, profile, }; +// `Copy` keeps the expectation cheap to pass by value; it only holds +// references. +#[derive(Clone, Copy)] pub(super) struct ExpectedSuggestion<'a> { pub(super) label: &'a str, pub(super) extraction_kind: SuggestedExtractionKind, @@ -21,10 +27,10 @@ pub(super) fn assert_suggestion( assert_eq!(actual.methods(), expected.methods); } -pub(super) fn assert_type_decomposition_is_empty(subject: &str, methods: Vec) { +pub(super) fn assert_type_decomposition_is_empty(subject: &str, methods: &[MethodProfile]) { let context = DecompositionContext::new(subject, SubjectKind::Type); assert!( - suggest_decomposition(&context, &methods).is_empty(), + suggest_decomposition(&context, methods).is_empty(), "expected no decomposition suggestions for {subject}" ); } diff --git a/common/src/decomposition_advice/tests/vector_algebra.rs b/common/src/decomposition_advice/tests/vector_algebra.rs index da36c3585..c57eca385 100644 --- a/common/src/decomposition_advice/tests/vector_algebra.rs +++ b/common/src/decomposition_advice/tests/vector_algebra.rs @@ -1,8 +1,9 @@ //! Validates the runtime vector algebra used by decomposition advice. -use crate::decomposition_advice::vector::{dot_product, test_feature_vector}; use rstest::rstest; +use crate::decomposition_advice::vector::{dot_product, test_feature_vector}; + #[rstest] #[case::left_smaller( test_feature_vector("left", &[("field:grammar", 6), ("keyword:parse", 2)]), diff --git a/common/src/rstest/tests/fingerprint.rs b/common/src/rstest/tests/fingerprint.rs index 563c39006..1b9bbfae3 100644 --- a/common/src/rstest/tests/fingerprint.rs +++ b/common/src/rstest/tests/fingerprint.rs @@ -1,10 +1,11 @@ //! Unit tests for shared `rstest` fingerprint data models. +use rstest::rstest; + use crate::rstest::{ ArgAtom, ArgFingerprint, CalleeShape, ExprShape, LocalSlot, ParagraphFingerprint, ParagraphNormalizer, StmtShape, }; -use rstest::rstest; #[rstest] fn argument_fingerprints_compare_identical_atom_sequences() { diff --git a/common/src/rstest/tests/fingerprint_props.rs b/common/src/rstest/tests/fingerprint_props.rs index 6c1c2e70a..8893fbf44 100644 --- a/common/src/rstest/tests/fingerprint_props.rs +++ b/common/src/rstest/tests/fingerprint_props.rs @@ -1,8 +1,9 @@ //! Property tests for shared `rstest` fingerprint data models. -use crate::rstest::{ArgAtom, ArgFingerprint, LocalSlot, ParagraphNormalizer}; use proptest::prelude::*; +use crate::rstest::{ArgAtom, ArgFingerprint, LocalSlot, ParagraphNormalizer}; + proptest! { /// Slot indices are assigned in strict first-appearance order: the first /// distinct name always receives slot 0, the second slot 1, etc. @@ -15,7 +16,8 @@ proptest! { for name in &names { let slot = norm.local_slot(name.as_str()); if !seen.contains(name) { - let expected = seen.len() as u32; + let expected = u32::try_from(seen.len()) + .expect("at most 32 generated names, so the count fits in u32"); prop_assert_eq!( slot.index(), expected, @@ -37,7 +39,7 @@ proptest! { ) { let mut norm = ParagraphNormalizer::new(); for p in &prefix { - let _ = norm.local_slot(p.as_str()); + let _prefix_slot = norm.local_slot(p.as_str()); } let first = norm.local_slot(name.as_str()); let second = norm.local_slot(name.as_str()); diff --git a/common/src/rstest/tests/mod.rs b/common/src/rstest/tests/mod.rs index 9df51af80..18aa0b383 100644 --- a/common/src/rstest/tests/mod.rs +++ b/common/src/rstest/tests/mod.rs @@ -3,16 +3,20 @@ mod fingerprint; mod fingerprint_props; +use std::collections::BTreeSet; + +use rstest::rstest; + use super::{ ExpansionTrace, ParameterBinding, RstestDetectionOptions, RstestParameter, RstestParameterKind, SpanRecoveryFrame, UserEditableSpan, classify_rstest_parameter, fixture_local_names, is_rstest_fixture, is_rstest_fixture_with, is_rstest_test, is_rstest_test_with, recover_user_editable_span, }; -use crate::attributes::{Attribute, AttributeKind, AttributePath}; -use crate::span::{SourceLocation, SourceSpan}; -use rstest::rstest; -use std::collections::BTreeSet; +use crate::{ + attributes::{Attribute, AttributeKind, AttributePath}, + span::{SourceLocation, SourceSpan}, +}; fn outer(path: &str) -> Attribute { Attribute::new(AttributePath::from(path), AttributeKind::Outer) @@ -20,7 +24,7 @@ fn outer(path: &str) -> Attribute { fn provider_parameter(path: &str) -> RstestParameter { RstestParameter::new( - ParameterBinding::Ident("value".to_string()), + ParameterBinding::Ident("value".to_owned()), vec![outer(path)], ) } @@ -74,7 +78,7 @@ fn classifies_identifier_parameters_as_fixture_locals() { assert_eq!( classify_rstest_parameter(¶meter, &RstestDetectionOptions::default()), RstestParameterKind::FixtureLocal { - name: "db".to_string() + name: "db".to_owned() } ); } @@ -125,7 +129,7 @@ fn rejects_unknown_custom_provider_parameters() { assert_eq!( classify_rstest_parameter(¶meter, &options), RstestParameterKind::FixtureLocal { - name: "value".to_string() + name: "value".to_owned() } ); } @@ -197,55 +201,62 @@ fn collects_supported_fixture_local_names_in_order() { assert_eq!( fixture_local_names(¶meters, &RstestDetectionOptions::default()), - BTreeSet::from(["clock".to_string(), "db".to_string()]) + BTreeSet::from(["clock".to_owned(), "db".to_owned()]) ); } -fn source_span(line: usize, start: usize, end: usize) -> SourceSpan { - SourceSpan::new( - SourceLocation::new(line, start), - SourceLocation::new(line, end), - ) - .expect("test spans should always be valid") +/// Builds a single-line [`SourceSpan`] for test data. +/// +/// This is a macro rather than a helper function so the fallible construction +/// is inlined into the calling `#[rstest]` body, where a failure is the test +/// verdict, and so it can be used inside `#[case(...)]` attributes. +macro_rules! source_span { + ($line:expr, $start:expr, $end:expr) => { + SourceSpan::new( + SourceLocation::new($line, $start), + SourceLocation::new($line, $end), + ) + .expect("test spans should always be valid") + }; } fn assert_span_recovery( frame_specs: impl IntoIterator, - expected: UserEditableSpan, + expected: &UserEditableSpan, ) { let frames: Vec> = frame_specs .into_iter() .map(|(span, is_macro)| SpanRecoveryFrame::new(span, is_macro)) .collect(); - assert_eq!(recover_user_editable_span(&frames), expected); + assert_eq!(&recover_user_editable_span(&frames), expected); } #[rstest] #[case::keeps_direct_user_editable_span( - vec![(source_span(1, 1, 8), false)], - UserEditableSpan::Direct(source_span(1, 1, 8)), + vec![(source_span!(1, 1, 8), false)], + UserEditableSpan::Direct(source_span!(1, 1, 8)), )] #[case::recovers_macro_frame_to_first_user_span( - vec![(source_span(2, 1, 8), true), (source_span(10, 1, 12), false)], - UserEditableSpan::Recovered(source_span(10, 1, 12)), + vec![(source_span!(2, 1, 8), true), (source_span!(10, 1, 12), false)], + UserEditableSpan::Recovered(source_span!(10, 1, 12)), )] #[case::recovers_first_user_span_from_nested_macro_chain( vec![ - (source_span(2, 1, 4), true), - (source_span(3, 1, 5), true), - (source_span(14, 1, 6), false), - (source_span(20, 1, 9), false), + (source_span!(2, 1, 4), true), + (source_span!(3, 1, 5), true), + (source_span!(14, 1, 6), false), + (source_span!(20, 1, 9), false), ], - UserEditableSpan::Recovered(source_span(14, 1, 6)), + UserEditableSpan::Recovered(source_span!(14, 1, 6)), )] #[case::treats_empty_frame_list_as_macro_only(vec![], UserEditableSpan::MacroOnly)] #[case::treats_all_expansion_frames_as_macro_only( - vec![(source_span(4, 1, 4), true), (source_span(5, 1, 6), true)], + vec![(source_span!(4, 1, 4), true), (source_span!(5, 1, 6), true)], UserEditableSpan::MacroOnly, )] fn recovers_user_editable_span_from_frame_sequences( #[case] frames: Vec<(SourceSpan, bool)>, #[case] expected: UserEditableSpan, ) { - assert_span_recovery(frames, expected); + assert_span_recovery(frames, &expected); } diff --git a/common/src/test_support/decomposition.rs b/common/src/test_support/decomposition.rs index b4f534bdb..cc31a9998 100644 --- a/common/src/test_support/decomposition.rs +++ b/common/src/test_support/decomposition.rs @@ -11,16 +11,17 @@ mod label_propagation; #[path = "decomposition_vector_algebra.rs"] mod vector_algebra; +pub use self::{ + adjacency::{AdjacencyError, AdjacencyReport, EdgeInput, adjacency_report}, + label_propagation::{LabelPropagationReport, label_propagation_report}, + vector_algebra::{MethodVectorAlgebraReport, method_vector_algebra}, +}; use crate::decomposition_advice::{ DecompositionContext, DecompositionSuggestion, MethodProfile, MethodProfileBuilder, SubjectKind, methods_meet_cosine_threshold as runtime_methods_meet_cosine_threshold, suggest_decomposition, }; -pub use self::adjacency::{AdjacencyError, AdjacencyReport, EdgeInput, adjacency_report}; -pub use self::label_propagation::{LabelPropagationReport, label_propagation_report}; -pub use self::vector_algebra::{MethodVectorAlgebraReport, method_vector_algebra}; - /// Input data for building a [`MethodProfile`] in tests. /// /// # Examples @@ -38,11 +39,17 @@ pub use self::vector_algebra::{MethodVectorAlgebraReport, method_vector_algebra} /// /// assert_eq!(profile.name(), "parse_tokens"); /// ``` +#[derive(Clone, Copy)] pub struct MethodInput<'a> { + /// Method name recorded on the resulting profile. pub name: &'a str, + /// Struct fields the method accesses. pub fields: &'a [&'a str], + /// Types that appear in the method signature. pub signature_types: &'a [&'a str], + /// Types bound locally within the method body. pub local_types: &'a [&'a str], + /// External domains (modules or crates) the method touches. pub domains: &'a [&'a str], } diff --git a/common/src/test_support/decomposition_adjacency.rs b/common/src/test_support/decomposition_adjacency.rs index 6a87f7f5e..fcf0c0e6d 100644 --- a/common/src/test_support/decomposition_adjacency.rs +++ b/common/src/test_support/decomposition_adjacency.rs @@ -1,8 +1,9 @@ //! Observable adjacency-construction seams for decomposition advice tests. -use crate::decomposition_advice::community::{SimilarityEdge, build_adjacency}; use thiserror::Error; +use crate::decomposition_advice::community::{SimilarityEdge, build_adjacency}; + /// Declarative edge input for test scenarios. /// /// # Examples @@ -89,7 +90,7 @@ impl AdjacencyReport { /// assert_eq!(report.node_count(), 4); /// ``` #[must_use] - pub fn node_count(&self) -> usize { + pub const fn node_count(&self) -> usize { self.node_count } @@ -100,9 +101,15 @@ impl AdjacencyReport { /// ```rust /// use whitaker_common::test_support::decomposition::{EdgeInput, adjacency_report}; /// - /// let report = adjacency_report(3, &[ - /// EdgeInput { left: 0, right: 2, weight: 7 }, - /// ]).expect("valid input"); + /// let report = adjacency_report( + /// 3, + /// &[EdgeInput { + /// left: 0, + /// right: 2, + /// weight: 7, + /// }], + /// ) + /// .expect("valid input"); /// assert_eq!(report.neighbours_of(0), Some(&[(2, 7)][..])); /// assert_eq!(report.neighbours_of(10), None); /// ``` @@ -116,9 +123,15 @@ impl AdjacencyReport { /// ```rust /// use whitaker_common::test_support::decomposition::{EdgeInput, adjacency_report}; /// - /// let report = adjacency_report(3, &[ - /// EdgeInput { left: 0, right: 1, weight: 5 }, - /// ]).expect("valid input"); + /// let report = adjacency_report( + /// 3, + /// &[EdgeInput { + /// left: 0, + /// right: 1, + /// weight: 5, + /// }], + /// ) + /// .expect("valid input"); /// assert!(report.all_indices_in_bounds()); /// ``` #[must_use] @@ -136,9 +149,15 @@ impl AdjacencyReport { /// ```rust /// use whitaker_common::test_support::decomposition::{EdgeInput, adjacency_report}; /// - /// let report = adjacency_report(3, &[ - /// EdgeInput { left: 0, right: 2, weight: 7 }, - /// ]).expect("valid input"); + /// let report = adjacency_report( + /// 3, + /// &[EdgeInput { + /// left: 0, + /// right: 2, + /// weight: 7, + /// }], + /// ) + /// .expect("valid input"); /// assert!(report.is_symmetric()); /// ``` #[must_use] @@ -156,17 +175,31 @@ impl AdjacencyReport { /// ```rust /// use whitaker_common::test_support::decomposition::{EdgeInput, adjacency_report}; /// - /// let report = adjacency_report(4, &[ - /// EdgeInput { left: 0, right: 1, weight: 5 }, - /// EdgeInput { left: 0, right: 3, weight: 3 }, - /// ]).expect("valid input"); + /// let report = adjacency_report( + /// 4, + /// &[ + /// EdgeInput { + /// left: 0, + /// right: 1, + /// weight: 5, + /// }, + /// EdgeInput { + /// left: 0, + /// right: 3, + /// weight: 3, + /// }, + /// ], + /// ) + /// .expect("valid input"); /// assert!(report.is_sorted()); /// ``` #[must_use] pub fn is_sorted(&self) -> bool { - self.neighbours - .iter() - .all(|bucket| bucket.windows(2).all(|pair| pair[0].0 <= pair[1].0)) + self.neighbours.iter().all(|bucket| { + bucket + .windows(2) + .all(|pair| matches!(pair, [(left, _), (right, _)] if left <= right)) + }) } } @@ -182,7 +215,8 @@ fn has_mirror( ) -> bool { debug_assert!( neighbour < neighbours.len(), - "has_mirror: neighbour index out of bounds - callers (e.g. adjacency_report) must guarantee valid indices" + "has_mirror: neighbour index out of bounds - callers (e.g. adjacency_report) must \ + guarantee valid indices" ); neighbours.get(neighbour).map_or(false, |list| { list.iter() @@ -201,9 +235,15 @@ fn has_mirror( /// ```rust /// use whitaker_common::test_support::decomposition::{EdgeInput, adjacency_report}; /// -/// let report = adjacency_report(3, &[ -/// EdgeInput { left: 0, right: 2, weight: 7 }, -/// ]).expect("valid input"); +/// let report = adjacency_report( +/// 3, +/// &[EdgeInput { +/// left: 0, +/// right: 2, +/// weight: 7, +/// }], +/// ) +/// .expect("valid input"); /// assert_eq!(report.node_count(), 3); /// ``` /// @@ -263,6 +303,8 @@ pub(crate) fn validate_edges( #[cfg(test)] mod tests { + //! Tests for adjacency-report validation of decomposition edge inputs. + use super::{AdjacencyError, EdgeInput, adjacency_report}; #[test] diff --git a/common/src/test_support/decomposition_label_propagation.rs b/common/src/test_support/decomposition_label_propagation.rs index 774b3da00..cc37f947e 100644 --- a/common/src/test_support/decomposition_label_propagation.rs +++ b/common/src/test_support/decomposition_label_propagation.rs @@ -1,5 +1,6 @@ //! Observable label-propagation seams for decomposition advice tests. +use super::adjacency::{AdjacencyError, EdgeInput, validate_edges}; use crate::decomposition_advice::{ MethodProfileBuilder, build_feature_vector, community::{ @@ -8,16 +9,12 @@ use crate::decomposition_advice::{ }, }; -use super::adjacency::{AdjacencyError, EdgeInput, validate_edges}; - /// Observable label-propagation results for declarative graph input. /// /// # Examples /// /// ```rust -/// use whitaker_common::test_support::decomposition::{ -/// EdgeInput, label_propagation_report, -/// }; +/// use whitaker_common::test_support::decomposition::{EdgeInput, label_propagation_report}; /// /// let report = label_propagation_report( /// &["gamma", "alpha", "beta"], @@ -54,13 +51,13 @@ impl LabelPropagationReport { /// Returns the number of propagation passes performed. #[must_use] - pub fn iteration_count(&self) -> usize { + pub const fn iteration_count(&self) -> usize { self.runtime.iteration_count } /// Returns `true` when the graph contains at least one active node. #[must_use] - pub fn has_active_nodes(&self) -> bool { + pub const fn has_active_nodes(&self) -> bool { self.has_active_nodes } diff --git a/common/src/test_support/decomposition_vector_algebra.rs b/common/src/test_support/decomposition_vector_algebra.rs index 8dc8d6c79..8b2079af8 100644 --- a/common/src/test_support/decomposition_vector_algebra.rs +++ b/common/src/test_support/decomposition_vector_algebra.rs @@ -1,7 +1,9 @@ //! Observable vector-algebra seams for decomposition advice tests. -use crate::MethodProfile; -use crate::decomposition_advice::{build_feature_vector, dot_product}; +use crate::{ + MethodProfile, + decomposition_advice::{build_feature_vector, dot_product}, +}; /// Observable runtime vector-algebra results for two methods. /// @@ -43,7 +45,11 @@ impl MethodVectorAlgebraReport { /// This is the dot product of the left and right method vectors. /// /// ```rust - /// use whitaker_common::test_support::decomposition::{MethodInput, method_vector_algebra, profile}; + /// use whitaker_common::test_support::decomposition::{ + /// MethodInput, + /// method_vector_algebra, + /// profile, + /// }; /// /// let left = profile(MethodInput { /// name: "parse_tokens", @@ -64,7 +70,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.left_dot_right(), 40); /// ``` #[must_use] - pub fn left_dot_right(self) -> u64 { + pub const fn left_dot_right(self) -> u64 { self.left_dot_right } @@ -73,7 +79,11 @@ impl MethodVectorAlgebraReport { /// This is the dot product of the right and left method vectors. /// /// ```rust - /// use whitaker_common::test_support::decomposition::{MethodInput, method_vector_algebra, profile}; + /// use whitaker_common::test_support::decomposition::{ + /// MethodInput, + /// method_vector_algebra, + /// profile, + /// }; /// /// let left = profile(MethodInput { /// name: "parse_tokens", @@ -94,7 +104,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.right_dot_left(), 40); /// ``` #[must_use] - pub fn right_dot_left(self) -> u64 { + pub const fn right_dot_left(self) -> u64 { self.right_dot_left } @@ -103,7 +113,11 @@ impl MethodVectorAlgebraReport { /// This is the squared L2 norm of the left method vector. /// /// ```rust - /// use whitaker_common::test_support::decomposition::{MethodInput, method_vector_algebra, profile}; + /// use whitaker_common::test_support::decomposition::{ + /// MethodInput, + /// method_vector_algebra, + /// profile, + /// }; /// /// let left = profile(MethodInput { /// name: "parse_tokens", @@ -124,7 +138,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.left_norm_squared(), 44); /// ``` #[must_use] - pub fn left_norm_squared(self) -> u64 { + pub const fn left_norm_squared(self) -> u64 { self.left_norm_squared } @@ -133,7 +147,11 @@ impl MethodVectorAlgebraReport { /// This is the squared L2 norm of the right method vector. /// /// ```rust - /// use whitaker_common::test_support::decomposition::{MethodInput, method_vector_algebra, profile}; + /// use whitaker_common::test_support::decomposition::{ + /// MethodInput, + /// method_vector_algebra, + /// profile, + /// }; /// /// let left = profile(MethodInput { /// name: "parse_tokens", @@ -154,7 +172,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.right_norm_squared(), 44); /// ``` #[must_use] - pub fn right_norm_squared(self) -> u64 { + pub const fn right_norm_squared(self) -> u64 { self.right_norm_squared } } diff --git a/common/src/test_support/fixtures.rs b/common/src/test_support/fixtures.rs index 292b23df0..9f3408e3e 100644 --- a/common/src/test_support/fixtures.rs +++ b/common/src/test_support/fixtures.rs @@ -4,9 +4,7 @@ //! supporting assets into a temporary workspace so UI harnesses only need to //! focus on executing the lint runner. -use std::fs; -use std::io; -use std::path::Path; +use std::{fs, io, path::Path}; const MAX_DIRECTORY_DEPTH: usize = 64; @@ -19,10 +17,10 @@ const MAX_DIRECTORY_DEPTH: usize = 64; /// # Examples /// /// ``` -/// use whitaker_common::test_support::fixtures::copy_fixture; -/// use std::fs; -/// use std::path::PathBuf; +/// use std::{fs, path::PathBuf}; +/// /// use tempfile::tempdir; +/// use whitaker_common::test_support::fixtures::copy_fixture; /// /// # fn demo() -> std::io::Result<()> { /// let fixtures = tempdir()?; @@ -34,6 +32,12 @@ const MAX_DIRECTORY_DEPTH: usize = 64; /// # Ok(()) /// # } /// ``` +/// +/// # Errors +/// +/// Returns an [`io::Error`] when the fixture or stderr path lacks a usable +/// file name, or when copying the fixture, its `.stderr` expectation, or a +/// support directory fails. pub fn copy_fixture(fixture_root: &Path, source: &Path, destination_root: &Path) -> io::Result<()> { let file_name = source .file_name() @@ -71,9 +75,10 @@ pub fn copy_fixture(fixture_root: &Path, source: &Path, destination_root: &Path) /// # Examples /// /// ``` -/// use whitaker_common::test_support::fixtures::copy_directory; /// use std::fs; +/// /// use tempfile::tempdir; +/// use whitaker_common::test_support::fixtures::copy_directory; /// /// # fn demo() -> std::io::Result<()> { /// let source = tempdir()?; @@ -84,6 +89,12 @@ pub fn copy_fixture(fixture_root: &Path, source: &Path, destination_root: &Path) /// # Ok(()) /// # } /// ``` +/// +/// # Errors +/// +/// Returns an [`io::Error`] when `source` is not a directory, when a symlink +/// is encountered, when nesting exceeds `MAX_DIRECTORY_DEPTH`, or when any +/// underlying filesystem operation fails. pub fn copy_directory(source: &Path, destination: &Path) -> io::Result<()> { copy_directory_with_depth(source, destination, MAX_DIRECTORY_DEPTH) } @@ -102,8 +113,8 @@ fn copy_directory_with_depth( ensure_not_symlink(source, metadata.file_type())?; fs::create_dir_all(destination)?; - for entry in fs::read_dir(source)? { - let entry = entry?; + for entry_result in fs::read_dir(source)? { + let entry = entry_result?; let entry_path = entry.path(); let file_type = entry_path.symlink_metadata()?.file_type(); @@ -158,12 +169,17 @@ fn depth_limit_error(path: &Path) -> io::Error { #[cfg(test)] mod tests { - use super::*; - use std::fs; - use std::io; - use std::path::{Path, PathBuf}; + //! Tests for fixture staging helpers used by lint test suites. + + use std::{ + fs, io, + path::{Path, PathBuf}, + }; + use tempfile::{TempDir, tempdir}; + use super::*; + #[test] fn copy_fixture_clones_support_assets() { let root = tempdir().expect("fixture root"); @@ -211,28 +227,29 @@ mod tests { fn setup_copy_fixture_test( with_stderr: bool, with_support: bool, - ) -> (TempDir, PathBuf, TempDir) { - let root = tempdir().expect("fixture root"); + ) -> io::Result<(TempDir, PathBuf, TempDir)> { + let root = tempdir()?; let fixture = root.path().join("case.rs"); - fs::write(&fixture, "fn main() {}").expect("fixture file"); + fs::write(&fixture, "fn main() {}")?; if with_stderr { - fs::write(root.path().join("case.stderr"), "stderr").expect("stderr file"); + fs::write(root.path().join("case.stderr"), "stderr")?; } if with_support { let support_dir = root.path().join("case"); - fs::create_dir_all(&support_dir).expect("support dir"); - fs::write(support_dir.join("helper.rs"), "fn helper() {}").expect("support helper"); + fs::create_dir_all(&support_dir)?; + fs::write(support_dir.join("helper.rs"), "fn helper() {}")?; } - let destination = tempdir().expect("destination root"); - (root, fixture, destination) + let destination = tempdir()?; + Ok((root, fixture, destination)) } #[test] fn copy_fixture_without_stderr_succeeds() { - let (root, fixture, destination) = setup_copy_fixture_test(false, false); + let (root, fixture, destination) = + setup_copy_fixture_test(false, false).expect("stage fixture without stderr"); copy_fixture(root.path(), &fixture, destination.path()) .expect("copy succeeds without stderr"); @@ -243,7 +260,8 @@ mod tests { #[test] fn copy_fixture_without_support_directory_succeeds() { - let (root, fixture, destination) = setup_copy_fixture_test(true, false); + let (root, fixture, destination) = + setup_copy_fixture_test(true, false).expect("stage fixture with stderr"); copy_fixture(root.path(), &fixture, destination.path()) .expect("copy succeeds without support dir"); diff --git a/common/src/test_support/mod.rs b/common/src/test_support/mod.rs index 37676df8f..1cf75e5dd 100644 --- a/common/src/test_support/mod.rs +++ b/common/src/test_support/mod.rs @@ -6,40 +6,49 @@ //! //! ## Available helpers //! -//! - [`fixtures`]: Copies UI fixtures (source files, `.stderr` expectations and -//! support directories) into isolated workspaces for dylint UI harnesses. -//! - [`decomposition`]: Reusable decomposition-advice fixtures for unit and -//! behaviour tests. -//! - [`env_test_guard`]: Serializes tests that temporarily mutate process-wide -//! environment variables. -//! - [`ui`]: Discovers fixtures, prepares isolated workspaces, and runs dylint -//! UI tests with consistent panic handling. -//! - [`LocaleOverride`]: Temporarily mutates `DYLINT_LOCALE` so locale-sensitive -//! tests can execute without leaking global state between cases. +//! - [`fixtures`]: Copies UI fixtures (source files, `.stderr` expectations and support +//! directories) into isolated workspaces for dylint UI harnesses. +//! - [`decomposition`]: Reusable decomposition-advice fixtures for unit and behaviour tests. +//! - [`env_test_guard`]: Serializes tests that temporarily mutate process-wide environment +//! variables. +//! - [`ui`]: Discovers fixtures, prepares isolated workspaces, and runs dylint UI tests with +//! consistent panic handling. +//! - [`EnvVarGuard`]: Sets or removes one environment variable and restores +//! its prior state on drop. +//! - [`with_locale`], [`with_env_var`], and [`with_env_var_removed`]: Scope temporary environment +//! mutations (such as `DYLINT_LOCALE` overrides) to a callback so tests cannot leak global state +//! between cases. pub mod decomposition; pub mod fixtures; pub mod ui; +use std::{ + ffi::{OsStr, OsString}, + sync::{Mutex, MutexGuard, OnceLock, PoisonError}, +}; + pub use fixtures::{copy_directory, copy_fixture}; pub use ui::{ FixtureEnvironment, discover_fixtures, prepare_fixture, read_directory_config, read_fixture_config, resolve_fixture_config, run_fixtures_with, run_test_runner, }; -use std::ffi::{OsStr, OsString}; -use std::sync::{Mutex, MutexGuard, OnceLock}; - /// Serializes tests that mutate process-wide environment variables. /// /// Use this guard around helpers such as `temp_env::with_var` or /// `temp_env::with_vars_unset` when the test would otherwise race with other /// cases changing the same global process state. +/// +/// The mutex guards `()`, so a poisoned lock carries no corrupted state: it +/// only records that some earlier test panicked while holding the +/// serialization token. Recovering the guard is therefore sound, and keeps one +/// failing test from cascading into every later one. pub fn env_test_guard() -> MutexGuard<'static, ()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())) .lock() - .unwrap_or_else(|error| panic!("expected environment test lock: {error}")) + .unwrap_or_else(PoisonError::into_inner) } /// Guard that sets one environment variable and restores its prior state. @@ -124,100 +133,69 @@ impl Drop for EnvVarGuard { } } } -/// Guard that overrides `DYLINT_LOCALE` for the lifetime of the instance. + +/// Runs `callback` with one environment variable temporarily set. /// -/// The guard captures any existing value and restores it when dropped. The -/// mutation itself must be executed under a serialized test harness (for -/// example via the `serial_test::serial` attribute) to ensure the unsafe -/// environment access remains race-free. +/// The mutation is scoped to the callback and the prior value is restored +/// afterwards, even on panic. Serialization is provided by `temp_env`'s +/// re-entrant global lock, so nested scoped mutations from the same thread +/// do not deadlock. /// /// # Examples /// -/// ```ignore -/// use whitaker_common::test_support::LocaleOverride; -/// use serial_test::serial; +/// ```rust +/// use whitaker_common::test_support::with_env_var; /// -/// #[test] -/// #[serial] -/// fn ui_runs_in_welsh_locale() { -/// let _guard = LocaleOverride::set("cy"); -/// // Execute lint UI harness here. -/// } +/// with_env_var("WHITAKER_TEST_ENV_VAR", "enabled", || { +/// assert_eq!( +/// std::env::var("WHITAKER_TEST_ENV_VAR").expect("test env var should be set"), +/// "enabled", +/// ); +/// }); /// ``` -pub struct LocaleOverride { - previous: Option, +pub fn with_env_var(key: &str, value: impl AsRef, callback: impl FnOnce() -> R) -> R { + temp_env::with_var(key, Some(value.as_ref()), callback) } -impl LocaleOverride { - /// Sets `DYLINT_LOCALE` to `locale`, returning a guard that will restore - /// the prior value (if any) when dropped. - pub fn set(locale: &str) -> Self { - let previous = std::env::var_os("DYLINT_LOCALE"); - // SAFETY: Callers must serialize the surrounding test using a - // synchronization primitive such as the `serial_test::serial` - // attribute. The guard is thread-local and dropped before another - // serialized test begins, so no two threads mutate the environment - // concurrently. - unsafe { - std::env::set_var("DYLINT_LOCALE", locale); - } - Self { previous } - } - - /// Removes `DYLINT_LOCALE`, returning a guard that reinstates the prior - /// value (if any) when dropped. - /// - /// # Examples - /// - /// ```ignore - /// use whitaker_common::test_support::LocaleOverride; - /// use serial_test::serial; - /// use std::ffi::OsString; - /// - /// #[test] - /// #[serial] - /// fn clears_then_restores_locale() { - /// unsafe { - /// std::env::set_var("DYLINT_LOCALE", "cy"); - /// } - /// { - /// let _guard = LocaleOverride::clear(); - /// assert!(std::env::var_os("DYLINT_LOCALE").is_none()); - /// } - /// assert_eq!( - /// std::env::var_os("DYLINT_LOCALE"), - /// Some(OsString::from("cy")) - /// ); - /// } - /// ``` - pub fn clear() -> Self { - let previous = std::env::var_os("DYLINT_LOCALE"); - // SAFETY: Callers must serialize the surrounding test using a - // synchronization primitive such as the `serial_test::serial` - // attribute. Clearing the environment therefore cannot race with other - // threads. - unsafe { - std::env::remove_var("DYLINT_LOCALE"); - } - Self { previous } - } +/// Runs `callback` with one environment variable temporarily removed. +/// +/// The prior value (if any) is restored after the callback completes or +/// panics. +/// +/// # Examples +/// +/// ```rust +/// use whitaker_common::test_support::with_env_var_removed; +/// +/// with_env_var_removed("WHITAKER_REMOVED_TEST_ENV_VAR", || { +/// assert!(std::env::var_os("WHITAKER_REMOVED_TEST_ENV_VAR").is_none()); +/// }); +/// ``` +pub fn with_env_var_removed(key: &str, callback: impl FnOnce() -> R) -> R { + temp_env::with_var_unset(key, callback) } -impl Drop for LocaleOverride { - fn drop(&mut self) { - if let Some(value) = &self.previous { - // SAFETY: By construction the guard only lives within a serialized - // test, so restoring the prior value cannot race with another - // thread mutating the environment. - unsafe { - std::env::set_var("DYLINT_LOCALE", value); - } - } else { - // SAFETY: Serialized execution also guarantees removal has no - // concurrent callers. - unsafe { - std::env::remove_var("DYLINT_LOCALE"); - } - } +/// Runs `callback` with `DYLINT_LOCALE` overridden. +/// +/// `Some(locale)` sets the variable for the duration of the callback; +/// `None` removes it. Any prior value is restored afterwards, so +/// locale-sensitive tests cannot leak global state between cases. +/// +/// # Examples +/// +/// ```rust +/// use whitaker_common::test_support::with_locale; +/// +/// with_locale(Some("cy"), || { +/// assert_eq!( +/// std::env::var("DYLINT_LOCALE").expect("locale should be set"), +/// "cy", +/// ); +/// }); +/// ``` +pub fn with_locale(locale: Option<&str>, callback: impl FnOnce() -> R) -> R { + match locale { + Some(locale_value) => with_env_var("DYLINT_LOCALE", locale_value, callback), + None => with_env_var_removed("DYLINT_LOCALE", callback), } } diff --git a/common/src/test_support/ui.rs b/common/src/test_support/ui.rs index 690c1e379..5773a105d 100644 --- a/common/src/test_support/ui.rs +++ b/common/src/test_support/ui.rs @@ -4,14 +4,17 @@ //! clone them into an isolated workspace, and execute each case via //! `dylint_testing` while capturing panics into deterministic error messages. -use crate::test_support::copy_fixture; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + use camino::Utf8Path; use glob::glob; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; use tempfile::{TempDir, tempdir}; +use crate::test_support::copy_fixture; + /// Temporary workspace prepared for a single UI fixture run. pub struct FixtureEnvironment { _tempdir: TempDir, @@ -21,17 +24,23 @@ pub struct FixtureEnvironment { impl FixtureEnvironment { /// Returns the root directory containing the cloned fixture files. + #[must_use] pub fn workdir(&self) -> &Path { &self.workdir } /// Moves the optional `dylint.toml` contents out of the environment. - pub fn take_config(&mut self) -> Option { + pub const fn take_config(&mut self) -> Option { self.config.take() } } /// Discovers `.rs` fixtures inside `directory`, returning the paths unsorted. +/// +/// # Errors +/// +/// Returns an [`io::Error`] when the glob pattern is invalid or a matched +/// path cannot be read. pub fn discover_fixtures(directory: &Utf8Path) -> io::Result> { let pattern = directory.join("*.rs").to_string(); let walker = glob(&pattern).map_err(|error| io::Error::other(error.to_string()))?; @@ -48,6 +57,10 @@ pub fn discover_fixtures(directory: &Utf8Path) -> io::Result> { } /// Runs fixtures discovered under `directory` using the provided `runner`. +/// +/// # Errors +/// +/// Returns the first error produced by fixture discovery or by `runner`. pub fn run_fixtures_with( crate_name: &str, directory: &Utf8Path, @@ -67,6 +80,11 @@ where } /// Copies `source` into a temporary directory, including stderr/config files. +/// +/// # Errors +/// +/// Returns an [`io::Error`] when the temporary directory cannot be created or +/// the fixture files cannot be copied or read. pub fn prepare_fixture(directory: &Utf8Path, source: &Path) -> io::Result { let tempdir = tempdir()?; copy_fixture(directory.as_std_path(), source, tempdir.path())?; @@ -79,31 +97,48 @@ pub fn prepare_fixture(directory: &Utf8Path, source: &Path) -> io::Result: "` when the +/// runner unwinds. pub fn run_test_runner(fixture_name: &str, runner: F) -> Result<(), String> where F: FnOnce(), { - std::panic::catch_unwind(std::panic::AssertUnwindSafe(runner)).map_err(|payload| match payload - .downcast::( - ) { - Ok(message) => format!("{fixture_name}: {message}"), - Err(payload) => match payload.downcast::<&'static str>() { - Ok(message) => format!("{fixture_name}: {message}"), - Err(_) => format!("{fixture_name}: dylint UI tests panicked without a message"), - }, - }) + std::panic::catch_unwind(std::panic::AssertUnwindSafe(runner)) + .map_err(|payload| format!("{fixture_name}: {}", panic_message(payload))) +} + +/// Renders a panic payload as a human-readable message. +fn panic_message(payload: Box) -> String { + match payload.downcast::() { + Ok(message) => *message, + Err(other_payload) => other_payload.downcast::<&'static str>().map_or_else( + |_| String::from("dylint UI tests panicked without a message"), + |message| (*message).to_owned(), + ), + } } /// Resolves the configuration content for `source`, preferring per-fixture files. +/// +/// # Errors +/// +/// Returns an [`io::Error`] when either configuration file cannot be read. pub fn resolve_fixture_config(directory: &Utf8Path, source: &Path) -> io::Result> { - if let Some(config) = read_fixture_config(source)? { - Ok(Some(config)) - } else { - read_directory_config(directory) - } + read_fixture_config(source)?.map_or_else( + || read_directory_config(directory), + |config| Ok(Some(config)), + ) } /// Loads `case.dylint.toml` for a fixture when present. +/// +/// # Errors +/// +/// Returns an [`io::Error`] when the fixture has no usable file stem or the +/// configuration file cannot be read. pub fn read_fixture_config(source: &Path) -> io::Result> { let stem = source .file_stem() @@ -119,6 +154,11 @@ pub fn read_fixture_config(source: &Path) -> io::Result> { } /// Loads `ui/dylint.toml` style directory-level configuration when present. +/// +/// # Errors +/// +/// Returns an [`io::Error`] when the configuration file exists but cannot be +/// read. pub fn read_directory_config(directory: &Utf8Path) -> io::Result> { let path = directory.as_std_path().join("dylint.toml"); if path.exists() { @@ -130,12 +170,23 @@ pub fn read_directory_config(directory: &Utf8Path) -> io::Result> #[cfg(test)] mod tests { - use super::*; - use camino::Utf8PathBuf; + //! Tests for the UI test harness helpers. + use std::fs; - fn utf8_path(buf: &Path) -> Utf8PathBuf { - Utf8PathBuf::from_path_buf(buf.to_path_buf()).expect("utf8 path") + use camino::Utf8PathBuf; + + use super::*; + + /// Converts a [`Path`] into a [`Utf8PathBuf`] for test data. + /// + /// This is a macro rather than a helper function so the fallible + /// conversion is inlined into the calling `#[test]` body, where a + /// non-UTF-8 temporary directory is the test verdict. + macro_rules! utf8_path { + ($path:expr) => { + Utf8PathBuf::from_path_buf(::std::path::Path::to_path_buf($path)).expect("utf8 path") + }; } #[test] @@ -143,21 +194,21 @@ mod tests { let dir = tempdir().expect("fixture directory"); fs::write(dir.path().join("b.rs"), "fn main() {}").expect("write first fixture"); fs::write(dir.path().join("a.rs"), "fn main() {}").expect("write second fixture"); - let directory = utf8_path(dir.path()); + let directory = utf8_path!(dir.path()); let mut visited = Vec::new(); run_fixtures_with("crate", &directory, |_, _, source| { let name = source .file_name() .and_then(|value| value.to_str()) - .ok_or_else(|| "utf8 file name".to_string())? + .ok_or_else(|| "utf8 file name".to_owned())? .to_owned(); visited.push(name); Ok(()) }) .expect("fixtures run"); - assert_eq!(visited, vec!["a.rs".to_string(), "b.rs".to_string()]); + assert_eq!(visited, vec!["a.rs".to_owned(), "b.rs".to_owned()]); } #[test] @@ -165,19 +216,20 @@ mod tests { let dir = tempdir().expect("fixture directory"); fs::write(dir.path().join("first.rs"), "").expect("first fixture"); fs::write(dir.path().join("second.txt"), "").expect("second fixture"); - let directory = utf8_path(dir.path()); + let directory = utf8_path!(dir.path()); let mut fixtures = discover_fixtures(&directory).expect("discover fixtures"); fixtures.sort(); assert_eq!(fixtures.len(), 1); - assert!(fixtures[0].ends_with("first.rs")); + let discovered = fixtures.first().expect("one fixture should be discovered"); + assert!(discovered.ends_with("first.rs")); } #[test] fn discover_fixtures_returns_empty_directory() { let dir = tempdir().expect("fixture directory"); - let directory = utf8_path(dir.path()); + let directory = utf8_path!(dir.path()); let fixtures = discover_fixtures(&directory).expect("discover fixtures"); @@ -199,7 +251,7 @@ mod tests { #[test] fn read_directory_config_loads_global_file() { let dir = tempdir().expect("fixture directory"); - let directory = utf8_path(dir.path()); + let directory = utf8_path!(dir.path()); fs::write(directory.as_std_path().join("dylint.toml"), "max_lines = 5") .expect("global config"); @@ -210,7 +262,7 @@ mod tests { #[test] fn resolve_fixture_config_prefers_fixture_specific_file() { let dir = tempdir().expect("fixture directory"); - let directory = utf8_path(dir.path()); + let directory = utf8_path!(dir.path()); let fixture = directory.as_std_path().join("case.rs"); fs::write(&fixture, "").expect("fixture"); fs::write( diff --git a/common/tests/brain_trait_evaluation_behaviour.rs b/common/tests/brain_trait_evaluation_behaviour.rs index 9ebbaa48d..b7b35899a 100644 --- a/common/tests/brain_trait_evaluation_behaviour.rs +++ b/common/tests/brain_trait_evaluation_behaviour.rs @@ -1,13 +1,17 @@ //! Behaviour-driven coverage for brain trait threshold evaluation. +use std::cell::{Cell, RefCell}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; -use whitaker_common::brain_trait_metrics::evaluation::{ - BrainTraitDiagnostic, BrainTraitDisposition, BrainTraitThresholds, BrainTraitThresholdsBuilder, - evaluate_brain_trait, format_primary_message, +use whitaker_common::brain_trait_metrics::{ + TraitMetrics, TraitMetricsBuilder, + evaluation::{ + BrainTraitDiagnostic, BrainTraitDisposition, BrainTraitThresholds, + BrainTraitThresholdsBuilder, evaluate_brain_trait, format_primary_message, + }, }; -use whitaker_common::brain_trait_metrics::{TraitMetrics, TraitMetricsBuilder}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct EvaluationWorld { @@ -46,8 +50,14 @@ fn add_distributed_defaults(builder: &mut TraitMetricsBuilder, count: usize, cc_ if count == 0 { return; } - let base_cc = cc_sum / count; - let remainder = cc_sum % count; + // Derive the per-method base and remainder by repeated subtraction so + // the test avoids the disallowed `/` and `%` operators. + let mut base_cc = 0; + let mut remainder = cc_sum; + while remainder >= count { + base_cc += 1; + remainder -= count; + } for i in 0..count { let cc = base_cc + if i == count - 1 { remainder } else { 0 }; builder.add_default_method(format!("default_{i}"), cc, false); @@ -89,6 +99,7 @@ impl EvaluationWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> EvaluationWorld { EvaluationWorld::default() @@ -179,15 +190,17 @@ fn then_disposition_deny(world: &EvaluationWorld) { #[then("the primary message contains {text}")] fn then_primary_message_contains(world: &EvaluationWorld, text: String) -> Result<(), String> { - let msg = world.primary_message.borrow(); - let msg = msg + let message_ref = world.primary_message.borrow(); + let message = message_ref .as_deref() .ok_or("primary message must be formatted first")?; - assert!( - msg.contains(&text), - "expected primary message to contain '{text}', got: {msg}" - ); - Ok(()) + if message.contains(&text) { + Ok(()) + } else { + Err(format!( + "expected primary message to contain '{text}', got: {message}" + )) + } } // Scenario indices must match their declaration order in diff --git a/common/tests/brain_trait_metrics_behaviour.rs b/common/tests/brain_trait_metrics_behaviour.rs index e8839f4ee..8e630f393 100644 --- a/common/tests/brain_trait_metrics_behaviour.rs +++ b/common/tests/brain_trait_metrics_behaviour.rs @@ -1,9 +1,11 @@ //! Behaviour-driven coverage for brain trait metric collection. +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; use whitaker_common::brain_trait_metrics::{TraitMetrics, TraitMetricsBuilder}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Debug)] enum PendingTraitItem { @@ -24,14 +26,15 @@ struct TraitMetricsWorld { metrics: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> TraitMetricsWorld { TraitMetricsWorld::default() } fn with_metrics(world: &TraitMetricsWorld, assert_fn: impl FnOnce(&TraitMetrics)) { - let metrics = world.metrics.borrow(); - match metrics.as_ref() { + let metrics_ref = world.metrics.borrow(); + match metrics_ref.as_ref() { Some(metrics) => assert_fn(metrics), None => panic!("metrics must be built before running assertions"), } @@ -124,35 +127,55 @@ fn when_metrics_are_built(world: &TraitMetricsWorld) { #[then("total trait items is {count}")] fn then_total_trait_items(world: &TraitMetricsWorld, count: usize) { with_metrics(world, |metrics| { - assert_eq!(metrics.total_item_count(), count); + assert_eq!( + metrics.total_item_count(), + count, + "expected total trait item count of {count}" + ); }); } #[then("required method count is {count}")] fn then_required_method_count(world: &TraitMetricsWorld, count: usize) { with_metrics(world, |metrics| { - assert_eq!(metrics.required_method_count(), count); + assert_eq!( + metrics.required_method_count(), + count, + "expected required method count of {count}" + ); }); } #[then("default method count is {count}")] fn then_default_method_count(world: &TraitMetricsWorld, count: usize) { with_metrics(world, |metrics| { - assert_eq!(metrics.default_method_count(), count); + assert_eq!( + metrics.default_method_count(), + count, + "expected default method count of {count}" + ); }); } #[then("default method CC sum is {sum}")] fn then_default_method_cc_sum(world: &TraitMetricsWorld, sum: usize) { with_metrics(world, |metrics| { - assert_eq!(metrics.default_method_cc_sum(), sum); + assert_eq!( + metrics.default_method_cc_sum(), + sum, + "expected default method CC sum of {sum}" + ); }); } #[then("implementor burden is {count}")] fn then_implementor_burden(world: &TraitMetricsWorld, count: usize) { with_metrics(world, |metrics| { - assert_eq!(metrics.implementor_burden(), count); + assert_eq!( + metrics.implementor_burden(), + count, + "expected implementor burden of {count}" + ); }); } diff --git a/common/tests/brain_type_evaluation_behaviour.rs b/common/tests/brain_type_evaluation_behaviour.rs index 4602ef4d7..89b5f8237 100644 --- a/common/tests/brain_type_evaluation_behaviour.rs +++ b/common/tests/brain_type_evaluation_behaviour.rs @@ -1,13 +1,17 @@ //! Behaviour-driven coverage for brain type threshold evaluation. +use std::cell::{Cell, RefCell}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; -use whitaker_common::brain_type_metrics::evaluation::{ - BrainTypeDiagnostic, BrainTypeDisposition, BrainTypeThresholds, BrainTypeThresholdsBuilder, - evaluate_brain_type, format_primary_message, +use whitaker_common::brain_type_metrics::{ + MethodMetrics, TypeMetrics, TypeMetricsBuilder, + evaluation::{ + BrainTypeDiagnostic, BrainTypeDisposition, BrainTypeThresholds, BrainTypeThresholdsBuilder, + evaluate_brain_type, format_primary_message, + }, }; -use whitaker_common::brain_type_metrics::{MethodMetrics, TypeMetrics, TypeMetricsBuilder}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct EvaluationWorld { @@ -85,6 +89,7 @@ impl EvaluationWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> EvaluationWorld { EvaluationWorld::default() @@ -184,13 +189,13 @@ fn then_disposition_deny(world: &EvaluationWorld) { reason = "primary message is required for this behaviour test" )] fn then_primary_message_contains(world: &EvaluationWorld, text: String) { - let msg = world.primary_message.borrow(); - let msg = msg + let message_ref = world.primary_message.borrow(); + let message = message_ref .as_deref() .expect("primary message must be formatted first"); assert!( - msg.contains(&text), - "expected primary message to contain '{text}', got: {msg}" + message.contains(&text), + "expected primary message to contain '{text}', got: {message}" ); } diff --git a/common/tests/brain_type_metrics_behaviour.rs b/common/tests/brain_type_metrics_behaviour.rs index f4ee76ee4..c01213f3a 100644 --- a/common/tests/brain_type_metrics_behaviour.rs +++ b/common/tests/brain_type_metrics_behaviour.rs @@ -1,12 +1,14 @@ //! Behaviour-driven coverage for brain type metric collection. +use std::cell::{Cell, RefCell}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; use whitaker_common::brain_type_metrics::{ ForeignReferenceSet, MethodMetrics, TypeMetricsBuilder, brain_methods, foreign_reach_count, weighted_methods_count, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct MetricsWorld { @@ -47,6 +49,7 @@ impl Default for MetricsWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> MetricsWorld { MetricsWorld::default() @@ -66,7 +69,11 @@ fn record_foreign_ref(world: &MetricsWorld, path: &str, is_from_expansion: bool) } fn assert_brain_count(world: &MetricsWorld, n: usize) { - assert_eq!(world.type_metrics_brain_count.get(), Some(n)); + assert_eq!( + world.type_metrics_brain_count.get(), + Some(n), + "expected {n} brain methods to be recorded" + ); } // --- Given steps --- diff --git a/common/tests/cognitive_complexity_behaviour.rs b/common/tests/cognitive_complexity_behaviour.rs index 16f9846eb..9b0876384 100644 --- a/common/tests/cognitive_complexity_behaviour.rs +++ b/common/tests/cognitive_complexity_behaviour.rs @@ -1,9 +1,11 @@ //! Behaviour-driven coverage for cognitive complexity computation. +use std::cell::{Cell, RefCell}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; use whitaker_common::brain_type_metrics::cognitive_complexity::CognitiveComplexityBuilder; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct CcWorld { @@ -20,6 +22,7 @@ impl Default for CcWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> CcWorld { CcWorld::default() @@ -28,11 +31,16 @@ fn world() -> CcWorld { // --- Helpers --- /// Borrows the builder mutably and applies a closure to it. -fn with_builder(world: &CcWorld, f: impl FnOnce(&mut CognitiveComplexityBuilder)) { +fn with_builder( + world: &CcWorld, + f: impl FnOnce(&mut CognitiveComplexityBuilder), +) -> Result<(), String> { let mut slot = world.builder.borrow_mut(); - f(slot + let builder = slot .as_mut() - .unwrap_or_else(|| panic!("builder already consumed"))); + .ok_or_else(|| String::from("builder already consumed"))?; + f(builder); + Ok(()) } // --- Given steps --- @@ -44,55 +52,59 @@ fn given_new_builder(world: &CcWorld) { } #[given("a structural increment not from expansion")] -fn given_structural_not_expanded(world: &CcWorld) { - with_builder(world, |b| b.record_structural_increment(false)); +fn given_structural_not_expanded(world: &CcWorld) -> Result<(), String> { + with_builder(world, |b| b.record_structural_increment(false)) } #[given("a structural increment from expansion")] -fn given_structural_expanded(world: &CcWorld) { - with_builder(world, |b| b.record_structural_increment(true)); +fn given_structural_expanded(world: &CcWorld) -> Result<(), String> { + with_builder(world, |b| b.record_structural_increment(true)) } #[given("a nesting increment not from expansion")] -fn given_nesting_not_expanded(world: &CcWorld) { - with_builder(world, |b| b.record_nesting_increment(false)); +fn given_nesting_not_expanded(world: &CcWorld) -> Result<(), String> { + with_builder(world, |b| b.record_nesting_increment(false)) } #[given("a fundamental increment not from expansion")] -fn given_fundamental_not_expanded(world: &CcWorld) { - with_builder(world, |b| b.record_fundamental_increment(false)); +fn given_fundamental_not_expanded(world: &CcWorld) -> Result<(), String> { + with_builder(world, |b| b.record_fundamental_increment(false)) } #[given("a fundamental increment from expansion")] -fn given_fundamental_expanded(world: &CcWorld) { - with_builder(world, |b| b.record_fundamental_increment(true)); +fn given_fundamental_expanded(world: &CcWorld) -> Result<(), String> { + with_builder(world, |b| b.record_fundamental_increment(true)) } #[given("nesting is pushed not from expansion")] -fn given_push_nesting_not_expanded(world: &CcWorld) { - with_builder(world, |b| b.push_nesting(false)); +fn given_push_nesting_not_expanded(world: &CcWorld) -> Result<(), String> { + with_builder(world, |b| b.push_nesting(false)) } #[given("nesting is pushed from expansion")] -fn given_push_nesting_expanded(world: &CcWorld) { - with_builder(world, |b| b.push_nesting(true)); +fn given_push_nesting_expanded(world: &CcWorld) -> Result<(), String> { + with_builder(world, |b| b.push_nesting(true)) } #[given("nesting is popped")] -fn given_pop_nesting(world: &CcWorld) { - with_builder(world, |b| b.pop_nesting()); +fn given_pop_nesting(world: &CcWorld) -> Result<(), String> { + with_builder( + world, + whitaker_common::CognitiveComplexityBuilder::pop_nesting, + ) } // --- When steps --- -#[when("the complexity is finalised")] -fn when_finalised(world: &CcWorld) { +#[when("the complexity is finalized")] +fn when_finalized(world: &CcWorld) -> Result<(), String> { let builder = world .builder .borrow_mut() .take() - .unwrap_or_else(|| panic!("builder already consumed")); + .ok_or_else(|| String::from("builder already consumed"))?; world.score_result.set(Some(builder.build())); + Ok(()) } // --- Then steps --- diff --git a/common/tests/complexity_signal_behaviour.rs b/common/tests/complexity_signal_behaviour.rs index ad05d2520..7091f462f 100644 --- a/common/tests/complexity_signal_behaviour.rs +++ b/common/tests/complexity_signal_behaviour.rs @@ -1,11 +1,13 @@ //! Behaviour-driven coverage for per-line complexity signal building and smoothing. +use std::cell::{Cell, RefCell}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; use whitaker_common::complexity_signal::{ LineSegment, SignalBuildError, SmoothingError, rasterize_signal, smooth_moving_average, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct SignalWorld { @@ -97,6 +99,7 @@ impl SignalWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> SignalWorld { SignalWorld::default() @@ -106,7 +109,11 @@ fn world() -> SignalWorld { /// /// The feature text uses values like `0.0, 1.0, 2.0`. Whitespace is ignored and /// empty segments are skipped. -fn parse_f64_list(values: &str) -> Vec { +/// +/// # Errors +/// +/// Returns a description of the first segment that is not a valid `f64`. +fn parse_f64_list(values: &str) -> Result, String> { values .split(',') .map(str::trim) @@ -114,15 +121,41 @@ fn parse_f64_list(values: &str) -> Vec { .map(|chunk| { chunk .parse::() - .unwrap_or_else(|error| panic!("failed to parse `{chunk}` as f64: {error}")) + .map_err(|error| format!("failed to parse `{chunk}` as f64: {error}")) }) .collect() } +/// Maximum permitted distance between two floats in units of least precision. +const MAX_ULP_DISTANCE: u64 = 4; + +/// Maps a float to a monotonically ordered integer for ULP comparison. +/// +/// Negative values have their bits inverted and non-negative values have the +/// sign bit set, so the resulting integers order the same way as the floats. +const fn monotonic_bits(value: f64) -> u64 { + let bits = value.to_bits(); + if (bits & (1 << 63)) != 0 { + !bits + } else { + bits | (1 << 63) + } +} + +/// Returns the distance between two floats in units of least precision, or +/// `None` when either value is NaN. +const fn ulp_distance(left: f64, right: f64) -> Option { + if left.is_nan() || right.is_nan() { + return None; + } + Some(monotonic_bits(left).abs_diff(monotonic_bits(right))) +} + /// Asserts that two floating-point vectors are equal within a tiny tolerance. /// /// This helper is intended for deterministic test values that may experience -/// insignificant rounding differences. +/// insignificant rounding differences. Comparison uses units of least +/// precision (ULPs), which avoids floating-point arithmetic in the test. fn assert_vec_approx_eq(actual: &[f64], expected: &[f64]) { assert_eq!( actual.len(), @@ -132,11 +165,12 @@ fn assert_vec_approx_eq(actual: &[f64], expected: &[f64]) { actual_len = actual.len() ); - for (idx, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() { - let delta = (actual - expected).abs(); + for (idx, (actual_value, expected_value)) in actual.iter().zip(expected.iter()).enumerate() { + let distance = ulp_distance(*actual_value, *expected_value); assert!( - delta <= 1e-12, - "expected element {idx} to be {expected}, got {actual} (delta {delta})", + distance.is_some_and(|ulps| ulps <= MAX_ULP_DISTANCE), + "expected element {idx} to be {expected_value}, got {actual_value} (ULP distance \ + {distance:?})", ); } } @@ -147,15 +181,17 @@ fn given_function_range(world: &SignalWorld, start: usize, end: usize) { } #[given("a segment from line {start} to {end} with value {value}")] -fn given_segment(world: &SignalWorld, start: usize, end: usize, value: f64) { +fn given_segment(world: &SignalWorld, start: usize, end: usize, value: f64) -> Result<(), String> { let segment = LineSegment::new(start, end, value) - .unwrap_or_else(|error| panic!("segment inputs should be valid: {error}")); + .map_err(|error| format!("segment inputs should be valid: {error}"))?; world.push_segment(segment); + Ok(()) } #[given("the raw signal is {values}")] -fn given_raw_signal(world: &SignalWorld, values: String) { - world.set_raw_signal(parse_f64_list(&values)); +fn given_raw_signal(world: &SignalWorld, values: String) -> Result<(), String> { + world.set_raw_signal(parse_f64_list(&values)?); + Ok(()) } #[given("the smoothing window is {window}")] @@ -174,31 +210,39 @@ fn when_smooth(world: &SignalWorld) { } #[then("the built signal equals {expected}")] -fn then_built_signal(world: &SignalWorld, expected: String) { +fn then_built_signal(world: &SignalWorld, expected: String) -> Result<(), String> { let actual = world .built_signal() - .unwrap_or_else(|error| panic!("signal build should succeed: {error}")); - let expected = parse_f64_list(&expected); - assert_vec_approx_eq(&actual, &expected); + .map_err(|error| format!("signal build should succeed: {error}"))?; + let expected_values = parse_f64_list(&expected)?; + assert_vec_approx_eq(&actual, &expected_values); + Ok(()) } #[then("signal building fails")] fn then_build_fails(world: &SignalWorld) { - assert!(world.built_signal().is_err()); + assert!( + world.built_signal().is_err(), + "expected signal building to fail" + ); } #[then("the smoothed signal equals {expected}")] -fn then_smoothed_signal(world: &SignalWorld, expected: String) { +fn then_smoothed_signal(world: &SignalWorld, expected: String) -> Result<(), String> { let actual = world .smoothed_signal() - .unwrap_or_else(|error| panic!("smoothing should succeed: {error}")); - let expected = parse_f64_list(&expected); - assert_vec_approx_eq(&actual, &expected); + .map_err(|error| format!("smoothing should succeed: {error}"))?; + let expected_values = parse_f64_list(&expected)?; + assert_vec_approx_eq(&actual, &expected_values); + Ok(()) } #[then("smoothing fails")] fn then_smoothing_fails(world: &SignalWorld) { - assert!(world.smoothed_signal().is_err()); + assert!( + world.smoothed_signal().is_err(), + "expected smoothing to fail" + ); } #[scenario(path = "tests/features/complexity_signal.feature", index = 0)] diff --git a/common/tests/context_behaviour.rs b/common/tests/context_behaviour.rs index 27e8f64d7..5d0acd154 100644 --- a/common/tests/context_behaviour.rs +++ b/common/tests/context_behaviour.rs @@ -3,11 +3,15 @@ //! Validates detection of standard test attributes (`#[rstest]`, `#[tokio::test]`) //! and custom attributes configured via the additional attribute set. +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -use whitaker_common::context::{ContextEntry, in_test_like_context_with, is_test_fn_with}; +use whitaker_common::{ + attributes::{Attribute, AttributeKind, AttributePath}, + context::{ContextEntry, in_test_like_context_with, is_test_fn_with}, +}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Debug, Default)] struct FunctionFixture { @@ -57,6 +61,7 @@ impl FunctionFixture { } } +#[allow_fixture_expansion_lints] #[fixture] fn function() -> FunctionFixture { FunctionFixture::new() @@ -67,6 +72,7 @@ struct Evaluation { in_context: bool, } +#[allow_fixture_expansion_lints] #[fixture] fn evaluation() -> Evaluation { Evaluation::default() @@ -89,7 +95,7 @@ fn given_plain(function: &FunctionFixture) { function.clear(); } -#[given("the lint recognises {path} as a test attribute")] +#[given("the lint recognizes {path} as a test attribute")] fn given_custom_attribute(function: &FunctionFixture, path: String) { function.configure_additional(&path); } @@ -111,7 +117,7 @@ fn when_check(function: &FunctionFixture) -> Evaluation { } } -#[then("the function is recognised as test-like")] +#[then("the function is recognized as test-like")] fn then_positive(evaluation: &Evaluation) { assert!(evaluation.is_test); } @@ -121,7 +127,7 @@ fn then_context_positive(evaluation: &Evaluation) { assert!(evaluation.in_context); } -#[then("the function is recognised as not test-like")] +#[then("the function is recognized as not test-like")] fn then_negative(evaluation: &Evaluation) { assert!(!evaluation.is_test); } @@ -147,6 +153,6 @@ fn scenario_ignores_plain(function: FunctionFixture, evaluation: Evaluation) { } #[scenario(path = "tests/features/context_detection.feature", index = 3)] -fn scenario_recognises_custom(function: FunctionFixture, evaluation: Evaluation) { +fn scenario_recognizes_custom(function: FunctionFixture, evaluation: Evaluation) { let _ = (function, evaluation); } diff --git a/common/tests/cosine_threshold_behaviour.rs b/common/tests/cosine_threshold_behaviour.rs index e30f1f5c8..18dcf6bb9 100644 --- a/common/tests/cosine_threshold_behaviour.rs +++ b/common/tests/cosine_threshold_behaviour.rs @@ -1,12 +1,13 @@ //! Behaviour-driven coverage for the decomposition cosine threshold. +use std::{cell::RefCell, collections::BTreeMap, str::FromStr}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use std::collections::BTreeMap; -use std::str::FromStr; -use whitaker_common::MethodProfileBuilder; -use whitaker_common::test_support::decomposition::methods_meet_cosine_threshold; +use whitaker_common::{ + MethodProfileBuilder, test_support::decomposition::methods_meet_cosine_threshold, +}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Copy, Debug)] enum MethodSide { @@ -15,7 +16,7 @@ enum MethodSide { } impl MethodSide { - fn key(self) -> &'static str { + const fn key(self) -> &'static str { match self { Self::Left => "left", Self::Right => "right", @@ -64,6 +65,7 @@ struct CosineThresholdWorld { threshold_met: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> CosineThresholdWorld { CosineThresholdWorld::default() diff --git a/common/tests/decomposition_adjacency_behaviour.rs b/common/tests/decomposition_adjacency_behaviour.rs index c0f8cd790..d257c31a3 100644 --- a/common/tests/decomposition_adjacency_behaviour.rs +++ b/common/tests/decomposition_adjacency_behaviour.rs @@ -1,11 +1,13 @@ //! Behaviour-driven coverage for decomposition adjacency construction. +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; use whitaker_common::test_support::decomposition::{ AdjacencyError, AdjacencyReport, EdgeInput, adjacency_report, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct AdjacencyWorld { @@ -14,6 +16,7 @@ struct AdjacencyWorld { result: RefCell>>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> AdjacencyWorld { AdjacencyWorld::default() @@ -46,15 +49,28 @@ fn with_report( assert_fn: impl FnOnce(&AdjacencyReport) -> Result<(), String>, ) -> Result<(), String> { let result = world.result.borrow(); - let report = result + let outcome = result .as_ref() .ok_or_else(|| String::from("adjacency must be built before assertions"))?; - match report { + match outcome { Ok(report) => assert_fn(report), Err(message) => Err(format!("expected successful build, got error: {message}")), } } +fn with_neighbours_of_node( + world: &AdjacencyWorld, + node: usize, + assert_fn: impl FnOnce(&[(usize, u64)]) -> Result<(), String>, +) -> Result<(), String> { + with_report(world, |report| { + let neighbours = report + .neighbours_of(node) + .ok_or_else(|| format!("node {node} is out of bounds"))?; + assert_fn(neighbours) + }) +} + #[then("the adjacency is symmetric")] fn then_adjacency_is_symmetric(world: &AdjacencyWorld) -> Result<(), String> { with_report(world, |report| { @@ -93,10 +109,7 @@ fn then_build_is_rejected(world: &AdjacencyWorld) -> Result<(), String> { #[then("node {node} has no neighbours")] fn then_node_has_no_neighbours(world: &AdjacencyWorld, node: usize) -> Result<(), String> { - with_report(world, |report| { - let neighbours = report - .neighbours_of(node) - .ok_or_else(|| format!("node {node} is out of bounds"))?; + with_neighbours_of_node(world, node, |neighbours| { if neighbours.is_empty() { Ok(()) } else { @@ -107,11 +120,8 @@ fn then_node_has_no_neighbours(world: &AdjacencyWorld, node: usize) -> Result<() #[then("the neighbours of node {node} are sorted")] fn then_neighbours_of_node_are_sorted(world: &AdjacencyWorld, node: usize) -> Result<(), String> { - with_report(world, |report| { - let neighbours = report - .neighbours_of(node) - .ok_or_else(|| format!("node {node} is out of bounds"))?; - let is_sorted = neighbours.windows(2).all(|pair| pair[0].0 <= pair[1].0); + with_neighbours_of_node(world, node, |neighbours| { + let is_sorted = neighbours.is_sorted_by_key(|neighbour| neighbour.0); if is_sorted { Ok(()) } else { diff --git a/common/tests/decomposition_advice_behaviour.rs b/common/tests/decomposition_advice_behaviour.rs index 42966ac6a..d2798aa28 100644 --- a/common/tests/decomposition_advice_behaviour.rs +++ b/common/tests/decomposition_advice_behaviour.rs @@ -1,13 +1,14 @@ //! Behaviour-driven coverage for decomposition advice analysis. +use std::{cell::RefCell, collections::BTreeMap}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use std::collections::BTreeMap; use whitaker_common::decomposition_advice::{ DecompositionContext, DecompositionSuggestion, MethodProfileBuilder, SubjectKind, SuggestedExtractionKind, suggest_decomposition, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Clone)] struct CsvList(Vec); @@ -28,7 +29,7 @@ impl std::str::FromStr for CsvList { .filter(|v| !v.is_empty()) .map(ToOwned::to_owned) .collect(); - Ok(CsvList(items)) + Ok(Self(items)) } } @@ -41,12 +42,13 @@ struct DecompositionWorld { suggestions: RefCell>>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> DecompositionWorld { DecompositionWorld::default() } -fn create_method_builder(world: &DecompositionWorld, method_name: &str) { +fn create_method_builder(world: &DecompositionWorld, method_name: &str) -> usize { let mut next_method_id = world.next_method_id.borrow_mut(); let method_id = *next_method_id; *next_method_id += 1; @@ -61,6 +63,17 @@ fn create_method_builder(world: &DecompositionWorld, method_name: &str) { .entry(method_name.to_owned()) .or_default() .push(method_id); + method_id +} + +/// Returns the identifier of the most recently created builder for +/// `method_name`, if any. +fn lookup_method_id(world: &DecompositionWorld, method_name: &str) -> Option { + world + .method_ids_by_name + .borrow() + .get(method_name) + .and_then(|ids| ids.last().copied()) } fn with_method_builder( @@ -68,27 +81,8 @@ fn with_method_builder( method_name: &str, update: impl FnOnce(&mut MethodProfileBuilder), ) { - let method_id = world - .method_ids_by_name - .borrow() - .get(method_name) - .and_then(|ids| ids.last().copied()); - - let method_id = match method_id { - Some(method_id) => method_id, - None => { - create_method_builder(world, method_name); - let method_id = world - .method_ids_by_name - .borrow() - .get(method_name) - .and_then(|ids| ids.last().copied()); - let Some(method_id) = method_id else { - panic!("method id must exist after creation"); - }; - method_id - } - }; + let method_id = lookup_method_id(world, method_name) + .unwrap_or_else(|| create_method_builder(world, method_name)); let mut methods = world.methods.borrow_mut(); let Some(builder) = methods.get_mut(&method_id) else { @@ -101,8 +95,8 @@ fn with_suggestions( world: &DecompositionWorld, assert_fn: impl FnOnce(&[DecompositionSuggestion]) -> Result<(), String>, ) -> Result<(), String> { - let suggestions = world.suggestions.borrow(); - let Some(suggestions) = suggestions.as_ref() else { + let suggestions_ref = world.suggestions.borrow(); + let Some(suggestions) = suggestions_ref.as_ref() else { return Err(String::from( "suggestions must be generated before running assertions", )); @@ -153,13 +147,8 @@ fn duplicate_method_names_use_distinct_builders() { } #[given("decomposition analysis for a {kind} named {name}")] -fn given_context( - world: &DecompositionWorld, - kind: SubjectKind, - name: String, -) -> Result<(), String> { +fn given_context(world: &DecompositionWorld, kind: SubjectKind, name: String) { *world.context.borrow_mut() = Some(DecompositionContext::new(name, kind)); - Ok(()) } #[given("a method named {name}")] @@ -275,8 +264,8 @@ fn then_matching_suggestion( .map(|s| format!("{}:{}:{:?}", s.label(), s.extraction_kind(), s.methods())) .collect::>(); Err(format!( - "missing {kind} suggestion labelled {label} containing methods {:?}; actual suggestions: {:?}", - expected_methods, actual + "missing {kind} suggestion labelled {label} containing methods \ + {expected_methods:?}; actual suggestions: {actual:?}" )) } }) diff --git a/common/tests/decomposition_diagnostic_notes_behaviour.rs b/common/tests/decomposition_diagnostic_notes_behaviour.rs index 966acd2dc..f701f7aa5 100644 --- a/common/tests/decomposition_diagnostic_notes_behaviour.rs +++ b/common/tests/decomposition_diagnostic_notes_behaviour.rs @@ -1,16 +1,17 @@ //! Behaviour-driven coverage for decomposition diagnostic-note rendering. +use std::{cell::RefCell, collections::BTreeMap}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use std::collections::BTreeMap; -use whitaker_common::decomposition_advice::{ - DecompositionContext, MethodProfileBuilder, SubjectKind, format_diagnostic_note, - suggest_decomposition, -}; -use whitaker_common::test_support::decomposition::{ - parser_serde_fs_fixture, transport_trait_fixture, +use whitaker_common::{ + decomposition_advice::{ + DecompositionContext, MethodProfileBuilder, SubjectKind, format_diagnostic_note, + suggest_decomposition, + }, + test_support::decomposition::{parser_serde_fs_fixture, transport_trait_fixture}, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Clone)] struct CsvList(Vec); @@ -62,12 +63,13 @@ struct DiagnosticNoteWorld { rendered_note: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> DiagnosticNoteWorld { DiagnosticNoteWorld::default() } -fn create_method_builder(world: &DiagnosticNoteWorld, method_name: &str) { +fn create_method_builder(world: &DiagnosticNoteWorld, method_name: &str) -> usize { let mut next_method_id = world.next_method_id.borrow_mut(); let method_id = *next_method_id; *next_method_id += 1; @@ -82,37 +84,33 @@ fn create_method_builder(world: &DiagnosticNoteWorld, method_name: &str) { .entry(method_name.to_owned()) .or_default() .push(method_id); + method_id +} + +/// Returns the identifier of the most recently created builder for +/// `method_name`, if any. +fn lookup_method_id(world: &DiagnosticNoteWorld, method_name: &str) -> Option { + world + .method_ids_by_name + .borrow() + .get(method_name) + .and_then(|ids| ids.last().copied()) } fn with_method_builder( world: &DiagnosticNoteWorld, method_name: &str, update: impl FnOnce(&mut MethodProfileBuilder), -) { - let method_id = world - .method_ids_by_name - .borrow() - .get(method_name) - .and_then(|ids| ids.last().copied()); - - let method_id = match method_id { - Some(method_id) => method_id, - None => { - create_method_builder(world, method_name); - world - .method_ids_by_name - .borrow() - .get(method_name) - .and_then(|ids| ids.last().copied()) - .unwrap_or_else(|| panic!("method id must exist after creation")) - } - }; +) -> Result<(), String> { + let method_id = lookup_method_id(world, method_name) + .unwrap_or_else(|| create_method_builder(world, method_name)); let mut methods = world.methods.borrow_mut(); let builder = methods .get_mut(&method_id) - .unwrap_or_else(|| panic!("method id {method_id} must exist while applying updates")); + .ok_or_else(|| format!("method id {method_id} must exist while applying updates"))?; update(builder); + Ok(()) } fn with_rendered_note( @@ -181,23 +179,27 @@ fn given_transport_fixture(world: &DiagnosticNoteWorld) { } #[given("method {name} accesses fields {fields}")] -fn given_fields(world: &DiagnosticNoteWorld, name: String, fields: CsvList) { +fn given_fields(world: &DiagnosticNoteWorld, name: String, fields: CsvList) -> Result<(), String> { let parsed_fields = fields.into_vec(); with_method_builder(world, &name, |builder| { for field in &parsed_fields { builder.record_accessed_field(field.as_str()); } - }); + }) } #[given("method {name} uses external domains {domains}")] -fn given_external_domains(world: &DiagnosticNoteWorld, name: String, domains: CsvList) { +fn given_external_domains( + world: &DiagnosticNoteWorld, + name: String, + domains: CsvList, +) -> Result<(), String> { let parsed_domains = domains.into_vec(); with_method_builder(world, &name, |builder| { for domain in &parsed_domains { builder.record_external_domain(domain.as_str()); } - }); + }) } #[when("the decomposition diagnostic note is rendered")] @@ -228,24 +230,25 @@ fn then_note_is_present(world: &DiagnosticNoteWorld) -> Result<(), String> { #[then("there is no note")] fn then_there_is_no_note(world: &DiagnosticNoteWorld) -> Result<(), String> { - with_rendered_note(world, |rendered_note| match rendered_note { - Some(note) => Err(format!("expected no note but found:\n{note}")), - None => Ok(()), + with_rendered_note(world, |rendered_note| { + rendered_note.as_ref().map_or(Ok(()), |note| { + Err(format!("expected no note but found:\n{note}")) + }) }) } #[then("the note contains line {line}")] fn then_note_contains_line(world: &DiagnosticNoteWorld, line: QuotedString) -> Result<(), String> { - let line = line.into_inner(); + let expected_line = line.into_inner(); with_rendered_note(world, |rendered_note| { let note = rendered_note .as_ref() .ok_or_else(|| String::from("expected a rendered note but found none"))?; - if note.lines().any(|candidate| candidate == line) { + if note.lines().any(|candidate| candidate == expected_line) { Ok(()) } else { Err(format!( - "expected note to contain line `{line}` but found:\n{note}" + "expected note to contain line `{expected_line}` but found:\n{note}" )) } }) @@ -256,14 +259,14 @@ fn then_note_does_not_contain( world: &DiagnosticNoteWorld, fragment: QuotedString, ) -> Result<(), String> { - let fragment = fragment.into_inner(); + let unexpected_fragment = fragment.into_inner(); with_rendered_note(world, |rendered_note| { let note = rendered_note .as_ref() .ok_or_else(|| String::from("expected a rendered note but found none"))?; - if note.contains(&fragment) { + if note.contains(&unexpected_fragment) { Err(format!( - "expected note not to contain `{fragment}` but found:\n{note}" + "expected note not to contain `{unexpected_fragment}` but found:\n{note}" )) } else { Ok(()) diff --git a/common/tests/decomposition_label_propagation_behaviour.rs b/common/tests/decomposition_label_propagation_behaviour.rs index 6a1ee8a3e..c46d6e765 100644 --- a/common/tests/decomposition_label_propagation_behaviour.rs +++ b/common/tests/decomposition_label_propagation_behaviour.rs @@ -1,12 +1,13 @@ //! Behaviour-driven coverage for decomposition label propagation. +use std::{cell::RefCell, str::FromStr}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use std::str::FromStr; use whitaker_common::test_support::decomposition::{ AdjacencyError, EdgeInput, LabelPropagationReport, label_propagation_report, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Debug)] struct CsvList(Vec); @@ -67,6 +68,7 @@ struct LabelPropagationWorld { result: RefCell>>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> LabelPropagationWorld { LabelPropagationWorld::default() diff --git a/common/tests/decomposition_vector_algebra_behaviour.rs b/common/tests/decomposition_vector_algebra_behaviour.rs index fafbc1c98..feb08aac8 100644 --- a/common/tests/decomposition_vector_algebra_behaviour.rs +++ b/common/tests/decomposition_vector_algebra_behaviour.rs @@ -1,14 +1,14 @@ //! Behaviour-driven coverage for decomposition vector algebra helpers. +use std::{cell::RefCell, collections::BTreeMap, str::FromStr}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use std::collections::BTreeMap; -use std::str::FromStr; -use whitaker_common::MethodProfileBuilder; -use whitaker_common::test_support::decomposition::{ - MethodVectorAlgebraReport, method_vector_algebra, +use whitaker_common::{ + MethodProfileBuilder, + test_support::decomposition::{MethodVectorAlgebraReport, method_vector_algebra}, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Copy, Debug)] enum MethodSide { @@ -17,7 +17,7 @@ enum MethodSide { } impl MethodSide { - fn key(self) -> &'static str { + const fn key(self) -> &'static str { match self { Self::Left => "left", Self::Right => "right", @@ -66,6 +66,7 @@ struct VectorAlgebraWorld { report: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> VectorAlgebraWorld { VectorAlgebraWorld::default() diff --git a/common/tests/features/brain_trait_metrics.feature b/common/tests/features/brain_trait_metrics.feature index c2f45a589..7ce72bdf4 100644 --- a/common/tests/features/brain_trait_metrics.feature +++ b/common/tests/features/brain_trait_metrics.feature @@ -52,7 +52,7 @@ Feature: Brain trait metric collection Given a trait named Transformer And a required method parse And a required method validate - And a default method normalise with CC 7 + And a default method normalize with CC 7 And an associated const VERSION When trait metrics are built Then total trait items is 4 diff --git a/common/tests/features/cognitive_complexity.feature b/common/tests/features/cognitive_complexity.feature index 9fcd51de0..488c15087 100644 --- a/common/tests/features/cognitive_complexity.feature +++ b/common/tests/features/cognitive_complexity.feature @@ -5,14 +5,14 @@ Feature: Cognitive complexity with macro-expansion filtering Scenario: Empty function has zero complexity Given a new complexity builder - When the complexity is finalised + When the complexity is finalized Then the complexity score is 0 Scenario: Single if adds one structural increment Given a new complexity builder And a structural increment not from expansion And a nesting increment not from expansion - When the complexity is finalised + When the complexity is finalized Then the complexity score is 1 Scenario: Nested if adds nesting-depth penalty @@ -23,13 +23,13 @@ Feature: Cognitive complexity with macro-expansion filtering And a structural increment not from expansion And a nesting increment not from expansion And nesting is popped - When the complexity is finalised + When the complexity is finalized Then the complexity score is 3 Scenario: Macro-expanded structural increment is excluded Given a new complexity builder And a structural increment from expansion - When the complexity is finalised + When the complexity is finalized Then the complexity score is 0 Scenario: Macro-expanded nesting does not inflate depth @@ -38,7 +38,7 @@ Feature: Cognitive complexity with macro-expansion filtering And a structural increment not from expansion And a nesting increment not from expansion And nesting is popped - When the complexity is finalised + When the complexity is finalized Then the complexity score is 1 Scenario: Boolean operators add fundamental increments @@ -46,7 +46,7 @@ Feature: Cognitive complexity with macro-expansion filtering And a structural increment not from expansion And a fundamental increment not from expansion And a fundamental increment not from expansion - When the complexity is finalised + When the complexity is finalized Then the complexity score is 3 Scenario: Mixed real and expansion increments @@ -57,11 +57,11 @@ Feature: Cognitive complexity with macro-expansion filtering And a structural increment not from expansion And a nesting increment not from expansion And nesting is popped - When the complexity is finalised + When the complexity is finalized Then the complexity score is 3 Scenario: Fundamental increment from expansion is excluded Given a new complexity builder And a fundamental increment from expansion - When the complexity is finalised + When the complexity is finalized Then the complexity score is 0 diff --git a/common/tests/features/context_detection.feature b/common/tests/features/context_detection.feature index 137ff8600..5431d9a6c 100644 --- a/common/tests/features/context_detection.feature +++ b/common/tests/features/context_detection.feature @@ -1,26 +1,26 @@ Feature: Context detection - Scenario: Recognise rstest decorated functions + Scenario: Recognize rstest decorated functions Given a function annotated with rstest When I check whether the function is test-like - Then the function is recognised as test-like + Then the function is recognized as test-like And its context is marked as test-like - Scenario: Recognise tokio::test decorated functions + Scenario: Recognize tokio::test decorated functions Given a function annotated with tokio::test When I check whether the function is test-like - Then the function is recognised as test-like + Then the function is recognized as test-like And its context is marked as test-like Scenario: Ignore plain functions Given a function without test attributes When I check whether the function is test-like - Then the function is recognised as not test-like + Then the function is recognized as not test-like And its context is not marked as test-like - Scenario: Recognise configured custom test attribute - Given the lint recognises custom::test as a test attribute + Scenario: Recognize configured custom test attribute + Given the lint recognizes custom::test as a test attribute And a function annotated with the custom test attribute custom::test When I check whether the function is test-like - Then the function is recognised as test-like + Then the function is recognized as test-like And its context is marked as test-like diff --git a/common/tests/features/decomposition_advice.feature b/common/tests/features/decomposition_advice.feature index 34907b485..6a14a4fed 100644 --- a/common/tests/features/decomposition_advice.feature +++ b/common/tests/features/decomposition_advice.feature @@ -2,7 +2,7 @@ Feature: Decomposition advice analysis Community detection groups related methods into reusable decomposition suggestions for brain type and brain trait diagnostics. - Scenario: Type methods split into parsing, serialisation, and filesystem groups + Scenario: Type methods split into parsing, serialization, and filesystem groups Given decomposition analysis for a type named Foo And a method named parse_tokens And method parse_tokens accesses fields grammar,tokens diff --git a/common/tests/features/rstest_detection.feature b/common/tests/features/rstest_detection.feature index 2fde144dd..b690acb6e 100644 --- a/common/tests/features/rstest_detection.feature +++ b/common/tests/features/rstest_detection.feature @@ -3,12 +3,12 @@ Feature: Strict rstest detection Scenario: Detect an rstest test from a direct attribute Given a function annotated with rstest When I check whether the function is an rstest test - Then the function is recognised as an rstest test + Then the function is recognized as an rstest test Scenario: Detect an rstest fixture from a direct attribute Given a function annotated with rstest::fixture When I check whether the function is an rstest fixture - Then the function is recognised as an rstest fixture + Then the function is recognized as an rstest fixture Scenario: Classify a plain identifier parameter as fixture-local Given a parameter named db @@ -30,18 +30,18 @@ Feature: Strict rstest detection Scenario: Ignore expansion traces while fallback is disabled Given the expansion trace contains rstest When I check whether the function is an rstest test - Then the function is recognised as not being an rstest test + Then the function is recognized as not being an rstest test Scenario: Use expansion traces when fallback is enabled Given the expansion trace contains rstest And expansion fallback is enabled When I check whether the function is an rstest test - Then the function is recognised as an rstest test + Then the function is recognized as an rstest test Scenario: Detect rstest test with multiple attributes Given a function annotated with rstest and allow When I check whether the function is an rstest test - Then the function is recognised as an rstest test + Then the function is recognized as an rstest test Scenario: Classify custom provider parameters Given a parameter annotated with a custom provider attribute @@ -53,4 +53,4 @@ Feature: Strict rstest detection Given the expansion trace contains outer_macro and rstest And expansion fallback is enabled When I check whether the function is an rstest test - Then the function is recognised as an rstest test + Then the function is recognized as an rstest test diff --git a/common/tests/i18n_behaviour.rs b/common/tests/i18n_behaviour.rs index 3ea2c62f0..a5a9a6c5b 100644 --- a/common/tests/i18n_behaviour.rs +++ b/common/tests/i18n_behaviour.rs @@ -4,12 +4,12 @@ //! missing message handling to ensure lint crates can rely on predictable //! diagnostics across locales. +use std::{borrow::Cow, cell::RefCell, collections::HashMap}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::borrow::Cow; -use std::cell::RefCell; -use std::collections::HashMap; use whitaker_common::i18n::{Arguments, FluentValue, I18nError, Localizer, branch_phrase}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[path = "support/i18n_helpers.rs"] mod i18n_helpers; @@ -39,12 +39,11 @@ impl I18nFixture { *self.outcome.borrow_mut() = Some(result); } - fn result(&self) -> Result { - self.outcome - .borrow() - .as_ref() - .cloned() - .unwrap_or_else(|| panic!("lookup should have been performed")) + /// Returns the stored lookup outcome, or `None` when no lookup has + /// been performed yet. Callers assert on the absence themselves so + /// this accessor stays panic-free. + fn result(&self) -> Option> { + self.outcome.borrow().as_ref().cloned() } } @@ -52,16 +51,17 @@ fn lint_count_from_key(key: &str) -> Option<(String, u32)> { let suffix = " with lint count "; let (base, count) = key.rsplit_once(suffix)?; let value = count.trim().parse().ok()?; - Some((base.to_string(), value)) + Some((base.to_owned(), value)) } fn branch_count_from_key(key: &str) -> Option<(String, u32)> { let suffix = " with branches "; let (base, count) = key.rsplit_once(suffix)?; let value = count.trim().parse().ok()?; - Some((base.to_string(), value)) + Some((base.to_owned(), value)) } +#[allow_fixture_expansion_lints] #[fixture] fn fixture() -> I18nFixture { I18nFixture::default() @@ -93,7 +93,7 @@ fn when_attribute(fixture: &I18nFixture, attribute: String, key: String) { let mut args = default_arguments(); args.insert( Cow::Borrowed("branches"), - FluentValue::from(branches as i64), + FluentValue::from(i64::from(branches)), ); let phrase = branch_phrase(localizer.locale(), branches as usize); args.insert( @@ -107,7 +107,10 @@ fn when_attribute(fixture: &I18nFixture, attribute: String, key: String) { if let Some((base_key, lint_count)) = lint_count_from_key(&key) { let mut args: Arguments<'static> = HashMap::new(); - args.insert(Cow::Borrowed("lint"), FluentValue::from(lint_count as i64)); + args.insert( + Cow::Borrowed("lint"), + FluentValue::from(i64::from(lint_count)), + ); let result = localizer.attribute_with_args(&base_key, &attribute, &args); fixture.store_message(result); return; @@ -121,33 +124,44 @@ fn when_attribute(fixture: &I18nFixture, attribute: String, key: String) { #[then("the resolved locale is {expected}")] fn then_locale(fixture: &I18nFixture, expected: String) { let localizer = fixture.ensure_localizer(); - assert_eq!(localizer.locale(), expected); + assert_eq!( + localizer.locale(), + expected, + "expected resolved locale `{expected}`" + ); } #[then("the loader reports fallback usage")] fn then_fallback_used(fixture: &I18nFixture) { let localizer = fixture.ensure_localizer(); - assert!(localizer.used_fallback()); + assert!( + localizer.used_fallback(), + "expected the loader to report fallback usage" + ); } #[then("the message contains {snippet}")] -fn then_contains(fixture: &I18nFixture, snippet: String) { +fn then_contains(fixture: &I18nFixture, snippet: String) -> Result<(), String> { let message = fixture .result() - .unwrap_or_else(|error| panic!("message should resolve: {error}")); - let message = strip_isolation_marks(&message); - let snippet = strip_isolation_marks(&snippet); - assert!( - message.contains(snippet.as_ref()), - "expected `{message}` to contain `{snippet}`", - ); + .ok_or_else(|| String::from("lookup should have been performed"))? + .map_err(|error| format!("message should resolve: {error}"))?; + let cleaned_message = strip_isolation_marks(&message); + let cleaned_snippet = strip_isolation_marks(&snippet); + if cleaned_message.contains(cleaned_snippet.as_ref()) { + Ok(()) + } else { + Err(format!( + "expected `{cleaned_message}` to contain `{cleaned_snippet}`" + )) + } } #[then("localization fails with a missing message error")] fn then_missing(fixture: &I18nFixture) { match fixture.result() { - Err(I18nError::MissingMessage { .. }) => {} - other => panic!("unexpected result: {other:?}", other = other), + Some(Err(I18nError::MissingMessage { .. })) => {} + other => panic!("unexpected result: {other:?}"), } } @@ -220,16 +234,17 @@ fn scenario_welsh_conditional_note_lenition(fixture: I18nFixture) { mod tests { //! Validates lint count key parsing so attribute helpers feed deterministic //! arguments into i18n scenarios. - use super::lint_count_from_key; use rstest::rstest; + use super::lint_count_from_key; + #[rstest] - #[case("foo with lint count 42", Some(("foo".to_string(), 42)))] + #[case("foo with lint count 42", Some(("foo".to_owned(), 42)))] #[case("foo with lint 42", None)] #[case("foo with lint count ", None)] #[case("foo with lint count abc", None)] #[case("", None)] - #[case(" with lint count 10", Some(("".to_string(), 10)))] + #[case(" with lint count 10", Some((String::new(), 10)))] fn lint_count_from_key_parsing(#[case] input: &str, #[case] expected: Option<(String, u32)>) { assert_eq!(lint_count_from_key(input), expected); } diff --git a/common/tests/i18n_packaging.rs b/common/tests/i18n_packaging.rs index 3456d534b..b3c853d78 100644 --- a/common/tests/i18n_packaging.rs +++ b/common/tests/i18n_packaging.rs @@ -6,43 +6,55 @@ #[cfg(unix)] mod unix { - use std::fs; - use std::path::{Path, PathBuf}; - use std::process::Command; - use whitaker_common::i18n::packaged_fallback_locale_path; + //! Unix-only packaging checks that shell out to `cargo package` and `tar`. + //! + //! These helpers stage a temporary target directory, build the package + //! tarball, and list its contents so the test can assert that the + //! fallback Fluent bundle ships with the crate. + + use std::{ + error::Error, + fs, io, + path::{Path, PathBuf}, + process::Command, + }; use tempfile::{Builder, TempDir}; + use whitaker_common::i18n::packaged_fallback_locale_path; + + type TestResult = Result>; #[test] - fn fluent_bundles_are_included_in_the_package_tarball() { - let target_dir = package_target_dir(); - let crate_path = package_crate_path(target_dir.path()); - let tar_listing = package_tar_listing(&crate_path); + fn fluent_bundles_are_included_in_the_package_tarball() -> TestResult { + let target_dir = package_target_dir()?; + let crate_path = package_crate_path(target_dir.path())?; + let tar_listing = package_tar_listing(&crate_path)?; let expected_entry = packaged_fallback_locale_path() .to_string_lossy() .replace('\\', "/"); - assert!( - tar_listing.lines().any(|line| line == expected_entry), - "expected packaged tarball to include the fallback Fluent bundle, but it did not" - ); + if tar_listing.lines().any(|line| line == expected_entry) { + Ok(()) + } else { + Err(format!( + "expected packaged tarball to include the fallback Fluent bundle \ + `{expected_entry}`, but it did not" + ) + .into()) + } } - fn package_target_dir() -> TempDir { + fn package_target_dir() -> io::Result { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let target_root = manifest_dir.join("target"); - fs::create_dir_all(&target_root) - .unwrap_or_else(|error| panic!("target directory should be creatable: {error}")); + fs::create_dir_all(&target_root)?; Builder::new() .prefix("whitaker-common-package-") .tempdir_in(&target_root) - .unwrap_or_else(|error| { - panic!("temporary package directory should be creatable: {error}") - }) } - fn package_crate_path(target_dir: &Path) -> PathBuf { + fn package_crate_path(target_dir: &Path) -> TestResult { let status = Command::new("cargo") .current_dir(env!("CARGO_MANIFEST_DIR")) .env("CARGO_TARGET_DIR", target_dir) @@ -53,10 +65,11 @@ mod unix { "--allow-dirty", "--no-verify", ]) - .status() - .unwrap_or_else(|error| panic!("cargo package should run: {error}")); + .status()?; - assert!(status.success(), "cargo package should succeed"); + if !status.success() { + return Err(format!("cargo package should succeed, but exited with {status}").into()); + } let expected_name = format!( "{}-{}.crate", @@ -64,36 +77,30 @@ mod unix { env!("CARGO_PKG_VERSION") ); let package_dir = target_dir.join("package"); - fs::read_dir(&package_dir) - .unwrap_or_else(|error| panic!("package directory should be readable: {error}")) - .map(|entry| { - entry - .unwrap_or_else(|error| { - panic!("package directory entry should be readable: {error}") - }) - .path() - }) - .find(|path| { - path.file_name() - .is_some_and(|name| name == expected_name.as_str()) - }) - .unwrap_or_else(|| panic!("cargo package should produce {expected_name}")) + for entry_result in fs::read_dir(&package_dir)? { + let path = entry_result?.path(); + if path + .file_name() + .is_some_and(|name| name == expected_name.as_str()) + { + return Ok(path); + } + } + + Err(format!("cargo package should produce {expected_name}").into()) } - fn package_tar_listing(crate_path: &Path) -> String { - let output = Command::new("tar") - .arg("-tf") - .arg(crate_path) - .output() - .unwrap_or_else(|error| panic!("tar should list package contents: {error}")); + fn package_tar_listing(crate_path: &Path) -> TestResult { + let output = Command::new("tar").arg("-tf").arg(crate_path).output()?; - assert!( - output.status.success(), - "tar should succeed when listing the packaged crate: {}", - String::from_utf8_lossy(&output.stderr) - ); + if !output.status.success() { + return Err(format!( + "tar should succeed when listing the packaged crate: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } - String::from_utf8(output.stdout) - .unwrap_or_else(|error| panic!("tar listing should be valid UTF-8: {error}")) + String::from_utf8(output.stdout).map_err(Into::into) } } diff --git a/common/tests/i18n_quality/ftl_smoke_behaviour.rs b/common/tests/i18n_quality/ftl_smoke_behaviour.rs index 67554b630..3cf1c7ac5 100644 --- a/common/tests/i18n_quality/ftl_smoke_behaviour.rs +++ b/common/tests/i18n_quality/ftl_smoke_behaviour.rs @@ -114,7 +114,7 @@ impl ParsingWorld { .borrow() .as_ref() .cloned() - .expect("Fluent source should be initialised"); + .expect("Fluent source should be initialized"); let result = match FluentResource::try_new(source) { Ok(resource) => bundle_duplicate_result(resource), Err((resource, errors)) => { diff --git a/common/tests/i18n_quality/suite.rs b/common/tests/i18n_quality/suite.rs index d15604afd..096ff50bc 100644 --- a/common/tests/i18n_quality/suite.rs +++ b/common/tests/i18n_quality/suite.rs @@ -133,7 +133,7 @@ fn validate_entry_placeables( validate_attribute_placeables(&context, message_id, en_entry, locale_entry); } -fn validate_pluralisation_coverage(locale: &str, max_branches: i64) { +fn validate_pluralization_coverage(locale: &str, max_branches: i64) { let localizer = Localizer::new(Some(locale)); let mut args = HashMap::new(); @@ -196,7 +196,7 @@ fn ftl_bundles_parse_successfully() { } #[test] -fn localised_help_attributes_are_complete() { +fn localized_help_attributes_are_complete() { for (locale, en_path, locale_path) in file_pairs() { let locale_code = LocaleCode::from(locale.as_str()); let en_entries = parse_ftl(&en_path); @@ -231,8 +231,8 @@ fn localised_help_attributes_are_complete() { #[case("en-GB", 12)] #[case("cy", 12)] #[case("gd", 25)] -fn pluralisation_covers_sample_range(#[case] locale: &str, #[case] max_branches: i64) { - validate_pluralisation_coverage(locale, max_branches); +fn pluralization_covers_sample_range(#[case] locale: &str, #[case] max_branches: i64) { + validate_pluralization_coverage(locale, max_branches); } #[rstest] diff --git a/common/tests/lcom4_behaviour.rs b/common/tests/lcom4_behaviour.rs index 367d7b803..903a6d1ed 100644 --- a/common/tests/lcom4_behaviour.rs +++ b/common/tests/lcom4_behaviour.rs @@ -1,10 +1,14 @@ //! Behaviour-driven coverage for LCOM4 cohesion analysis. +use std::{ + cell::{Cell, RefCell}, + collections::BTreeSet, +}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; -use std::collections::BTreeSet; use whitaker_common::lcom4::{MethodInfo, cohesion_components}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct LcomWorld { @@ -22,11 +26,12 @@ impl LcomWorld { self.result.set(Some(cohesion_components(&methods))); } - fn result(&self) -> Option { + const fn result(&self) -> Option { self.result.get() } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> LcomWorld { LcomWorld::default() @@ -125,9 +130,13 @@ fn scenario_self_call(world: LcomWorld) { #[cfg(test)] mod parse_field_set_tests { - use super::parse_field_set; + //! Unit tests for `parse_field_set`, the comma-separated field parser used + //! by the LCOM4 behaviour steps. + use std::collections::BTreeSet; + use super::parse_field_set; + #[test] fn basic_comma_separated() { let result = parse_field_set("a, b"); diff --git a/common/tests/localizer_helpers.rs b/common/tests/localizer_helpers.rs index 5fc322406..754beedfb 100644 --- a/common/tests/localizer_helpers.rs +++ b/common/tests/localizer_helpers.rs @@ -3,18 +3,23 @@ //! Scenarios validate locale resolution and fallback handling so lints can //! depend on deterministic localization outcomes. +use std::{ + borrow::Cow, + cell::RefCell, + sync::{Mutex, MutexGuard, PoisonError}, +}; + use logtest::Logger; use rstest::{fixture, rstest}; use rstest_bdd_macros::{given, scenario, then, when}; -use std::borrow::Cow; -use std::cell::RefCell; -use std::sync::{Mutex, MutexGuard}; -use whitaker_common::i18n::testing::RecordingEmitter; -use whitaker_common::i18n::{ - Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, - get_localizer_for_lint, noop_reporter, safe_resolve_message_set, +use whitaker_common::{ + i18n::{ + Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, + get_localizer_for_lint, noop_reporter, safe_resolve_message_set, testing::RecordingEmitter, + }, + test_support::with_locale, }; -use whitaker_common::test_support::LocaleOverride; +use whitaker_test_macros::allow_fixture_expansion_lints; static ENVIRONMENT_LOCK: Mutex<()> = Mutex::new(()); @@ -33,15 +38,17 @@ struct HelperWorld { fallback: RefCell>, result: RefCell>, emitter: RecordingEmitter, - environment_override: RefCell>, + environment_locale: RefCell>, _guard: MutexGuard<'static, ()>, } impl HelperWorld { fn new() -> Self { + // The guard protects only process-wide environment mutation; a + // poisoned lock carries no invalid state, so recovering is safe. let guard = ENVIRONMENT_LOCK .lock() - .unwrap_or_else(|error| panic!("environment lock poisoned: {error}")); + .unwrap_or_else(PoisonError::into_inner); Self { configuration: RefCell::new(None), @@ -51,19 +58,13 @@ impl HelperWorld { fallback: RefCell::new(None), result: RefCell::new(None), emitter: RecordingEmitter::default(), - environment_override: RefCell::new(None), + environment_locale: RefCell::new(None), _guard: guard, } } fn set_environment(&self, value: Option) { - let mut guard = self.environment_override.borrow_mut(); - guard.take(); - let override_guard = match value { - Some(locale) => LocaleOverride::set(locale.as_str()), - None => LocaleOverride::clear(), - }; - *guard = Some(override_guard); + *self.environment_locale.borrow_mut() = value; } fn set_configuration(&self, locale: Option) { @@ -72,21 +73,31 @@ impl HelperWorld { fn request_localizer(&self, lint: &str) { let config = self.configuration.borrow(); - let localizer = get_localizer_for_lint(lint, config.as_deref()); + let environment = self.environment_locale.borrow(); + let localizer = with_locale(environment.as_deref(), || { + get_localizer_for_lint(lint, config.as_deref()) + }); self.localizer.borrow_mut().replace(localizer); } - fn ensure_localizer(&self) -> Localizer { + fn ensure_localizer(&self) -> Result { self.localizer .borrow() .as_ref() .cloned() - .unwrap_or_else(|| panic!("localizer should be initialised")) + .ok_or_else(|| String::from("localizer should be initialized")) } - fn assert_locale(&self, expected: &str) { - let localizer = self.ensure_localizer(); - assert_eq!(localizer.locale(), expected); + fn assert_locale(&self, expected: &str) -> Result<(), String> { + let localizer = self.ensure_localizer()?; + let actual = localizer.locale(); + if actual == expected { + Ok(()) + } else { + Err(format!( + "expected resolved locale `{expected}`, but found `{actual}`" + )) + } } fn set_message_key(&self, key: String) { @@ -128,18 +139,18 @@ impl HelperWorld { args.insert(Cow::Borrowed("subject"), FluentValue::from("functions")); args.insert( Cow::Borrowed("attribute"), - FluentValue::from("#[inline]".to_string()), + FluentValue::from("#[inline]".to_owned()), ); *self.arguments.borrow_mut() = args; } - fn resolve_messages(&self) { - let localizer = self.ensure_localizer(); + fn resolve_messages(&self) -> Result<(), String> { + let localizer = self.ensure_localizer()?; let key = self .message_key .borrow() .clone() - .unwrap_or_else(|| panic!("a message key should be configured")); + .ok_or_else(|| String::from("a message key should be configured"))?; let args = self.arguments.borrow().clone(); let fallback = self.ensure_fallback(); @@ -158,14 +169,15 @@ impl HelperWorld { ); self.result.borrow_mut().replace(messages); + Ok(()) } - fn resolved_messages(&self) -> DiagnosticMessageSet { + fn resolved_messages(&self) -> Result { self.result .borrow() .as_ref() .cloned() - .unwrap_or_else(|| panic!("diagnostic messages should be resolved")) + .ok_or_else(|| String::from("diagnostic messages should be resolved")) } fn recorded_messages(&self) -> Vec { @@ -173,6 +185,7 @@ impl HelperWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> HelperWorld { HelperWorld::new() @@ -185,7 +198,7 @@ fn given_env_cleared(world: &HelperWorld) { #[given("DYLINT_LOCALE is {locale}")] fn given_env(world: &HelperWorld, locale: String) { - world.set_environment(Some(unquote(&locale).to_string())); + world.set_environment(Some(unquote(&locale).to_owned())); } #[given("no configuration locale is provided")] @@ -195,7 +208,7 @@ fn given_no_config(world: &HelperWorld) { #[given("the configuration locale is {locale}")] fn given_config(world: &HelperWorld, locale: String) { - world.set_configuration(Some(unquote(&locale).to_string())); + world.set_configuration(Some(unquote(&locale).to_owned())); } #[when("I request the localizer for {lint}")] @@ -205,8 +218,8 @@ fn when_request_localizer(world: &HelperWorld, lint: String) { } #[then("the resolved locale is {locale}")] -fn then_locale(world: &HelperWorld, locale: String) { - world.assert_locale(unquote(&locale)); +fn then_locale(world: &HelperWorld, locale: String) -> Result<(), String> { + world.assert_locale(unquote(&locale)) } #[given("fallback messages are defined")] @@ -216,12 +229,12 @@ fn given_fallback(world: &HelperWorld) { #[given("a missing message key {key} is requested")] fn given_missing_key(world: &HelperWorld, key: String) { - world.set_message_key(unquote(&key).to_string()); + world.set_message_key(unquote(&key).to_owned()); } #[given("a message key {key} is requested")] fn given_message_key(world: &HelperWorld, key: String) { - world.set_message_key(unquote(&key).to_string()); + world.set_message_key(unquote(&key).to_owned()); } #[given("I prepare arguments for the doc attribute diagnostic")] @@ -235,30 +248,50 @@ fn given_no_doc_arguments(world: &HelperWorld) { } #[when("I resolve the diagnostic message set")] -fn when_resolve_messages(world: &HelperWorld) { - world.resolve_messages(); +fn when_resolve_messages(world: &HelperWorld) -> Result<(), String> { + world.resolve_messages() } #[then("the fallback primary message contains {snippet}")] -fn then_fallback_primary(world: &HelperWorld, snippet: String) { - let messages = world.resolved_messages(); - let snippet = unquote(&snippet); - assert!(messages.primary().contains(snippet)); +fn then_fallback_primary(world: &HelperWorld, snippet: String) -> Result<(), String> { + let messages = world.resolved_messages()?; + let expected_snippet = unquote(&snippet); + if messages.primary().contains(expected_snippet) { + Ok(()) + } else { + Err(format!( + "expected fallback primary message to contain `{expected_snippet}`" + )) + } } #[then("a delayed bug is recorded mentioning {snippet}")] fn then_bug_recorded(world: &HelperWorld, snippet: String) { let messages = world.recorded_messages(); - let snippet = unquote(&snippet); - assert!(!messages.is_empty()); - assert!(messages.iter().any(|message| message.contains(snippet))); + let expected_snippet = unquote(&snippet); + assert!( + !messages.is_empty(), + "expected at least one delayed bug to be recorded" + ); + assert!( + messages + .iter() + .any(|message| message.contains(expected_snippet)), + "expected a recorded bug mentioning `{expected_snippet}`" + ); } #[then("the resolved primary message contains {snippet}")] -fn then_primary_message(world: &HelperWorld, snippet: String) { - let messages = world.resolved_messages(); - let snippet = unquote(&snippet); - assert!(messages.primary().contains(snippet)); +fn then_primary_message(world: &HelperWorld, snippet: String) -> Result<(), String> { + let messages = world.resolved_messages()?; + let expected_snippet = unquote(&snippet); + if messages.primary().contains(expected_snippet) { + Ok(()) + } else { + Err(format!( + "expected resolved primary message to contain `{expected_snippet}`" + )) + } } #[then("no delayed bug is recorded")] @@ -298,13 +331,13 @@ fn invalid_locale_warns_and_falls_back() { world.set_environment(Some(String::from("xx-XX"))); world.set_configuration(None); world.request_localizer("function_attrs_follow_docs"); - world.assert_locale("en-GB"); + world.assert_locale("en-GB").expect("locale should resolve"); let mut warned = false; while let Some(record) = logger.pop() { if record .args() - .to_string() + .to_owned() .contains("unsupported DYLINT_LOCALE `xx-XX`") { warned = true; @@ -324,8 +357,8 @@ fn repeated_failures_record_all_bugs() { world.set_fallback_messages(); world.set_message_key(String::from("missing-key")); - world.resolve_messages(); - world.resolve_messages(); + world.resolve_messages().expect("messages should resolve"); + world.resolve_messages().expect("messages should resolve"); let recorded = world.recorded_messages(); assert_eq!(recorded.len(), 2); @@ -343,7 +376,9 @@ fn missing_key_with_noop_reporter_uses_fallback(world: HelperWorld) { world.request_localizer("no_expect_outside_tests"); world.set_fallback_messages(); - let localizer = world.ensure_localizer(); + let localizer = world + .ensure_localizer() + .expect("localizer should initialize"); let args = world.ensure_arguments(); let fallback = world.ensure_fallback(); let resolution = MessageResolution { diff --git a/common/tests/method_extraction_behaviour.rs b/common/tests/method_extraction_behaviour.rs index a84e86d98..93a4e4edb 100644 --- a/common/tests/method_extraction_behaviour.rs +++ b/common/tests/method_extraction_behaviour.rs @@ -1,9 +1,11 @@ //! Behaviour-driven coverage for method metadata extraction. +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; use whitaker_common::lcom4::{MethodInfo, MethodInfoBuilder}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct ExtractionWorld { @@ -56,6 +58,7 @@ impl ExtractionWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> ExtractionWorld { ExtractionWorld::default() diff --git a/common/tests/rstest_detection_behaviour.rs b/common/tests/rstest_detection_behaviour.rs index 0ec2d9557..b68d386a3 100644 --- a/common/tests/rstest_detection_behaviour.rs +++ b/common/tests/rstest_detection_behaviour.rs @@ -1,14 +1,18 @@ //! Behaviour-driven tests for strict `rstest` detection helpers. +use std::{cell::RefCell, collections::BTreeSet}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use std::collections::BTreeSet; -use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -use whitaker_common::rstest::{ - ExpansionTrace, ParameterBinding, RstestDetectionOptions, RstestParameter, RstestParameterKind, - classify_rstest_parameter, fixture_local_names, is_rstest_fixture_with, is_rstest_test_with, +use whitaker_common::{ + attributes::{Attribute, AttributeKind, AttributePath}, + rstest::{ + ExpansionTrace, ParameterBinding, RstestDetectionOptions, RstestParameter, + RstestParameterKind, classify_rstest_parameter, fixture_local_names, + is_rstest_fixture_with, is_rstest_test_with, + }, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Debug, Default)] struct DetectionWorld { @@ -100,6 +104,7 @@ impl DetectionWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> DetectionWorld { DetectionWorld::default() @@ -123,7 +128,7 @@ fn given_fixture_local_parameter(world: &DetectionWorld) { #[given("a parameter named case_input annotated with case")] fn given_provider_parameter(world: &DetectionWorld) { world.set_parameter(RstestParameter::new( - ParameterBinding::Ident("case_input".to_string()), + ParameterBinding::Ident("case_input".to_owned()), vec![Attribute::new( AttributePath::from("case"), AttributeKind::Outer, @@ -155,7 +160,7 @@ fn given_rstest_and_allow(world: &DetectionWorld) { #[given("a parameter annotated with a custom provider attribute")] fn given_custom_provider_parameter(world: &DetectionWorld) { world.set_parameter(RstestParameter::new( - ParameterBinding::Ident("custom_value".to_string()), + ParameterBinding::Ident("custom_value".to_owned()), vec![Attribute::new( AttributePath::from("custom::provider"), AttributeKind::Outer, @@ -193,17 +198,17 @@ fn when_fixture_names_evaluated(world: &DetectionWorld) { world.evaluate_fixture_names(); } -#[then("the function is recognised as an rstest test")] +#[then("the function is recognized as an rstest test")] fn then_test_positive(world: &DetectionWorld) { assert_eq!(*world.test_result.borrow(), Some(true)); } -#[then("the function is recognised as not being an rstest test")] +#[then("the function is recognized as not being an rstest test")] fn then_test_negative(world: &DetectionWorld) { assert_eq!(*world.test_result.borrow(), Some(false)); } -#[then("the function is recognised as an rstest fixture")] +#[then("the function is recognized as an rstest fixture")] fn then_fixture_positive(world: &DetectionWorld) { assert_eq!(*world.fixture_result.borrow(), Some(true)); } @@ -213,7 +218,7 @@ fn then_fixture_local(world: &DetectionWorld) { assert_eq!( *world.parameter_kind.borrow(), Some(RstestParameterKind::FixtureLocal { - name: "db".to_string() + name: "db".to_owned() }) ); } @@ -238,7 +243,7 @@ fn then_unsupported(world: &DetectionWorld) { fn then_fixture_names(world: &DetectionWorld) { assert_eq!( *world.fixture_names.borrow(), - Some(BTreeSet::from(["db".to_string()])) + Some(BTreeSet::from(["db".to_owned()])) ); } diff --git a/common/tests/rstest_fingerprint_behaviour.rs b/common/tests/rstest_fingerprint_behaviour.rs index 058fa87b2..8666c5901 100644 --- a/common/tests/rstest_fingerprint_behaviour.rs +++ b/common/tests/rstest_fingerprint_behaviour.rs @@ -1,12 +1,14 @@ //! Behaviour-driven tests for shared `rstest` fingerprint models. +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; use whitaker_common::rstest::{ ArgAtom, ArgFingerprint, CalleeShape, ExprShape, LocalSlot, ParagraphFingerprint, ParagraphNormalizer, StmtShape, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Default)] struct FingerprintWorld { @@ -48,6 +50,7 @@ impl FingerprintWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> FingerprintWorld { FingerprintWorld::default() diff --git a/common/tests/rstest_span_recovery_behaviour.rs b/common/tests/rstest_span_recovery_behaviour.rs index 4733ef5ee..a98e73d02 100644 --- a/common/tests/rstest_span_recovery_behaviour.rs +++ b/common/tests/rstest_span_recovery_behaviour.rs @@ -1,10 +1,14 @@ //! Behaviour-driven tests for shared `rstest` span recovery helpers. +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use whitaker_common::rstest::{SpanRecoveryFrame, UserEditableSpan, recover_user_editable_span}; -use whitaker_common::span::{SourceLocation, SourceSpan}; +use whitaker_common::{ + rstest::{SpanRecoveryFrame, UserEditableSpan, recover_user_editable_span}, + span::{SourceLocation, SourceSpan}, +}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Default)] struct SpanRecoveryWorld { @@ -27,6 +31,7 @@ impl SpanRecoveryWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> SpanRecoveryWorld { SpanRecoveryWorld::default() diff --git a/common/tests/support/i18n_helpers.rs b/common/tests/support/i18n_helpers.rs index e307c7293..bdbf58ed4 100644 --- a/common/tests/support/i18n_helpers.rs +++ b/common/tests/support/i18n_helpers.rs @@ -3,6 +3,7 @@ //! that keep suites aligned and readable. use std::borrow::Cow; + use whitaker_common::i18n::{Arguments, FluentValue}; const UNICODE_ISOLATION_MARKS: [char; 2] = ['\u{2068}', '\u{2069}']; @@ -14,7 +15,7 @@ const UNICODE_ISOLATION_MARKS: [char; 2] = ['\u{2068}', '\u{2069}']; /// let cleaned = strip_isolation_marks("\u{2068}42\u{2069}"); /// assert_eq!(cleaned, "42"); /// ``` -pub fn strip_isolation_marks<'a>(text: &'a str) -> Cow<'a, str> { +pub fn strip_isolation_marks(text: &str) -> Cow<'_, str> { if text .chars() .any(|character| UNICODE_ISOLATION_MARKS.contains(&character)) @@ -67,7 +68,7 @@ pub fn default_arguments() -> Arguments<'static> { /// assert!(should_skip_line(" attribute = value")); /// assert!(!should_skip_line("identifier = value")); /// ``` -pub fn should_skip_line(line: &str) -> bool { +pub const fn should_skip_line(line: &str) -> bool { matches!(line.as_bytes().first(), Some(b' ' | b'\t')) } @@ -96,11 +97,13 @@ pub fn extract_identifier(line: &str) -> Option { if id.is_empty() { return None; } - Some(id.to_string()) + Some(id.to_owned()) } #[cfg(test)] mod tests { + //! Tests for the i18n behaviour-test helpers that parse Fluent fixtures. + use super::*; #[test] @@ -115,7 +118,7 @@ mod tests { fn extract_identifier_handles_basic_messages() { assert_eq!( extract_identifier("message-id = Value"), - Some("message-id".to_string()) + Some("message-id".to_owned()) ); } @@ -130,7 +133,7 @@ mod tests { fn extract_identifier_handles_multiple_equals() { assert_eq!( extract_identifier("message = part = extra"), - Some("message".to_string()) + Some("message".to_owned()) ); } diff --git a/crates/bumpy_road_function/Cargo.toml b/crates/bumpy_road_function/Cargo.toml index 1b1e1e3d6..66d54a0dd 100644 --- a/crates/bumpy_road_function/Cargo.toml +++ b/crates/bumpy_road_function/Cargo.toml @@ -42,6 +42,7 @@ serde = { workspace = true, optional = true } whitaker = { workspace = true, features = ["dylint-driver"], optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } diff --git a/crates/bumpy_road_function/tests/analysis_behaviour.rs b/crates/bumpy_road_function/tests/analysis_behaviour.rs index 909b8fe43..7cfb191e7 100644 --- a/crates/bumpy_road_function/tests/analysis_behaviour.rs +++ b/crates/bumpy_road_function/tests/analysis_behaviour.rs @@ -7,13 +7,13 @@ #[cfg(feature = "dylint-driver")] extern crate rustc_driver; +use std::cell::RefCell; + use bumpy_road_function::analysis::{ DEFAULT_THRESHOLD, Settings, Weights, detect_bumps, normalise_settings, top_two_bumps, }; -use rstest::fixture; -use rstest::rstest; +use rstest::{fixture, rstest}; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; #[rstest] #[case::even_window( @@ -62,8 +62,8 @@ fn normalise_settings_falls_back_to_defaults( #[case] settings: Settings, #[case] expected: Settings, ) { - let normalised = normalise_settings(settings); - assert_eq!(normalised, expected); + let normalized = normalise_settings(settings); + assert_eq!(normalized, expected); } #[rstest] @@ -71,9 +71,11 @@ fn detect_bumps_reports_two_intervals() { let smoothed = vec![0.0, 3.0, 3.0, 0.0, 3.1, 3.0]; let bumps = detect_bumps(&smoothed, 3.0, 2); - assert_eq!(bumps.len(), 2); - assert_eq!((bumps[0].start_index(), bumps[0].end_index()), (1, 2)); - assert_eq!((bumps[1].start_index(), bumps[1].end_index()), (4, 5)); + let ranges: Vec<(usize, usize)> = bumps + .iter() + .map(|bump| (bump.start_index(), bump.end_index())) + .collect(); + assert_eq!(ranges, vec![(1, 2), (4, 5)]); } #[rstest] @@ -90,9 +92,11 @@ fn top_two_bumps_prefers_area_then_length() { let bumps = detect_bumps(&smoothed, 3.0, 2); let top = top_two_bumps(bumps); - assert_eq!(top.len(), 2); - assert_eq!((top[0].start_index(), top[0].end_index()), (6, 8)); - assert_eq!((top[1].start_index(), top[1].end_index()), (1, 2)); + let ranges: Vec<(usize, usize)> = top + .iter() + .map(|bump| (bump.start_index(), bump.end_index())) + .collect(); + assert_eq!(ranges, vec![(6, 8), (1, 2)]); } #[derive(Default)] @@ -102,9 +106,10 @@ struct World { min_bump_lines: RefCell, bumps: RefCell>, settings: RefCell, - normalised: RefCell>, + normalized: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> World { World::default() @@ -166,29 +171,36 @@ fn when_set_threshold(world: &World, threshold: f64) { world.settings.borrow_mut().threshold = threshold; } -#[when("I normalise the settings")] -fn when_normalise(world: &World) { +#[when("I normalize the settings")] +fn when_normalize(world: &World) { let settings = *world.settings.borrow(); - let normalised = normalise_settings(settings); - world.normalised.replace(Some(normalised)); + let normalized = normalise_settings(settings); + world.normalized.replace(Some(normalized)); } #[then("the window becomes {window}")] fn then_window(world: &World, window: usize) { let settings = world - .normalised + .normalized .borrow() - .expect("settings should be normalised"); + .expect("settings should be normalized"); assert_eq!(settings.window, window); } #[then("the threshold becomes {threshold:f64}")] fn then_threshold(world: &World, threshold: f64) { let settings = world - .normalised + .normalized .borrow() - .expect("settings should be normalised"); - assert_eq!(settings.threshold, threshold); + .expect("settings should be normalized"); + // Compare bit patterns: the assertion is that the configured value + // round-tripped through parsing unchanged, which is exact equality of + // representation rather than numeric proximity. + assert_eq!( + settings.threshold.to_bits(), + threshold.to_bits(), + "the normalized threshold should match the configured value exactly", + ); } #[scenario(path = "tests/features/bumpy_road.feature", index = 0)] @@ -243,8 +255,11 @@ fn ui_dylint_toml_threshold_matches_default() { .and_then(toml::Value::as_float) .expect("ui/dylint.toml should contain [bumpy_road_function].threshold"); + // Bit-pattern comparison: this pins the fixture to the constant exactly, + // so any drift in either is caught rather than tolerated. assert_eq!( - threshold, DEFAULT_THRESHOLD, + threshold.to_bits(), + DEFAULT_THRESHOLD.to_bits(), concat!( "ui/dylint.toml threshold must equal DEFAULT_THRESHOLD; ", "update the config file or the constant to keep them in sync" diff --git a/crates/bumpy_road_function/tests/features/bumpy_road.feature b/crates/bumpy_road_function/tests/features/bumpy_road.feature index bd32d17dc..65f27c97d 100644 --- a/crates/bumpy_road_function/tests/features/bumpy_road.feature +++ b/crates/bumpy_road_function/tests/features/bumpy_road.feature @@ -24,12 +24,12 @@ Feature: Detect bumpy road intervals Scenario: Even smoothing windows fall back to defaults Given default settings When the smoothing window is set to 2 - And I normalise the settings + And I normalize the settings Then the window becomes 3 Scenario: Negative thresholds fall back to defaults Given default settings When the threshold is set to -1.0 - And I normalise the settings + And I normalize the settings Then the threshold becomes 2.5 diff --git a/crates/bumpy_road_function/tests/ui.rs b/crates/bumpy_road_function/tests/ui.rs index 7693b624b..02256b5b3 100644 --- a/crates/bumpy_road_function/tests/ui.rs +++ b/crates/bumpy_road_function/tests/ui.rs @@ -7,23 +7,24 @@ #[cfg(feature = "dylint-driver")] extern crate rustc_driver; +use std::path::Path; + use camino::Utf8Path; use dylint_testing::ui::Test; -use std::path::Path; use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_test_runner}; #[test] fn ui() { let crate_name = env!("CARGO_PKG_NAME"); let directory = "ui"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, dir| { - run_fixtures(crate_name, dir) - }) - .unwrap_or_else(|error| { - panic!( - "UI tests should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error} }}" - ) - }); + whitaker::testing::ui::run_with_runner(crate_name, directory, run_fixtures).unwrap_or_else( + |error| { + panic!( + "UI tests should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error} }}" + ) + }, + ); } fn run_fixtures(crate_name: &str, directory: &Utf8Path) -> Result<(), String> { diff --git a/crates/conditional_max_n_branches/Cargo.toml b/crates/conditional_max_n_branches/Cargo.toml index bcbc70f97..511c57152 100644 --- a/crates/conditional_max_n_branches/Cargo.toml +++ b/crates/conditional_max_n_branches/Cargo.toml @@ -42,6 +42,7 @@ serde = { workspace = true, optional = true } whitaker = { workspace = true, features = ["dylint-driver"], optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } whitaker-common = { workspace = true } whitaker = { workspace = true } camino = { workspace = true } diff --git a/crates/conditional_max_n_branches/src/lib_ui_tests.rs b/crates/conditional_max_n_branches/src/lib_ui_tests.rs index 5afc924cb..c05dc47a6 100644 --- a/crates/conditional_max_n_branches/src/lib_ui_tests.rs +++ b/crates/conditional_max_n_branches/src/lib_ui_tests.rs @@ -2,23 +2,24 @@ //! `conditional_max_n_branches` lint. These tests ensure curated fixtures execute without //! diffs and provide coverage for the fixture discovery helpers. +use std::path::Path; + use camino::Utf8Path; use dylint_testing::ui::Test; -use std::path::Path; use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_test_runner}; #[test] fn ui() { let crate_name = env!("CARGO_PKG_NAME"); let directory = "ui"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, dir| { - run_fixtures(crate_name, dir) - }) - .unwrap_or_else(|error| { - panic!( - "UI tests should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error} }}" - ) - }); + whitaker::testing::ui::run_with_runner(crate_name, directory, run_fixtures).unwrap_or_else( + |error| { + panic!( + "UI tests should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error} }}" + ) + }, + ); } fn run_fixtures(crate_name: &str, directory: &Utf8Path) -> Result<(), String> { diff --git a/crates/conditional_max_n_branches/src/tests/behaviour.rs b/crates/conditional_max_n_branches/src/tests/behaviour.rs index 48dddf3ed..b3bf13c49 100644 --- a/crates/conditional_max_n_branches/src/tests/behaviour.rs +++ b/crates/conditional_max_n_branches/src/tests/behaviour.rs @@ -1,9 +1,11 @@ //! Behaviour-driven coverage for predicate branch evaluation logic. -use super::{ConditionDisposition, evaluate_condition}; +use std::cell::Cell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::Cell; + +use super::{ConditionDisposition, evaluate_condition}; #[derive(Default)] struct PredicateWorld { @@ -33,6 +35,7 @@ impl PredicateWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> PredicateWorld { PredicateWorld::default() diff --git a/crates/function_attrs_follow_docs/Cargo.toml b/crates/function_attrs_follow_docs/Cargo.toml index bb4d06c81..81f5e3a9e 100644 --- a/crates/function_attrs_follow_docs/Cargo.toml +++ b/crates/function_attrs_follow_docs/Cargo.toml @@ -41,6 +41,7 @@ log = { workspace = true, optional = true } whitaker = { workspace = true, features = ["dylint-driver"], optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } diff --git a/crates/function_attrs_follow_docs/src/tests/localization.rs b/crates/function_attrs_follow_docs/src/tests/localization.rs index da1dcda97..7afb76f67 100644 --- a/crates/function_attrs_follow_docs/src/tests/localization.rs +++ b/crates/function_attrs_follow_docs/src/tests/localization.rs @@ -3,15 +3,16 @@ //! Exercises locale selection, attribute fallback, and missing-message paths via //! `rstest-bdd` scenarios and a custom failing lookup to validate fallbacks. +use std::cell::{Cell, Ref, RefCell}; + +use rstest::fixture; +use rstest_bdd_macros::{given, scenario, then, when}; +use whitaker_common::i18n::{I18nError, testing::FailingLookup}; + use super::{ FunctionAttrsMessages, FunctionKind, Localizer, MESSAGE_KEY, attribute_fallback, localised_messages, }; -use rstest::fixture; -use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, Ref, RefCell}; -use whitaker_common::i18n::I18nError; -use whitaker_common::i18n::testing::FailingLookup; #[derive(Default)] struct LocalizationWorld { @@ -53,6 +54,7 @@ impl LocalizationWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> LocalizationWorld { LocalizationWorld::default() @@ -88,7 +90,7 @@ fn given_failure(world: &LocalizationWorld) { world.failing.set(true); } -#[when("I localise the diagnostic")] +#[when("I localize the diagnostic")] fn when_localize(world: &LocalizationWorld) { let kind = *world.subject.borrow(); let failing = world.failing.get(); diff --git a/crates/function_attrs_follow_docs/src/tests/order_detection.rs b/crates/function_attrs_follow_docs/src/tests/order_detection.rs index 891ee2a6e..7a37b7433 100644 --- a/crates/function_attrs_follow_docs/src/tests/order_detection.rs +++ b/crates/function_attrs_follow_docs/src/tests/order_detection.rs @@ -3,18 +3,19 @@ //! These scenarios exercise `detect_misordered_doc` to ensure doc comments //! continue to precede other outer attributes across common layouts. -use super::{ - AttrInfo, OrderedAttribute, attribute_within_item, detect_misordered_doc, parsed_attribute_span, -}; -use rstest::fixture; -use rstest::rstest; +use std::cell::RefCell; + +use rstest::{fixture, rstest}; use rstest_bdd_macros::{given, scenario, then, when}; -use rustc_hir::attrs::AttributeKind as HirAttributeKind; -use rustc_hir::attrs::{InlineAttr, OptimizeAttr}; +use rustc_attr_data_structures::thin_vec::ThinVec; +use rustc_hir::attrs::{AttributeKind as HirAttributeKind, InlineAttr, OptimizeAttr}; use rustc_span::{BytePos, DUMMY_SP, Span}; -use std::cell::RefCell; use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; +use super::{ + AttrInfo, OrderedAttribute, attribute_within_item, detect_misordered_doc, parsed_attribute_span, +}; + impl OrderedAttribute for Attribute { fn is_outer(&self) -> bool { self.is_outer() @@ -46,11 +47,13 @@ impl AttributeWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> AttributeWorld { AttributeWorld::default() } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn result() -> Option<(usize, usize)> { None @@ -239,7 +242,7 @@ fn parsed_attribute_span_recovers_whitelisted_kinds() { ( "target_feature", HirAttributeKind::TargetFeature { - features: Default::default(), + features: ThinVec::default(), attr_span: span, was_forced: false, }, diff --git a/crates/function_attrs_follow_docs/src/tests/ui.rs b/crates/function_attrs_follow_docs/src/tests/ui.rs index 4d131ffff..d90bddc52 100644 --- a/crates/function_attrs_follow_docs/src/tests/ui.rs +++ b/crates/function_attrs_follow_docs/src/tests/ui.rs @@ -6,7 +6,7 @@ //! fixtures. use serial_test::serial; -use whitaker_common::test_support::LocaleOverride; +use whitaker_common::test_support::with_locale; #[test] #[serial] @@ -21,6 +21,7 @@ fn ui_runs_in_welsh_locale() { } fn run_ui_with_locale(directory: &str, locale: Option<&str>) { - let _guard = locale.map(LocaleOverride::set); - whitaker::run_ui_tests!(directory).expect("UI tests should execute without diffs"); + with_locale(locale, || { + whitaker::run_ui_tests!(directory).expect("UI tests should execute without diffs"); + }); } diff --git a/crates/function_attrs_follow_docs/tests/features/function_attrs_localization.feature b/crates/function_attrs_follow_docs/tests/features/function_attrs_localization.feature index b30dd7a8f..044fa9beb 100644 --- a/crates/function_attrs_follow_docs/tests/features/function_attrs_localization.feature +++ b/crates/function_attrs_follow_docs/tests/features/function_attrs_localization.feature @@ -1,9 +1,9 @@ -Feature: Localised diagnostics for function attribute ordering +Feature: Localized diagnostics for function attribute ordering Scenario: English fallback locale Given the locale "en-GB" is selected And the subject kind is "function" And the attribute label is "#[inline]" - When I localise the diagnostic + When I localize the diagnostic Then the primary message contains "Doc comments" And the note mentions "#[inline]" And the help mentions "#[inline]" @@ -12,7 +12,7 @@ Feature: Localised diagnostics for function attribute ordering Given the locale "cy" is selected And the subject kind is "method" And the attribute label is "#[allow(clippy::bool_comparison)]" - When I localise the diagnostic + When I localize the diagnostic Then the primary message contains "sylwadau doc" And the note mentions "#[allow(clippy::bool_comparison)]" @@ -20,7 +20,7 @@ Feature: Localised diagnostics for function attribute ordering Given the locale "en-GB" is selected And the subject kind is "trait method" And the attribute snippet cannot be retrieved - When I localise the diagnostic + When I localize the diagnostic Then the note mentions "the preceding attribute" And the help mentions "the preceding attribute" @@ -28,12 +28,12 @@ Feature: Localised diagnostics for function attribute ordering Given the locale "zz" is selected And the subject kind is "trait method" And the attribute label is "#[allow(dead_code)]" - When I localise the diagnostic + When I localize the diagnostic Then the primary message contains "Doc comments" Scenario: Localization failure reports missing message Given localization fails And the subject kind is "function" And the attribute label is "#[cfg(test)]" - When I localise the diagnostic + When I localize the diagnostic Then localization fails for "function_attrs_follow_docs" diff --git a/crates/module_max_lines/src/lib_ui_tests.rs b/crates/module_max_lines/src/lib_ui_tests.rs index 3ac392068..62e4989e4 100644 --- a/crates/module_max_lines/src/lib_ui_tests.rs +++ b/crates/module_max_lines/src/lib_ui_tests.rs @@ -2,23 +2,24 @@ //! `module_max_lines` lint. These tests ensure curated fixtures execute without //! diffs and provide coverage for the shared fixture-discovery helpers. +use std::path::Path; + use camino::Utf8Path; use dylint_testing::ui::Test; -use std::path::Path; use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_test_runner}; #[test] fn ui() { let crate_name = env!("CARGO_PKG_NAME"); let directory = "ui"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, dir| { - run_fixtures(crate_name, dir) - }) - .unwrap_or_else(|error| { - panic!( - "UI tests should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error} }}" - ) - }); + whitaker::testing::ui::run_with_runner(crate_name, directory, run_fixtures).unwrap_or_else( + |error| { + panic!( + "UI tests should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error} }}" + ) + }, + ); } fn run_fixtures(crate_name: &str, directory: &Utf8Path) -> Result<(), String> { diff --git a/crates/module_must_have_inner_docs/Cargo.toml b/crates/module_must_have_inner_docs/Cargo.toml index 284e79b42..07c6b425b 100644 --- a/crates/module_must_have_inner_docs/Cargo.toml +++ b/crates/module_must_have_inner_docs/Cargo.toml @@ -34,6 +34,7 @@ whitaker = { workspace = true, features = ["dylint-driver"], optional = true } newt-hype = "0.2" [dev-dependencies] +whitaker_test_macros = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } diff --git a/crates/module_must_have_inner_docs/src/tests/behaviour.rs b/crates/module_must_have_inner_docs/src/tests/behaviour.rs index 0e245e9c1..90569f841 100644 --- a/crates/module_must_have_inner_docs/src/tests/behaviour.rs +++ b/crates/module_must_have_inner_docs/src/tests/behaviour.rs @@ -3,10 +3,12 @@ //! These scenarios exercise the snippet classifier to ensure modules only pass //! when they begin with an inner doc comment. -use super::{ModuleDocDisposition, detect_module_docs_from_snippet}; +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; + +use super::{ModuleDocDisposition, detect_module_docs_from_snippet}; #[derive(Default)] struct ModuleWorld { @@ -34,6 +36,7 @@ impl ModuleWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> ModuleWorld { ModuleWorld::default() diff --git a/crates/module_must_have_inner_docs/src/tests/classifier.rs b/crates/module_must_have_inner_docs/src/tests/classifier.rs index 74e3e7c2b..4eddbce30 100644 --- a/crates/module_must_have_inner_docs/src/tests/classifier.rs +++ b/crates/module_must_have_inner_docs/src/tests/classifier.rs @@ -1,8 +1,9 @@ //! Unit tests for snippet-based module doc detection. -use super::{ModuleDocDisposition, detect_module_docs_from_snippet}; use rstest::rstest; +use super::{ModuleDocDisposition, detect_module_docs_from_snippet}; + #[rstest] #[case("\n \n", ModuleDocDisposition::MissingDocs)] #[case("//! module docs", ModuleDocDisposition::HasLeadingDoc)] @@ -58,7 +59,8 @@ fn rejects_mixed_case_doc_identifiers(#[case] snippet: &str) { fn accepts_nested_cfg_attr_doc() { assert_eq!( detect_module_docs_from_snippet( - "#![cfg_attr(feature = \"outer\", cfg_attr(feature = \"inner\", doc = \"Module docs\"))]" + "#![cfg_attr(feature = \"outer\", cfg_attr(feature = \"inner\", doc = \"Module \ + docs\"))]" .into() ), ModuleDocDisposition::HasLeadingDoc diff --git a/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs b/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs index 34df41221..4bc544dcd 100644 --- a/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs +++ b/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs @@ -1,9 +1,12 @@ //! Span-to-snippet tests for module doc detection fallbacks. -use super::{ModuleDocDisposition, detect_module_docs_in_span, primary_span_for_disposition}; use rstest::{fixture, rstest}; -use rustc_span::source_map::{FilePathMapping, SourceMap}; -use rustc_span::{FileName, Span}; +use rustc_span::{ + FileName, Span, + source_map::{FilePathMapping, SourceMap}, +}; + +use super::{ModuleDocDisposition, detect_module_docs_in_span, primary_span_for_disposition}; #[rstest] fn span_to_snippet_failure_returns_unknown(unresolvable_span_fixture: (SourceMap, Span)) { @@ -25,6 +28,7 @@ fn span_to_snippet_failure_skips_diagnostic(unresolvable_span_fixture: (SourceMa ); } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn unresolvable_span_fixture() -> (SourceMap, Span) { unresolvable_span() diff --git a/crates/module_must_have_inner_docs/src/tests/ui.rs b/crates/module_must_have_inner_docs/src/tests/ui.rs index 88673b054..7782bbd25 100644 --- a/crates/module_must_have_inner_docs/src/tests/ui.rs +++ b/crates/module_must_have_inner_docs/src/tests/ui.rs @@ -3,7 +3,7 @@ use rstest::rstest; use serial_test::serial; -use whitaker_common::test_support::LocaleOverride; +use whitaker_common::test_support::with_locale; /// Runs UI regression tests for the `module_must_have_inner_docs` lint under /// different locale configurations, verifying that diagnostics render correctly. @@ -12,8 +12,8 @@ use whitaker_common::test_support::LocaleOverride; /// /// - `default_locale`: Uses the default English locale (`"ui"` fixtures, `None`). /// - `welsh_locale`: Uses Welsh localization (`"ui-cy"` fixtures, `Some("cy")`). -/// - `unsupported_locale_falls_back_to_english`: Uses an unsupported locale -/// (`"xx-YY"`), expecting fallback to English (`"ui"` fixtures). +/// - `unsupported_locale_falls_back_to_english`: Uses an unsupported locale (`"xx-YY"`), expecting +/// fallback to English (`"ui"` fixtures). /// /// # Example /// @@ -27,6 +27,7 @@ use whitaker_common::test_support::LocaleOverride; #[case::unsupported_locale_falls_back_to_english("ui", Some("xx-YY"))] #[serial] fn ui_tests_across_locales(#[case] directory: &str, #[case] locale: Option<&str>) { - let _guard = locale.map(LocaleOverride::set); - whitaker::run_ui_tests!(directory).expect("UI tests should execute without diffs"); + with_locale(locale, || { + whitaker::run_ui_tests!(directory).expect("UI tests should execute without diffs"); + }); } diff --git a/crates/no_expect_outside_tests/Cargo.toml b/crates/no_expect_outside_tests/Cargo.toml index 8e32153f3..12fc5299e 100644 --- a/crates/no_expect_outside_tests/Cargo.toml +++ b/crates/no_expect_outside_tests/Cargo.toml @@ -37,6 +37,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } log = { workspace = true, optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } diff --git a/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs index 0200c8a76..8e5bdbe7b 100644 --- a/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs +++ b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs @@ -1,10 +1,14 @@ //! Coverage for dependency artefact selection and related test fixtures. -use super::dependency_rlib; +use std::{ + fs::File, + path::{Path, PathBuf}, + time::{Duration, SystemTime}, +}; + use rstest::{fixture, rstest}; -use std::fs::File; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; + +use super::dependency_rlib; /// A temporary directory that is removed automatically when dropped. #[derive(Debug)] @@ -49,6 +53,7 @@ const TIED_ARTEFACTS: [ArtefactSpec<'static>; 2] = [ /// rstest fixture that creates a uniquely named temporary directory for a /// selection test. +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn selection_directory() -> TemporaryDirectory { TemporaryDirectory::new("selection") @@ -79,11 +84,10 @@ fn resolve_dependency_rlib_selection( } #[rstest] -#[case("newest", &NEWEST_ARTEFACTS, "libtokio-newer.rlib")] -#[case("ties", &TIED_ARTEFACTS, "libtokio-alpha.rlib")] +#[case::newest(&NEWEST_ARTEFACTS, "libtokio-newer.rlib")] +#[case::ties(&TIED_ARTEFACTS, "libtokio-alpha.rlib")] fn dependency_rlib_selects_expected_artefact( selection_directory: TemporaryDirectory, - #[case] _directory_name: &str, #[case] artefacts: &[ArtefactSpec<'_>], #[case] expected_file_name: &str, ) { @@ -129,7 +133,9 @@ impl TemporaryDirectory { impl Drop for TemporaryDirectory { fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.0); + // Best-effort cleanup: the directory lives under the OS temporary root, + // so a removal failure only leaks a fixture rather than failing a test. + let _cleanup_result = std::fs::remove_dir_all(&self.0); } } @@ -154,8 +160,10 @@ fn set_modified_time(path: &Path, seconds_since_epoch: u64) { .expect("rlib fixture metadata should be readable") .accessed(); let times = existing_accessed - .map(|accessed| std::fs::FileTimes::new().set_accessed(accessed)) - .unwrap_or_else(|_| std::fs::FileTimes::new()) + .map_or_else( + |_| std::fs::FileTimes::new(), + |accessed| std::fs::FileTimes::new().set_accessed(accessed), + ) .set_modified(modified); file.set_times(times) .expect("rlib fixture modified time should be set"); diff --git a/crates/no_expect_outside_tests/src/lib_ui_tests.rs b/crates/no_expect_outside_tests/src/lib_ui_tests.rs index 64dc2e9fe..a642015fb 100644 --- a/crates/no_expect_outside_tests/src/lib_ui_tests.rs +++ b/crates/no_expect_outside_tests/src/lib_ui_tests.rs @@ -1,11 +1,14 @@ //! Additional UI-style regressions that need compiler flags or example-target //! support beyond the basic `ui/` source fixtures. +use std::{ + path::{Path, PathBuf}, + time::SystemTime, +}; + use camino::Utf8Path; use dylint_testing::ui::Test; use rstest::rstest; -use std::path::{Path, PathBuf}; -use std::time::SystemTime; use temp_env::with_vars_unset; use whitaker_common::test_support::{env_test_guard, prepare_fixture, run_test_runner}; @@ -25,7 +28,7 @@ struct ExampleHarnessRun<'a> { impl<'a> ExampleHarnessRun<'a> { /// Creates a run spec using the default `--test` harness flag. - fn new(name: &'a str, label: &'a str) -> Self { + const fn new(name: &'a str, label: &'a str) -> Self { Self { name, label, @@ -35,7 +38,7 @@ impl<'a> ExampleHarnessRun<'a> { /// Creates a run spec with caller-supplied rustc flags (no defaults /// applied). - fn with_flags(name: &'a str, label: &'a str, rustc_flags: &'a [&'a str]) -> Self { + const fn with_flags(name: &'a str, label: &'a str, rustc_flags: &'a [&'a str]) -> Self { Self { name, label, @@ -81,11 +84,15 @@ struct DependencyRlib { fn run_example_under_test_harness(spec: &ExampleHarnessRun<'_>) { let crate_name = env!("CARGO_PKG_NAME"); let directory = "examples"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, _| { + whitaker::testing::ui::run_with_runner(crate_name, directory, |_, _| { run_test_runner(spec.name, || { let _guard = env_test_guard(); with_vars_unset( - ["RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", "CARGO_BUILD_RUSTC_WRAPPER"], + [ + "RUSTC_WRAPPER", + "RUSTC_WORKSPACE_WRAPPER", + "CARGO_BUILD_RUSTC_WRAPPER", + ], || { let mut test = Test::example(crate_name, spec.name); test.rustc_flags(spec.rustc_flags); @@ -96,7 +103,8 @@ fn run_example_under_test_harness(spec: &ExampleHarnessRun<'_>) { }) .unwrap_or_else(|error| { panic!( - "{} example regression should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error:?} }}", + "{} example regression should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error:?} }}", spec.label ) }); @@ -150,9 +158,8 @@ fn run_fixture_harness_test(spec: &FixtureHarnessRun<'_>) { }) .unwrap_or_else(|error| { panic!( - "{} regression should execute without diffs: \ - RunnerFailure {{ crate_name: \"{crate_name}\", \ - directory: \"{directory}\", message: {error:?} }}", + "{} regression should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error:?} }}", spec.label ) }); @@ -252,7 +259,7 @@ fn dependency_rlib_matches(deps_dir: &Path, prefix: &str) -> Result bool { - path.file_name().is_some_and(|name| { - name.to_str() - .is_some_and(|name| name.starts_with(prefix) && name.ends_with(".rlib")) - }) + let has_rlib_extension = path + .extension() + .is_some_and(|extension| extension == "rlib"); + has_rlib_extension + && path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with(prefix)) } #[rstest] diff --git a/crates/no_expect_outside_tests/src/tests/localization.rs b/crates/no_expect_outside_tests/src/tests/localization.rs index 879375bee..5890b799f 100644 --- a/crates/no_expect_outside_tests/src/tests/localization.rs +++ b/crates/no_expect_outside_tests/src/tests/localization.rs @@ -1,20 +1,21 @@ -//! BDD-style localization tests for no_expect_outside_tests diagnostic +//! BDD-style localization tests for `no_expect_outside_tests` diagnostic //! messages. //! //! Exercises localization scenarios including locale selection, receiver type //! handling, context label generation, and error paths using `rstest-bdd` and a //! `FailingLookup` test double. +use std::cell::{Cell, Ref, RefCell}; + +use rstest::fixture; +use rstest_bdd_macros::{given, scenario, then, when}; +use whitaker_common::i18n::{BundleLookup, testing::FailingLookup}; + use super::{ I18nError, Localizer, MESSAGE_KEY, NoExpectMessages, ReceiverCategory, ReceiverLabel, context_label, fallback_messages, localised_messages, }; use crate::context::ContextSummary; -use rstest::fixture; -use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, Ref, RefCell}; -use whitaker_common::i18n::BundleLookup; -use whitaker_common::i18n::testing::FailingLookup; fn unquote(value: &str) -> &str { value @@ -25,7 +26,7 @@ fn unquote(value: &str) -> &str { fn format_receiver(receiver: &str) -> String { if receiver.is_empty() || receiver.starts_with('`') { - receiver.to_string() + receiver.to_owned() } else { format!("`{receiver}`") } @@ -65,7 +66,7 @@ impl LocalizationWorld { fn set_function(&self, name: Option<&str>) { let mut summary = self.summary.borrow_mut(); - summary.function_name = name.map(ToString::to_string); + summary.function_name = name.map(str::to_owned); } fn record_result(&self, value: Result) { @@ -91,6 +92,7 @@ impl LocalizationWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> LocalizationWorld { LocalizationWorld::default() @@ -108,8 +110,12 @@ fn given_receiver(world: &LocalizationWorld, receiver: String) { #[given("the function context is {name}")] fn given_function(world: &LocalizationWorld, name: String) { - let name = unquote(&name); - let value = if name.is_empty() { None } else { Some(name) }; + let function_name = unquote(&name); + let value = if function_name.is_empty() { + None + } else { + Some(function_name) + }; world.set_function(value); } @@ -138,7 +144,7 @@ fn given_failure(world: &LocalizationWorld) { world.failing.set(true); } -#[when("I localise the expect diagnostic")] +#[when("I localize the expect diagnostic")] fn when_localize(world: &LocalizationWorld) { let receiver = world.receiver.borrow().clone(); let summary = world.summary.borrow().clone(); @@ -155,31 +161,31 @@ fn when_localize(world: &LocalizationWorld) { #[then("the diagnostic mentions {snippet}")] fn then_primary(world: &LocalizationWorld, snippet: String) { - let snippet = normalize_for_assertion(unquote(&snippet)); + let expected = normalize_for_assertion(unquote(&snippet)); let primary = normalize_for_assertion(world.messages().primary()); assert!( - primary.contains(&snippet), - "primary message `{primary}` did not contain `{snippet}`" + primary.contains(&expected), + "primary message `{primary}` did not contain `{expected}`" ); } #[then("the note references {snippet}")] fn then_note(world: &LocalizationWorld, snippet: String) { - let snippet = normalize_for_assertion(unquote(&snippet)); + let expected = normalize_for_assertion(unquote(&snippet)); let note = normalize_for_assertion(world.messages().note()); assert!( - note.contains(&snippet), - "note `{note}` did not contain `{snippet}`" + note.contains(&expected), + "note `{note}` did not contain `{expected}`" ); } #[then("the help references {snippet}")] fn then_help(world: &LocalizationWorld, snippet: String) { - let snippet = normalize_for_assertion(unquote(&snippet)); + let expected = normalize_for_assertion(unquote(&snippet)); let help = normalize_for_assertion(world.messages().help()); assert!( - help.contains(&snippet), - "help `{help}` did not contain `{snippet}`" + help.contains(&expected), + "help `{help}` did not contain `{expected}`" ); } @@ -194,10 +200,13 @@ fn then_receiver_type_edge_cases_are_handled(world: &LocalizationWorld) { #[then("localization fails for {key}")] fn then_failure(world: &LocalizationWorld, key: String) { - let key = unquote(&key); + let expected_key = unquote(&key); let error = world.error(); match &*error { - I18nError::MissingMessage { key: missing, .. } => assert_eq!(missing, key), + I18nError::MissingMessage { key: missing, .. } => assert_eq!( + missing, &expected_key, + "localization should fail for the requested key" + ), } } @@ -238,7 +247,7 @@ fn scenario_failure(world: LocalizationWorld) { #[then("the fallback help mentions {snippet}")] fn then_fallback(world: &LocalizationWorld, snippet: String) { - let snippet = normalize_for_assertion(unquote(&snippet)); + let expected = normalize_for_assertion(unquote(&snippet)); let summary = world.summary.borrow().clone(); let context = context_label(&summary); let receiver = world.receiver.borrow().clone(); @@ -246,8 +255,8 @@ fn then_fallback(world: &LocalizationWorld, snippet: String) { let fallback = fallback_messages(&receiver, &context, category); let help = normalize_for_assertion(fallback.help()); assert!( - help.contains(&snippet), - "fallback help `{help}` did not contain `{snippet}`" + help.contains(&expected), + "fallback help `{help}` did not contain `{expected}`" ); } diff --git a/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs b/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs index 10e7761c1..b4d40b19f 100644 --- a/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs +++ b/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs @@ -1,9 +1,10 @@ //! Edge-case localization tests covering unusual receiver labels. +use rstest::rstest; + use super::{ ContextLabel, Localizer, NoExpectMessages, ReceiverCategory, ReceiverLabel, localised_messages, }; -use rstest::rstest; #[rstest] #[case("", "the surrounding scope", |messages: &NoExpectMessages| !messages.primary().is_empty())] diff --git a/crates/no_expect_outside_tests/tests/features/context_summary.feature b/crates/no_expect_outside_tests/tests/features/context_summary.feature index a9453c234..ae6b995a9 100644 --- a/crates/no_expect_outside_tests/tests/features/context_summary.feature +++ b/crates/no_expect_outside_tests/tests/features/context_summary.feature @@ -1,4 +1,4 @@ -Feature: Summarise traversal context for `.expect(..)` linting +Feature: Summarize traversal context for `.expect(..)` linting Scenario: Plain function without test attributes Given a non-test function named handler @@ -18,7 +18,7 @@ Feature: Summarise traversal context for `.expect(..)` linting Then the context is marked as test And no function name is recorded - Scenario: Function recognised via configured attribute + Scenario: Function recognized via configured attribute Given an additional test attribute custom::test is configured And a function annotated with the additional attribute custom::test When I summarise the context diff --git a/crates/no_expect_outside_tests/tests/features/localization.feature b/crates/no_expect_outside_tests/tests/features/localization.feature index b6075630d..6ebdf5bf9 100644 --- a/crates/no_expect_outside_tests/tests/features/localization.feature +++ b/crates/no_expect_outside_tests/tests/features/localization.feature @@ -1,9 +1,9 @@ -Feature: Localised diagnostics for expect usage +Feature: Localized diagnostics for expect usage Scenario: English fallback locale Given the locale "en-GB" is selected And the receiver type is "Result" And the function context is "handler" - When I localise the expect diagnostic + When I localize the expect diagnostic Then the diagnostic mentions "calling expect on `Result`" And the note references "function `handler`" And the help references "`Result`" @@ -13,7 +13,7 @@ Feature: Localised diagnostics for expect usage Given the locale "cy" is selected And the receiver type is "Option" And the function context is "" - When I localise the expect diagnostic + When I localize the expect diagnostic Then the diagnostic mentions "Peidiwch" And the note references "Daw’r galwad" @@ -21,7 +21,7 @@ Feature: Localised diagnostics for expect usage Given the locale "zz" is selected And the receiver type is "Result" And the call occurs outside any function - When I localise the expect diagnostic + When I localize the expect diagnostic Then the diagnostic mentions "calling expect on `Result`" And the fallback help mentions "`Result`" And the fallback help mentions "`Err` variant" @@ -30,26 +30,26 @@ Feature: Localised diagnostics for expect usage Given the locale "en-GB" is selected And the receiver type is empty And the function context is "" - When I localise the expect diagnostic + When I localize the expect diagnostic Then the fallback and localization logic should handle the receiver type robustly Scenario: Receiver type is malformed Given the locale "en-GB" is selected And the receiver type is malformed And the function context is "worker" - When I localise the expect diagnostic + When I localize the expect diagnostic Then the fallback and localization logic should handle the receiver type robustly Scenario: Receiver type is unexpected Given the locale "en-GB" is selected And the receiver type is unexpected And the function context is "handler" - When I localise the expect diagnostic + When I localize the expect diagnostic Then the fallback and localization logic should handle the receiver type robustly Scenario: Localization failure surfaces missing message Given localization fails And the receiver type is "Result<(), ()>" And the function context is "worker" - When I localise the expect diagnostic + When I localize the expect diagnostic Then localization fails for "no_expect_outside_tests" diff --git a/crates/no_std_fs_operations/src/config_tests.rs b/crates/no_std_fs_operations/src/config_tests.rs index fd5e77f8e..65395732c 100644 --- a/crates/no_std_fs_operations/src/config_tests.rs +++ b/crates/no_std_fs_operations/src/config_tests.rs @@ -9,25 +9,25 @@ //! behavioural tests validating new functionality. End-to-end integration testing //! of the exclusion feature is not feasible with `dylint_testing` because: //! -//! 1. **Technical constraint**: The `dylint_testing` harness uses `CARGO_PKG_NAME` -//! at compile time, preventing fixture crates from having controlled names. +//! 1. **Technical constraint**: The `dylint_testing` harness uses `CARGO_PKG_NAME` at compile time, +//! preventing fixture crates from having controlled names. //! -//! 2. **Unit test coverage is sufficient**: The exclusion implementation is -//! straightforward—when `self.excluded` is true, `emit_optional` returns early -//! (see `driver.rs`). All configuration parsing, deserialization, and matching -//! logic is fully validated by the tests below. +//! 2. **Unit test coverage is sufficient**: The exclusion implementation is straightforward—when +//! `self.excluded` is true, `emit_optional` returns early (see `driver.rs`). All configuration +//! parsing, deserialization, and matching logic is fully validated by the tests below. //! -//! 3. **Behaviour is deterministic**: The `check_crate` method sets `self.excluded` -//! based on `config.is_excluded(crate_name)`, which is exhaustively tested here. +//! 3. **Behaviour is deterministic**: The `check_crate` method sets `self.excluded` based on +//! `config.is_excluded(crate_name)`, which is exhaustively tested here. //! //! The integration tests in `tests/integration_exclusion.rs` provide additional //! coverage by invoking `cargo dylint` on fixture projects with real exclusion //! configurations. -use super::*; +use std::{collections::HashSet, io}; + use rstest::rstest; -use std::collections::HashSet; -use std::io; + +use super::*; #[test] fn config_default_has_empty_excluded_crates() { @@ -40,8 +40,8 @@ fn config_default_has_empty_excluded_paths() { } #[rstest] -#[case::empty_config(r#""#, &[])] -#[case::empty_excluded(r#"excluded_crates = []"#, &[])] +#[case::empty_config(r"", &[])] +#[case::empty_excluded(r"excluded_crates = []", &[])] #[case::single_crate(r#"excluded_crates = ["foo"]"#, &["foo"])] #[case::multiple_crates(r#"excluded_crates = ["foo", "bar", "baz"]"#, &["foo", "bar", "baz"])] fn config_deserializes_excluded_crates(#[case] toml: &str, #[case] expected: &[&str]) { @@ -56,8 +56,8 @@ fn config_deserializes_excluded_crates(#[case] toml: &str, #[case] expected: &[& } #[rstest] -#[case::empty_config(r#""#, &[])] -#[case::empty_excluded(r#"excluded_paths = []"#, &[])] +#[case::empty_config(r"", &[])] +#[case::empty_excluded(r"excluded_paths = []", &[])] #[case::single_path(r#"excluded_paths = ["my_app::legacy_io"]"#, &["my_app::legacy_io"])] #[case::multiple_paths( r#"excluded_paths = ["my_app::legacy_io", "my_app::bin::migrate"]"#, @@ -104,7 +104,7 @@ fn legacy_config_without_excluded_paths_still_parses() { #[rstest] #[case::wrong_type(r#"excluded_paths = "not_an_array""#)] -#[case::wrong_element_type(r#"excluded_paths = [1, 2, 3]"#)] +#[case::wrong_element_type(r"excluded_paths = [1, 2, 3]")] #[case::mixed_element_types(r#"excluded_paths = ["my_app::legacy_io", 1]"#)] fn config_rejects_invalid_excluded_paths(#[case] toml: &str) { assert!( @@ -136,9 +136,9 @@ fn path_exclusions_reflect_configuration( } #[rstest] -#[case::unknown_field(r#"unknown_field = true"#)] +#[case::unknown_field(r"unknown_field = true")] #[case::wrong_type(r#"excluded_crates = "not_an_array""#)] -#[case::wrong_element_type(r#"excluded_crates = [1, 2, 3]"#)] +#[case::wrong_element_type(r"excluded_crates = [1, 2, 3]")] fn config_rejects_invalid_toml(#[case] toml: &str) { assert!( toml::from_str::(toml).is_err(), diff --git a/crates/no_std_fs_operations/src/tests/ui.rs b/crates/no_std_fs_operations/src/tests/ui.rs index cc86012c6..0ef049167 100644 --- a/crates/no_std_fs_operations/src/tests/ui.rs +++ b/crates/no_std_fs_operations/src/tests/ui.rs @@ -1,7 +1,7 @@ //! UI regression tests for the `no_std_fs_operations` lint. use serial_test::serial; -use whitaker_common::test_support::LocaleOverride; +use whitaker_common::test_support::with_locale; #[test] #[serial] @@ -28,6 +28,7 @@ fn ui_runs_in_fallback_locale() { } fn run_with_locale(directory: &str, locale: Option<&str>) { - let _locale_guard = locale.map(LocaleOverride::set); - whitaker::run_ui_tests!(directory).expect("UI tests should execute without diffs"); + with_locale(locale, || { + whitaker::run_ui_tests!(directory).expect("UI tests should execute without diffs"); + }); } diff --git a/crates/no_std_fs_operations/tests/integration_exclusion.rs b/crates/no_std_fs_operations/tests/integration_exclusion.rs index 0741591ec..e40a88821 100644 --- a/crates/no_std_fs_operations/tests/integration_exclusion.rs +++ b/crates/no_std_fs_operations/tests/integration_exclusion.rs @@ -12,11 +12,13 @@ //! These tests are marked `#[ignore]` by default because they require external //! dependencies. Run with `--ignored` to execute. -use std::env; -use std::io::Cursor; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::OnceLock; +use std::{ + env, + io::Cursor, + path::{Path, PathBuf}, + process::Command, + sync::OnceLock, +}; use anyhow::Context as _; use cargo_metadata::{Message, Metadata, MetadataCommand}; @@ -124,8 +126,8 @@ fn find_cdylib_in_artefacts( stdout: &[u8], package_id: &cargo_metadata::PackageId, ) -> anyhow::Result { - for message in Message::parse_stream(Cursor::new(stdout)) { - let message = message.context("failed to parse cargo build JSON output")?; + for parsed in Message::parse_stream(Cursor::new(stdout)) { + let message = parsed.context("failed to parse cargo build JSON output")?; let Message::CompilerArtifact(artefact) = message else { continue; }; @@ -198,7 +200,7 @@ fn diagnostic_count(output: &[u8]) -> Result { Ok(messages .into_iter() .filter_map(|message| match message { - Message::CompilerMessage(message) => Some(message.message), + Message::CompilerMessage(compiler_message) => Some(compiler_message.message), _ => None, }) .filter(|diagnostic| { @@ -219,12 +221,12 @@ fn redact_path_prefix(value: serde_json::Value, prefix: &str) -> serde_json::Val } serde_json::Value::Array(arr) => serde_json::Value::Array( arr.into_iter() - .map(|value| redact_path_prefix(value, prefix)) + .map(|element| redact_path_prefix(element, prefix)) .collect(), ), serde_json::Value::Object(map) => serde_json::Value::Object( map.into_iter() - .map(|(key, value)| (key, redact_path_prefix(value, prefix))) + .map(|(key, entry)| (key, redact_path_prefix(entry, prefix))) .collect(), ), other => other, @@ -234,7 +236,8 @@ fn redact_path_prefix(value: serde_json::Value, prefix: &str) -> serde_json::Val #[expect( clippy::useless_asref, clippy::redundant_closure, - reason = "anyhow::Error is not Clone, so .as_ref().map(Clone::clone) is necessary to convert &Result into Result" + reason = "anyhow::Error is not Clone, so .as_ref().map(Clone::clone) is necessary to convert \ + &Result into Result" )] fn lint_library_path() -> anyhow::Result { static LINT_LIBRARY_PATH: OnceLock> = OnceLock::new(); @@ -246,6 +249,7 @@ fn lint_library_path() -> anyhow::Result { .map_err(|e| anyhow::anyhow!("{e:#}")) } +#[derive(Clone, Copy)] struct Expectation { should_emit_diagnostics: bool, should_succeed: bool, @@ -355,19 +359,19 @@ fn assert_fixture_behaviour( ) -> anyhow::Result<()> { let (is_success, count) = evaluate_fixture(fixture_dir, lint_library_path, crate_name)?; - assert!( + anyhow::ensure!( is_success == expectation.should_succeed, "crate `{crate_name}` should return success={}", expectation.should_succeed ); if expectation.should_emit_diagnostics { - assert!( + anyhow::ensure!( count > 0, "crate `{crate_name}` should emit `no_std_fs_operations` diagnostics" ); } else { - assert!( + anyhow::ensure!( count == 0, "crate `{crate_name}` should emit zero `no_std_fs_operations` diagnostics" ); @@ -400,7 +404,8 @@ fn non_excluded_crate_diagnostics_match_snapshot() -> anyhow::Result<()> { .collect::, _>>() .with_context(|| { format!( - "non_excluded_crate_diagnostics_match_snapshot produced malformed cargo output\nstderr:\n{}", + "non_excluded_crate_diagnostics_match_snapshot produced malformed cargo \ + output\nstderr:\n{}", result.stderr ) })?; @@ -408,16 +413,16 @@ fn non_excluded_crate_diagnostics_match_snapshot() -> anyhow::Result<()> { let diagnostics: Vec = messages .into_iter() .filter_map(|message| match message { - Message::CompilerMessage(message) - if message + Message::CompilerMessage(compiler_message) + if compiler_message .message .code .as_ref() .is_some_and(|code| code.code == LINT_CRATE_NAME) => { Some( - serde_json::to_value(message.message) - .context("failed to serialise diagnostic for snapshot"), + serde_json::to_value(compiler_message.message) + .context("failed to serialize diagnostic for snapshot"), ) } _ => None, diff --git a/crates/no_std_fs_operations/tests/no_default_features_build.rs b/crates/no_std_fs_operations/tests/no_default_features_build.rs index 5a1a7e276..833472309 100644 --- a/crates/no_std_fs_operations/tests/no_default_features_build.rs +++ b/crates/no_std_fs_operations/tests/no_default_features_build.rs @@ -15,8 +15,7 @@ //! the Makefile gate applies) but uses an isolated target directory so it never //! contends with the outer build. -use std::path::PathBuf; -use std::process::Command; +use std::{path::PathBuf, process::Command}; use anyhow::Context as _; use serde_json::Value; @@ -50,23 +49,24 @@ fn crate_builds_without_dylint_driver_feature() -> anyhow::Result<()> { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - output.status.success(), - "`cargo check --no-default-features --lib` for `{CRATE}` failed \ - (it must compile without the `dylint-driver` feature now that the \ - no-driver stub is gone):\n{stderr}" - ); + if !output.status.success() { + anyhow::bail!( + "`cargo check --no-default-features --lib` for `{CRATE}` failed (it must compile \ + without the `dylint-driver` feature now that the no-driver stub is gone):\n{stderr}" + ); + } let diagnostics = stdout .lines() .filter_map(|line| serde_json::from_str::(line).ok()) .filter(|message| message["reason"] == "compiler-message") .collect::>(); - assert!( - diagnostics.is_empty(), - "`cargo check --no-default-features --lib` for `{CRATE}` emitted \ - compiler diagnostics under `-D warnings`: {diagnostics:#?}" - ); + if !diagnostics.is_empty() { + anyhow::bail!( + "`cargo check --no-default-features --lib` for `{CRATE}` emitted compiler diagnostics \ + under `-D warnings`: {diagnostics:#?}" + ); + } Ok(()) } diff --git a/crates/no_std_fs_operations/tests/test_support/mod.rs b/crates/no_std_fs_operations/tests/test_support/mod.rs index 55778e5bc..6b132c873 100644 --- a/crates/no_std_fs_operations/tests/test_support/mod.rs +++ b/crates/no_std_fs_operations/tests/test_support/mod.rs @@ -1,8 +1,10 @@ //! Shared, test-only fixture helpers for `no_std_fs_operations` integration //! tests. -use std::fs; -use std::path::{Path, PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use anyhow::Context as _; use tempfile::TempDir; @@ -35,7 +37,7 @@ pub(super) enum FixtureKind { impl FixtureKind { /// Short label naming the mechanism, used in error context. - pub(super) fn label(self) -> &'static str { + pub(super) const fn label(self) -> &'static str { match self { Self::CrateExclusion => "crate", Self::PathExclusion => "path", @@ -220,12 +222,13 @@ mod tests { let config = fixture_dylint_config(crate_name, FixtureKind::CrateExclusion, true); let parsed: toml::Value = toml::from_str(&config).expect("config should parse as TOML"); - assert_eq!( - parsed["no_std_fs_operations"]["excluded_crates"][0] - .as_str() - .expect("excluded crate should be a string"), - crate_name - ); + let excluded_crate = parsed + .get("no_std_fs_operations") + .and_then(|section| section.get("excluded_crates")) + .and_then(|entries| entries.get(0)) + .and_then(toml::Value::as_str) + .expect("excluded crate should be a string"); + assert_eq!(excluded_crate, crate_name); assert!(parsed.get("other").is_none(), "config was:\n{config}"); assert!(parsed.get("injected").is_none(), "config was:\n{config}"); } @@ -237,14 +240,17 @@ mod tests { let manifest = std::fs::read_to_string(fixture.root().join("Cargo.toml"))?; let parsed: toml::Value = toml::from_str(&manifest)?; - assert_eq!( - parsed["package"]["name"] - .as_str() - .expect("package name should be a string"), - crate_name + let package_name = parsed + .get("package") + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .expect("package name should be a string"); + anyhow::ensure!( + package_name == crate_name, + "manifest package name should round-trip, manifest was:\n{manifest}" ); - assert!(parsed.get("other").is_none(), "manifest was:\n{manifest}"); - assert!( + anyhow::ensure!(parsed.get("other").is_none(), "manifest was:\n{manifest}"); + anyhow::ensure!( parsed.get("injected").is_none(), "manifest was:\n{manifest}" ); @@ -257,12 +263,13 @@ mod tests { let config = fixture_dylint_config("my_app", FixtureKind::PathExclusion, true); let parsed: toml::Value = toml::from_str(&config).expect("config should parse as TOML"); - assert_eq!( - parsed["no_std_fs_operations"]["excluded_paths"][0] - .as_str() - .expect("excluded path should be a string"), - "my_app::guarded" - ); + let excluded_path = parsed + .get("no_std_fs_operations") + .and_then(|section| section.get("excluded_paths")) + .and_then(|entries| entries.get(0)) + .and_then(toml::Value::as_str) + .expect("excluded path should be a string"); + assert_eq!(excluded_path, "my_app::guarded"); } #[test] @@ -270,11 +277,14 @@ mod tests { let config = fixture_dylint_config("my_app", FixtureKind::PathExclusion, false); let parsed: toml::Value = toml::from_str(&config).expect("config should parse as TOML"); + let excluded_paths = parsed + .get("no_std_fs_operations") + .and_then(|section| section.get("excluded_paths")) + .and_then(toml::Value::as_array) + .expect("excluded_paths should be an array"); assert!( - parsed["no_std_fs_operations"]["excluded_paths"] - .as_array() - .expect("excluded_paths should be an array") - .is_empty() + excluded_paths.is_empty(), + "no paths should be excluded, config was:\n{config}" ); } @@ -285,22 +295,20 @@ mod tests { let source = std::fs::read_to_string(fixture.root().join("src/lib.rs"))?; // The fixture must declare the module the config excludes. - let guarded_start = source.find("pub mod guarded").unwrap_or_else(|| { - panic!("fixture should declare a guarded module, source was:\n{source}") - }); + let Some(guarded_start) = source.find("pub mod guarded") else { + anyhow::bail!("fixture should declare a guarded module, source was:\n{source}"); + }; // The `std::fs` usage must be the *only* one, and must sit inside the // guarded module. Otherwise a passing exclusion test could reflect an // accidental global suppression (or a stray crate-root usage) rather // than genuine module-scoped suppression. let fs_usages: Vec = source.match_indices("std::fs").map(|(i, _)| i).collect(); - assert_eq!( - fs_usages.len(), - 1, - "expected exactly one std::fs usage, source was:\n{source}" - ); - assert!( - fs_usages[0] > guarded_start, + let [fs_usage] = fs_usages.as_slice() else { + anyhow::bail!("expected exactly one std::fs usage, source was:\n{source}"); + }; + anyhow::ensure!( + *fs_usage > guarded_start, "the std::fs usage must sit inside the guarded module, source was:\n{source}" ); Ok(()) diff --git a/crates/no_unwrap_or_else_panic/Cargo.toml b/crates/no_unwrap_or_else_panic/Cargo.toml index 2b30b4ec3..4f15d343c 100644 --- a/crates/no_unwrap_or_else_panic/Cargo.toml +++ b/crates/no_unwrap_or_else_panic/Cargo.toml @@ -39,6 +39,7 @@ whitaker = { workspace = true, features = ["dylint-driver"], optional = true } clippy_utils = { workspace = true, optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } camino = { workspace = true } dylint_testing = { workspace = true } rstest = { workspace = true } diff --git a/crates/no_unwrap_or_else_panic/src/lib_ui_tests.rs b/crates/no_unwrap_or_else_panic/src/lib_ui_tests.rs index dd67f9a4a..3783215a0 100644 --- a/crates/no_unwrap_or_else_panic/src/lib_ui_tests.rs +++ b/crates/no_unwrap_or_else_panic/src/lib_ui_tests.rs @@ -1,11 +1,11 @@ //! UI harness for `no_unwrap_or_else_panic` fixtures. +use std::{fs, io, path::Path}; + use camino::Utf8Path; use dylint_testing::ui::Test; #[cfg(not(windows))] use rstest::rstest; -use std::path::Path; -use std::{fs, io}; #[cfg(not(windows))] use temp_env::with_vars_unset; #[cfg(not(windows))] @@ -30,7 +30,7 @@ struct ExampleHarnessRun<'a> { #[cfg(not(windows))] impl<'a> ExampleHarnessRun<'a> { /// Creates a run spec using the default `--test` harness flag. - fn new(name: &'a str, label: &'a str) -> Self { + const fn new(name: &'a str, label: &'a str) -> Self { Self { name, label, @@ -40,7 +40,7 @@ impl<'a> ExampleHarnessRun<'a> { /// Creates a run spec with caller-supplied rustc flags (no defaults /// applied). - fn with_flags(name: &'a str, label: &'a str, rustc_flags: &'a [&'a str]) -> Self { + const fn with_flags(name: &'a str, label: &'a str, rustc_flags: &'a [&'a str]) -> Self { Self { name, label, @@ -53,14 +53,14 @@ impl<'a> ExampleHarnessRun<'a> { fn ui() { let crate_name = env!("CARGO_PKG_NAME"); let directory = "ui"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, dir| { - run_fixtures(crate_name, dir) - }) - .unwrap_or_else(|error| { - panic!( - "UI tests should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error} }}" - ) - }); + whitaker::testing::ui::run_with_runner(crate_name, directory, run_fixtures).unwrap_or_else( + |error| { + panic!( + "UI tests should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error} }}" + ) + }, + ); } /// Runs an example-based regression under the dylint UI test harness. @@ -71,7 +71,7 @@ fn ui() { fn run_example_under_test_harness(spec: &ExampleHarnessRun<'_>) { let crate_name = env!("CARGO_PKG_NAME"); let directory = "examples"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, _| { + whitaker::testing::ui::run_with_runner(crate_name, directory, |_, _| { run_test_runner(spec.name, || { let _guard = env_test_guard(); with_vars_unset( @@ -93,7 +93,8 @@ fn run_example_under_test_harness(spec: &ExampleHarnessRun<'_>) { }) .unwrap_or_else(|error| { panic!( - "{} example regression should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error:?} }}", + "{} example regression should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error:?} }}", spec.label ) }); diff --git a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs index dcdaef7bf..5f0d6a065 100644 --- a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs +++ b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs @@ -1,11 +1,15 @@ //! Behaviour-driven coverage for lint decision logic. -use crate::context::ContextSummary; -use crate::panic_detector::PanicInfo; -use crate::policy::{LintPolicy, should_flag}; +use std::cell::Cell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::Cell; + +use crate::{ + context::ContextSummary, + panic_detector::PanicInfo, + policy::{LintPolicy, should_flag}, +}; #[derive(Default)] struct DecisionWorld { @@ -28,6 +32,7 @@ impl DecisionWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> DecisionWorld { DecisionWorld::default() diff --git a/crates/rstest_helper_should_be_fixture/src/collector_tests.rs b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs index af287e963..23bc3085a 100644 --- a/crates/rstest_helper_should_be_fixture/src/collector_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs @@ -6,9 +6,12 @@ //! collector module, while these tests keep the record store cheap to exercise //! without constructing a rustc lint context. +use proptest::prelude::*; use rstest::rstest; -use rustc_hir::ItemLocalId; -use rustc_hir::def_id::{DefId, DefIndex}; +use rustc_hir::{ + ItemLocalId, + def_id::{DefId, DefIndex}, +}; use rustc_span::{BytePos, DUMMY_SP, FileName, Span}; use whitaker_common::rstest::{ArgAtom, ArgFingerprint}; @@ -17,8 +20,6 @@ use super::{ should_skip_arg_for_unrecoverable_span, }; -use proptest::prelude::*; - #[test] fn collector_iterates_callees_in_definition_path_order() { let mut collector = CallSiteCollector::default(); @@ -34,7 +35,7 @@ fn collector_iterates_callees_in_definition_path_order() { let keys = collector .iter() - .map(|(callee, _)| callee.to_string()) + .map(|(callee, _)| callee.to_owned()) .collect::>(); assert_eq!(keys, ["crate::a_helper", "crate::z_helper"]); @@ -111,7 +112,7 @@ fn collector_orders_large_single_callee_bucket_by_span() { #[test] fn literal_lowering_records_const_lit_atom() { assert_eq!( - literal_text_atom("\"literal\"".to_string()), + literal_text_atom("\"literal\"".to_owned()), ArgAtom::const_lit("\"literal\""), ); } @@ -162,7 +163,7 @@ fn location_with_hir_id( hir_local_id: u32, ) -> CallSiteLocation { CallSiteLocation::new( - callee.to_string(), + callee.to_owned(), source_file(), Span::with_root_ctxt(lo, hi), ItemLocalId::from_u32(hir_local_id), @@ -170,7 +171,7 @@ fn location_with_hir_id( } fn source_file() -> FileName { - FileName::Custom("src/lib.rs".to_string()) + FileName::Custom("src/lib.rs".to_owned()) } proptest! { @@ -232,11 +233,11 @@ fn collect_spans(spans: &[(u32, u32, u32)]) -> Vec<(String, Vec<(u32, u32)>)> { collector .iter() .map(|(callee, records)| { - let spans = records + let recorded_spans = records .iter() .map(|record| (record.span.lo().0, record.span.hi().0)) .collect(); - (callee.to_string(), spans) + (callee.to_owned(), recorded_spans) }) .collect() } diff --git a/crates/rstest_helper_should_be_fixture/src/driver_tests.rs b/crates/rstest_helper_should_be_fixture/src/driver_tests.rs index ecc59bbec..b855167f1 100644 --- a/crates/rstest_helper_should_be_fixture/src/driver_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/driver_tests.rs @@ -2,7 +2,7 @@ //! `rstest` detection option construction. //! //! NOTE: `SharedConfig::load` is treated as infallible at the driver call site -//! pending https://github.com/leynos/whitaker/issues/233. +//! pending . use proptest::prelude::*; use rstest::rstest; @@ -69,24 +69,24 @@ fn normalizes_numeric_thresholds_to_two() { } #[rstest] -#[case::plain(vec!["case".to_string()], vec!["case"])] -#[case::qualified(vec!["rstest::values".to_string()], vec!["values"])] +#[case::plain(vec!["case".to_owned()], vec!["case"])] +#[case::qualified(vec!["rstest::values".to_owned()], vec!["values"])] #[case::mixed_equivalent_spellings( - vec!["case".to_string(), "rstest::case".to_string()], + vec!["case".to_owned(), "rstest::case".to_owned()], vec!["case"] )] -#[case::blank(vec![" ".to_string()], vec!["case", "values", "files", "future", "context"])] +#[case::blank(vec![" ".to_owned()], vec!["case", "values", "files", "future", "context"])] fn normalizes_provider_attributes(#[case] input: Vec, #[case] expected: Vec<&str>) { let normalized = normalize_provider_attributes(input); - let expected: Vec = expected.into_iter().map(ToString::to_string).collect(); + let expected_owned: Vec = expected.into_iter().map(str::to_owned).collect(); - assert_eq!(normalized, expected); + assert_eq!(normalized, expected_owned); } #[rstest] fn detection_options_expand_plain_and_rstest_qualified_provider_paths() { let config = Config { - provider_param_attributes: vec!["case".to_string(), "custom".to_string()], + provider_param_attributes: vec!["case".to_owned(), "custom".to_owned()], use_source_callee_fallback: true, ..Config::default() }; @@ -133,7 +133,7 @@ fn loaded_configuration_normalizes_present_config() { let config = Config { min_calls: 1, min_distinct_tests: 1, - provider_param_attributes: vec!["rstest::case".to_string()], + provider_param_attributes: vec!["rstest::case".to_owned()], ..Config::default() }; @@ -149,7 +149,7 @@ fn loaded_configuration_normalizes_present_config() { fn applying_crate_configuration_initializes_pass_state() { let mut pass = RstestHelperShouldBeFixture::default(); let config = Config { - provider_param_attributes: vec!["custom".to_string()], + provider_param_attributes: vec!["custom".to_owned()], use_source_callee_fallback: true, ..Config::default() }; @@ -167,7 +167,7 @@ fn check_crate_configuration_loads_and_normalizes_config() { let config = Config { min_calls: 0, min_distinct_tests: 1, - provider_param_attributes: vec!["rstest::case".to_string()], + provider_param_attributes: vec!["rstest::case".to_owned()], use_source_callee_fallback: true, ..Config::default() }; diff --git a/crates/rstest_helper_should_be_fixture/tests/ui.rs b/crates/rstest_helper_should_be_fixture/tests/ui.rs index 33f66f2d0..25c39ca4a 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui.rs @@ -11,13 +11,15 @@ #[cfg(feature = "dylint-driver")] extern crate rustc_driver; -use dylint_testing::ui::Test; -use rstest::rstest; -use std::path::PathBuf; -use std::sync::atomic::{AtomicU64, Ordering}; -use whitaker_common::test_support::{EnvVarGuard, run_test_runner}; +use std::{ + path::PathBuf, + sync::atomic::{AtomicU64, Ordering}, +}; +use dylint_testing::ui::Test; use harness_lock::ExampleHarnessLock; +use rstest::rstest; +use whitaker_common::test_support::{run_test_runner, with_env_var}; // Internal test-only hook mirrored in the lint driver. It asks // `check_crate_post` to append redacted, shape-only passive collection @@ -41,7 +43,11 @@ fn example_harness_collects_call_site_evidence() { for expected in [ "callee_count=3", "record_count=9", - "callee=Builder::<'_>::build;records=2\nfingerprint=unsupported,fixture-local\nfingerprint=unsupported,fixture-local", + concat!( + "callee=Builder::<'_>::build;records=2\n", + "fingerprint=unsupported,fixture-local\n", + "fingerprint=unsupported,fixture-local", + ), "callee=helper;records=2", "callee=nested_helper;records=5", "fingerprint=unsupported,fixture-local", @@ -72,29 +78,34 @@ fn trybuild_fixtures_compile_without_diagnostics() { /// releasing the lock mid-assertion, which would let a concurrent run append to /// the same summary path. struct ExampleHarness { - _lock: ExampleHarnessLock, + lock: ExampleHarnessLock, } impl ExampleHarness { fn acquire() -> Self { - let lock = ExampleHarnessLock::acquire().expect("example harness lock should be acquired"); - Self { _lock: lock } + match ExampleHarnessLock::acquire() { + Ok(lock) => Self { lock }, + Err(error) => panic!("example harness lock should be acquired: {error}"), + } } /// Compiles and runs one example while the lock is held. fn run_example(&self, example: &str) { let crate_name = env!("CARGO_PKG_NAME"); let directory = "examples"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, _| { + let lock_path = self.lock.path().display(); + whitaker::testing::ui::run_with_runner(crate_name, directory, |runner_crate, _| { run_test_runner(example, || { - let mut test = Test::example(crate_name, example); + let mut test = Test::example(runner_crate, example); test.rustc_flags(["--test"]); test.run(); }) }) .unwrap_or_else(|error| { panic!( - "UI tests should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error} }}" + "UI tests should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", lock: \"{lock_path}\", message: \ + {error} }}" ) }); } @@ -102,17 +113,20 @@ impl ExampleHarness { /// Runs `example` with the collection-summary env var pointed at a fresh /// path, returning the appended summary text. /// - /// The env-var guard and the harness lock are both held across the run and - /// the read, and the summary file is removed before returning, so no - /// concurrently scheduled run can append to the same path mid-read. + /// The scoped env override and the harness lock are both held across the + /// run and the read, and the summary file is removed before returning, so + /// no concurrently scheduled run can append to the same path mid-read. fn collect_summary(&self, example: &str) -> String { let summary_path = unique_summary_path(); - let _guard = EnvVarGuard::set(COLLECTION_SUMMARY_ENV, summary_path.as_os_str()); - self.run_example(example); - let summary = - std::fs::read_to_string(&summary_path).expect("collection summary should be written"); - let _ = std::fs::remove_file(&summary_path); - summary + with_env_var(COLLECTION_SUMMARY_ENV, summary_path.as_os_str(), || { + self.run_example(example); + let summary = match std::fs::read_to_string(&summary_path) { + Ok(summary) => summary, + Err(error) => panic!("collection summary should be written: {error}"), + }; + let _cleanup_result = std::fs::remove_file(&summary_path); + summary + }) } } diff --git a/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs b/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs index 553d9354d..bc306996b 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs @@ -6,24 +6,27 @@ //! tests that exercise it — so `ui.rs` stays focused on the example and //! trybuild assertions. +use std::{ + fs::{File, OpenOptions}, + io, + path::{Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + use filetime::{FileTime, set_file_mtime}; use fs2::FileExt; use log::debug; use rstest::rstest; -use std::fs::{File, OpenOptions}; -use std::io; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; // The example harness lock coordinates separate nextest processes. Windows CI // can legitimately hold it for several minutes, so only remove directories // old enough to be abandoned by a crashed process. -const EXAMPLE_HARNESS_LOCK_STALE_AFTER: Duration = Duration::from_secs(30 * 60); +const EXAMPLE_HARNESS_LOCK_STALE_AFTER: Duration = Duration::from_mins(30); // Bound the default `acquire()` wait so a wedged live owner surfaces a timeout // instead of polling forever. It exceeds the stale-recovery window so genuinely // abandoned locks are reclaimed before this ceiling is reached. -const EXAMPLE_HARNESS_LOCK_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(31 * 60); +const EXAMPLE_HARNESS_LOCK_ACQUIRE_TIMEOUT: Duration = Duration::from_mins(31); const EXAMPLE_HARNESS_LOCK_POLL_INTERVAL: Duration = Duration::from_millis(100); const EXAMPLE_HARNESS_LOCK_OWNER_FILENAME: &str = "owner"; const EXAMPLE_HARNESS_LOCK_LIVENESS_EXTENSION: &str = "owner-lock"; @@ -35,6 +38,13 @@ pub(crate) struct ExampleHarnessLock { owner: ExampleHarnessLockOwner, owner_liveness: File, } + +impl ExampleHarnessLock { + /// Returns the lock directory serializing the current example run. + pub(crate) fn path(&self) -> &Path { + &self.path + } +} #[derive(Clone, Debug, Eq, PartialEq)] struct ExampleHarnessLockOwner(String); impl ExampleHarnessLockOwner { @@ -51,30 +61,30 @@ impl ExampleHarnessLockOwner { impl ExampleHarnessLock { pub(crate) fn acquire() -> io::Result { Self::acquire_at( - std::env::temp_dir().join("rstest-helper-example-harness.lock"), + &std::env::temp_dir().join("rstest-helper-example-harness.lock"), Some(EXAMPLE_HARNESS_LOCK_ACQUIRE_TIMEOUT), ) } - fn acquire_at(path: PathBuf, wait_limit: Option) -> io::Result { + fn acquire_at(path: &Path, wait_limit: Option) -> io::Result { let started_at = Instant::now(); let mut attempt = 0_u64; loop { attempt += 1; - let state_guard = lock_example_harness_state(&path)?; - match create_example_harness_lock(path.clone()) { + let state_guard = lock_example_harness_state(path)?; + match create_example_harness_lock(path.to_path_buf()) { Ok(lock) => { debug!(target: EXAMPLE_HARNESS_LOCK_LOG_TARGET, "event=acquired path={} owner={} attempt={} elapsed_ms={}", path.display(), lock.owner.0, attempt, started_at.elapsed().as_millis()); return Ok(lock); } Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { debug!(target: EXAMPLE_HARNESS_LOCK_LOG_TARGET, "event=contended path={} attempt={} elapsed_ms={}", path.display(), attempt, started_at.elapsed().as_millis()); - recover_stale_example_harness_lock_while_locked(&path)?; + recover_stale_example_harness_lock_while_locked(path)?; } Err(error) => return Err(error), } drop(state_guard); - wait_for_example_harness_lock_release(&path, started_at, wait_limit)?; + wait_for_example_harness_lock_release(path, started_at, wait_limit)?; } } } @@ -85,7 +95,8 @@ fn create_example_harness_lock(path: PathBuf) -> io::Result std::fs::create_dir(&path)?; let owner = ExampleHarnessLockOwner::new(); if let Err(error) = write_lock_owner(&path, &owner) { - let _ = remove_example_harness_lock_directory(&path); + // Best-effort rollback; the original write failure is the useful one. + let _rollback_result = remove_example_harness_lock_directory(&path); return Err(error); } Ok(ExampleHarnessLock { @@ -97,10 +108,11 @@ fn create_example_harness_lock(path: PathBuf) -> io::Result impl Drop for ExampleHarnessLock { fn drop(&mut self) { if let Ok(_state_guard) = lock_example_harness_state(&self.path) { - let _ = remove_lock_if_owned(&self.path, &self.owner); + // Best-effort cleanup: a failure only leaves a directory to reclaim. + let _cleanup_result = remove_lock_if_owned(&self.path, &self.owner); } // Release liveness after owner-aware cleanup to avoid successor races. - let _ = FileExt::unlock(&self.owner_liveness); + let _unlock_result = FileExt::unlock(&self.owner_liveness); } } @@ -110,7 +122,7 @@ fn wait_for_example_harness_lock_release( wait_limit: Option, ) -> io::Result<()> { recover_stale_example_harness_lock(path)?; - if wait_limit.is_some_and(|wait_limit| started_at.elapsed() >= wait_limit) { + if wait_limit.is_some_and(|limit| started_at.elapsed() >= limit) { debug!(target: EXAMPLE_HARNESS_LOCK_LOG_TARGET, "event=timed_out path={} elapsed_ms={}", path.display(), started_at.elapsed().as_millis()); return Err(example_harness_lock_timeout(path)); } @@ -154,10 +166,10 @@ fn remove_stale_example_harness_lock_while_locked(path: &Path) -> io::Result<()> debug!(target: EXAMPLE_HARNESS_LOCK_LOG_TARGET, "event=stale_recovery_live_owner path={}", path.display()); return Ok(()); }; - match read_example_harness_lock_owner(path)? { - Some(owner) => remove_lock_if_owned(path, &owner), - None => remove_example_harness_lock_directory(path), - } + read_example_harness_lock_owner(path)?.map_or_else( + || remove_example_harness_lock_directory(path), + |owner| remove_lock_if_owned(path, &owner), + ) } fn lock_example_harness_state(path: &Path) -> io::Result { let state_path = path.with_extension("state"); @@ -230,14 +242,18 @@ fn example_harness_lock_is_stale(modified: SystemTime, now: SystemTime) -> bool } fn make_example_harness_lock_stale(path: &Path) { - let stale_modified = SystemTime::now() - .checked_sub(EXAMPLE_HARNESS_LOCK_STALE_AFTER + Duration::from_secs(1)) - .expect("stale timestamp should be representable"); - set_file_mtime(path, FileTime::from_system_time(stale_modified)).expect("adjust lock mtime"); + let Some(stale_modified) = + SystemTime::now().checked_sub(EXAMPLE_HARNESS_LOCK_STALE_AFTER + Duration::from_secs(1)) + else { + panic!("stale timestamp should be representable") + }; + if let Err(error) = set_file_mtime(path, FileTime::from_system_time(stale_modified)) { + panic!("lock mtime should be adjustable: {error}"); + } } #[rstest] -#[case(Duration::from_secs(60), false)] +#[case(Duration::from_mins(1), false)] #[case(EXAMPLE_HARNESS_LOCK_STALE_AFTER + Duration::from_secs(1), true)] fn example_harness_lock_stale_policy(#[case] age: Duration, #[case] expected: bool) { let now = SystemTime::now(); @@ -264,10 +280,10 @@ fn stale_lock_operations_treat_missing_directory_as_released(#[case] recover: bo fn example_harness_lock_reports_active_contention_timeout() { let path = super::unique_summary_path(); std::fs::create_dir(&path).expect("create test lock directory"); - let Err(error) = ExampleHarnessLock::acquire_at(path.clone(), Some(Duration::ZERO)) else { + let Err(error) = ExampleHarnessLock::acquire_at(&path, Some(Duration::ZERO)) else { panic!("active lock contention should time out"); }; - let _ = std::fs::remove_dir(&path); + let _cleanup_result = std::fs::remove_dir(&path); assert_eq!(error.kind(), io::ErrorKind::TimedOut); } @@ -293,7 +309,7 @@ fn held_example_harness_liveness_lock_is_reported_as_contended() { #[test] fn stale_recovery_preserves_active_owner_then_reclaims_after_release() { let path = super::unique_summary_path(); - let owner = ExampleHarnessLock::acquire_at(path.clone(), None).expect("acquire active owner"); + let owner = ExampleHarnessLock::acquire_at(&path, None).expect("acquire active owner"); make_example_harness_lock_stale(&path); recover_stale_example_harness_lock(&path).expect("active owner blocks recovery"); assert!(path.is_dir(), "live owner directory must remain intact"); @@ -315,7 +331,7 @@ fn stale_recovery_preserves_active_owner_then_reclaims_after_release() { #[test] fn lock_cleanup_does_not_remove_a_different_owner() { let path = super::unique_summary_path(); - let original = ExampleHarnessLock::acquire_at(path.clone(), None).expect("acquire lock"); + let original = ExampleHarnessLock::acquire_at(&path, None).expect("acquire lock"); let successor = ExampleHarnessLockOwner::new(); { let _state_guard = lock_example_harness_state(&path).expect("lock state"); @@ -323,7 +339,10 @@ fn lock_cleanup_does_not_remove_a_different_owner() { remove_lock_if_owned(&path, &original.owner).expect("inspect successor ownership"); } - assert!(path.is_dir()); + assert!( + path.is_dir(), + "a successor-owned lock directory must survive" + ); drop(original); remove_stale_example_harness_lock(&path).expect("remove released different-owner directory"); } diff --git a/crates/rstest_helper_should_be_fixture/tests/ui/lock_model.rs b/crates/rstest_helper_should_be_fixture/tests/ui/lock_model.rs index 599ea1b08..6a2239590 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui/lock_model.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui/lock_model.rs @@ -90,7 +90,7 @@ impl LockModel { } } - fn acquire(&mut self, owner: Owner) { + const fn acquire(&mut self, owner: Owner) { if self.liveness_owner.is_none() && self.directory.is_none() { self.liveness_owner = Some(owner); self.directory = Some(Directory { @@ -100,7 +100,7 @@ impl LockModel { } } - fn mark_stale(&mut self) { + const fn mark_stale(&mut self) { if let Some(directory) = &mut self.directory { directory.is_stale = true; } @@ -117,13 +117,13 @@ impl LockModel { } } - fn replace_owner(&mut self, owner: Owner) { + const fn replace_owner(&mut self, owner: Owner) { if let Some(directory) = &mut self.directory { directory.owner = Some(owner); } } - fn remove_owner_metadata(&mut self) { + const fn remove_owner_metadata(&mut self) { if let Some(directory) = &mut self.directory { directory.owner = None; } @@ -134,7 +134,9 @@ impl LockModel { return; } - self.cleanup_attempted[owner.index()] = true; + if let Some(attempted) = self.cleanup_attempted.get_mut(owner.index()) { + *attempted = true; + } if self .directory .as_ref() @@ -144,7 +146,11 @@ impl LockModel { self.remove_directory(Some(owner)); } - self.last_release_followed_cleanup = self.cleanup_attempted[owner.index()]; + self.last_release_followed_cleanup = self + .cleanup_attempted + .get(owner.index()) + .copied() + .unwrap_or(false); self.liveness_owner = None; } @@ -164,13 +170,13 @@ impl LockModel { return; } - if let (Some(cleaner), Some(owner)) = ( + if let (Some(cleaning_owner), Some(owner)) = ( cleaner, self.directory .as_ref() .and_then(|directory| directory.owner), ) { - self.last_owner_aware_removal = Some((cleaner, owner)); + self.last_owner_aware_removal = Some((cleaning_owner, owner)); } self.directory = None; } diff --git a/crates/test_must_not_have_example/src/lib_ui_tests.rs b/crates/test_must_not_have_example/src/lib_ui_tests.rs index 1cf040e87..a97121d88 100644 --- a/crates/test_must_not_have_example/src/lib_ui_tests.rs +++ b/crates/test_must_not_have_example/src/lib_ui_tests.rs @@ -1,23 +1,23 @@ //! UI harness for `test_must_not_have_example` fixtures. +use std::{fs, io, path::Path}; + use camino::Utf8Path; use dylint_testing::ui::Test; -use std::path::Path; -use std::{fs, io}; use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_test_runner}; #[test] fn ui() { let crate_name = env!("CARGO_PKG_NAME"); let directory = "ui"; - whitaker::testing::ui::run_with_runner(crate_name, directory, |crate_name, dir| { - run_fixtures(crate_name, dir) - }) - .unwrap_or_else(|error| { - panic!( - "UI tests should execute without diffs: RunnerFailure {{ crate_name: \"{crate_name}\", directory: \"{directory}\", message: {error} }}" - ) - }); + whitaker::testing::ui::run_with_runner(crate_name, directory, run_fixtures).unwrap_or_else( + |error| { + panic!( + "UI tests should execute without diffs: RunnerFailure {{ crate_name: \ + \"{crate_name}\", directory: \"{directory}\", message: {error} }}" + ) + }, + ); } fn run_fixtures(crate_name: &str, directory: &Utf8Path) -> Result<(), String> { diff --git a/crates/whitaker_clones_core/Cargo.toml b/crates/whitaker_clones_core/Cargo.toml index d9c317d87..a5b7d4ebf 100644 --- a/crates/whitaker_clones_core/Cargo.toml +++ b/crates/whitaker_clones_core/Cargo.toml @@ -21,6 +21,7 @@ tracing = { workspace = true } whitaker_sarif = { workspace = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } camino = { workspace = true } cap-std = { workspace = true } insta = { workspace = true } diff --git a/crates/whitaker_clones_core/src/ast/lowering_tests.rs b/crates/whitaker_clones_core/src/ast/lowering_tests.rs index 28f6878ae..cc166147c 100644 --- a/crates/whitaker_clones_core/src/ast/lowering_tests.rs +++ b/crates/whitaker_clones_core/src/ast/lowering_tests.rs @@ -1,17 +1,16 @@ //! Adapter-scoped tests for Rust syntax lowering. use insta::assert_json_snapshot; +use ra_ap_syntax::{AstNode, Edition, SourceFile}; use rstest::rstest; use serde_json::json; -use ra_ap_syntax::{AstNode, Edition, SourceFile}; - use super::{ LoweringLimits, MAX_AST_DEPTH, MAX_AST_NODES, kind_id, leaf_class, validate_covering_node_budget, }; use crate::{ - AstError, ByteSpan, Production, + AstError, AstResult, ByteSpan, Production, ast::{KindId, LeafClass, NormalizedNode, NormalizedTree, PARSER_SCHEMA_VERSION}, canonical_hash, kind_counts, lower_span, production_multiset, }; @@ -21,6 +20,14 @@ fn kind_name(kind: KindId) -> String { format!("{parser_kind:?}") } +fn offset_u32(offset: usize) -> AstResult { + u32::try_from(offset).map_err(|_| AstError::OffsetTooLarge(offset)) +} + +fn whole_span(source: &str) -> AstResult { + ByteSpan::new(source, 0, offset_u32(source.len())?) +} + #[rstest] fn pinned_parser_snapshot_parses_current_edition_source() { let tree = lower_span_for("fn f() {}", "fn f").expect("source should lower"); @@ -49,35 +56,33 @@ fn two_sibling_span_selects_common_expression_ancestor() { } #[rstest] -fn whole_file_span_selects_source_file() -> Result<(), AstError> { +fn whole_file_span_selects_source_file() { let source = "fn f() {}"; - let span = ByteSpan::new(source, 0, source.len() as u32)?; - let tree = lower_span(source, span)?; + let span = whole_span(source).expect("span should validate"); + let tree = lower_span(source, span).expect("source should lower"); assert_eq!(kind_name(tree.root().kind()), "SOURCE_FILE"); - Ok(()) } #[rstest] -fn large_synthetic_source_still_lowers() -> Result<(), AstError> { +fn large_synthetic_source_still_lowers() { let statements = (0..600) .map(|index| format!("let value_{index} = {index};")) .collect::>() .join(" "); let source = format!("fn generated() {{ {statements} }}"); - let span = ByteSpan::new(&source, 0, source.len() as u32)?; - let tree = lower_span(&source, span)?; + let span = whole_span(&source).expect("span should validate"); + let tree = lower_span(&source, span).expect("source should lower"); assert_eq!(kind_name(tree.root().kind()), "SOURCE_FILE"); - Ok(()) } #[rstest] -fn oversized_source_is_rejected_by_the_node_budget() -> Result<(), AstError> { +fn oversized_source_is_rejected_by_the_node_budget() { let statements = (0..=MAX_AST_NODES) .map(|index| format!("let value_{index} = {index};")) .collect::>() .join(" "); let source = format!("fn generated() {{ {statements} }}"); - let span = ByteSpan::new(&source, 0, source.len() as u32)?; + let span = whole_span(&source).expect("span should validate"); assert_eq!( lower_span(&source, span), @@ -85,30 +90,28 @@ fn oversized_source_is_rejected_by_the_node_budget() -> Result<(), AstError> { limit: MAX_AST_NODES }) ); - Ok(()) } #[rstest] -fn deeply_nested_syntax_obeys_the_lowering_depth_budget() -> Result<(), AstError> { +fn deeply_nested_syntax_obeys_the_lowering_depth_budget() { let source = "fn f() { if true { if true { if true { if true { 0; } } } } }"; let root = SourceFile::parse(source, Edition::CURRENT) .tree() .syntax() .clone(); - let span = ByteSpan::new(source, 0, source.len() as u32)?; + let span = whole_span(source).expect("span should validate"); assert_eq!( LoweringLimits::with_depth_limit(2, span).lower(&root, 0), Err(AstError::TreeTooDeep { limit: 2 }) ); - Ok(()) } #[rstest] -fn covering_node_selection_budget_surfaces_typed_errors() -> Result<(), AstError> { +fn covering_node_selection_budget_surfaces_typed_errors() { // The selection budget guards the covering-node walk independently of the // lowering budget, and both breaches must surface as the same typed errors. - let span = ByteSpan::new("fn f() {}", 0, 2)?; + let span = ByteSpan::new("fn f() {}", 0, 2).expect("span should validate"); assert_eq!( validate_covering_node_budget(span, MAX_AST_DEPTH + 1, 0), Err(AstError::TreeTooDeep { @@ -121,11 +124,10 @@ fn covering_node_selection_budget_surfaces_typed_errors() -> Result<(), AstError limit: MAX_AST_NODES }) ); - Ok(()) } #[rstest] -fn small_candidate_amid_unrelated_nodes_is_not_rejected_by_the_budget() -> Result<(), AstError> { +fn small_candidate_amid_unrelated_nodes_is_not_rejected_by_the_budget() { // A tiny valid candidate (`a + b`) buried in a function whose remaining // statements far exceed the node budget. Pruned covering-node selection must // descend only the ancestor chain, so the unrelated statements neither count @@ -138,7 +140,6 @@ fn small_candidate_amid_unrelated_nodes_is_not_rejected_by_the_budget() -> Resul let tree = lower_span_for(&source, "a + b").expect("small candidate should lower"); assert_eq!(kind_name(tree.root().kind()), "BIN_EXPR"); - Ok(()) } #[rstest] @@ -165,18 +166,17 @@ fn span_validation_reports_specific_errors( } #[rstest] -fn source_mismatch_non_char_boundary_is_reported_by_lowering() -> Result<(), AstError> { - let span = ByteSpan::new("ab", 0, 1)?; +fn source_mismatch_non_char_boundary_is_reported_by_lowering() { + let span = ByteSpan::new("ab", 0, 1).expect("span should validate"); assert_eq!( lower_span("é", span), Err(AstError::NonCharBoundary { offset: 1 }) ); - Ok(()) } #[rstest] -fn source_mismatch_out_of_bounds_is_reported_by_lowering() -> Result<(), AstError> { - let span = ByteSpan::new("longer", 0, 6)?; +fn source_mismatch_out_of_bounds_is_reported_by_lowering() { + let span = ByteSpan::new("longer", 0, 6).expect("span should validate"); assert_eq!( lower_span("short", span), Err(AstError::SpanOutOfBounds { @@ -185,21 +185,17 @@ fn source_mismatch_out_of_bounds_is_reported_by_lowering() -> Result<(), AstErro len: 5 }) ); - Ok(()) } #[rstest] -fn error_subtree_is_rejected() -> Result<(), AstError> { +fn error_subtree_is_rejected() { let source = "@error@"; - let span = ByteSpan::new(source, 0, source.len() as u32)?; + let end = offset_u32(source.len()).expect("source length should fit in u32"); + let span = ByteSpan::new(source, 0, end).expect("span should validate"); assert_eq!( lower_span(source, span), - Err(AstError::UnparsableSpan { - start: 0, - end: source.len() as u32 - }) + Err(AstError::UnparsableSpan { start: 0, end }) ); - Ok(()) } fn lower_span_for(source: &str, needle: &str) -> Result { @@ -207,7 +203,10 @@ fn lower_span_for(source: &str, needle: &str) -> Result bool { @@ -236,7 +235,7 @@ fn kind_names_are_available_for_adapter_snapshots() { #[rstest] fn ast_feature_vector_snapshot() -> Result<(), AstError> { let source = "fn add(a: i32, b: i32) -> i32 { a + b }"; - let span = ByteSpan::new(source, 0, source.len() as u32)?; + let span = whole_span(source)?; let tree = lower_span(source, span)?; let counts = kind_counts(&tree) .iter() diff --git a/crates/whitaker_clones_core/tests/ast_boundary.rs b/crates/whitaker_clones_core/tests/ast_boundary.rs index 689179ffd..acf740728 100644 --- a/crates/whitaker_clones_core/tests/ast_boundary.rs +++ b/crates/whitaker_clones_core/tests/ast_boundary.rs @@ -12,10 +12,9 @@ const ADAPTER_OR_TEST_FILES: &[&str] = &["kani.rs", "lowering.rs", "lowering_tes #[test] fn ast_domain_files_do_not_import_parser_crates() -> Result<(), Box> { let domain_files = domain_files()?; - assert!( - !domain_files.is_empty(), - "AST domain-file discovery must not be empty" - ); + if domain_files.is_empty() { + return Err("AST domain-file discovery must not be empty".into()); + } for file in domain_files { assert_domain_boundary(&file.path, &file.contents); @@ -35,8 +34,8 @@ fn domain_files() -> Result, std::io::Error> { let mut files = Vec::new(); for entry in ast_directory.entries()? { - let entry = entry?; - let filename = entry.file_name()?; + let dir_entry = entry?; + let filename = dir_entry.file_name()?; let path = ast_path.join(&filename); let should_include = path.extension() == Some("rs") && !ADAPTER_OR_TEST_FILES.contains(&filename.as_str()); @@ -108,17 +107,19 @@ fn use_trees(contents: &str) -> Vec> { let mut imports = Vec::new(); let mut index = 0; - while index < tokens.len() { - if tokens[index] != "use" { + while let Some(&token) = tokens.get(index) { + if token != "use" { index += 1; continue; } - let end = tokens[index + 1..] + let end = tokens + .get(index + 1..) + .unwrap_or_default() .iter() - .position(|token| *token == ";") + .position(|candidate| *candidate == ";") .map_or(tokens.len(), |offset| index + 1 + offset); - imports.push(tokens[index + 1..end].to_vec()); + imports.push(tokens.get(index + 1..end).unwrap_or_default().to_vec()); index = end + 1; } @@ -135,14 +136,14 @@ fn non_comment_lexemes(contents: &str) -> Vec<&str> { tokenize(contents) .filter_map(|token| { let end = offset + token.len; - let lexeme = &contents[offset..end]; + let lexeme = contents.get(offset..end).unwrap_or(""); offset = end; is_non_comment_lexeme(token.kind).then(|| normalize_raw_identifier(token.kind, lexeme)) }) .collect() } -fn is_non_comment_lexeme(kind: TokenKind) -> bool { +const fn is_non_comment_lexeme(kind: TokenKind) -> bool { !matches!( kind, TokenKind::Whitespace @@ -170,12 +171,12 @@ fn coalesce_path_separators<'a>(tokens: &[&'a str]) -> Vec<&'a str> { let mut coalesced = Vec::new(); let mut index = 0; - while index < tokens.len() { - if tokens[index] == ":" && tokens.get(index + 1) == Some(&":") { + while let Some(&token) = tokens.get(index) { + if token == ":" && tokens.get(index + 1) == Some(&":") { coalesced.push("::"); index += 2; } else { - coalesced.push(tokens[index]); + coalesced.push(token); index += 1; } } @@ -190,7 +191,7 @@ fn imports_crate(import: &[&str], forbidden: &str) -> bool { index .checked_sub(1) .and_then(|previous| import.get(previous)), - None | Some(&"::") | Some(&"{") | Some(&",") + None | Some(&"::" | &"{" | &",") ) }) } @@ -217,10 +218,11 @@ fn parse_use_tree<'a>(tokens: &[&'a str], prefix: &[&'a str]) -> (Vec { - let (leaves, consumed) = parse_group(&tokens[index + 1..], &path); + let group_tokens = tokens.get(index + 1..).unwrap_or_default(); + let (leaves, consumed) = parse_group(group_tokens, &path); return (leaves, index + 1 + consumed); } "," | "}" => break, @@ -243,7 +245,8 @@ fn parse_group<'a>(tokens: &[&'a str], prefix: &[&'a str]) -> (Vec> let mut position = 0; loop { - let (sibling_leaves, consumed) = parse_use_tree(&tokens[position..], prefix); + let (sibling_leaves, consumed) = + parse_use_tree(tokens.get(position..).unwrap_or_default(), prefix); leaves.extend(sibling_leaves); position += consumed; @@ -294,7 +297,8 @@ fn non_import_text_does_not_trigger_the_boundary_guard(#[case] source: &str) { #[test] fn non_comment_tokens_discard_comments_and_strings_but_keep_paths() { let tokens = non_comment_tokens( - "// hidden_comment\nconst HIDDEN: &str = \"hidden string\";\nuse crate::ast::tree::ByteSpan;", + "// hidden_comment\nconst HIDDEN: &str = \"hidden string\";\nuse \ + crate::ast::tree::ByteSpan;", ); assert!(!tokens.iter().any(|token| token.contains("hidden_comment"))); diff --git a/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs b/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs index 994797f08..0e96f77c4 100644 --- a/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs +++ b/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs @@ -11,6 +11,7 @@ use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_clones_core::{ AstError, AstHash, ByteSpan, NormalizedTree, canonical_hash, lower_span, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Clone, Copy)] enum SnippetName { @@ -33,7 +34,7 @@ impl SnippetName { } } - fn source(self) -> &'static str { + const fn source(self) -> &'static str { match self { Self::AddFunction => "fn add(a: i32, b: i32) -> i32 { a + b }", Self::AddExpression => "a + b", @@ -61,7 +62,7 @@ impl ExpectedKind { } } - fn syntax_kind(self) -> SyntaxKind { + const fn syntax_kind(self) -> SyntaxKind { match self { Self::BinExpr => SyntaxKind::BIN_EXPR, Self::SourceFile => SyntaxKind::SOURCE_FILE, @@ -81,34 +82,46 @@ struct AstFeatureWorld { right_hash: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> AstFeatureWorld { AstFeatureWorld::default() } -fn whole_source_hash(source: &str) -> Result { - let span = ByteSpan::new(source, 0, source.len() as u32)?; - Ok(canonical_hash(&lower_span(source, span)?)) +fn whole_source_hash(source: &str) -> Result { + let length = u32::try_from(source.len()) + .map_err(|error| format!("source length must fit in u32: {error}"))?; + let span = ByteSpan::new(source, 0, length).map_err(|error| error.to_string())?; + let tree = lower_span(source, span).map_err(|error| error.to_string())?; + Ok(canonical_hash(&tree)) } #[given("the source snippet {name}")] fn given_source(world: &AstFeatureWorld, name: String) { - *world.source.borrow_mut() = SnippetName::parse(&name).source().to_owned(); + SnippetName::parse(&name) + .source() + .clone_into(&mut world.source.borrow_mut()); } #[given("the candidate span snippet {name}")] fn given_candidate_span(world: &AstFeatureWorld, name: String) { - *world.span_needle.borrow_mut() = SnippetName::parse(&name).source().to_owned(); + SnippetName::parse(&name) + .source() + .clone_into(&mut world.span_needle.borrow_mut()); } #[given("the left source snippet {name}")] fn given_left_source(world: &AstFeatureWorld, name: String) { - *world.left_source.borrow_mut() = SnippetName::parse(&name).source().to_owned(); + SnippetName::parse(&name) + .source() + .clone_into(&mut world.left_source.borrow_mut()); } #[given("the right source snippet {name}")] fn given_right_source(world: &AstFeatureWorld, name: String) { - *world.right_source.borrow_mut() = SnippetName::parse(&name).source().to_owned(); + SnippetName::parse(&name) + .source() + .clone_into(&mut world.right_source.borrow_mut()); } #[when("the candidate span is lowered")] @@ -120,10 +133,11 @@ fn when_candidate_span_is_lowered(world: &AstFeatureWorld) { return; }; let end = start + needle.len(); + let (Ok(span_start), Ok(span_end)) = (u32::try_from(start), u32::try_from(end)) else { + panic!("candidate span offsets {start}..{end} must fit in u32"); + }; - match ByteSpan::new(&source, start as u32, end as u32) - .and_then(|span| lower_span(&source, span)) - { + match ByteSpan::new(&source, span_start, span_end).and_then(|span| lower_span(&source, span)) { Ok(tree) => { *world.lowered.borrow_mut() = Some(tree); *world.lowering_error.borrow_mut() = None; @@ -136,7 +150,7 @@ fn when_candidate_span_is_lowered(world: &AstFeatureWorld) { } #[when("both whole sources are lowered and hashed")] -fn when_both_whole_sources_are_lowered_and_hashed(world: &AstFeatureWorld) -> Result<(), AstError> { +fn when_both_whole_sources_are_lowered_and_hashed(world: &AstFeatureWorld) -> Result<(), String> { *world.left_hash.borrow_mut() = Some(whole_source_hash(&world.left_source.borrow())?); *world.right_hash.borrow_mut() = Some(whole_source_hash(&world.right_source.borrow())?); Ok(()) @@ -151,22 +165,26 @@ fn then_lowered_root_kind_is(world: &AstFeatureWorld, kind: String) { ); let expected = u16::from(ExpectedKind::parse(&kind).syntax_kind()); let lowered = world.lowered.borrow(); - let tree = lowered - .as_ref() - .expect("lowered tree should be available after lowering"); + let Some(tree) = lowered.as_ref() else { + panic!("lowered tree must be available after lowering"); + }; - assert_eq!(tree.root().kind().get(), expected); + assert_eq!( + tree.root().kind().get(), + expected, + "lowered root kind must match the scenario expectation" + ); } fn ast_hash_pair(world: &AstFeatureWorld) -> (AstHash, AstHash) { let left_hash = world .left_hash .borrow() - .expect("left AST hash should be available after lowering"); + .unwrap_or_else(|| panic!("left AST hash must be available after lowering")); let right_hash = world .right_hash .borrow() - .expect("right AST hash should be available after lowering"); + .unwrap_or_else(|| panic!("right AST hash must be available after lowering")); (left_hash, right_hash) } @@ -175,14 +193,20 @@ fn ast_hash_pair(world: &AstFeatureWorld) -> (AstHash, AstHash) { fn then_ast_hashes_match(world: &AstFeatureWorld) { let (left_hash, right_hash) = ast_hash_pair(world); - assert_eq!(left_hash, right_hash); + assert_eq!( + left_hash, right_hash, + "AST hashes must match for these snippets" + ); } #[then("the AST hashes differ")] fn then_ast_hashes_differ(world: &AstFeatureWorld) { let (left_hash, right_hash) = ast_hash_pair(world); - assert_ne!(left_hash, right_hash); + assert_ne!( + left_hash, right_hash, + "AST hashes must differ for these snippets" + ); } /// The `#[scenario]` macro runs every Gherkin step for this scenario before diff --git a/crates/whitaker_clones_core/tests/build_script_integration.rs b/crates/whitaker_clones_core/tests/build_script_integration.rs index 9b063dcba..d3a016226 100644 --- a/crates/whitaker_clones_core/tests/build_script_integration.rs +++ b/crates/whitaker_clones_core/tests/build_script_integration.rs @@ -25,18 +25,29 @@ fn build_script_accepts_an_exact_workspace_parser_pin( let fixture = build_fixture?; let output = cargo_check(&fixture.manifest_path)?; - assert!( - output.status.success(), - "exact parser pin should pass the build script:\n{}", - String::from_utf8_lossy(&output.stderr) - ); + if !output.status.success() { + return Err(format!( + "exact parser pin must pass the build script:\n{}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } let run_output = cargo_run(&fixture.manifest_path)?; - assert!( - run_output.status.success(), - "parser-version fixture should run:\n{}", - String::from_utf8_lossy(&run_output.stderr) - ); - assert_eq!(String::from_utf8(run_output.stdout)?.trim(), "0.0.334"); + if !run_output.status.success() { + return Err(format!( + "parser-version fixture must run:\n{}", + String::from_utf8_lossy(&run_output.stderr) + ) + .into()); + } + let stdout = String::from_utf8(run_output.stdout)?; + if stdout.trim() != "0.0.334" { + return Err(format!( + "parser-version fixture must print 0.0.334, printed `{}`", + stdout.trim() + ) + .into()); + } Ok(()) } @@ -48,14 +59,14 @@ fn build_script_rejects_a_loose_workspace_parser_pin( let output = cargo_check(&fixture.manifest_path)?; let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - !output.status.success(), - "loose parser pin should fail the build script" - ); - assert!( - stderr.contains("must be exact-pinned"), - "build-script failure should explain the parser-pin rule:\n{stderr}" - ); + if output.status.success() { + return Err("loose parser pin must fail the build script".into()); + } + if !stderr.contains("must be exact-pinned") { + return Err( + format!("build-script failure must explain the parser-pin rule:\n{stderr}").into(), + ); + } Ok(()) } @@ -67,14 +78,14 @@ fn build_script_rejects_a_missing_workspace_parser_pin( let output = cargo_check(&fixture.manifest_path)?; let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - !output.status.success(), - "missing parser pin should fail the build script" - ); - assert!( - stderr.contains("workspace dependency `ra_ap_syntax` is missing"), - "build-script failure should explain the missing parser pin:\n{stderr}" - ); + if output.status.success() { + return Err("missing parser pin must fail the build script".into()); + } + if !stderr.contains("workspace dependency `ra_ap_syntax` is missing") { + return Err( + format!("build-script failure must explain the missing parser pin:\n{stderr}").into(), + ); + } Ok(()) } diff --git a/crates/whitaker_clones_core/tests/build_script_parsing.rs b/crates/whitaker_clones_core/tests/build_script_parsing.rs index 24e7b6f80..c8a688111 100644 --- a/crates/whitaker_clones_core/tests/build_script_parsing.rs +++ b/crates/whitaker_clones_core/tests/build_script_parsing.rs @@ -111,7 +111,13 @@ fn finds_nearest_workspace_manifest( let member = fixture.root.join("nested-workspace").join("member"); let workspace = fixture.root.join("nested-workspace").join("Cargo.toml"); - assert_eq!(find_workspace_manifest(&member)?, workspace); + let located = find_workspace_manifest(&member)?; + if located != workspace { + return Err(format!( + "expected the nearest workspace manifest at `{workspace}`, found `{located}`" + ) + .into()); + } Ok(()) } @@ -124,7 +130,9 @@ fn ignores_non_workspace_manifests( let manifest = fixture.root.join("Cargo.toml"); - assert!(!is_workspace_manifest(&manifest)?); + if is_workspace_manifest(&manifest)? { + return Err("a package-only manifest must not count as a workspace manifest".into()); + } Ok(()) } @@ -139,12 +147,12 @@ fn reports_missing_workspace_manifest( let error = find_workspace_manifest(&nested) .expect_err("a directory without a workspace manifest should fail"); - assert_eq!( - error - .downcast_ref::() - .map(std::io::Error::kind), - Some(std::io::ErrorKind::NotFound) - ); + let kind = error + .downcast_ref::() + .map(std::io::Error::kind); + if kind != Some(std::io::ErrorKind::NotFound) { + return Err(format!("expected a NotFound I/O error, found {kind:?}").into()); + } Ok(()) } @@ -157,7 +165,10 @@ fn reads_a_located_manifest( let manifest = fixture.root.join("Cargo.toml"); - assert_eq!(read_workspace_manifest(&manifest)?, WORKSPACE_MANIFEST); + let contents = read_workspace_manifest(&manifest)?; + if contents != WORKSPACE_MANIFEST { + return Err(format!("manifest contents must round-trip, found `{contents}`").into()); + } Ok(()) } @@ -166,23 +177,24 @@ fn read_of_absent_manifest_reports_not_found() -> Result<(), Box() - .map(std::io::Error::kind), - Some(std::io::ErrorKind::NotFound) - ); + let kind = error + .downcast_ref::() + .map(std::io::Error::kind); + if kind != Some(std::io::ErrorKind::NotFound) { + return Err(format!("expected a NotFound I/O error, found {kind:?}").into()); + } Ok(()) } proptest! { #[test] fn exact_version_accepts_only_non_empty_exact_pins( - prefix in prop_oneof![Just("=".to_owned()), Just("^".to_owned()), Just("".to_owned())], + prefix in prop_oneof![Just("=".to_owned()), Just("^".to_owned()), Just(String::new())], suffix in "[A-Za-z0-9._-]{0,32}", ) { let requirement = format!("{prefix}{suffix}"); diff --git a/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs b/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs index 832448edb..08ed6baba 100644 --- a/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs +++ b/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs @@ -7,6 +7,7 @@ use std::cell::RefCell; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_clones_core::{CandidatePair, FragmentId}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct CandidatePairWorld { @@ -15,17 +16,18 @@ struct CandidatePairWorld { pair: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> CandidatePairWorld { CandidatePairWorld::default() } fn with_pair(world: &CandidatePairWorld, assert_fn: impl FnOnce(&CandidatePair)) { - let pair = world.pair.borrow(); - match pair.as_ref() { - Some(pair) => assert_fn(pair), - None => panic!("candidate pair must be present before running assertions"), - } + let borrowed_pair = world.pair.borrow(); + let Some(candidate_pair) = borrowed_pair.as_ref() else { + panic!("candidate pair must be present before running assertions"); + }; + assert_fn(candidate_pair); } #[given("input fragment IDs {left} and {right}")] @@ -56,14 +58,18 @@ fn then_the_canonical_pair_is(world: &CandidatePairWorld, left: String, right: S with_pair(world, |pair| { assert_eq!( (pair.left().as_str(), pair.right().as_str()), - (left.as_str(), right.as_str()) + (left.as_str(), right.as_str()), + "canonical pair ordering must match the scenario expectation" ); }); } #[then("no candidate pair is returned")] fn then_no_candidate_pair_is_returned(world: &CandidatePairWorld) { - assert!(world.pair.borrow().is_none()); + assert!( + world.pair.borrow().is_none(), + "no candidate pair must be returned for identical fragment IDs" + ); } #[scenario(path = "tests/features/candidate_pair.feature", index = 0)] diff --git a/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs b/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs index 01b013b4c..3a85db254 100644 --- a/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs +++ b/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs @@ -1,4 +1,4 @@ -//! Behaviour-driven coverage for MinHash and LSH candidate generation. +//! Behaviour-driven coverage for `MinHash` and LSH candidate generation. //! //! Keep this harness in sync with `tests/features/min_hash_lsh.feature`. @@ -9,6 +9,7 @@ use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_clones_core::{ CandidatePair, Fingerprint, FragmentId, IndexError, LshConfig, LshIndex, MinHasher, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct MinHashLshWorld { @@ -19,6 +20,7 @@ struct MinHashLshWorld { candidate_error: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> MinHashLshWorld { MinHashLshWorld::default() @@ -52,15 +54,15 @@ fn expected_error(name: &str) -> Result { "invalid InvalidBandRowProduct arguments `{arguments}`" )); }; - let bands = bands + let band_count = bands .trim() .parse::() .map_err(|error| format!("invalid bands value `{bands}`: {error}"))?; - let rows = rows + let row_count = rows .trim() .parse::() .map_err(|error| format!("invalid rows value `{rows}`: {error}"))?; - return Ok(IndexError::invalid_band_row_product(bands, rows)); + return Ok(IndexError::invalid_band_row_product(band_count, row_count)); } match name { @@ -104,7 +106,7 @@ fn given_fragment_without_hashes(world: &MinHashLshWorld, id: String) { #[when("candidate pairs are generated")] fn when_candidate_pairs_are_generated(world: &MinHashLshWorld) { let Some(config) = *world.config.borrow() else { - *world.candidate_error.borrow_mut() = world.config_error.borrow().clone(); + (*world.candidate_error.borrow_mut()).clone_from(&world.config_error.borrow()); world.candidates.borrow_mut().clear(); return; }; @@ -129,7 +131,11 @@ fn when_candidate_pairs_are_generated(world: &MinHashLshWorld) { #[then("candidate pair count is {count}")] fn then_candidate_pair_count_is(world: &MinHashLshWorld, count: usize) { with_candidates(world, |candidates| { - assert_eq!(candidates.len(), count); + assert_eq!( + candidates.len(), + count, + "candidate pair count must match the scenario expectation" + ); }); } @@ -142,27 +148,42 @@ fn then_only_candidate_pair_is(world: &MinHashLshWorld, left: String, right: Str candidates.len() ); }; - let expected = CandidatePair::new(FragmentId::from(left), FragmentId::from(right)) - .expect("distinct fragment IDs should form a canonical pair"); - assert_eq!(candidate, &expected); + let Some(expected) = CandidatePair::new(FragmentId::from(left), FragmentId::from(right)) + else { + panic!("distinct fragment IDs must form a canonical pair"); + }; + assert_eq!( + candidate, &expected, + "candidate pair must match the scenario expectation" + ); }); } #[then("no candidate pairs are returned")] fn then_no_candidate_pairs_are_returned(world: &MinHashLshWorld) { - with_candidates(world, |candidates| assert!(candidates.is_empty())); + with_candidates(world, |candidates| { + assert!( + candidates.is_empty(), + "no candidate pairs must be returned for this scenario" + ); + }); } #[then("the candidate generation error is {name}")] fn then_candidate_generation_error_is(world: &MinHashLshWorld, name: String) -> Result<(), String> { let expected = expected_error(&name)?; - match world.candidate_error.borrow().clone() { - Some(actual) => { - assert_eq!(actual, expected); - Ok(()) - } - None => Err("candidate generation error must be present".to_owned()), - } + world.candidate_error.borrow().as_ref().map_or_else( + || Err("candidate generation error must be present".to_owned()), + |actual| { + if *actual == expected { + Ok(()) + } else { + Err(format!( + "expected candidate generation error `{expected:?}`, found `{actual:?}`" + )) + } + }, + ) } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 0)] diff --git a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs index a11090df1..698f84990 100644 --- a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs +++ b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs @@ -11,6 +11,7 @@ use whitaker_clones_core::{ emit_run0, }; use whitaker_sarif::{Run, SarifResult, WhitakerProperties}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct Run0World { @@ -33,17 +34,18 @@ impl Default for Run0World { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> Run0World { Run0World::default() } fn with_results(world: &Run0World, assert_fn: impl FnOnce(&[SarifResult])) { - let run = world.run.borrow(); - match run.as_ref() { - Some(run) => assert_fn(&run.results), - None => panic!("run must be emitted before checking results"), - } + let borrowed_run = world.run.borrow(); + let Some(emitted_run) = borrowed_run.as_ref() else { + panic!("run must be emitted before checking results"); + }; + assert_fn(&emitted_run.results); } fn with_whitaker_properties(world: &Run0World, assert_fn: impl FnOnce(&WhitakerProperties)) { @@ -51,12 +53,13 @@ fn with_whitaker_properties(world: &Run0World, assert_fn: impl FnOnce(&WhitakerP let [result] = results else { panic!("exactly one result must exist before checking Whitaker properties"); }; - let properties = result - .properties - .as_ref() - .unwrap_or_else(|| panic!("Whitaker properties must be present")); - let extracted = WhitakerProperties::try_from(properties) - .unwrap_or_else(|error| panic!("unexpected property extraction error: {error}")); + let Some(properties) = result.properties.as_ref() else { + panic!("Whitaker properties must be present"); + }; + let extracted = match WhitakerProperties::try_from(properties) { + Ok(extracted) => extracted, + Err(error) => panic!("unexpected property extraction error: {error}"), + }; assert_fn(&extracted); }); } @@ -178,7 +181,13 @@ fn when_run_zero_is_emitted(world: &Run0World) { #[then("exactly {count} result is emitted")] fn then_exactly_one_result_is_emitted(world: &Run0World, count: usize) { - with_results(world, |results| assert_eq!(results.len(), count)); + with_results(world, |results| { + assert_eq!( + results.len(), + count, + "emitted result count must match the scenario expectation" + ); + }); } #[then("the emitted rule is {rule_id}")] @@ -187,7 +196,10 @@ fn then_emitted_rule_is(world: &Run0World, rule_id: String) { let [result] = results else { panic!("exactly one result must exist before checking the rule"); }; - assert_eq!(result.rule_id, rule_id); + assert_eq!( + result.rule_id, rule_id, + "emitted rule identifier must match the scenario expectation" + ); }); } @@ -197,40 +209,71 @@ fn then_result_has_locations(world: &Run0World, primary_count: usize, related_co let [result] = results else { panic!("exactly one result must exist before checking locations"); }; - assert_eq!(result.locations.len(), primary_count); - assert_eq!(result.related_locations.len(), related_count); + assert_eq!( + result.locations.len(), + primary_count, + "primary location count must match the scenario expectation" + ); + assert_eq!( + result.related_locations.len(), + related_count, + "related location count must match the scenario expectation" + ); }); } #[then("the Whitaker profile is {profile}")] fn then_whitaker_profile_is(world: &Run0World, profile: String) { - with_whitaker_properties(world, |props| assert_eq!(props.profile, profile)); + with_whitaker_properties(world, |props| { + assert_eq!( + props.profile, profile, + "Whitaker profile must match the scenario expectation" + ); + }); } #[then("the Whitaker k is {k}")] fn then_whitaker_k_is(world: &Run0World, k: usize) { - with_whitaker_properties(world, |props| assert_eq!(props.k, k)); + with_whitaker_properties(world, |props| { + assert_eq!(props.k, k, "Whitaker k must match the scenario expectation"); + }); } #[then("the Whitaker window is {window}")] fn then_whitaker_window_is(world: &Run0World, window: usize) { - with_whitaker_properties(world, |props| assert_eq!(props.window, window)); + with_whitaker_properties(world, |props| { + assert_eq!( + props.window, window, + "Whitaker window must match the scenario expectation" + ); + }); } #[then("no results are emitted")] fn then_no_results_are_emitted(world: &Run0World) { - with_results(world, |results| assert!(results.is_empty())); + with_results(world, |results| { + assert!( + results.is_empty(), + "no results must be emitted for this scenario" + ); + }); } #[then("the emission error is {message}")] fn then_emission_error_is(world: &Run0World, message: String) -> Result<(), String> { - match world.error.borrow().as_ref() { - Some(error) => { - assert_eq!(error.to_string(), message); - Ok(()) - } - None => Err("an emission error must be present".to_owned()), - } + world.error.borrow().as_ref().map_or_else( + || Err("an emission error must be present".to_owned()), + |error| { + let actual = error.to_string(); + if actual == message { + Ok(()) + } else { + Err(format!( + "expected emission error `{message}`, but the run reported `{actual}`" + )) + } + }, + ) } #[then("the primary region is {region}")] @@ -239,13 +282,11 @@ fn then_primary_region_is(world: &Run0World, region: String) { let [result] = results else { panic!("exactly one result must exist before checking the primary region"); }; - let location = match result.locations.first() { - Some(location) => location, - None => panic!("a primary location must be present"), + let Some(location) = result.locations.first() else { + panic!("a primary location must be present"); }; - let region_value = match location.physical_location.region.as_ref() { - Some(region_value) => region_value, - None => panic!("a primary region must be present"), + let Some(region_value) = location.physical_location.region.as_ref() else { + panic!("a primary region must be present"); }; let actual = format!( "{}:{}-{}:{}", @@ -254,9 +295,12 @@ fn then_primary_region_is(world: &Run0World, region: String) { region_value.end_line.unwrap_or(region_value.start_line), region_value .end_column - .unwrap_or(region_value.start_column.unwrap_or(1)) + .unwrap_or_else(|| region_value.start_column.unwrap_or(1)) + ); + assert_eq!( + actual, region, + "primary region must match the scenario expectation" ); - assert_eq!(actual, region); }); } @@ -266,11 +310,13 @@ fn then_primary_file_is(world: &Run0World, file_uri: String) { let [result] = results else { panic!("exactly one result must exist before checking the primary file"); }; - let location = match result.locations.first() { - Some(location) => location, - None => panic!("a primary location must be present"), + let Some(location) = result.locations.first() else { + panic!("a primary location must be present"); }; - assert_eq!(location.physical_location.artifact_location.uri, file_uri); + assert_eq!( + location.physical_location.artifact_location.uri, file_uri, + "primary file URI must match the scenario expectation" + ); }); } diff --git a/crates/whitaker_clones_core/tests/token_pass_behaviour.rs b/crates/whitaker_clones_core/tests/token_pass_behaviour.rs index 5cc459c99..3b23e79ec 100644 --- a/crates/whitaker_clones_core/tests/token_pass_behaviour.rs +++ b/crates/whitaker_clones_core/tests/token_pass_behaviour.rs @@ -11,6 +11,7 @@ use whitaker_clones_core::{ Fingerprint, NormProfile, ShingleSize, TokenPassError, WinnowWindow, hash_shingles, normalize, winnow, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct TokenPassWorld { @@ -28,6 +29,7 @@ struct TokenPassWorld { window_error: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> TokenPassWorld { TokenPassWorld::default() @@ -65,12 +67,12 @@ fn snippet_for_name(name: &str) -> &'static str { #[given("the source snippet {name}")] fn given_source(world: &TokenPassWorld, name: String) { - *world.source.borrow_mut() = snippet_for_name(&name).to_owned(); + snippet_for_name(&name).clone_into(&mut world.source.borrow_mut()); } #[given("the comparison source snippet {name}")] fn given_comparison_source(world: &TokenPassWorld, name: String) { - *world.comparison_source.borrow_mut() = snippet_for_name(&name).to_owned(); + snippet_for_name(&name).clone_into(&mut world.comparison_source.borrow_mut()); } #[given("the profile is {profile}")] @@ -85,8 +87,8 @@ fn given_profile(world: &TokenPassWorld, profile: String) { #[given("shingle size {size}")] fn given_shingle_size(world: &TokenPassWorld, size: usize) { match ShingleSize::try_from(size) { - Ok(size) => { - *world.k.borrow_mut() = Some(size); + Ok(shingle_size) => { + *world.k.borrow_mut() = Some(shingle_size); *world.k_error.borrow_mut() = None; } Err(error) => { @@ -99,8 +101,8 @@ fn given_shingle_size(world: &TokenPassWorld, size: usize) { #[given("winnow window {window}")] fn given_window(world: &TokenPassWorld, window: usize) { match WinnowWindow::try_from(window) { - Ok(window) => { - *world.window.borrow_mut() = Some(window); + Ok(winnow_window) => { + *world.window.borrow_mut() = Some(winnow_window); *world.window_error.borrow_mut() = None; } Err(error) => { @@ -199,7 +201,11 @@ fn then_normalized_labels_are(world: &TokenPassWorld, labels: String) { .split_whitespace() .map(ToOwned::to_owned) .collect::>(); - assert_eq!(*world.normalized_labels.borrow(), expected); + assert_eq!( + *world.normalized_labels.borrow(), + expected, + "normalized labels must match the scenario expectation" + ); } #[then("the normalized labels match exactly")] @@ -214,7 +220,11 @@ fn then_normalized_labels_match_exactly(world: &TokenPassWorld) { #[then("the fingerprint count is {count}")] fn then_fingerprint_count_is(world: &TokenPassWorld, count: usize) { with_fingerprints(world, |fingerprints| { - assert_eq!(fingerprints.len(), count); + assert_eq!( + fingerprints.len(), + count, + "fingerprint count must match the scenario expectation" + ); }); } @@ -224,17 +234,23 @@ fn then_first_fingerprint_spans(world: &TokenPassWorld, start: usize, end: usize let Some(first) = fingerprints.first() else { panic!("fingerprints must exist before checking the first span"); }; - assert_eq!(first.range, start..end); + assert_eq!( + first.range, + start..end, + "first fingerprint span must match the scenario expectation" + ); }); } #[then("the retained hashes are {hashes}")] fn then_retained_hashes_are(world: &TokenPassWorld, hashes: String) { - let expected = hashes + let Ok(expected) = hashes .split_whitespace() - .map(|value| value.parse::()) - .collect::, _>>() - .expect("expected hash list should be valid"); + .map(str::parse::) + .collect::, _>>() + else { + panic!("expected hash list `{hashes}` must be valid"); + }; with_retained(world, |retained| { assert_eq!( @@ -242,7 +258,8 @@ fn then_retained_hashes_are(world: &TokenPassWorld, hashes: String) { .iter() .map(|fingerprint| fingerprint.hash) .collect::>(), - expected + expected, + "retained fingerprint hashes must match the scenario expectation" ); }); } @@ -258,7 +275,11 @@ fn then_error_is(world: &TokenPassWorld, message: String) { .collect::>(); if let [error] = errors.as_slice() { - assert_eq!(error.to_string(), message); + assert_eq!( + error.to_string(), + message, + "reported error must match the scenario expectation" + ); return; } diff --git a/crates/whitaker_sarif/Cargo.toml b/crates/whitaker_sarif/Cargo.toml index 0e44258b3..55fd5cd95 100644 --- a/crates/whitaker_sarif/Cargo.toml +++ b/crates/whitaker_sarif/Cargo.toml @@ -19,6 +19,7 @@ thiserror = { workspace = true } camino = { workspace = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } diff --git a/crates/whitaker_sarif/tests/sarif_behaviour.rs b/crates/whitaker_sarif/tests/sarif_behaviour.rs index ec1355269..8e7f3aedd 100644 --- a/crates/whitaker_sarif/tests/sarif_behaviour.rs +++ b/crates/whitaker_sarif/tests/sarif_behaviour.rs @@ -10,11 +10,10 @@ use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_sarif::{ Level, LocationBuilder, RegionBuilder, ResultBuilder, RunBuilder, SarifLog, SarifLogBuilder, SarifResult, WhitakerProperties, WhitakerPropertiesBuilder, all_rules, merge_runs, + model::{descriptor::ReportingDescriptor, run::Run}, token_pass_path, }; - -use whitaker_sarif::model::descriptor::ReportingDescriptor; -use whitaker_sarif::model::run::Run; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct SarifWorld { @@ -33,6 +32,7 @@ struct SarifWorld { computed_path: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> SarifWorld { SarifWorld::default() @@ -41,48 +41,48 @@ fn world() -> SarifWorld { // -- Helper functions (match-based to avoid expect/unwrap) -- fn with_log(world: &SarifWorld, assert_fn: impl FnOnce(&SarifLog)) { - let log = world.built_log.borrow(); - match log.as_ref() { + let guard = world.built_log.borrow(); + match guard.as_ref() { Some(log) => assert_fn(log), None => panic!("log must be built before running assertions"), } } fn with_result(world: &SarifWorld, assert_fn: impl FnOnce(&SarifResult)) { - let result = world.built_result.borrow(); - match result.as_ref() { + let guard = world.built_result.borrow(); + match guard.as_ref() { Some(result) => assert_fn(result), None => panic!("result must be built before running assertions"), } } fn with_props_json(world: &SarifWorld, assert_fn: impl FnOnce(&serde_json::Value)) { - let json = world.props_json.borrow(); - match json.as_ref() { + let guard = world.props_json.borrow(); + match guard.as_ref() { Some(json) => assert_fn(json), None => panic!("properties JSON must exist before running assertions"), } } fn with_merged_run(world: &SarifWorld, assert_fn: impl FnOnce(&Run)) { - let merged = world.merged_run.borrow(); - match merged.as_ref() { + let guard = world.merged_run.borrow(); + match guard.as_ref() { Some(merged) => assert_fn(merged), None => panic!("merged run must exist before running assertions"), } } fn with_serialized_json(world: &SarifWorld, assert_fn: impl FnOnce(&str)) { - let json = world.serialized_json.borrow(); - match json.as_ref() { + let guard = world.serialized_json.borrow(); + match guard.as_ref() { Some(json) => assert_fn(json), None => panic!("serialized JSON must exist before running assertions"), } } fn with_computed_path(world: &SarifWorld, assert_fn: impl FnOnce(&Utf8PathBuf)) { - let path = world.computed_path.borrow(); - match path.as_ref() { + let guard = world.computed_path.borrow(); + match guard.as_ref() { Some(path) => assert_fn(path), None => panic!("computed path must exist before running assertions"), } @@ -179,8 +179,8 @@ fn given_target_dir(world: &SarifWorld, path: String) { #[when("the SARIF log is built with that run")] fn when_log_built_with_run(world: &SarifWorld) { - let run = world.pending_run.borrow_mut().take(); - if let Some(run) = run { + let pending = world.pending_run.borrow_mut().take(); + if let Some(run) = pending { let log = SarifLogBuilder::new().with_run(run).build(); *world.built_log.borrow_mut() = Some(log); } @@ -188,8 +188,8 @@ fn when_log_built_with_run(world: &SarifWorld) { #[when("the result is built")] fn when_result_built(world: &SarifWorld) { - let builder = world.result_builder.borrow_mut().take(); - if let Some(builder) = builder { + let taken = world.result_builder.borrow_mut().take(); + if let Some(builder) = taken { match builder.build() { Ok(result) => *world.built_result.borrow_mut() = Some(result), Err(e) => panic!("failed to build result: {e}"), @@ -199,8 +199,8 @@ fn when_result_built(world: &SarifWorld) { #[when("properties are converted to JSON")] fn when_properties_to_json(world: &SarifWorld) { - let builder = world.props_builder.borrow_mut().take(); - if let Some(builder) = builder { + let taken = world.props_builder.borrow_mut().take(); + if let Some(builder) = taken { match builder.build() { Ok(props) => match props.try_to_value() { Ok(value) => *world.props_json.borrow_mut() = Some(value), @@ -222,8 +222,8 @@ fn when_runs_merged(world: &SarifWorld) { #[when("the log is serialized to JSON")] fn when_log_serialized(world: &SarifWorld) { - let log = world.built_log.borrow(); - if let Some(log) = log.as_ref() { + let guard = world.built_log.borrow(); + if let Some(log) = guard.as_ref() { match serde_json::to_string_pretty(log) { Ok(json) => *world.serialized_json.borrow_mut() = Some(json), Err(e) => panic!("failed to serialize log: {e}"), @@ -233,8 +233,8 @@ fn when_log_serialized(world: &SarifWorld) { #[when("the JSON is deserialized back")] fn when_json_deserialized(world: &SarifWorld) { - let json = world.serialized_json.borrow(); - if let Some(json) = json.as_ref() { + let guard = world.serialized_json.borrow(); + if let Some(json) = guard.as_ref() { match serde_json::from_str::(json) { Ok(log) => *world.deserialized_log.borrow_mut() = Some(log), Err(e) => panic!("failed to deserialize log: {e}"), @@ -249,8 +249,8 @@ fn when_rules_retrieved(world: &SarifWorld) { #[when("the token pass path is requested")] fn when_token_path_requested(world: &SarifWorld) { - let dir = world.target_dir.borrow(); - if let Some(dir) = dir.as_ref() { + let guard = world.target_dir.borrow(); + if let Some(dir) = guard.as_ref() { let path = token_pass_path(dir); *world.computed_path.borrow_mut() = Some(path); } @@ -260,41 +260,55 @@ fn when_token_path_requested(world: &SarifWorld) { #[then("the log version is {version}")] fn then_log_version(world: &SarifWorld, version: String) { - with_log(world, |log| assert_eq!(log.version, version)); + with_log(world, |log| { + assert_eq!(log.version, version, "log version should match"); + }); } #[then("the log has {count} run")] fn then_log_has_runs(world: &SarifWorld, count: usize) { - with_log(world, |log| assert_eq!(log.runs.len(), count)); + with_log(world, |log| { + assert_eq!(log.runs.len(), count, "log run count should match"); + }); } #[then("the run tool name is {name}")] fn then_run_tool_name(world: &SarifWorld, name: String) { with_log(world, |log| match log.runs.first() { - Some(run) => assert_eq!(run.tool.driver.name, name), + Some(run) => assert_eq!(run.tool.driver.name, name, "tool name should match"), None => panic!("log must have at least one run"), }); } #[then("the result rule ID is {rule_id}")] fn then_result_rule_id(world: &SarifWorld, rule_id: String) { - with_result(world, |result| assert_eq!(result.rule_id, rule_id)); + with_result(world, |result| { + assert_eq!(result.rule_id, rule_id, "result rule ID should match"); + }); } #[then("the result level is warning")] fn then_result_level_warning(world: &SarifWorld) { - with_result(world, |result| assert_eq!(result.level, Level::Warning)); + with_result(world, |result| { + assert_eq!( + result.level, + Level::Warning, + "result level should be warning" + ); + }); } #[then("the result has {count} location")] fn then_result_location_count(world: &SarifWorld, count: usize) { - with_result(world, |result| assert_eq!(result.locations.len(), count)); + with_result(world, |result| { + assert_eq!(result.locations.len(), count, "location count should match"); + }); } #[then("the JSON contains whitaker profile {profile}")] fn then_json_has_profile(world: &SarifWorld, profile: String) { with_props_json(world, |json| match WhitakerProperties::try_from(json) { - Ok(extracted) => assert_eq!(extracted.profile, profile), + Ok(extracted) => assert_eq!(extracted.profile, profile, "profile should match"), Err(e) => panic!("failed to extract WhitakerProperties: {e}"), }); } @@ -302,14 +316,20 @@ fn then_json_has_profile(world: &SarifWorld, profile: String) { #[then("the JSON contains whitaker k {k}")] fn then_json_has_k(world: &SarifWorld, k: usize) { with_props_json(world, |json| match WhitakerProperties::try_from(json) { - Ok(extracted) => assert_eq!(extracted.k, k), + Ok(extracted) => assert_eq!(extracted.k, k, "k value should match"), Err(e) => panic!("failed to extract WhitakerProperties: {e}"), }); } #[then("the merged run has {count} results")] fn then_merged_run_results(world: &SarifWorld, count: usize) { - with_merged_run(world, |merged| assert_eq!(merged.results.len(), count)); + with_merged_run(world, |merged| { + assert_eq!( + merged.results.len(), + count, + "merged result count should match" + ); + }); } #[then("the deserialized log equals the original")] @@ -317,7 +337,9 @@ fn then_deserialized_equals_original(world: &SarifWorld) { let original = world.built_log.borrow(); let deserialized = world.deserialized_log.borrow(); match (original.as_ref(), deserialized.as_ref()) { - (Some(orig), Some(deser)) => assert_eq!(orig, deser), + (Some(orig), Some(deser)) => { + assert_eq!(orig, deser, "deserialized log should equal the original"); + } _ => panic!("both original and deserialized logs must exist"), } } @@ -325,7 +347,10 @@ fn then_deserialized_equals_original(world: &SarifWorld) { #[then("the JSON contains version {version}")] fn then_json_has_version(world: &SarifWorld, version: String) { with_serialized_json(world, |json| { - assert!(json.contains(&format!("\"version\": \"{version}\""))); + assert!( + json.contains(&format!("\"version\": \"{version}\"")), + "serialized JSON should contain version {version}" + ); }); } diff --git a/crates/whitaker_test_macros/Cargo.toml b/crates/whitaker_test_macros/Cargo.toml new file mode 100644 index 000000000..bceff1c9f --- /dev/null +++ b/crates/whitaker_test_macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "whitaker_test_macros" +version = "0.2.7" +edition = "2024" +publish = false +description = "Proc-macros for test fixtures that suppress lints triggered by macro expansion" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } diff --git a/crates/whitaker_test_macros/src/lib.rs b/crates/whitaker_test_macros/src/lib.rs new file mode 100644 index 000000000..eb2ce157e --- /dev/null +++ b/crates/whitaker_test_macros/src/lib.rs @@ -0,0 +1,32 @@ +//! Proc-macros for test fixtures that suppress lints triggered by macro expansion. + +use proc_macro::TokenStream; +use quote::quote; +use syn::{Item, parse_macro_input}; + +/// Allows `unused_braces` lint for fixture functions. +/// +/// This attribute is used on rstest fixture functions that expand to single-expression +/// bodies. When combined with `fn_single_line = true` in rustfmt.toml, the generated +/// code triggers the `unused_braces` lint. This attribute suppresses that lint +/// specifically for fixture expansions. +#[proc_macro_attribute] +pub fn allow_fixture_expansion_lints(_attr: TokenStream, item: TokenStream) -> TokenStream { + let parsed_item = parse_macro_input!(item as Item); + + quote! { + #[allow( + unused_braces, + reason = "fixture macro expansion triggers unused-braces on expression bodies" + )] + #[cfg_attr( + clippy, + expect( + clippy::allow_attributes, + reason = "needed to allow unused_braces for fixture macro expansion" + ) + )] + #parsed_item + } + .into() +} diff --git a/installer/Cargo.toml b/installer/Cargo.toml index 6d514e9e9..570e46ff5 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -78,6 +78,7 @@ zip = { workspace = true } zstd = { workspace = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } libc = { workspace = true } mockall = { workspace = true } rstest = { workspace = true } diff --git a/installer/src/artefact/manifest_tests.rs b/installer/src/artefact/manifest_tests.rs index 784f4d044..67a597571 100644 --- a/installer/src/artefact/manifest_tests.rs +++ b/installer/src/artefact/manifest_tests.rs @@ -1,51 +1,49 @@ //! Tests for manifest schema types. -use super::*; use rstest::{fixture, rstest}; use serde_json::Value; +use super::*; +use crate::artefact::error::ArtefactError; + #[fixture] -fn sample_provenance() -> ManifestProvenance { - ManifestProvenance { - git_sha: GitSha::try_from("abc1234").expect("valid sha"), +fn sample_provenance() -> Result { + Ok(ManifestProvenance { + git_sha: GitSha::try_from("abc1234")?, schema_version: SchemaVersion::current(), - toolchain: ToolchainChannel::try_from("nightly-2026-05-28").expect("valid channel"), - target: TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"), - } + toolchain: ToolchainChannel::try_from("nightly-2026-05-28")?, + target: TargetTriple::try_from("x86_64-unknown-linux-gnu")?, + }) } #[fixture] -fn sample_content() -> ManifestContent { - ManifestContent { +fn sample_content() -> Result { + Ok(ManifestContent { generated_at: GeneratedAt::new("2026-05-28T00:00:00Z"), files: vec!["libwhitaker_lints@nightly-2026-05-28-x86_64-unknown-linux-gnu.so".to_owned()], - sha256: Sha256Digest::try_from("a".repeat(64).as_str()).expect("valid digest"), - } + sha256: Sha256Digest::try_from("a".repeat(64).as_str())?, + }) } #[fixture] fn sample_manifest( - sample_provenance: ManifestProvenance, - sample_content: ManifestContent, -) -> Manifest { - Manifest::new(sample_provenance, sample_content) + sample_provenance: Result, + sample_content: Result, +) -> Result { + Ok(Manifest::new(sample_provenance?, sample_content?)) } #[rstest] -fn accessors_return_all_fields(sample_manifest: Manifest) { - assert_eq!(sample_manifest.git_sha().as_str(), "abc1234"); - assert_eq!(sample_manifest.schema_version().as_u32(), 1); - assert_eq!(sample_manifest.toolchain().as_str(), "nightly-2026-05-28"); - assert_eq!( - sample_manifest.target().as_str(), - "x86_64-unknown-linux-gnu" - ); - assert_eq!( - sample_manifest.generated_at().as_str(), - "2026-05-28T00:00:00Z" - ); - assert_eq!(sample_manifest.files().len(), 1); - assert_eq!(sample_manifest.sha256().as_str().len(), 64); +fn accessors_return_all_fields(sample_manifest: Result) { + let manifest = sample_manifest.expect("sample manifest should build"); + + assert_eq!(manifest.git_sha().as_str(), "abc1234"); + assert_eq!(manifest.schema_version().as_u32(), 1); + assert_eq!(manifest.toolchain().as_str(), "nightly-2026-05-28"); + assert_eq!(manifest.target().as_str(), "x86_64-unknown-linux-gnu"); + assert_eq!(manifest.generated_at().as_str(), "2026-05-28T00:00:00Z"); + assert_eq!(manifest.files().len(), 1); + assert_eq!(manifest.sha256().as_str().len(), 64); } #[rstest] @@ -55,8 +53,9 @@ fn generated_at_display() { } #[rstest] -fn serialized_json_contains_all_adr_001_keys(sample_manifest: Manifest) { - let json = serde_json::to_string(&sample_manifest).expect("serialization succeeds"); +fn serialized_json_contains_all_adr_001_keys(sample_manifest: Result) { + let manifest = sample_manifest.expect("sample manifest should build"); + let json = serde_json::to_string(&manifest).expect("serialization succeeds"); let parsed: Value = serde_json::from_str(&json).expect("valid JSON"); let obj = parsed.as_object().expect("top-level object"); @@ -110,10 +109,11 @@ fn manifest_with_multiple_files() { } #[rstest] -fn serde_round_trip(sample_manifest: Manifest) { - let json = serde_json::to_string_pretty(&sample_manifest).expect("serialize"); +fn serde_round_trip(sample_manifest: Result) { + let manifest = sample_manifest.expect("sample manifest should build"); + let json = serde_json::to_string_pretty(&manifest).expect("serialize"); let back: Manifest = serde_json::from_str(&json).expect("deserialize"); - assert_eq!(sample_manifest, back); + assert_eq!(manifest, back); } #[rstest] diff --git a/installer/src/artefact/packaging_tests.rs b/installer/src/artefact/packaging_tests.rs index 6e682bdfd..9b48349a7 100644 --- a/installer/src/artefact/packaging_tests.rs +++ b/installer/src/artefact/packaging_tests.rs @@ -1,31 +1,39 @@ //! Unit tests for the artefact packaging module. -use super::*; use rstest::{fixture, rstest}; use tempfile::TempDir; +use super::*; +use crate::artefact::error::ArtefactError; + +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn temp_dir() -> TempDir { - TempDir::new().expect("temp dir creation succeeds") +fn temp_dir() -> std::io::Result { + TempDir::new() } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn sample_git_sha() -> GitSha { - GitSha::try_from("abc1234").expect("valid sha") +fn sample_git_sha() -> Result { + GitSha::try_from("abc1234") } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn sample_toolchain() -> ToolchainChannel { - ToolchainChannel::try_from("nightly-2026-05-28").expect("valid channel") +fn sample_toolchain() -> Result { + ToolchainChannel::try_from("nightly-2026-05-28") } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn sample_target() -> TargetTriple { - TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target") +fn sample_target() -> Result { + TargetTriple::try_from("x86_64-unknown-linux-gnu") } #[rstest] -fn compute_sha256_of_known_content(temp_dir: TempDir) { +fn compute_sha256_of_known_content(#[from(temp_dir)] temp_dir_res: std::io::Result) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let path = temp_dir.path().join("test.bin"); // SHA-256 of empty file is the well-known constant. fs::write(&path, b"").expect("write"); @@ -40,7 +48,9 @@ fn compute_sha256_of_known_content(temp_dir: TempDir) { } #[rstest] -fn create_archive_contains_files(temp_dir: TempDir) { +fn create_archive_contains_files(#[from(temp_dir)] temp_dir_res: std::io::Result) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let file_a = temp_dir.path().join("a.txt"); let file_b = temp_dir.path().join("b.txt"); fs::write(&file_a, b"alpha").expect("write a"); @@ -53,17 +63,21 @@ fn create_archive_contains_files(temp_dir: TempDir) { ) .expect("archive creation succeeds"); - let entry_names = list_archive_entries(&archive_path); + let entry_names = list_archive_entries(&archive_path).expect("archive should list entries"); assert!(entry_names.contains(&"a.txt".to_owned())); assert!(entry_names.contains(&"b.txt".to_owned())); } #[rstest] fn generate_manifest_json_matches_schema( - sample_git_sha: GitSha, - sample_toolchain: ToolchainChannel, - sample_target: TargetTriple, + #[from(sample_git_sha)] sample_git_sha_res: Result, + #[from(sample_toolchain)] sample_toolchain_res: Result, + #[from(sample_target)] sample_target_res: Result, ) { + let sample_git_sha = sample_git_sha_res.expect("sample git SHA should validate"); + let sample_toolchain = sample_toolchain_res.expect("sample toolchain should validate"); + let sample_target = sample_target_res.expect("sample target should validate"); + let provenance = ManifestProvenance { git_sha: sample_git_sha, schema_version: SchemaVersion::current(), @@ -95,11 +109,16 @@ fn generate_manifest_json_matches_schema( #[rstest] fn package_artefact_produces_valid_archive( - temp_dir: TempDir, - sample_git_sha: GitSha, - sample_toolchain: ToolchainChannel, - sample_target: TargetTriple, + #[from(temp_dir)] temp_dir_res: std::io::Result, + #[from(sample_git_sha)] sample_git_sha_res: Result, + #[from(sample_toolchain)] sample_toolchain_res: Result, + #[from(sample_target)] sample_target_res: Result, ) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let sample_git_sha = sample_git_sha_res.expect("sample git SHA should validate"); + let sample_toolchain = sample_toolchain_res.expect("sample toolchain should validate"); + let sample_target = sample_target_res.expect("sample target should validate"); + let lib_path = temp_dir.path().join("libwhitaker_suite.so"); fs::write(&lib_path, b"fake library").expect("write lib"); @@ -128,7 +147,8 @@ fn package_artefact_produces_valid_archive( expected_name.filename() ); - let entry_names = list_archive_entries(&output.archive_path); + let entry_names = + list_archive_entries(&output.archive_path).expect("archive should list entries"); assert!(entry_names.contains(&"libwhitaker_suite.so".to_owned())); assert!( !entry_names.contains(&"manifest.json".to_owned()), @@ -137,7 +157,9 @@ fn package_artefact_produces_valid_archive( } #[rstest] -fn package_artefact_rejects_empty_files(temp_dir: TempDir) { +fn package_artefact_rejects_empty_files(#[from(temp_dir)] temp_dir_res: std::io::Result) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let output_dir = temp_dir.path().join("dist"); fs::create_dir_all(&output_dir).expect("mkdir"); @@ -158,7 +180,11 @@ fn package_artefact_rejects_empty_files(temp_dir: TempDir) { } #[rstest] -fn package_artefact_fails_when_library_file_missing(temp_dir: TempDir) { +fn package_artefact_fails_when_library_file_missing( + #[from(temp_dir)] temp_dir_res: std::io::Result, +) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let missing = temp_dir.path().join("nonexistent_lib.so"); let output_dir = temp_dir.path().join("dist"); fs::create_dir_all(&output_dir).expect("mkdir"); @@ -182,11 +208,16 @@ fn package_artefact_fails_when_library_file_missing(temp_dir: TempDir) { #[rstest] fn archive_name_follows_adr_convention( - sample_git_sha: GitSha, - sample_toolchain: ToolchainChannel, - sample_target: TargetTriple, - temp_dir: TempDir, + #[from(sample_git_sha)] sample_git_sha_res: Result, + #[from(sample_toolchain)] sample_toolchain_res: Result, + #[from(sample_target)] sample_target_res: Result, + #[from(temp_dir)] temp_dir_res: std::io::Result, ) { + let sample_git_sha = sample_git_sha_res.expect("sample git SHA should validate"); + let sample_toolchain = sample_toolchain_res.expect("sample toolchain should validate"); + let sample_target = sample_target_res.expect("sample target should validate"); + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let lib_path = temp_dir.path().join("libtest.so"); fs::write(&lib_path, b"content").expect("write"); @@ -216,28 +247,30 @@ fn archive_name_follows_adr_convention( /// Create a [`PackageOutput`] with a single dummy library for tests that /// only need a valid package without caring about specific file content. -fn create_test_package(temp_dir: &TempDir) -> PackageOutput { +fn create_test_package(temp_dir: &TempDir) -> Result { let lib_path = temp_dir.path().join("libtest.so"); - fs::write(&lib_path, b"test content for hash").expect("write"); + fs::write(&lib_path, b"test content for hash")?; let output_dir = temp_dir.path().join("dist"); - fs::create_dir_all(&output_dir).expect("mkdir"); + fs::create_dir_all(&output_dir)?; let params = PackageParams { - git_sha: GitSha::try_from("deadbeef").expect("valid"), - toolchain: ToolchainChannel::try_from("nightly-2026-05-28").expect("valid"), - target: TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid"), + git_sha: GitSha::try_from("deadbeef")?, + toolchain: ToolchainChannel::try_from("nightly-2026-05-28")?, + target: TargetTriple::try_from("x86_64-unknown-linux-gnu")?, library_files: vec![lib_path], output_dir, generated_at: GeneratedAt::new("2026-02-11T12:00:00Z"), }; - package_artefact(params).expect("packaging") + package_artefact(params) } #[rstest] -fn manifest_sha256_is_valid_hex(temp_dir: TempDir) { - let output = create_test_package(&temp_dir); +fn manifest_sha256_is_valid_hex(#[from(temp_dir)] temp_dir_res: std::io::Result) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + + let output = create_test_package(&temp_dir).expect("test package should build"); assert_eq!(output.manifest.sha256().as_str().len(), 64); assert!( output @@ -250,8 +283,12 @@ fn manifest_sha256_is_valid_hex(temp_dir: TempDir) { } #[rstest] -fn manifest_sha256_matches_archive_digest(temp_dir: TempDir) { - let output = create_test_package(&temp_dir); +fn manifest_sha256_matches_archive_digest( + #[from(temp_dir)] temp_dir_res: std::io::Result, +) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + + let output = create_test_package(&temp_dir).expect("test package should build"); let archive_digest = compute_sha256(&output.archive_path).expect("sha256 of archive"); assert_eq!( archive_digest.as_str(), @@ -261,7 +298,11 @@ fn manifest_sha256_matches_archive_digest(temp_dir: TempDir) { } #[rstest] -fn packaging_produces_deterministic_digest(temp_dir: TempDir) { +fn packaging_produces_deterministic_digest( + #[from(temp_dir)] temp_dir_res: std::io::Result, +) { + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let lib_path = temp_dir.path().join("libtest.so"); fs::write(&lib_path, b"content for hash check").expect("write"); @@ -281,8 +322,11 @@ fn packaging_produces_deterministic_digest(temp_dir: TempDir) { let output = package_artefact(params).expect("packaging"); digests.push(output.manifest.sha256().as_str().to_owned()); } + let [first, second] = digests.as_slice() else { + panic!("expected exactly two digests, got {}", digests.len()); + }; assert_eq!( - digests[0], digests[1], + first, second, "identical inputs must produce identical manifest digests" ); } @@ -301,16 +345,16 @@ fn collect_file_names_rejects_path_without_filename() { } /// Extract entry names from a `.tar.zst` archive for test assertions. -fn list_archive_entries(archive_path: &Path) -> Vec { - let file = fs::File::open(archive_path).expect("open archive"); - let decoder = zstd::Decoder::new(file).expect("zstd decode"); +fn list_archive_entries(archive_path: &Path) -> std::io::Result> { + let file = fs::File::open(archive_path)?; + let decoder = zstd::Decoder::new(file)?; let mut archive = tar::Archive::new(decoder); archive - .entries() - .expect("entries") - .map(|e| { - let entry = e.expect("entry"); - entry.path().expect("path").to_string_lossy().into_owned() + .entries()? + .map(|entry_result| { + let entry = entry_result?; + let path = entry.path()?; + Ok(path.to_string_lossy().into_owned()) }) .collect() } diff --git a/installer/src/binstall_metadata_tests.rs b/installer/src/binstall_metadata_tests.rs index dab46c363..d2407d922 100644 --- a/installer/src/binstall_metadata_tests.rs +++ b/installer/src/binstall_metadata_tests.rs @@ -4,9 +4,10 @@ //! the `[package.metadata.binstall]` section matches the specification in //! the design document (§ Installer release artefacts). -use super::*; use rstest::rstest; +use super::*; + #[rstest] fn pkg_url_matches_design_document() { let table = load_cargo_toml(); @@ -99,8 +100,9 @@ fn essential_binstall_fields_present() { #[case::macos_arm("aarch64-apple-darwin")] fn non_windows_targets_expand_to_tgz(#[case] target: &str) { let url = expand_pkg_url("0.2.0", target); + let extension = std::path::Path::new(&url).extension(); assert!( - url.ends_with(".tgz"), + extension.is_some_and(|ext| ext.eq_ignore_ascii_case("tgz")), "expected URL for {target} to end with .tgz, got {url}" ); assert!(url.contains(target)); @@ -110,8 +112,9 @@ fn non_windows_targets_expand_to_tgz(#[case] target: &str) { #[rstest] fn windows_target_expands_to_zip() { let url = expand_pkg_url("0.2.0", WINDOWS_OVERRIDE_TARGET); + let extension = std::path::Path::new(&url).extension(); assert!( - url.ends_with(".zip"), + extension.is_some_and(|ext| ext.eq_ignore_ascii_case("zip")), "expected URL for Windows to end with .zip, got {url}" ); } diff --git a/installer/src/cli_tests.rs b/installer/src/cli_tests.rs index 8b732bd26..048118e9b 100644 --- a/installer/src/cli_tests.rs +++ b/installer/src/cli_tests.rs @@ -1,24 +1,54 @@ //! Tests for installer CLI parsing and default behaviours. -use super::*; use rstest::rstest; +use super::*; + #[test] fn cli_parses_defaults() { let cli = Cli::parse_from(["whitaker-installer"]); assert!(cli.command.is_none()); - assert!(cli.install.target_dir.is_none()); - assert!(cli.install.lint.is_empty()); - assert!(!cli.install.individual_lints); - assert!(!cli.install.experimental); - assert!(!cli.install.cranelift); - assert!(!cli.install.dry_run); - assert_eq!(cli.install.verbosity, 0); - assert!(!cli.install.quiet); - assert!(!cli.install.skip_deps); - assert!(!cli.install.skip_wrapper); - assert!(!cli.install.no_update); - assert!(!cli.install.is_build_only); + assert_default_lint_selection(&cli); + assert_default_execution_options(&cli); + assert_default_skip_flags(&cli); +} + +/// Assert each named condition holds, reporting the first field that differs +/// from its documented default. +fn assert_default_conditions(conditions: &[(&str, bool)]) { + for &(name, condition) in conditions { + assert!(condition, "{name} must have its default value"); + } +} + +/// Assert that no target directory or lint selection is present by default. +fn assert_default_lint_selection(cli: &Cli) { + assert_default_conditions(&[ + ("target_dir", cli.install.target_dir.is_none()), + ("lint", cli.install.lint.is_empty()), + ("individual_lints", !cli.install.individual_lints), + ("experimental", !cli.install.experimental), + ]); +} + +/// Assert that execution options default to a full, non-quiet build. +fn assert_default_execution_options(cli: &Cli) { + assert_default_conditions(&[ + ("cranelift", !cli.install.cranelift), + ("dry_run", !cli.install.dry_run), + ("verbosity", cli.install.verbosity == 0), + ("quiet", !cli.install.quiet), + ]); +} + +/// Assert that no pipeline steps are skipped by default. +fn assert_default_skip_flags(cli: &Cli) { + assert_default_conditions(&[ + ("skip_deps", !cli.install.skip_deps), + ("skip_wrapper", !cli.install.skip_wrapper), + ("no_update", !cli.install.no_update), + ("is_build_only", !cli.install.is_build_only), + ]); } #[test] @@ -165,10 +195,12 @@ fn cli_rejects_conflicting_flags(#[case] args: &[&str]) { #[test] fn install_args_default_is_valid() { let args = InstallArgs::default(); - assert!(!args.individual_lints); - assert!(!args.experimental); - assert!(!args.cranelift); - assert!(!args.skip_deps); + assert_default_conditions(&[ + ("individual_lints", !args.individual_lints), + ("experimental", !args.experimental), + ("cranelift", !args.cranelift), + ("skip_deps", !args.skip_deps), + ]); } #[test] diff --git a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs index a45210ad1..81b06990f 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs @@ -5,30 +5,22 @@ //! runs without the network. The server helper lives here (not in `downloader.rs`) //! to keep that module within its size budget; it is test support for these cases. -use super::downloader::download_from_urls; -// Only the non-UTF-8 production-path test (gated `#[cfg(unix)]`) drives the -// trait and concrete downloader; keep these imports on the same gate so other -// platforms do not see them as unused. -#[cfg(unix)] -use super::downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}; -use super::http_test_server::{CannedResponse, LocalServer}; -use super::installer::DependencyBinaryInstallError; -use crate::hex::to_lower_hex; +// `Read` is used across platforms for `read_to_end`. +use std::{collections::HashMap, io::Read, path::PathBuf, time::Duration}; + use camino::Utf8Path; -use cap_std::ambient_authority; -use cap_std::fs_utf8::Dir; +use cap_std::{ambient_authority, fs_utf8::Dir}; use rstest::{fixture, rstest}; use sha2::{Digest, Sha256}; -use std::collections::HashMap; -// `io` is only referenced by the `#[cfg(unix)]` non-UTF-8 tests (`io::ErrorKind`); -// `Read` is used across platforms for `read_to_end`. -#[cfg(unix)] -use std::io; -use std::io::Read; -use std::path::PathBuf; -use std::time::Duration; use tempfile::TempDir; +use super::{ + downloader::download_from_urls, + http_test_server::{CannedResponse, LocalServer}, + installer::DependencyBinaryInstallError, +}; +use crate::hex::to_lower_hex; + /// A short-timeout agent fixture for driving the local server. #[fixture] fn agent() -> ureq::Agent { @@ -61,14 +53,15 @@ impl DownloadHarness { /// Open the destination's parent directory as a capability, for asserting on /// the written archive. - fn destination_dir(&self) -> Dir { - let parent = Utf8Path::from_path( - self.destination - .parent() - .expect("destination has a parent directory"), - ) - .expect("temp path is UTF-8"); - Dir::open_ambient_dir(parent, ambient_authority()).expect("open temp dir capability") + fn destination_dir(&self) -> std::io::Result { + let parent = self + .destination + .parent() + .and_then(Utf8Path::from_path) + .ok_or_else(|| { + std::io::Error::other("destination must have a UTF-8 parent directory") + })?; + Dir::open_ambient_dir(parent, ambient_authority()) } } @@ -79,15 +72,15 @@ impl DownloadHarness { #[fixture] fn download_harness( #[default(HashMap::new())] routes: HashMap, -) -> DownloadHarness { +) -> std::io::Result { let server = LocalServer::start(routes); - let temp = TempDir::new().expect("create temp dir"); + let temp = TempDir::new()?; let destination = temp.path().join("archive.tgz"); - DownloadHarness { + Ok(DownloadHarness { server, _temp: temp, destination, - } + }) } #[rstest] @@ -103,7 +96,7 @@ fn download_from_urls_writes_the_archive_and_requests_both_endpoints(agent: ureq "/archive.tgz.sha256".to_owned(), CannedResponse::ok(format!("{checksum} archive.tgz\n").into_bytes()), ); - let harness = download_harness(routes); + let harness = download_harness(routes).expect("download harness should start"); download_from_urls( &agent, @@ -118,6 +111,7 @@ fn download_from_urls_writes_the_archive_and_requests_both_endpoints(agent: ureq let mut written = Vec::new(); harness .destination_dir() + .expect("destination directory should open") .open("archive.tgz") .expect("open written archive") .read_to_end(&mut written) @@ -140,7 +134,7 @@ fn download_from_urls_reports_a_checksum_mismatch(agent: ureq::Agent) { "/archive.tgz.sha256".to_owned(), CannedResponse::ok(format!("{wrong_checksum} archive.tgz\n").into_bytes()), ); - let harness = download_harness(routes); + let harness = download_harness(routes).expect("download harness should start"); let error = download_from_urls( &agent, @@ -171,7 +165,10 @@ fn download_from_urls_reports_a_checksum_mismatch(agent: ureq::Agent) { // The unverified archive must not survive a checksum mismatch, so a retry // never reads stale data from the destination. assert!( - !harness.destination_dir().exists("archive.tgz"), + !harness + .destination_dir() + .expect("destination directory should open") + .exists("archive.tgz"), "archive must be removed from the destination after a checksum mismatch", ); } @@ -187,7 +184,7 @@ fn download_from_urls_rejects_an_oversized_checksum_sidecar(agent: ureq::Agent) "/archive.tgz.sha256".to_owned(), CannedResponse::ok(vec![b'a'; 128 * 1024]), ); - let harness = download_harness(routes); + let harness = download_harness(routes).expect("download harness should start"); let error = download_from_urls( &agent, @@ -209,7 +206,10 @@ fn download_from_urls_rejects_an_oversized_checksum_sidecar(agent: ureq::Agent) } assert!( - !harness.destination_dir().exists("archive.tgz"), + !harness + .destination_dir() + .expect("destination directory should open") + .exists("archive.tgz"), "archive must be removed after an oversized checksum sidecar", ); } @@ -233,7 +233,7 @@ fn download_from_urls_removes_the_partial_archive_when_the_write_fails(agent: ur "/archive.tgz.sha256".to_owned(), CannedResponse::ok(sidecar.into_bytes()), ); - let harness = download_harness(routes); + let harness = download_harness(routes).expect("download harness should start"); let error = download_from_urls( &agent, @@ -259,7 +259,10 @@ fn download_from_urls_removes_the_partial_archive_when_the_write_fails(agent: ur ); assert!( - !harness.destination_dir().exists("archive.tgz"), + !harness + .destination_dir() + .expect("destination directory should open") + .exists("archive.tgz"), "partial archive must be removed after a write failure", ); } @@ -277,7 +280,7 @@ fn download_from_urls_accepts_an_uppercase_checksum_sidecar(agent: ureq::Agent) "/archive.tgz.sha256".to_owned(), CannedResponse::ok(format!("{checksum} archive.tgz\n").into_bytes()), ); - let harness = download_harness(routes); + let harness = download_harness(routes).expect("download harness should start"); download_from_urls( &agent, @@ -306,7 +309,7 @@ fn download_from_urls_rejects_a_malformed_checksum_sidecar( "/archive.tgz.sha256".to_owned(), CannedResponse::ok(sidecar.as_bytes().to_vec()), ); - let harness = download_harness(routes); + let harness = download_harness(routes).expect("download harness should start"); let checksum_url = harness.checksum_url(); let error = download_from_urls( @@ -327,73 +330,84 @@ fn download_from_urls_rejects_a_malformed_checksum_sidecar( // The archive was written before the checksum failed, so it must be removed. assert!( - !harness.destination_dir().exists("archive.tgz"), + !harness + .destination_dir() + .expect("destination directory should open") + .exists("archive.tgz"), "archive must be removed after a checksum retrieval failure", ); } +// The non-UTF-8 destination cases are Unix-only and live in their own inline +// module so their imports stay scoped to the platform that compiles them. #[cfg(unix)] -#[test] -fn download_from_urls_rejects_a_non_utf8_destination_before_any_request() { - use std::os::unix::ffi::OsStringExt as _; - - // No route is registered; the server exists only to prove it is never - // contacted for an invalid destination. - let server = LocalServer::start(HashMap::new()); - - // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8 - // and must be rejected during validation, before any HTTP request. - let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ - b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', - ])); - - let error = download_from_urls( - &agent(), - &server.url("/archive.tgz"), - &server.url("/archive.tgz.sha256"), - &invalid, - ) - .expect_err("non-UTF-8 destination must be rejected"); +mod non_utf8 { + use std::io; + + use super::super::downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}; + use super::*; + + #[test] + fn download_from_urls_rejects_a_non_utf8_destination_before_any_request() { + use std::os::unix::ffi::OsStringExt as _; + + // No route is registered; the server exists only to prove it is never + // contacted for an invalid destination. + let server = LocalServer::start(HashMap::new()); + + // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8 + // and must be rejected during validation, before any HTTP request. + let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ + b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', + ])); + + let error = download_from_urls( + &agent(), + &server.url("/archive.tgz"), + &server.url("/archive.tgz.sha256"), + &invalid, + ) + .expect_err("non-UTF-8 destination must be rejected"); - match error { - DependencyBinaryInstallError::Io(source) => { - assert_eq!(source.kind(), io::ErrorKind::InvalidInput); + match error { + DependencyBinaryInstallError::Io(source) => { + assert_eq!(source.kind(), io::ErrorKind::InvalidInput); + } + other => panic!("expected an Io error, got {other:?}"), } - other => panic!("expected an Io error, got {other:?}"), + assert!( + server.requested_paths().is_empty(), + "no HTTP request must be made for an invalid destination", + ); } - assert!( - server.requested_paths().is_empty(), - "no HTTP request must be made for an invalid destination", - ); -} - -#[cfg(unix)] -#[test] -fn download_rejects_a_non_utf8_destination_before_any_network_access() { - use std::os::unix::ffi::OsStringExt as _; - - // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8. - // The production `download` must reject it during path validation, which - // happens before any HTTP call, so the test needs no network. - let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ - b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', - ])); - - let downloader = RepositoryArchiveDownloader; - let error = downloader - .download("whitaker-dependency", &invalid) - .expect_err("non-UTF-8 destination must be rejected"); - match error { - DependencyBinaryInstallError::Io(source) => { - assert_eq!(source.kind(), io::ErrorKind::InvalidInput); - assert!( - source - .to_string() - .contains("destination archive path is not valid UTF-8"), - "error should identify the non-UTF-8 destination, got: {source}", - ); + #[test] + fn download_rejects_a_non_utf8_destination_before_any_network_access() { + use std::os::unix::ffi::OsStringExt as _; + + // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8. + // The production `download` must reject it during path validation, which + // happens before any HTTP call, so the test needs no network. + let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ + b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', + ])); + + let downloader = RepositoryArchiveDownloader; + let error = downloader + .download("whitaker-dependency", &invalid) + .expect_err("non-UTF-8 destination must be rejected"); + + match error { + DependencyBinaryInstallError::Io(source) => { + assert_eq!(source.kind(), io::ErrorKind::InvalidInput); + assert!( + source + .to_string() + .contains("destination archive path is not valid UTF-8"), + "error should identify the non-UTF-8 destination, got: {source}", + ); + } + other => panic!("expected an Io error, got {other:?}"), } - other => panic!("expected an Io error, got {other:?}"), } } diff --git a/installer/src/dependency_packaging_tests.rs b/installer/src/dependency_packaging_tests.rs index bb7fa5bfd..e4aa09fbe 100644 --- a/installer/src/dependency_packaging_tests.rs +++ b/installer/src/dependency_packaging_tests.rs @@ -1,15 +1,20 @@ //! Unit tests for dependency-binary packaging helpers. -use crate::dependency_binaries::find_dependency_binary; -use crate::dependency_packaging::{ - ArchiveFormat, DependencyPackageParams, DependencyPackagingError, archive_format, - inner_dir_name, package_dependency_binary, render_provenance_markdown, - write_provenance_markdown, -}; -use crate::installer_packaging::TargetTriple; -use rstest::{fixture, rstest}; use std::fs; +use rstest::{fixture, rstest}; + +use crate::{ + artefact::error::ArtefactError, + dependency_binaries::find_dependency_binary, + dependency_packaging::{ + ArchiveFormat, DependencyPackageParams, DependencyPackagingError, archive_format, + inner_dir_name, package_dependency_binary, render_provenance_markdown, + write_provenance_markdown, + }, + installer_packaging::TargetTriple, +}; + struct PackagingCase<'a> { package: &'a str, binary_name: &'a str, @@ -17,19 +22,16 @@ struct PackagingCase<'a> { should_expect_success: bool, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn linux_target() -> TargetTriple { - TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target") +fn linux_target() -> std::result::Result { + TargetTriple::try_from("x86_64-unknown-linux-gnu") } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn windows_target() -> TargetTriple { - TargetTriple::try_from("x86_64-pc-windows-msvc").expect("valid target") -} - -#[fixture] -fn temp_dir() -> tempfile::TempDir { - tempfile::tempdir().expect("temp dir") +fn temp_dir() -> std::io::Result { + tempfile::tempdir() } #[test] @@ -68,10 +70,13 @@ fn inner_dir_name_uses_dependency_version() { should_expect_success: true, })] fn package_dependency_binary_handles_binary_presence( - linux_target: TargetTriple, - temp_dir: tempfile::TempDir, + #[from(linux_target)] linux_target_res: std::result::Result, + #[from(temp_dir)] temp_dir_res: std::io::Result, #[case] case: PackagingCase<'_>, ) { + let linux_target = linux_target_res.expect("linux target triple should validate"); + let temp_dir = temp_dir_res.expect("temporary directory should be created"); + let dependency = find_dependency_binary(case.package) .expect("dependency manifest should load") .expect("dependency should exist"); @@ -147,7 +152,7 @@ fn write_provenance_markdown_writes_expected_file() { .expect("dependency should exist") .clone(), ]; - let temp_dir = tempfile::tempdir().expect("temp dir"); + let temp_dir = tempfile::tempdir().expect("temporary directory should be created"); let path = write_provenance_markdown(temp_dir.path(), &dependencies) .expect("provenance file should be written"); diff --git a/installer/src/deps/install_tests.rs b/installer/src/deps/install_tests.rs index dc41e6eee..a4c62899a 100644 --- a/installer/src/deps/install_tests.rs +++ b/installer/src/deps/install_tests.rs @@ -1,10 +1,11 @@ //! Tests for dependency-install status refresh behaviour. +use rstest::rstest; + use super::*; use crate::test_utils::dependency_binary_helpers::{ dylint_link_install_list_check, with_fake_binary_on_path, }; -use rstest::rstest; #[rstest] #[case(InstallOutcome::CargoBinstall)] diff --git a/installer/src/deps/path_tests.rs b/installer/src/deps/path_tests.rs index efc7506a3..c9385b36e 100644 --- a/installer/src/deps/path_tests.rs +++ b/installer/src/deps/path_tests.rs @@ -1,14 +1,20 @@ //! Tests for PATH-based dependency-binary discovery helpers. +use temp_env::with_vars_unset; + use super::*; -use crate::test_support::env_test_guard; -use crate::test_utils::dependency_binary_helpers::{ - cargo_dylint_check, cargo_dylint_check_with_result, dylint_link_install_list_check, - dylint_link_install_list_check_with_version, with_fake_binary_on_path, with_fake_path, - write_fake_binary, write_fake_binary_with_status, +use crate::{ + test_support::env_test_guard, + test_utils::{ + ExpectedCall, StubExecutor, + dependency_binary_helpers::{ + cargo_dylint_check, cargo_dylint_check_with_result, dylint_link_install_list_check, + dylint_link_install_list_check_with_version, with_fake_binary_on_path, with_fake_path, + write_fake_binary, write_fake_binary_with_status, + }, + stdout_output, + }, }; -use crate::test_utils::{ExpectedCall, StubExecutor, stdout_output}; -use temp_env::with_vars_unset; #[test] fn check_dylint_tools_reports_installed_tools() { @@ -35,7 +41,7 @@ fn check_dylint_tools_reports_installed_tools() { fn check_dylint_tools_rejects_unusable_cargo_dylint_output(#[case] version_stdout: &str) { // The fake PATH keeps dylint-link absent so only cargo-dylint is probed. with_fake_path( - |_| {}, + |_| (), || { let executor = StubExecutor::new(vec![cargo_dylint_check_with_result(Ok( stdout_output(version_stdout), @@ -83,12 +89,13 @@ fn check_dylint_tools_rejects_unpinned_dylint_link(#[case] install_list_check: E fn check_dylint_tools_rejects_non_invocable_dylint_link_on_path() { with_fake_path( |directories| { + let first_dir = &directories[0]; #[cfg(windows)] - let binary_path = directories[0].join("dylint-link.cmd"); + let binary_path = first_dir.join("dylint-link.cmd"); #[cfg(not(windows))] - let binary_path = directories[0].join("dylint-link"); + let binary_path = first_dir.join("dylint-link"); - write_fake_binary_with_status(&binary_path, true, 1); + write_fake_binary_with_status(&binary_path, true, 1) }, || { let executor = StubExecutor::new(vec![cargo_dylint_check()]); @@ -126,7 +133,7 @@ fn is_binary_on_path_returns_false_when_path_is_empty() { #[test] fn is_binary_on_path_returns_false_when_binary_is_missing_from_all_directories() { with_fake_path( - |_| {}, + |_| (), || { assert!(!is_binary_on_path("dylint-link")); }, @@ -137,12 +144,13 @@ fn is_binary_on_path_returns_false_when_binary_is_missing_from_all_directories() fn is_binary_on_path_checks_multiple_directories() { with_fake_path( |directories| { + let second_dir = &directories[1]; #[cfg(windows)] - let binary_path = directories[1].join("dylint-link.exe"); + let binary_path = second_dir.join("dylint-link.exe"); #[cfg(not(windows))] - let binary_path = directories[1].join("dylint-link"); + let binary_path = second_dir.join("dylint-link"); - write_fake_binary(&binary_path, true); + write_fake_binary(&binary_path, true) }, || { assert!(is_binary_on_path("dylint-link")); diff --git a/installer/src/install_metrics_tests.rs b/installer/src/install_metrics_tests.rs index bf9760d8b..dfb9787d1 100644 --- a/installer/src/install_metrics_tests.rs +++ b/installer/src/install_metrics_tests.rs @@ -1,12 +1,12 @@ //! Unit tests for installer metrics persistence and aggregation. +use std::{io::ErrorKind, path::PathBuf, time::Duration}; + +use rstest::{fixture, rstest}; + use crate::install_metrics::{ InstallMetrics, InstallMetricsError, InstallMode, record_install_at_path, }; -use rstest::{fixture, rstest}; -use std::io::ErrorKind; -use std::path::PathBuf; -use std::time::Duration; struct MetricsPathFixture { _temp_dir: tempfile::TempDir, @@ -14,20 +14,26 @@ struct MetricsPathFixture { } #[fixture] -fn metrics_path_fixture() -> MetricsPathFixture { - let temp_dir = tempfile::tempdir().expect("create tempdir"); +fn metrics_path_fixture() -> std::io::Result { + let temp_dir = tempfile::tempdir()?; let metrics_path = temp_dir.path().join("metrics").join("install_metrics.json"); - MetricsPathFixture { + Ok(MetricsPathFixture { _temp_dir: temp_dir, metrics_path, - } + }) } #[test] fn zero_state_rates_are_zero() { let metrics = InstallMetrics::default(); - assert_eq!(metrics.download_rate(), 0.0); - assert_eq!(metrics.build_rate(), 0.0); + assert!( + metrics.download_rate().abs() < f64::EPSILON, + "zero-state download rate" + ); + assert!( + metrics.build_rate().abs() < f64::EPSILON, + "zero-state build rate" + ); } #[test] @@ -40,12 +46,22 @@ fn record_install_updates_counts_and_duration() { assert_eq!(metrics.download_installs(), 1); assert_eq!(metrics.build_installs(), 1); assert_eq!(metrics.total_install_duration(), Duration::from_secs(2)); - assert!((metrics.download_rate() - 0.5).abs() < f64::EPSILON); - assert!((metrics.build_rate() - 0.5).abs() < f64::EPSILON); + assert!( + (metrics.download_rate() - 0.5).abs() < f64::EPSILON, + "download rate" + ); + assert!( + (metrics.build_rate() - 0.5).abs() < f64::EPSILON, + "build rate" + ); } #[rstest] -fn record_install_at_path_creates_metrics_file(metrics_path_fixture: MetricsPathFixture) { +fn record_install_at_path_creates_metrics_file( + #[from(metrics_path_fixture)] metrics_path_res: std::io::Result, +) { + let metrics_path_fixture = metrics_path_res.expect("metrics fixture should be created"); + let result = record_install_at_path( &metrics_path_fixture.metrics_path, InstallMode::Download, @@ -58,7 +74,11 @@ fn record_install_at_path_creates_metrics_file(metrics_path_fixture: MetricsPath } #[rstest] -fn malformed_metrics_file_is_reset_and_recovered(metrics_path_fixture: MetricsPathFixture) { +fn malformed_metrics_file_is_reset_and_recovered( + #[from(metrics_path_fixture)] metrics_path_res: std::io::Result, +) { + let metrics_path_fixture = metrics_path_res.expect("metrics fixture should be created"); + std::fs::create_dir_all( metrics_path_fixture .metrics_path @@ -82,7 +102,11 @@ fn malformed_metrics_file_is_reset_and_recovered(metrics_path_fixture: MetricsPa } #[rstest] -fn persistence_failure_is_reported(metrics_path_fixture: MetricsPathFixture) { +fn persistence_failure_is_reported( + #[from(metrics_path_fixture)] metrics_path_res: std::io::Result, +) { + let metrics_path_fixture = metrics_path_res.expect("metrics fixture should be created"); + std::fs::create_dir_all(&metrics_path_fixture.metrics_path).expect("create blocking directory"); let error = record_install_at_path( @@ -110,7 +134,11 @@ fn persistence_failure_is_reported(metrics_path_fixture: MetricsPathFixture) { } #[rstest] -fn summary_line_includes_rates_and_total_time(metrics_path_fixture: MetricsPathFixture) { +fn summary_line_includes_rates_and_total_time( + #[from(metrics_path_fixture)] metrics_path_res: std::io::Result, +) { + let metrics_path_fixture = metrics_path_res.expect("metrics fixture should be created"); + record_install_at_path( &metrics_path_fixture.metrics_path, InstallMode::Download, @@ -131,7 +159,11 @@ fn summary_line_includes_rates_and_total_time(metrics_path_fixture: MetricsPathF } #[rstest] -fn long_durations_saturate_total_install_time(metrics_path_fixture: MetricsPathFixture) { +fn long_durations_saturate_total_install_time( + #[from(metrics_path_fixture)] metrics_path_res: std::io::Result, +) { + let metrics_path_fixture = metrics_path_res.expect("metrics fixture should be created"); + std::fs::create_dir_all( metrics_path_fixture .metrics_path @@ -168,16 +200,24 @@ fn long_durations_saturate_total_install_time(metrics_path_fixture: MetricsPathF } #[rstest] -fn concurrent_records_do_not_lose_updates(metrics_path_fixture: MetricsPathFixture) { +fn concurrent_records_do_not_lose_updates( + #[from(metrics_path_fixture)] metrics_path_res: std::io::Result, +) { + let metrics_path_fixture = metrics_path_res.expect("metrics fixture should be created"); + let path = metrics_path_fixture.metrics_path; let mut threads = Vec::new(); for _ in 0..4 { - let path = path.clone(); + let writer_path = path.clone(); threads.push(std::thread::spawn(move || { for _ in 0..20 { - record_install_at_path(&path, InstallMode::Download, Duration::from_millis(1)) - .expect("record install from concurrent writer"); + record_install_at_path( + &writer_path, + InstallMode::Download, + Duration::from_millis(1), + ) + .expect("record install from concurrent writer"); } })); } diff --git a/installer/src/installer_packaging_tests.rs b/installer/src/installer_packaging_tests.rs index f3f0118d7..c575b4752 100644 --- a/installer/src/installer_packaging_tests.rs +++ b/installer/src/installer_packaging_tests.rs @@ -1,15 +1,26 @@ //! Unit tests for installer binary archive packaging. -use super::*; -use crate::binstall_metadata; +use std::{fs, io::Read}; + use rstest::rstest; -use std::fs; -use std::io::Read; + +use super::*; +use crate::{artefact::error::ArtefactError, binstall_metadata}; // --------------------------------------------------------------------------- // Shared fixtures // --------------------------------------------------------------------------- +/// Reports whether `name` ends in `extension`, ignoring ASCII case. +/// +/// Archive names are generated by the installer, but comparing case-insensitively +/// keeps the assertion honest on case-preserving file systems. +fn has_extension(name: &str, extension: &str) -> bool { + std::path::Path::new(name) + .extension() + .is_some_and(|actual| actual.eq_ignore_ascii_case(extension)) +} + /// A temporary directory with a fake binary suitable for packaging. struct PackagingFixture { temp: tempfile::TempDir, @@ -18,12 +29,12 @@ struct PackagingFixture { /// Create a [`PackagingFixture`] for the given target, writing a fake binary /// with the provided content into a fresh temporary directory. -fn packaging_fixture(target: &str, content: &[u8]) -> PackagingFixture { - let temp = tempfile::tempdir().expect("temp dir"); - let bin_name = binary_filename(&TargetTriple::try_from(target).expect("valid target")); - let binary_path = temp.path().join(bin_name); - fs::write(&binary_path, content).expect("write fake binary"); - PackagingFixture { temp, binary_path } +fn packaging_fixture(target: &str, content: &[u8]) -> std::io::Result { + let temp = tempfile::tempdir()?; + let triple = TargetTriple::try_from(target).map_err(std::io::Error::other)?; + let binary_path = temp.path().join(binary_filename(&triple)); + fs::write(&binary_path, content)?; + Ok(PackagingFixture { temp, binary_path }) } /// Build [`InstallerPackageParams`] from a fixture, version, and target. @@ -31,44 +42,42 @@ fn params_from_fixture( fixture: &PackagingFixture, version: &str, target: &str, -) -> InstallerPackageParams { - InstallerPackageParams { +) -> std::result::Result { + Ok(InstallerPackageParams { version: Version::new(version), - target: TargetTriple::try_from(target).expect("valid target"), + target: TargetTriple::try_from(target)?, binary_path: fixture.binary_path.clone(), output_dir: fixture.temp.path().to_path_buf(), - } + }) } /// Read entry paths from a `.tgz` archive, failing explicitly on errors. -fn read_tgz_entry_paths(archive_path: &std::path::Path) -> Vec { - let file = fs::File::open(archive_path).expect("open archive"); +fn read_tgz_entry_paths(archive_path: &std::path::Path) -> std::io::Result> { + let file = fs::File::open(archive_path)?; let gz = flate2::read::GzDecoder::new(file); let mut tar_archive = tar::Archive::new(gz); tar_archive - .entries() - .expect("entries") - .map(|e| { - let entry = e.expect("valid tar entry"); - entry - .path() - .expect("valid entry path") - .to_string_lossy() - .into_owned() + .entries()? + .map(|entry_result| { + let entry = entry_result?; + let path = entry.path()?; + Ok(path.to_string_lossy().into_owned()) }) .collect() } /// Read entry names from a `.zip` archive, failing explicitly on errors. -fn read_zip_entry_names(archive_path: &std::path::Path) -> Vec { - let file = fs::File::open(archive_path).expect("open archive"); - let zip_archive = zip::ZipArchive::new(file).expect("open zip"); +fn read_zip_entry_names( + archive_path: &std::path::Path, +) -> std::result::Result, zip::result::ZipError> { + let file = fs::File::open(archive_path)?; + let zip_archive = zip::ZipArchive::new(file)?; (0..zip_archive.len()) - .map(|i| { + .map(|index| { zip_archive - .name_for_index(i) - .expect("valid zip entry name") - .to_owned() + .name_for_index(index) + .map(str::to_owned) + .ok_or(zip::result::ZipError::FileNotFound) }) .collect() } @@ -86,7 +95,10 @@ fn archive_filename_tgz_for_non_windows(#[case] target: &str) { let v = Version::new("0.2.1"); let t = TargetTriple::try_from(target).expect("valid target"); let name = archive_filename(&v, &t); - assert!(name.ends_with(".tgz"), "expected .tgz suffix, got {name}"); + assert!( + has_extension(&name, "tgz"), + "expected .tgz suffix, got {name}" + ); assert!(name.contains(target), "expected target in name, got {name}"); assert!( name.contains("v0.2.1"), @@ -186,19 +198,21 @@ fn package_installer_creates_archive( #[case] expected_name: &str, #[case] expected_entry: &str, ) { - let fixture = packaging_fixture(target, content); - let params = params_from_fixture(&fixture, "0.2.1", target); + let fixture = packaging_fixture(target, content).expect("packaging fixture should be staged"); + let params = + params_from_fixture(&fixture, "0.2.1", target).expect("packaging params should build"); let output = package_installer(params).expect("packaging should succeed"); assert!(output.archive_path.exists(), "archive should exist"); assert_eq!(output.archive_name, expected_name); - let entries = if expected_name.ends_with(".tgz") { - read_tgz_entry_paths(&output.archive_path) + let entries = if has_extension(expected_name, "tgz") { + read_tgz_entry_paths(&output.archive_path).expect("tgz archive should list entries") } else { - read_zip_entry_names(&output.archive_path) + read_zip_entry_names(&output.archive_path).expect("zip archive should list entries") }; assert_eq!(entries.len(), 1, "expected 1 entry, got {entries:?}"); - assert_eq!(entries[0], expected_entry); + let entry = entries.first().expect("archive should contain one entry"); + assert_eq!(entry, expected_entry); } #[test] @@ -271,8 +285,10 @@ fn archive_name_matches_binstall_template(#[case] target: &str, #[case] version: #[test] fn tgz_archive_preserves_binary_content() { let content = b"binary-payload-12345"; - let fixture = packaging_fixture("aarch64-unknown-linux-gnu", content); - let params = params_from_fixture(&fixture, "0.2.1", "aarch64-unknown-linux-gnu"); + let fixture = packaging_fixture("aarch64-unknown-linux-gnu", content) + .expect("packaging fixture should be staged"); + let params = params_from_fixture(&fixture, "0.2.1", "aarch64-unknown-linux-gnu") + .expect("packaging params should build"); let output = package_installer(params).expect("packaging"); let file = fs::File::open(&output.archive_path).expect("open"); diff --git a/installer/src/pipeline_staging_tests.rs b/installer/src/pipeline_staging_tests.rs index 1a468ff3c..695576886 100644 --- a/installer/src/pipeline_staging_tests.rs +++ b/installer/src/pipeline_staging_tests.rs @@ -1,13 +1,13 @@ //! Staging-focused tests for pipeline orchestration. -use crate::builder::BuildResult; -use crate::crate_name::CrateName; -use crate::pipeline::stage_libraries; -use crate::toolchain::Toolchain; use camino::{Utf8Path, Utf8PathBuf}; use rstest::{fixture, rstest}; use tempfile::TempDir; +use crate::{ + builder::BuildResult, crate_name::CrateName, pipeline::stage_libraries, toolchain::Toolchain, +}; + /// Fixture providing a temporary directory for staging tests. /// /// Contains its own fields for real file system operations during staging @@ -25,15 +25,15 @@ struct StagingTestContext { } impl StagingTestContext { - fn new() -> Self { + fn new() -> std::io::Result { use std::fs; - let temp_dir = TempDir::new().expect("failed to create temp dir"); - let target_dir = - Utf8PathBuf::try_from(temp_dir.path().to_owned()).expect("non-UTF8 temp path"); + let temp_dir = TempDir::new()?; + let target_dir = Utf8PathBuf::try_from(temp_dir.path().to_owned()) + .map_err(|_| std::io::Error::other("temporary directory path must be UTF-8"))?; let workspace_root = target_dir.join("workspace"); - fs::create_dir_all(&workspace_root).expect("failed to create workspace root"); - Self { + fs::create_dir_all(&workspace_root)?; + Ok(Self { _temp_dir: temp_dir, target_dir, toolchain: Toolchain::with_override(&workspace_root, "nightly-2026-05-28"), @@ -42,7 +42,7 @@ impl StagingTestContext { verbosity: 0, experimental: false, quiet: false, - } + }) } fn target_dir(&self) -> &Utf8Path { @@ -72,60 +72,72 @@ impl StagingTestContext { } } -fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> BuildResult { - use crate::builder::{library_extension, library_prefix}; +fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> std::io::Result { use std::fs; + use crate::builder::{library_extension, library_prefix}; + let source_dir = target_dir.join("source"); - fs::create_dir_all(&source_dir).expect("failed to create source directory"); + fs::create_dir_all(&source_dir)?; let filename = format!("{}{}{}", library_prefix(), crate_name, library_extension()); let library_path = source_dir.join(&filename); - fs::write(&library_path, b"mock library content").expect("failed to write mock library"); + fs::write(&library_path, b"mock library content")?; - BuildResult { + Ok(BuildResult { crate_name: CrateName::from(crate_name), library_path, - } + }) } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn staging_ctx() -> StagingTestContext { +fn staging_ctx() -> std::io::Result { StagingTestContext::new() } -fn assert_bumpy_road_lint_in_staging_output(experimental: bool) { - let staging_ctx = StagingTestContext::new().with_experimental(experimental); - let context = staging_ctx.pipeline_context(); - let build_results = vec![create_mock_library( - staging_ctx.target_dir(), - "whitaker_suite", - )]; - let mut stderr = Vec::new(); +/// Asserts that staging output lists the stable `bumpy_road_function` lint. +/// +/// Expressed as a macro so the fallible setup stays inside the calling test +/// body and failures report the caller's line number. +macro_rules! assert_bumpy_road_lint_in_staging_output { + ($experimental:expr) => {{ + let staging_ctx = StagingTestContext::new() + .expect("staging context should be created") + .with_experimental($experimental); + let context = staging_ctx.pipeline_context(); + let build_results = vec![ + create_mock_library(staging_ctx.target_dir(), "whitaker_suite") + .expect("mock library should be staged"), + ]; + let mut stderr = Vec::new(); - stage_libraries(&context, &build_results, &mut stderr).expect("staging should succeed"); + stage_libraries(&context, &build_results, &mut stderr).expect("staging should succeed"); - let output = String::from_utf8_lossy(&stderr); - assert!( - output.contains("bumpy_road_function"), - "expected stable bumpy_road_function lint in output, got: {output}" - ); + let output = String::from_utf8_lossy(&stderr); + assert!( + output.contains("bumpy_road_function"), + "expected stable bumpy_road_function lint in output, got: {output}" + ); + }}; } #[rstest] -fn stage_libraries_returns_correct_staging_path(staging_ctx: StagingTestContext) { - let staging_ctx = staging_ctx.with_quiet(true); - let context = staging_ctx.pipeline_context(); +fn stage_libraries_returns_correct_staging_path( + #[from(staging_ctx)] staging_ctx_res: std::io::Result, +) { + let staging_ctx = staging_ctx_res.expect("staging context should be created"); + + let quiet_ctx = staging_ctx.with_quiet(true); + let context = quiet_ctx.pipeline_context(); let build_results = vec![]; let mut stderr = Vec::new(); - let result = stage_libraries(&context, &build_results, &mut stderr); - - assert!(result.is_ok(), "expected success, got: {result:?}"); - let staging_path = result.expect("already checked"); + let staging_path = + stage_libraries(&context, &build_results, &mut stderr).expect("staging should succeed"); // Keep this contract explicit so staged artefacts remain discoverable by // toolchain and profile when scanner logic depends on path layout. - let expected_path = staging_ctx + let expected_path = quiet_ctx .target_dir() .join("nightly-2026-05-28") .join("release"); @@ -138,9 +150,13 @@ fn stage_libraries_returns_correct_staging_path(staging_ctx: StagingTestContext) #[rstest] #[case::quiet_mode(true)] #[case::verbose_mode(false)] -fn stage_libraries_respects_quiet_flag(staging_ctx: StagingTestContext, #[case] quiet: bool) { - let staging_ctx = staging_ctx.with_quiet(quiet); - let context = staging_ctx.pipeline_context(); +fn stage_libraries_respects_quiet_flag( + #[from(staging_ctx)] staging_ctx_res: std::io::Result, + #[case] quiet: bool, +) { + let staging_ctx = staging_ctx_res.expect("staging context should be created"); + let quiet_ctx = staging_ctx.with_quiet(quiet); + let context = quiet_ctx.pipeline_context(); let build_results = vec![]; let mut stderr = Vec::new(); @@ -152,22 +168,25 @@ fn stage_libraries_respects_quiet_flag(staging_ctx: StagingTestContext, #[case] } else { assert!( output.contains("Staging libraries to"), - "expected progress message, got: {}", - output + "expected progress message, got: {output}" ); } } #[rstest] -fn stage_libraries_stages_build_results(staging_ctx: StagingTestContext) { +fn stage_libraries_stages_build_results( + #[from(staging_ctx)] staging_ctx_res: std::io::Result, +) { use crate::builder::{library_extension, library_prefix}; - let staging_ctx = staging_ctx.with_quiet(true); - let context = staging_ctx.pipeline_context(); - let build_results = vec![create_mock_library( - staging_ctx.target_dir(), - "whitaker_suite", - )]; + let staging_ctx = staging_ctx_res.expect("staging context should be created"); + + let quiet_ctx = staging_ctx.with_quiet(true); + let context = quiet_ctx.pipeline_context(); + let build_results = vec![ + create_mock_library(quiet_ctx.target_dir(), "whitaker_suite") + .expect("mock library should be staged"), + ]; let mut stderr = Vec::new(); let staging_path = @@ -188,7 +207,11 @@ fn stage_libraries_stages_build_results(staging_ctx: StagingTestContext) { } #[rstest] -fn stage_libraries_logs_installed_lints_when_not_quiet(staging_ctx: StagingTestContext) { +fn stage_libraries_logs_installed_lints_when_not_quiet( + #[from(staging_ctx)] staging_ctx_res: std::io::Result, +) { + let staging_ctx = staging_ctx_res.expect("staging context should be created"); + let context = staging_ctx.pipeline_context(); let build_results = vec![]; let mut stderr = Vec::new(); @@ -206,5 +229,5 @@ fn stage_libraries_logs_installed_lints_when_not_quiet(staging_ctx: StagingTestC #[case::without_experimental(false)] #[case::with_experimental(true)] fn stage_libraries_lists_bumpy_road_lint(#[case] experimental: bool) { - assert_bumpy_road_lint_in_staging_output(experimental); + assert_bumpy_road_lint_in_staging_output!(experimental); } diff --git a/installer/src/pipeline_tests.rs b/installer/src/pipeline_tests.rs index bbca11d01..724d58575 100644 --- a/installer/src/pipeline_tests.rs +++ b/installer/src/pipeline_tests.rs @@ -5,13 +5,16 @@ //! correctly invokes the builder with the provided crates, and that //! `stage_libraries` correctly stages build results. -use super::{PipelineContext, build_config_from_context, perform_build_with}; -use crate::builder::{BuildResult, MockCrateBuilder}; -use crate::crate_name::CrateName; -use crate::toolchain::Toolchain; use camino::{Utf8Path, Utf8PathBuf}; use rstest::{fixture, rstest}; +use super::{PipelineContext, build_config_from_context, perform_build_with}; +use crate::{ + builder::{BuildResult, MockCrateBuilder}, + crate_name::CrateName, + toolchain::Toolchain, +}; + // ------------------------------------------------------------------------- // Common trait for test context providers // ------------------------------------------------------------------------- @@ -86,7 +89,9 @@ impl PipelineContextProvider for TestContext { } } -/// Returns a default TestContext with owned paths and default settings for pipeline unit tests. +/// Returns a default `TestContext` with owned paths and default settings for +/// pipeline unit tests. +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn test_ctx() -> TestContext { TestContext::new() @@ -161,7 +166,10 @@ fn perform_build_with_calls_build_all_with_provided_crates(test_ctx: TestContext let mut mock = MockCrateBuilder::new(); mock.expect_build_all() .withf(|c| { - c.len() == 2 && c[0].as_str() == "whitaker_suite" && c[1].as_str() == "module_max_lines" + let [suite, lint] = c else { + return false; + }; + suite.as_str() == "whitaker_suite" && lint.as_str() == "module_max_lines" }) .times(1) .returning(|_| Ok(vec![])); @@ -187,7 +195,8 @@ fn perform_build_with_returns_builder_results(test_ctx: TestContext) { let results = perform_build_with(&ctx.pipeline_context(), &crates, &mock, &mut stderr) .expect("build should succeed"); assert_eq!(results.len(), 1); - assert_eq!(results[0].crate_name.as_str(), "whitaker_suite"); + let built = results.first().expect("build result should be recorded"); + assert_eq!(built.crate_name.as_str(), "whitaker_suite"); } #[rstest] diff --git a/installer/src/prebuilt_tests.rs b/installer/src/prebuilt_tests.rs index 7dd75a04c..11031fa0d 100644 --- a/installer/src/prebuilt_tests.rs +++ b/installer/src/prebuilt_tests.rs @@ -1,11 +1,13 @@ //! Unit tests for prebuilt artefact orchestration. -use super::*; -use crate::artefact::download::MockArtefactDownloader; -use crate::artefact::extraction::MockArtefactExtractor; -use crate::test_utils::{prebuilt_manifest_json, sha256_hex}; use rstest::rstest; +use super::*; +use crate::{ + artefact::{download::MockArtefactDownloader, extraction::MockArtefactExtractor}, + test_utils::{prebuilt_manifest_json, sha256_hex}, +}; + const FAKE_ARCHIVE: &[u8] = b"fake archive content"; const TARGET: &str = "x86_64-unknown-linux-gnu"; const TOOLCHAIN: &str = "nightly-2026-05-28"; @@ -19,43 +21,51 @@ fn base_config(destination_dir: &Utf8Path) -> PrebuiltConfig<'_> { } } -fn destination_dir() -> (tempfile::TempDir, Utf8PathBuf) { - let temp = tempfile::tempdir().expect("temp dir"); - let root = Utf8PathBuf::try_from(temp.path().to_path_buf()).expect("UTF-8 path"); +fn destination_dir() -> std::io::Result<(tempfile::TempDir, Utf8PathBuf)> { + let temp = tempfile::tempdir()?; + let root = Utf8PathBuf::try_from(temp.path().to_path_buf()) + .map_err(|_| std::io::Error::other("temporary directory path must be UTF-8"))?; let path = root.join("lints").join(TOOLCHAIN).join(TARGET).join("lib"); - (temp, path) + Ok((temp, path)) } -/// Run a fallback scenario: set up mocks via `setup_mocks`, call the +/// Run a fallback scenario: set up mocks via `$setup_mocks`, call the /// orchestrator, and assert `Fallback` whose reason contains -/// `expected_reason_substring`. -fn test_fallback_scenario( - setup_mocks: impl FnOnce(&mut MockArtefactDownloader, &mut MockArtefactExtractor), - expected_reason_substring: &str, -) { - let (_temp, destination_dir) = destination_dir(); - let config = base_config(&destination_dir); - - let mut downloader = MockArtefactDownloader::new(); - let mut extractor = MockArtefactExtractor::new(); - setup_mocks(&mut downloader, &mut extractor); - - let mut stderr = Vec::new(); - let result = attempt_prebuilt_with(&config, &downloader, &extractor, &mut stderr); - match result { - PrebuiltResult::Fallback { reason } => { - assert!( - reason.contains(expected_reason_substring), - "reason: {reason}" - ); +/// `$expected_reason_substring`. +/// +/// Expressed as a macro so the fallible setup stays inside the calling test +/// body and failures report the caller's line number. +macro_rules! test_fallback_scenario { + ($setup_mocks:expr, $expected_reason_substring:expr $(,)?) => {{ + let setup_mocks: &dyn Fn(&mut MockArtefactDownloader, &mut MockArtefactExtractor) = + &$setup_mocks; + let expected_reason_substring: &str = $expected_reason_substring; + let (_temp, destination_dir) = + destination_dir().expect("destination directory should be created"); + let config = base_config(&destination_dir); + + let mut downloader = MockArtefactDownloader::new(); + let mut extractor = MockArtefactExtractor::new(); + setup_mocks(&mut downloader, &mut extractor); + + let mut stderr = Vec::new(); + let result = attempt_prebuilt_with(&config, &downloader, &extractor, &mut stderr); + match result { + PrebuiltResult::Fallback { reason } => { + assert!( + reason.contains(expected_reason_substring), + "reason: {reason}" + ); + } + other @ PrebuiltResult::Success { .. } => panic!("expected Fallback, got {other:?}"), } - other => panic!("expected Fallback, got {other:?}"), - } + }}; } #[test] fn happy_path_returns_success() { - let (_temp, destination_dir) = destination_dir(); + let (_temp, destination_dir) = + destination_dir().expect("destination directory should be created"); let config = base_config(&destination_dir); let fake_sha = sha256_hex(FAKE_ARCHIVE); let manifest_json = prebuilt_manifest_json(TOOLCHAIN, TARGET, &fake_sha); @@ -79,7 +89,7 @@ fn happy_path_returns_success() { let result = attempt_prebuilt_with(&config, &downloader, &extractor, &mut stderr); match result { PrebuiltResult::Success { staging_path } => assert_eq!(staging_path, destination_dir), - other => panic!("expected Success, got {other:?}"), + other @ PrebuiltResult::Fallback { .. } => panic!("expected Success, got {other:?}"), } } @@ -90,7 +100,7 @@ fn manifest_download_errors_return_fallback( #[case] make_error: fn() -> DownloadError, #[case] expected_substring: &str, ) { - test_fallback_scenario( + test_fallback_scenario!( |downloader, _extractor| { downloader .expect_download_manifest() @@ -116,22 +126,12 @@ fn make_not_found_error() -> DownloadError { #[test] fn manifest_validation_errors_return_fallback() { let test_cases = vec![ - ( - "toolchain mismatch", - "nightly-2025-01-01", - TARGET, - "toolchain mismatch", - ), - ( - "target mismatch", - TOOLCHAIN, - "aarch64-apple-darwin", - "target mismatch", - ), + ("nightly-2025-01-01", TARGET, "toolchain mismatch"), + (TOOLCHAIN, "aarch64-apple-darwin", "target mismatch"), ]; - for (case_name, toolchain, target, expected_reason_substring) in test_cases { - test_fallback_scenario( + for (toolchain, target, expected_reason_substring) in test_cases { + test_fallback_scenario!( |downloader, _extractor| { let manifest_json = prebuilt_manifest_json(toolchain, target, "a".repeat(64)); downloader @@ -140,13 +140,12 @@ fn manifest_validation_errors_return_fallback() { }, expected_reason_substring, ); - eprintln!("manifest validation scenario passed: {case_name}"); } } #[test] fn checksum_mismatch_returns_fallback() { - test_fallback_scenario( + test_fallback_scenario!( |downloader, _extractor| { // Manifest claims SHA = "aaa...a" but the file will hash differently. let manifest_json = prebuilt_manifest_json(TOOLCHAIN, TARGET, "a".repeat(64)); @@ -165,7 +164,7 @@ fn checksum_mismatch_returns_fallback() { #[test] fn extraction_failure_returns_fallback() { - test_fallback_scenario( + test_fallback_scenario!( |downloader, extractor| { let fake_sha = sha256_hex(FAKE_ARCHIVE); let manifest_json = prebuilt_manifest_json(TOOLCHAIN, TARGET, &fake_sha); @@ -213,6 +212,6 @@ fn destination_creation_failure_returns_fallback() { reason.contains("download failed"), "unexpected fallback reason: {reason}" ), - other => panic!("expected Fallback, got {other:?}"), + other @ PrebuiltResult::Success { .. } => panic!("expected Fallback, got {other:?}"), } } diff --git a/installer/src/test_utils/dependency_binary_helpers.rs b/installer/src/test_utils/dependency_binary_helpers.rs index ddf5fdff7..193a89d9f 100644 --- a/installer/src/test_utils/dependency_binary_helpers.rs +++ b/installer/src/test_utils/dependency_binary_helpers.rs @@ -120,6 +120,20 @@ pub fn path_binary_location(directory: &Path, binary_name: &str) -> PathBuf { } } +/// Expected outcome of repository-install verification in a test scenario. +/// +/// Collapses the "should verification run" and "does verification fail" +/// questions into a single three-state value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RepositoryVerification { + /// Repository verification is not expected to run. + Skip, + /// Repository verification runs and succeeds. + Succeeds, + /// Repository verification runs and fails. + Fails, +} + /// Configuration for generating expected calls in dependency binary tests. pub struct ExpectedCallConfig<'a> { /// Whether cargo-binstall is available. @@ -128,10 +142,8 @@ pub struct ExpectedCallConfig<'a> { pub has_repository_context: bool, /// Whether the repository failure is a missing asset. pub is_repository_asset_missing: bool, - /// Whether to verify repository installation. - pub should_verify_repository_install: bool, - /// Whether repository verification should fail. - pub is_repository_verification_failing: bool, + /// Expected repository-install verification outcome. + pub repository_verification: RepositoryVerification, /// Error message for cargo binstall failure (None if succeeds). pub cargo_binstall_failure: Option<&'a str>, /// Error message for cargo install failure (None if succeeds). @@ -139,6 +151,7 @@ pub struct ExpectedCallConfig<'a> { } /// Creates an expected call for checking cargo-binstall availability. +#[must_use] pub fn binstall_version_check(is_binstall_available: bool) -> ExpectedCall { ExpectedCall { cmd: "cargo", @@ -152,6 +165,7 @@ pub fn binstall_version_check(is_binstall_available: bool) -> ExpectedCall { } /// Creates an expected call for checking cargo-binstall with a fixed result. +#[must_use] pub fn binstall_version_check_with_result(result: Result) -> ExpectedCall { ExpectedCall { cmd: "cargo", @@ -161,6 +175,7 @@ pub fn binstall_version_check_with_result(result: Result) -> ExpectedCal } /// Creates an expected call for installing a tool with cargo-binstall. +#[must_use] pub fn binstall_install(tool: &'static str, result: Result) -> ExpectedCall { let version = dependency_version(tool); ExpectedCall { @@ -171,6 +186,7 @@ pub fn binstall_install(tool: &'static str, result: Result) -> ExpectedC } /// Creates an expected call for installing a tool with cargo install. +#[must_use] pub fn cargo_install(tool: &'static str, result: Result) -> ExpectedCall { ExpectedCall { cmd: "cargo", @@ -196,14 +212,17 @@ fn cargo_source_install( /// # Panics /// /// Panics when the manifest cannot be parsed or the tool is unknown. +#[must_use] pub fn dependency_version(tool: &str) -> &'static str { - find_dependency_binary(tool) - .expect("dependency manifest should parse") - .map(|dependency| dependency.version()) - .unwrap_or_else(|| panic!("unexpected tool: {tool}")) + match find_dependency_binary(tool) { + Ok(Some(dependency)) => dependency.version(), + Ok(None) => panic!("unexpected tool: {tool}"), + Err(error) => panic!("dependency manifest should parse: {error}"), + } } /// Successful `cargo dylint --version` output reporting the manifest version. +#[must_use] pub fn cargo_dylint_version_output() -> Output { stdout_output(format!( "cargo-dylint {}\n", @@ -213,12 +232,14 @@ pub fn cargo_dylint_version_output() -> Output { /// Expected `cargo install --list` call reporting the manifest-pinned /// `dylint-link` version. +#[must_use] pub fn dylint_link_install_list_check() -> ExpectedCall { dylint_link_install_list_check_with_version(dependency_version("dylint-link")) } /// Expected `cargo install --list` call reporting the given `dylint-link` /// version. +#[must_use] pub fn dylint_link_install_list_check_with_version(version: &str) -> ExpectedCall { ExpectedCall { cmd: "cargo", @@ -230,6 +251,7 @@ pub fn dylint_link_install_list_check_with_version(version: &str) -> ExpectedCal } /// Creates an expected call for verifying repository installation. +#[must_use] pub fn repository_verification_call(tool: &str, verification_fails: bool) -> Option { match tool { "cargo-dylint" => Some(ExpectedCall { @@ -313,16 +335,15 @@ fn post_primary_calls(cfg: &PostPrimaryConfig) -> Vec { return calls; } // binstall failed and cargo install also fails - if let Some(message) = cfg.cargo_install_failure.as_deref() { - let cargo_call = repo_aware_cargo_install( - cfg.tool_static, - cfg.has_repository_context, - Ok(failure_output(message)), - ); - vec![cargo_call] - } else { - vec![] - } + cfg.cargo_install_failure + .as_deref() + .map_or_else(Vec::new, |message| { + vec![repo_aware_cargo_install( + cfg.tool_static, + cfg.has_repository_context, + Ok(failure_output(message)), + )] + }) } fn source_install_fallback_calls( @@ -383,10 +404,7 @@ pub fn cargo_fallback_calls(tool: &str, config: &ExpectedCallConfig<'_>) -> Vec< let install_call = ExpectedCall { cmd: "cargo", args, - result: Ok(match failure_message { - Some(message) => failure_output(message), - None => success_output(), - }), + result: Ok(failure_message.map_or_else(success_output, failure_output)), }; let mut calls = vec![install_call]; @@ -403,24 +421,27 @@ pub fn cargo_fallback_calls(tool: &str, config: &ExpectedCallConfig<'_>) -> Vec< } /// Builds the complete list of expected calls for a dependency binary test scenario. -pub fn expected_calls(tool: &str, config: ExpectedCallConfig<'_>) -> Vec { +#[must_use] +pub fn expected_calls(tool: &str, config: &ExpectedCallConfig<'_>) -> Vec { let mut calls = vec![binstall_version_check(config.is_binstall_available)]; - if config.should_verify_repository_install { - calls.extend(repository_verification_call( - tool, - config.is_repository_verification_failing, - )); - if !config.is_repository_verification_failing { + match config.repository_verification { + RepositoryVerification::Skip => {} + RepositoryVerification::Succeeds => { + calls.extend(repository_verification_call(tool, false)); return calls; } + RepositoryVerification::Fails => { + calls.extend(repository_verification_call(tool, true)); + } } - calls.extend(cargo_fallback_calls(tool, &config)); + calls.extend(cargo_fallback_calls(tool, config)); calls } /// Creates an expected call for verifying cargo-dylint installation. +#[must_use] pub fn cargo_dylint_check() -> ExpectedCall { ExpectedCall { cmd: "cargo", @@ -430,6 +451,7 @@ pub fn cargo_dylint_check() -> ExpectedCall { } /// Creates an expected call for verifying cargo-dylint with a fixed result. +#[must_use] pub fn cargo_dylint_check_with_result(result: Result) -> ExpectedCall { ExpectedCall { cmd: "cargo", diff --git a/installer/src/test_utils/dependency_binary_helpers_tests.rs b/installer/src/test_utils/dependency_binary_helpers_tests.rs index ddf41f689..de37b4f24 100644 --- a/installer/src/test_utils/dependency_binary_helpers_tests.rs +++ b/installer/src/test_utils/dependency_binary_helpers_tests.rs @@ -1,16 +1,14 @@ //! Tests for dependency binary helper fixtures and expected call builders. use crate::test_utils::dependency_binary_helpers::{ - ExpectedCallConfig, dependency_version, expected_calls, repository_verification_call, + ExpectedCallConfig, RepositoryVerification, dependency_version, expected_calls, + repository_verification_call, }; #[test] fn repository_verification_call_returns_probe_for_cargo_dylint() { - let call = repository_verification_call("cargo-dylint", false); - let call = match call { - Some(call) => call, - None => panic!("cargo-dylint should use a verification probe"), - }; + let call = repository_verification_call("cargo-dylint", false) + .expect("cargo-dylint should use a verification probe"); assert_eq!(call.cmd, "cargo"); assert_eq!(call.args, vec!["dylint", "--version"]); @@ -35,38 +33,38 @@ fn repository_verification_call_skips_executor_for_dylint_link(#[case] verificat fn expected_calls_include_repository_probe_for_cargo_dylint() { let calls = expected_calls( "cargo-dylint", - ExpectedCallConfig { + &ExpectedCallConfig { is_binstall_available: false, has_repository_context: true, is_repository_asset_missing: false, - should_verify_repository_install: true, - is_repository_verification_failing: false, + repository_verification: RepositoryVerification::Succeeds, cargo_binstall_failure: None, cargo_install_failure: None, }, ); assert_eq!(calls.len(), 2); - assert_eq!(calls[1].cmd, "cargo"); - assert_eq!(calls[1].args, vec!["dylint", "--version"]); + let verification = calls.get(1).expect("verification probe should be recorded"); + assert_eq!(verification.cmd, "cargo"); + assert_eq!(verification.args, vec!["dylint", "--version"]); } #[test] fn expected_calls_omit_executor_verification_for_dylint_link() { let calls = expected_calls( "dylint-link", - ExpectedCallConfig { + &ExpectedCallConfig { is_binstall_available: false, has_repository_context: true, is_repository_asset_missing: false, - should_verify_repository_install: true, - is_repository_verification_failing: false, + repository_verification: RepositoryVerification::Succeeds, cargo_binstall_failure: None, cargo_install_failure: None, }, ); assert_eq!(calls.len(), 1); - assert_eq!(calls[0].cmd, "cargo"); - assert_eq!(calls[0].args, vec!["binstall", "--version"]); + let probe = calls.first().expect("binstall probe should be recorded"); + assert_eq!(probe.cmd, "cargo"); + assert_eq!(probe.args, vec!["binstall", "--version"]); } diff --git a/installer/src/test_utils.rs b/installer/src/test_utils/mod.rs similarity index 84% rename from installer/src/test_utils.rs rename to installer/src/test_utils/mod.rs index 24f5745a9..6aa6de198 100644 --- a/installer/src/test_utils.rs +++ b/installer/src/test_utils/mod.rs @@ -1,15 +1,17 @@ //! Shared test utilities for the installer crate. +use std::{ + cell::RefCell, + collections::VecDeque, + path::PathBuf, + process::{ExitStatus, Output}, +}; + #[cfg(any(test, feature = "test-support"))] use crate::deps::CommandExecutor; -use crate::dirs::BaseDirs; #[cfg(any(test, feature = "test-support"))] use crate::error::InstallerError; -use crate::error::Result; -use std::cell::RefCell; -use std::collections::VecDeque; -use std::path::PathBuf; -use std::process::{ExitStatus, Output}; +use crate::{dirs::BaseDirs, error::Result}; /// Creates an `ExitStatus` from an exit code (Unix implementation). /// @@ -25,6 +27,7 @@ use std::process::{ExitStatus, Output}; /// assert!(!failure.success()); /// ``` #[cfg(unix)] +#[must_use] pub fn exit_status(code: i32) -> ExitStatus { use std::os::unix::process::ExitStatusExt; @@ -45,6 +48,7 @@ pub fn exit_status(code: i32) -> ExitStatus { /// assert!(!failure.success()); /// ``` #[cfg(windows)] +#[must_use] pub fn exit_status(code: i32) -> ExitStatus { use std::os::windows::process::ExitStatusExt; @@ -63,6 +67,7 @@ pub fn exit_status(code: i32) -> ExitStatus { /// assert!(output.stdout.is_empty()); /// assert!(output.stderr.is_empty()); /// ``` +#[must_use] pub fn success_output() -> Output { Output { status: exit_status(0), @@ -104,11 +109,11 @@ pub fn stdout_output(stdout: impl AsRef) -> Output { /// assert_eq!(output.stderr, b"command failed"); /// ``` pub fn failure_output(stderr: impl AsRef) -> Output { - let stderr = stderr.as_ref(); + let stderr_text = stderr.as_ref(); Output { status: exit_status(1), stdout: Vec::new(), - stderr: stderr.as_bytes().to_vec(), + stderr: stderr_text.as_bytes().to_vec(), } } @@ -135,6 +140,7 @@ impl BaseDirs for StubDirs { /// Compute the SHA-256 hex digest of a byte slice for test fixtures. #[cfg(any(test, feature = "test-support"))] +#[must_use] pub fn sha256_hex(data: &[u8]) -> String { use sha2::{Digest, Sha256}; crate::hex::to_lower_hex(&Sha256::digest(data)) @@ -147,9 +153,9 @@ pub fn prebuilt_manifest_json( target: impl AsRef, sha256: impl AsRef, ) -> String { - let toolchain = toolchain.as_ref(); - let target = target.as_ref(); - let sha256 = sha256.as_ref(); + let toolchain_value = toolchain.as_ref(); + let target_value = target.as_ref(); + let sha256_value = sha256.as_ref(); format!( concat!( r#"{{"git_sha":"abc1234","schema_version":1,"#, @@ -159,9 +165,9 @@ pub fn prebuilt_manifest_json( r#""files":["libwhitaker_suite.so"],"#, r#""sha256":"{sha256}"}}"#, ), - toolchain = toolchain, - target = target, - sha256 = sha256, + toolchain = toolchain_value, + target = target_value, + sha256 = sha256_value, ) } @@ -200,18 +206,20 @@ pub struct ExpectedCall { /// # Examples /// /// ``` -/// use whitaker_installer::deps::CommandExecutor; -/// use whitaker_installer::test_utils::{ExpectedCall, StubExecutor, success_output}; +/// use whitaker_installer::{ +/// deps::CommandExecutor, +/// test_utils::{ExpectedCall, StubExecutor, success_output}, +/// }; /// -/// let executor = StubExecutor::new(vec![ -/// ExpectedCall { -/// cmd: "cargo", -/// args: vec!["--version"], -/// result: Ok(success_output()), -/// }, -/// ]); +/// let executor = StubExecutor::new(vec![ExpectedCall { +/// cmd: "cargo", +/// args: vec!["--version"], +/// result: Ok(success_output()), +/// }]); /// -/// let output = executor.run("cargo", &["--version"]).expect("command failed"); +/// let output = executor +/// .run("cargo", &["--version"]) +/// .expect("command failed"); /// assert!(output.status.success()); /// /// executor.assert_finished(); @@ -229,14 +237,13 @@ impl StubExecutor { /// ``` /// use whitaker_installer::test_utils::{ExpectedCall, StubExecutor, success_output}; /// - /// let executor = StubExecutor::new(vec![ - /// ExpectedCall { - /// cmd: "cargo", - /// args: vec!["build"], - /// result: Ok(success_output()), - /// }, - /// ]); + /// let executor = StubExecutor::new(vec![ExpectedCall { + /// cmd: "cargo", + /// args: vec!["build"], + /// result: Ok(success_output()), + /// }]); /// ``` + #[must_use] pub fn new(expected: Vec) -> Self { Self { expected: RefCell::new(expected.into()), @@ -252,16 +259,16 @@ impl StubExecutor { /// # Examples /// /// ``` - /// use whitaker_installer::deps::CommandExecutor; - /// use whitaker_installer::test_utils::{ExpectedCall, StubExecutor, success_output}; + /// use whitaker_installer::{ + /// deps::CommandExecutor, + /// test_utils::{ExpectedCall, StubExecutor, success_output}, + /// }; /// - /// let executor = StubExecutor::new(vec![ - /// ExpectedCall { - /// cmd: "cargo", - /// args: vec!["test"], - /// result: Ok(success_output()), - /// }, - /// ]); + /// let executor = StubExecutor::new(vec![ExpectedCall { + /// cmd: "cargo", + /// args: vec!["test"], + /// result: Ok(success_output()), + /// }]); /// /// // Execute the expected command /// let _ = executor.run("cargo", &["test"]); diff --git a/installer/src/tests/fast_path.rs b/installer/src/tests/fast_path.rs index 4dd9aa084..3efbbb3c5 100644 --- a/installer/src/tests/fast_path.rs +++ b/installer/src/tests/fast_path.rs @@ -1,12 +1,15 @@ //! Tests for fast-path installer helper behaviour. -use super::*; use camino::{Utf8Path, Utf8PathBuf}; use rstest::{fixture, rstest}; use temp_env::with_var_unset; -use whitaker_installer::crate_name::CrateName; -use whitaker_installer::test_support::{TEST_STAGE_SUITE_ENV, env_test_guard}; -use whitaker_installer::toolchain::Toolchain; +use whitaker_installer::{ + crate_name::CrateName, + test_support::{TEST_STAGE_SUITE_ENV, env_test_guard}, + toolchain::Toolchain, +}; + +use super::*; struct FastPathFixture { args: InstallArgs, @@ -33,9 +36,9 @@ fn fast_path_fixture() -> FastPathFixture { FastPathFixture { args: InstallArgs::default(), dirs: TestBaseDirs { - home_dir: Some("/tmp".into()), - bin_dir: Some("/tmp/bin".into()), - data_dir: Some("/tmp".into()), + home: Some("/tmp".into()), + bin: Some("/tmp/bin".into()), + data: Some("/tmp".into()), }, toolchain: Toolchain::with_override(Utf8Path::new("."), "nightly-2026-05-28"), target_dir: Utf8PathBuf::from("/tmp/target"), @@ -46,7 +49,7 @@ fn fast_path_fixture() -> FastPathFixture { #[rstest] #[case::without_cranelift(false, &[])] #[case::with_cranelift(true, &["rustc-codegen-cranelift"])] -fn resolve_additional_components_parametrised(#[case] cranelift: bool, #[case] expected: &[&str]) { +fn resolve_additional_components_parametrized(#[case] cranelift: bool, #[case] expected: &[&str]) { let args = InstallArgs { cranelift, ..InstallArgs::default() @@ -59,7 +62,7 @@ fn resolve_additional_components_parametrised(#[case] cranelift: bool, #[case] e fn fast_path_context_holds_supplied_values(fast_path_fixture: FastPathFixture) { let ctx = fast_path_fixture.context(); - assert!(std::ptr::eq(ctx.args, &fast_path_fixture.args)); + assert!(std::ptr::eq(ctx.args, &raw const fast_path_fixture.args)); assert_eq!(ctx.dirs.home_dir(), Some(PathBuf::from("/tmp"))); assert_eq!(ctx.toolchain.channel(), "nightly-2026-05-28"); assert_eq!(ctx.target_dir, &Utf8PathBuf::from("/tmp/target")); diff --git a/installer/src/tests.rs b/installer/src/tests/mod.rs similarity index 83% rename from installer/src/tests.rs rename to installer/src/tests/mod.rs index 524be606e..f1e8e1f50 100644 --- a/installer/src/tests.rs +++ b/installer/src/tests/mod.rs @@ -2,40 +2,48 @@ mod fast_path; -use super::*; +use std::{path::PathBuf, time::Duration}; + use rstest::{fixture, rstest}; -use std::path::PathBuf; -use std::time::Duration; -use whitaker_installer::cli::InstallArgs; -use whitaker_installer::dependency_binaries::DependencyBinaryInstaller; -use whitaker_installer::deps::DependencyInstallOptions; -use whitaker_installer::dirs::BaseDirs; -use whitaker_installer::installer_packaging::TargetTriple; -use whitaker_installer::test_utils::dependency_binary_helpers::{ - AlwaysNotFoundRepositoryInstaller, cargo_dylint_check, dylint_link_install_list_check, - with_fake_binary_on_path, +use whitaker_installer::{ + cli::InstallArgs, + dependency_binaries::DependencyBinaryInstaller, + deps::DependencyInstallOptions, + dirs::BaseDirs, + installer_packaging::TargetTriple, + test_utils::{ + dependency_binary_helpers::{ + AlwaysNotFoundRepositoryInstaller, cargo_dylint_check, dylint_link_install_list_check, + with_fake_binary_on_path, + }, + *, + }, }; -use whitaker_installer::test_utils::*; + +use super::*; fn dependency_install_options<'a>( dirs: &'a TestBaseDirs, repository_installer: &'a dyn DependencyBinaryInstaller, quiet: bool, -) -> DependencyInstallOptions<'a> { - DependencyInstallOptions { +) -> std::result::Result< + DependencyInstallOptions<'a>, + whitaker_installer::artefact::error::ArtefactError, +> { + Ok(DependencyInstallOptions { dirs, repository_installer, - target: Some(TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target")), + target: Some(TargetTriple::try_from("x86_64-unknown-linux-gnu")?), quiet, - } + }) } #[fixture] fn test_base_dirs() -> TestBaseDirs { TestBaseDirs { - home_dir: Some(PathBuf::from("/tmp")), - bin_dir: Some(PathBuf::from("/tmp/bin")), - data_dir: Some(PathBuf::from("/tmp")), + home: Some(PathBuf::from("/tmp")), + bin: Some(PathBuf::from("/tmp/bin")), + data: Some(PathBuf::from("/tmp")), } } @@ -64,7 +72,10 @@ fn exit_code_for_run_result_prints_error_and_returns_one() { #[rstest] #[case::default_suite_only(InstallArgs::default(), false, true)] #[case::individual_lints( - InstallArgs { individual_lints: true, ..InstallArgs::default() }, + InstallArgs { + individual_lints: true, + ..InstallArgs::default() + }, true, false )] @@ -117,7 +128,8 @@ fn ensure_dylint_tools_skips_install_when_installed(test_base_dirs: TestBaseDirs let repository_installer = AlwaysNotFoundRepositoryInstaller; let mut stderr = Vec::new(); - let options = dependency_install_options(&test_base_dirs, &repository_installer, false); + let options = dependency_install_options(&test_base_dirs, &repository_installer, false) + .expect("dependency install options should build"); let result = ensure_dylint_tools_with_options(&executor, &mut stderr, options); assert!(result.is_ok()); @@ -162,7 +174,8 @@ fn ensure_dylint_tools_installs_missing_tools( let repository_installer = AlwaysNotFoundRepositoryInstaller; let mut stderr = Vec::new(); - let options = dependency_install_options(&test_base_dirs, &repository_installer, quiet); + let options = dependency_install_options(&test_base_dirs, &repository_installer, quiet) + .expect("dependency install options should build"); let result = ensure_dylint_tools_with_options(&executor, &mut stderr, options); assert!(result.is_ok()); @@ -210,7 +223,8 @@ fn ensure_dylint_tools_propagates_install_failures(test_base_dirs: TestBaseDirs) let repository_installer = AlwaysNotFoundRepositoryInstaller; let mut stderr = Vec::new(); - let options = dependency_install_options(&test_base_dirs, &repository_installer, false); + let options = dependency_install_options(&test_base_dirs, &repository_installer, false) + .expect("dependency install options should build"); let err = ensure_dylint_tools_with_options(&executor, &mut stderr, options) .expect_err("expected install failure"); @@ -226,22 +240,22 @@ fn ensure_dylint_tools_propagates_install_failures(test_base_dirs: TestBaseDirs) #[derive(Debug, Clone)] struct TestBaseDirs { - home_dir: Option, - bin_dir: Option, - data_dir: Option, + home: Option, + bin: Option, + data: Option, } impl BaseDirs for TestBaseDirs { fn home_dir(&self) -> Option { - self.home_dir.clone() + self.home.clone() } fn bin_dir(&self) -> Option { - self.bin_dir.clone() + self.bin.clone() } fn whitaker_data_dir(&self) -> Option { - self.data_dir.clone() + self.data.clone() } } @@ -249,9 +263,9 @@ impl BaseDirs for TestBaseDirs { fn write_install_metrics_prints_summary_and_persists_metrics() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let dirs = TestBaseDirs { - home_dir: Some(temp_dir.path().to_path_buf()), - bin_dir: Some(temp_dir.path().join("bin")), - data_dir: Some(temp_dir.path().to_path_buf()), + home: Some(temp_dir.path().to_path_buf()), + bin: Some(temp_dir.path().join("bin")), + data: Some(temp_dir.path().to_path_buf()), }; let mut stderr = Vec::new(); @@ -272,17 +286,16 @@ fn write_install_metrics_prints_summary_and_persists_metrics() { let metrics_path = temp_dir.path().join("metrics").join("install_metrics.json"); assert!( metrics_path.exists(), - "expected metrics file at {:?}", - metrics_path + "expected metrics file at {metrics_path:?}" ); } #[test] fn write_install_metrics_warns_when_recording_fails() { let dirs = TestBaseDirs { - home_dir: None, - bin_dir: None, - data_dir: None, + home: None, + bin: None, + data: None, }; let mut stderr = Vec::new(); @@ -302,9 +315,9 @@ fn write_install_metrics_warns_when_recording_fails() { fn write_install_metrics_suppresses_output_in_quiet_mode() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let dirs = TestBaseDirs { - home_dir: Some(temp_dir.path().to_path_buf()), - bin_dir: Some(temp_dir.path().join("bin")), - data_dir: Some(temp_dir.path().to_path_buf()), + home: Some(temp_dir.path().to_path_buf()), + bin: Some(temp_dir.path().join("bin")), + data: Some(temp_dir.path().to_path_buf()), }; let mut stderr = Vec::new(); diff --git a/installer/src/toolchain/tests/failure_mocks.rs b/installer/src/toolchain/tests/failure_mocks/mod.rs similarity index 90% rename from installer/src/toolchain/tests/failure_mocks.rs rename to installer/src/toolchain/tests/failure_mocks/mod.rs index f1871f051..e426d186b 100644 --- a/installer/src/toolchain/tests/failure_mocks.rs +++ b/installer/src/toolchain/tests/failure_mocks/mod.rs @@ -62,7 +62,7 @@ fn setup_toolchain_install_failure_mocks( expect_toolchain_install( runner, seq, - ToolchainInstallExpectation { + &ToolchainInstallExpectation { channel, exit_code: 1, stderr: Some(TOOLCHAIN_INSTALL_FAILURE_MESSAGE), @@ -97,7 +97,7 @@ fn setup_toolchain_unusable_failure_mocks( expect_toolchain_install( runner, seq, - ToolchainInstallExpectation { + &ToolchainInstallExpectation { channel, exit_code: 0, stderr: None, @@ -122,8 +122,7 @@ fn setup_toolchain_unusable_failure_mocks( /// /// # Arguments /// -/// - `runner` - Mock command runner that receives the expected `rustup` and -/// `rustc` calls. +/// - `runner` - Mock command runner that receives the expected `rustup` and `rustc` calls. /// - `seq` - Mockall sequence enforcing the order of expected commands. /// - `channel` - Toolchain channel being installed or checked. /// - `setup` - Failure mode and additional components to model. @@ -133,16 +132,16 @@ pub(super) fn setup_failure_mocks( channel: ToolchainChannel<'_>, setup: FailureSetup<'_>, ) { - let channel = channel.as_str(); + let channel_name = channel.as_str(); match setup.failure { InstallFailure::ToolchainInstall => { - setup_toolchain_install_failure_mocks(runner, seq, channel); + setup_toolchain_install_failure_mocks(runner, seq, channel_name); } InstallFailure::ComponentAdd => { setup_component_add_failure_mocks_inner( runner, seq, - channel, + channel_name, setup.additional_components, ); } @@ -150,7 +149,7 @@ pub(super) fn setup_failure_mocks( setup_toolchain_unusable_failure_mocks( runner, seq, - channel, + channel_name, setup.additional_components, ); } @@ -215,47 +214,50 @@ fn is_component_install_failed( /// /// - `err` - Installer error returned by the code under test. /// - `channel` - Toolchain channel expected in the error payload. -/// - `setup` - Failure mode and additional components that define the expected -/// error shape. +/// - `setup` - Failure mode and additional components that define the expected error shape. /// /// # Panics /// /// Panics with a descriptive message if the error variant or its fields do not /// match expectations. pub(super) fn assert_failure_error( - err: InstallerError, + err: &InstallerError, channel: ToolchainChannel<'_>, setup: FailureSetup<'_>, ) { - let channel = channel.as_str(); + let channel_name = channel.as_str(); let failure = failure_summary(setup); match setup.failure { InstallFailure::ToolchainInstall => assert_error_matches( - &err, - &format!("ToolchainInstallFailed for channel {channel} while exercising {failure}"), + err, + &format!( + "ToolchainInstallFailed for channel {channel_name} while exercising {failure}" + ), |e| { matches!( e, InstallerError::ToolchainInstallFailed { toolchain, message } - if toolchain == channel && message == TOOLCHAIN_INSTALL_FAILURE_MESSAGE + if toolchain == channel_name + && message == TOOLCHAIN_INSTALL_FAILURE_MESSAGE ) }, ), InstallFailure::ComponentAdd => assert_error_matches( - &err, + err, &format!( - "ToolchainComponentInstallFailed for channel {channel} while exercising {failure}" + "ToolchainComponentInstallFailed for channel {channel_name} while exercising \ + {failure}" ), - |e| is_component_install_failed(e, channel, setup.additional_components), + |e| is_component_install_failed(e, channel_name, setup.additional_components), ), InstallFailure::ToolchainUnusableAfterInstall => assert_error_matches( - &err, - &format!("ToolchainNotInstalled for channel {channel} while exercising {failure}"), + err, + &format!("ToolchainNotInstalled for channel {channel_name} while exercising {failure}"), |e| { matches!( e, InstallerError::ToolchainNotInstalled { toolchain } - if toolchain == channel + if toolchain == channel_name ) }, ), diff --git a/installer/src/toolchain/tests/mod.rs b/installer/src/toolchain/tests/mod.rs index 5d4b7a8d7..1806b0f5b 100644 --- a/installer/src/toolchain/tests/mod.rs +++ b/installer/src/toolchain/tests/mod.rs @@ -3,7 +3,6 @@ mod failure_mocks; mod test_helpers; -use super::*; use failure_mocks::{ COMPONENT_INSTALL_FAILURE_MESSAGE, FailureSetup, InstallFailure, ToolchainChannel, assert_failure_error, setup_failure_mocks, @@ -15,6 +14,8 @@ use test_helpers::{ test_toolchain, }; +use super::*; + const CRANELIFT_COMPONENT: &str = "rustc-codegen-cranelift"; // Asserts that a parsing function rejects invalid contents with an @@ -61,7 +62,10 @@ fn rejects_invalid_toolchain_file(#[case] contents: &str, #[case] expected_reaso assert_parse_fails_with_reason(contents, expected_reason, parse_toolchain_channel); } -fn run_missing_toolchain_install_test(extra: &[&str], expected_components: &[&str]) { +fn run_missing_toolchain_install_test( + extra: &[&str], + expected_components: &[&str], +) -> Result { let channel = "nightly-2026-05-28"; let toolchain = test_toolchain(channel); let mut runner = MockCommandRunner::new(); @@ -71,7 +75,7 @@ fn run_missing_toolchain_install_test(extra: &[&str], expected_components: &[&st expect_toolchain_install( &mut runner, &mut seq, - ToolchainInstallExpectation { + &ToolchainInstallExpectation { channel, exit_code: 0, stderr: None, @@ -87,11 +91,7 @@ fn run_missing_toolchain_install_test(extra: &[&str], expected_components: &[&st expect_rustc_version(&mut runner, &mut seq, channel, 0); - let status = toolchain - .ensure_installed_with(&runner, extra) - .expect("toolchain should install"); - - assert!(status.installed_toolchain()); + toolchain.ensure_installed_with(&runner, extra) } #[rstest] @@ -107,10 +107,16 @@ fn ensure_installed_installs_missing_toolchain( #[case] extra: Vec<&'static str>, #[case] expected_components: Vec<&'static str>, ) { - run_missing_toolchain_install_test(&extra, &expected_components); + let status = run_missing_toolchain_install_test(&extra, &expected_components) + .expect("toolchain should install"); + + assert!(status.installed_toolchain()); } -fn run_component_installation_test(extra: &[&str], expected: &[&str]) { +fn run_component_installation_test( + extra: &[&str], + expected: &[&str], +) -> Result { let channel = "nightly-2026-05-28"; let toolchain = test_toolchain(channel); let mut runner = MockCommandRunner::new(); @@ -124,11 +130,7 @@ fn run_component_installation_test(extra: &[&str], expected: &[&str]) { .in_sequence(&mut seq) .returning(|_, _| Ok(output_with_status(0))); - let status = toolchain - .ensure_installed_with(&runner, extra) - .expect("toolchain should be ready"); - - assert!(!status.installed_toolchain()); + toolchain.ensure_installed_with(&runner, extra) } #[rstest] @@ -141,7 +143,10 @@ fn ensure_installed_adds_correct_components( #[case] extra: Vec<&'static str>, #[case] expected: Vec<&'static str>, ) { - run_component_installation_test(&extra, &expected); + let status = + run_component_installation_test(&extra, &expected).expect("toolchain should be ready"); + + assert!(!status.installed_toolchain()); } #[test] @@ -177,19 +182,18 @@ fn install_components_with_failure_reports_all_components() { .install_components_with(&runner, &[CRANELIFT_COMPONENT]) .expect_err("component installation should fail"); - assert!( - matches!( - err, - InstallerError::ToolchainComponentInstallFailed { - ref toolchain, - ref components, - ref message, - } if toolchain == "nightly-2026-05-28" - && components == &expected_component_list - && message == COMPONENT_INSTALL_FAILURE_MESSAGE - ), - "expected ToolchainComponentInstallFailed with all components, got {err:?}" - ); + let InstallerError::ToolchainComponentInstallFailed { + toolchain: ref failed_toolchain, + components: ref failed_components, + ref message, + } = err + else { + panic!("expected ToolchainComponentInstallFailed, got {err:?}"); + }; + + assert_eq!(failed_toolchain, "nightly-2026-05-28"); + assert_eq!(failed_components, &expected_component_list); + assert_eq!(message, COMPONENT_INSTALL_FAILURE_MESSAGE); } #[rstest] @@ -216,10 +220,10 @@ fn ensure_installed_reports_failure( }; test_helpers::assert_install_fails_with( - toolchain, + &toolchain, |runner, seq| setup_failure_mocks(runner, seq, channel, setup), - |toolchain, runner| toolchain.ensure_installed_with(runner, additional_components), - |err| assert_failure_error(err, channel, setup), + |candidate, runner| candidate.ensure_installed_with(runner, additional_components), + |err| assert_failure_error(&err, channel, setup), ); } @@ -229,10 +233,7 @@ fn ensure_installed_reports_failure( #[case::trailing_whitespace(Some("some error message \n\n"), "some error message")] #[case::multiline_utf8(Some("line one\nline two\n"), "line one\nline two")] fn stderr_message_extracts_error(#[case] stderr: Option<&str>, #[case] expected: &str) { - let output = match stderr { - Some(s) => output_with_stderr(1, s), - None => output_with_status(1), - }; + let output = stderr.map_or_else(|| output_with_status(1), |s| output_with_stderr(1, s)); assert_eq!(stderr_message(&output), expected); } diff --git a/installer/src/toolchain/tests/test_helpers.rs b/installer/src/toolchain/tests/test_helpers.rs index 2426a3edb..d0f88d5ab 100644 --- a/installer/src/toolchain/tests/test_helpers.rs +++ b/installer/src/toolchain/tests/test_helpers.rs @@ -1,8 +1,8 @@ //! Test helpers for toolchain tests. +use std::{cell::RefCell, process::ExitStatus}; + use super::*; -use std::cell::RefCell; -use std::process::ExitStatus; #[cfg(unix)] pub fn exit_status(code: i32) -> ExitStatus { @@ -82,7 +82,7 @@ pub struct RustupExpectation<'a> { fn expect_rustup_command( runner: &mut MockCommandRunner, seq: &mut mockall::Sequence, - expectation: RustupExpectation<'_>, + expectation: &RustupExpectation<'_>, matcher: F, ) where F: Fn(&str, &[&str]) -> bool + Send + 'static, @@ -95,10 +95,10 @@ fn expect_rustup_command( .times(1) .in_sequence(seq) .returning(move |_, _| { - let output = match stderr.as_deref() { - Some(message) => output_with_stderr(exit_code, message), - None => output_with_status(exit_code), - }; + let output = stderr.as_deref().map_or_else( + || output_with_status(exit_code), + |message| output_with_stderr(exit_code, message), + ); Ok(output) }); } @@ -115,16 +115,18 @@ pub fn expect_rustc_version( channel: &str, exit_code: i32, ) { - let channel = channel.to_owned(); + let expected_channel = channel.to_owned(); runner .expect_run() .withf(move |program, args| { + let [run, actual_channel, rustc, version] = args else { + return false; + }; program == "rustup" - && args.len() == 4 - && args[0] == "run" - && args[1] == channel - && args[2] == "rustc" - && args[3] == "--version" + && *run == "run" + && *actual_channel == expected_channel + && *rustc == "rustc" + && *version == "--version" }) .times(1) .in_sequence(seq) @@ -134,29 +136,31 @@ pub fn expect_rustc_version( pub fn expect_toolchain_install( runner: &mut MockCommandRunner, seq: &mut mockall::Sequence, - expectation: ToolchainInstallExpectation<'_>, + expectation: &ToolchainInstallExpectation<'_>, ) { - let channel = expectation.channel.to_owned(); + let expected_channel = expectation.channel.to_owned(); expect_rustup_command( runner, seq, - RustupExpectation { + &RustupExpectation { exit_code: expectation.exit_code, stderr: expectation.stderr, }, move |program, args| { + let [toolchain, install, actual_channel] = args else { + return false; + }; program == "rustup" - && args.len() == 3 - && args[0] == "toolchain" - && args[1] == "install" - && args[2] == channel + && *toolchain == "toolchain" + && *install == "install" + && *actual_channel == expected_channel }, ); } -/// Helper to test that ensure_installed fails with the expected error. +/// Helper to test that `ensure_installed` fails with the expected error. pub fn assert_install_fails_with( - toolchain: Toolchain, + toolchain: &Toolchain, setup_mocks: F, install: I, error_matcher: E, @@ -170,7 +174,7 @@ pub fn assert_install_fails_with( setup_mocks(&mut runner, &mut seq); - let err = install(&toolchain, &runner).expect_err("expected installation failure"); + let err = install(toolchain, &runner).expect_err("expected installation failure"); error_matcher(err); } @@ -180,21 +184,23 @@ pub fn matches_multi_component_add( channel: &str, components: &[&str], ) -> impl Fn(&str, &[&str]) -> bool + use<> { - let channel = channel.to_owned(); - let components: Vec = components.iter().map(|s| (*s).to_owned()).collect(); + let expected_channel = channel.to_owned(); + let expected_components: Vec = components.iter().map(|s| (*s).to_owned()).collect(); move |program, args| { - let Some(actual_components) = args.get(4..) else { + let Some((&[component, add, toolchain_flag, actual_channel], actual_components)) = + args.split_at_checked(4) + else { return false; }; program == "rustup" - && args[0] == "component" - && args[1] == "add" - && args[2] == "--toolchain" - && args[3] == channel - && actual_components.len() == components.len() + && component == "component" + && add == "add" + && toolchain_flag == "--toolchain" + && actual_channel == expected_channel + && actual_components.len() == expected_components.len() && actual_components .iter() - .zip(&components) + .zip(&expected_components) .all(|(a, b)| *a == b) } } diff --git a/installer/tests/behaviour_artefact.rs b/installer/tests/behaviour_artefact.rs index 492664494..a05d37bae 100644 --- a/installer/tests/behaviour_artefact.rs +++ b/installer/tests/behaviour_artefact.rs @@ -3,21 +3,21 @@ //! //! These scenarios validate the domain types defined in the `artefact` module //! against the rules specified in ADR-001. Tests use the rstest-bdd v0.5.0 -//! mutable world pattern. +//! mutable world pattern with fallible steps. use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use whitaker_installer::artefact::error::ArtefactError; -use whitaker_installer::artefact::git_sha::GitSha; -use whitaker_installer::artefact::manifest::{ - GeneratedAt, Manifest, ManifestContent, ManifestProvenance, +use whitaker_installer::artefact::{ + error::ArtefactError, + git_sha::GitSha, + manifest::{GeneratedAt, Manifest, ManifestContent, ManifestProvenance}, + naming::ArtefactName, + schema_version::SchemaVersion, + sha256_digest::Sha256Digest, + target::TargetTriple, + toolchain_channel::ToolchainChannel, + verification::{VerificationFailureAction, VerificationPolicy}, }; -use whitaker_installer::artefact::naming::ArtefactName; -use whitaker_installer::artefact::schema_version::SchemaVersion; -use whitaker_installer::artefact::sha256_digest::Sha256Digest; -use whitaker_installer::artefact::target::TargetTriple; -use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; -use whitaker_installer::artefact::verification::{VerificationFailureAction, VerificationPolicy}; // --------------------------------------------------------------------------- // World types @@ -38,26 +38,40 @@ struct ArtefactWorld { all_triples_ok: Option, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> ArtefactWorld { ArtefactWorld::default() } -/// Helper to assert that an error option contains a specific ArtefactError variant. -fn assert_error_matches(error: &Option, field_name: &str, predicate: F) +/// Check that an error option contains a specific `ArtefactError` variant. +fn ensure_error_matches( + error: Option<&ArtefactError>, + field_name: &str, + predicate: F, +) -> Result<(), String> where F: FnOnce(&ArtefactError) -> bool, { - assert!( - error.is_some(), - "expected {} validation to fail", - field_name - ); - assert!( - predicate(error.as_ref().expect("checked above")), - "error variant mismatch for {}", - field_name - ); + let observed = error.ok_or_else(|| format!("expected {field_name} validation to fail"))?; + if predicate(observed) { + Ok(()) + } else { + Err(format!("error variant mismatch for {field_name}")) + } +} + +/// Compare two values for equality, reporting a mismatch as an error. +fn ensure_eq(actual: &T, expected: &U, context: &str) -> Result<(), String> +where + T: PartialEq + std::fmt::Debug + ?Sized, + U: std::fmt::Debug + ?Sized, +{ + if actual == expected { + Ok(()) + } else { + Err(format!("{context}: expected {expected:?}, got {actual:?}")) + } } // --------------------------------------------------------------------------- @@ -65,32 +79,49 @@ where // --------------------------------------------------------------------------- #[given("a git SHA \"{sha}\"")] -fn given_git_sha(world: &mut ArtefactWorld, sha: String) { - world.git_sha = Some(GitSha::try_from(sha).expect("test SHA")); +fn given_git_sha(world: &mut ArtefactWorld, sha: String) -> Result<(), String> { + world.git_sha = Some(GitSha::try_from(sha).map_err(|e| format!("test SHA: {e}"))?); + Ok(()) } #[given("a toolchain channel \"{channel}\"")] -fn given_toolchain_channel(world: &mut ArtefactWorld, channel: String) { - world.toolchain = Some(ToolchainChannel::try_from(channel).expect("test channel")); +fn given_toolchain_channel(world: &mut ArtefactWorld, channel: String) -> Result<(), String> { + world.toolchain = + Some(ToolchainChannel::try_from(channel).map_err(|e| format!("test channel: {e}"))?); + Ok(()) } #[given("a target triple \"{triple}\"")] -fn given_target_triple(world: &mut ArtefactWorld, triple: String) { - world.target = Some(TargetTriple::try_from(triple).expect("test triple")); +fn given_target_triple(world: &mut ArtefactWorld, triple: String) -> Result<(), String> { + world.target = Some(TargetTriple::try_from(triple).map_err(|e| format!("test triple: {e}"))?); + Ok(()) } #[when("an artefact name is constructed")] -fn when_artefact_name_constructed(world: &mut ArtefactWorld) { - let sha = world.git_sha.clone().expect("git_sha set"); - let ch = world.toolchain.clone().expect("toolchain set"); - let tgt = world.target.clone().expect("target set"); +fn when_artefact_name_constructed(world: &mut ArtefactWorld) -> Result<(), String> { + let sha = world + .git_sha + .clone() + .ok_or_else(|| String::from("git_sha set"))?; + let ch = world + .toolchain + .clone() + .ok_or_else(|| String::from("toolchain set"))?; + let tgt = world + .target + .clone() + .ok_or_else(|| String::from("target set"))?; world.artefact_name = Some(ArtefactName::new(sha, ch, tgt)); + Ok(()) } #[then("the filename is \"{expected}\"")] -fn then_filename_matches(world: &mut ArtefactWorld, expected: String) { - let name = world.artefact_name.as_ref().expect("artefact_name set"); - assert_eq!(name.filename(), expected); +fn then_filename_matches(world: &mut ArtefactWorld, expected: String) -> Result<(), String> { + let name = world + .artefact_name + .as_ref() + .ok_or_else(|| String::from("artefact_name set"))?; + ensure_eq(&name.filename(), &expected, "artefact filename") } #[given("an invalid target triple \"{triple}\"")] @@ -99,10 +130,10 @@ fn given_invalid_target(world: &mut ArtefactWorld, triple: String) { } #[then("the target triple is rejected")] -fn then_target_rejected(world: &mut ArtefactWorld) { - assert_error_matches(&world.target_error, "target", |e| { +fn then_target_rejected(world: &mut ArtefactWorld) -> Result<(), String> { + ensure_error_matches(world.target_error.as_ref(), "target", |e| { matches!(e, ArtefactError::UnsupportedTarget { .. }) - }); + }) } #[given("all supported target triples")] @@ -114,8 +145,8 @@ fn given_all_supported(world: &mut ArtefactWorld) { } #[then("every triple is accepted")] -fn then_all_accepted(world: &mut ArtefactWorld) { - assert_eq!(world.all_triples_ok, Some(true)); +fn then_all_accepted(world: &mut ArtefactWorld) -> Result<(), String> { + ensure_eq(&world.all_triples_ok, &Some(true), "all triples accepted") } #[given("an invalid git SHA \"{sha}\"")] @@ -124,10 +155,10 @@ fn given_invalid_sha(world: &mut ArtefactWorld, sha: String) { } #[then("the git SHA is rejected")] -fn then_sha_rejected(world: &mut ArtefactWorld) { - assert_error_matches(&world.sha_error, "SHA", |e| { +fn then_sha_rejected(world: &mut ArtefactWorld) -> Result<(), String> { + ensure_error_matches(world.sha_error.as_ref(), "SHA", |e| { matches!(e, ArtefactError::InvalidGitSha { .. }) - }); + }) } #[given("an empty toolchain channel")] @@ -136,47 +167,71 @@ fn given_empty_channel(world: &mut ArtefactWorld) { } #[then("the toolchain channel is rejected")] -fn then_channel_rejected(world: &mut ArtefactWorld) { - assert_error_matches(&world.channel_error, "channel", |e| { +fn then_channel_rejected(world: &mut ArtefactWorld) -> Result<(), String> { + ensure_error_matches(world.channel_error.as_ref(), "channel", |e| { matches!(e, ArtefactError::InvalidToolchainChannel { .. }) - }); + }) } #[given("a complete set of manifest fields")] -fn given_manifest_fields(world: &mut ArtefactWorld) { - world.git_sha = Some(GitSha::try_from("abc1234").expect("valid sha")); - world.toolchain = - Some(ToolchainChannel::try_from("nightly-2026-05-28").expect("valid channel")); - world.target = Some(TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target")); +fn given_manifest_fields(world: &mut ArtefactWorld) -> Result<(), String> { + world.git_sha = Some(GitSha::try_from("abc1234").map_err(|e| format!("valid sha: {e}"))?); + world.toolchain = Some( + ToolchainChannel::try_from("nightly-2026-05-28") + .map_err(|e| format!("valid channel: {e}"))?, + ); + world.target = Some( + TargetTriple::try_from("x86_64-unknown-linux-gnu") + .map_err(|e| format!("valid target: {e}"))?, + ); + Ok(()) } #[when("a manifest is constructed")] -fn when_manifest_constructed(world: &mut ArtefactWorld) { +fn when_manifest_constructed(world: &mut ArtefactWorld) -> Result<(), String> { let provenance = ManifestProvenance { - git_sha: world.git_sha.clone().expect("git_sha set"), + git_sha: world + .git_sha + .clone() + .ok_or_else(|| String::from("git_sha set"))?, schema_version: SchemaVersion::current(), - toolchain: world.toolchain.clone().expect("toolchain set"), - target: world.target.clone().expect("target set"), + toolchain: world + .toolchain + .clone() + .ok_or_else(|| String::from("toolchain set"))?, + target: world + .target + .clone() + .ok_or_else(|| String::from("target set"))?, }; let digest_hex = "a".repeat(64); let content = ManifestContent { generated_at: GeneratedAt::new("2026-05-28T00:00:00Z"), files: vec!["libwhitaker_lints.so".to_owned()], - sha256: Sha256Digest::try_from(digest_hex.as_str()).expect("valid digest"), + sha256: Sha256Digest::try_from(digest_hex.as_str()) + .map_err(|e| format!("valid digest: {e}"))?, }; world.manifest = Some(Manifest::new(provenance, content)); + Ok(()) } #[then("all manifest fields are accessible")] -fn then_manifest_accessible(world: &mut ArtefactWorld) { - let m = world.manifest.as_ref().expect("manifest set"); - assert_eq!(m.git_sha().as_str(), "abc1234"); - assert_eq!(m.schema_version().as_u32(), 1); - assert_eq!(m.toolchain().as_str(), "nightly-2026-05-28"); - assert_eq!(m.target().as_str(), "x86_64-unknown-linux-gnu"); - assert_eq!(m.generated_at().as_str(), "2026-05-28T00:00:00Z"); - assert_eq!(m.files().len(), 1); - assert_eq!(m.sha256().as_str().len(), 64); +fn then_manifest_accessible(world: &mut ArtefactWorld) -> Result<(), String> { + let m = world + .manifest + .as_ref() + .ok_or_else(|| String::from("manifest set"))?; + ensure_eq(m.git_sha().as_str(), "abc1234", "git sha")?; + ensure_eq(&m.schema_version().as_u32(), &1, "schema version")?; + ensure_eq(m.toolchain().as_str(), "nightly-2026-05-28", "toolchain")?; + ensure_eq(m.target().as_str(), "x86_64-unknown-linux-gnu", "target")?; + ensure_eq( + m.generated_at().as_str(), + "2026-05-28T00:00:00Z", + "generated at", + )?; + ensure_eq(&m.files().len(), &1, "file count")?; + ensure_eq(&m.sha256().as_str().len(), &64, "sha256 length") } #[given("the default verification policy")] @@ -185,9 +240,16 @@ fn given_default_policy(world: &mut ArtefactWorld) { } #[then("checksum verification is required")] -fn then_checksum_required(world: &mut ArtefactWorld) { - let policy = world.policy.as_ref().expect("policy set"); - assert!(policy.require_checksum()); +fn then_checksum_required(world: &mut ArtefactWorld) -> Result<(), String> { + let policy = world + .policy + .as_ref() + .ok_or_else(|| String::from("policy set"))?; + if policy.require_checksum() { + Ok(()) + } else { + Err(String::from("checksum verification must be required")) + } } #[given("the default failure action")] @@ -196,11 +258,12 @@ fn given_default_failure_action(world: &mut ArtefactWorld) { } #[then("the action is fallback with warning")] -fn then_action_is_fallback(world: &mut ArtefactWorld) { - assert_eq!( - world.failure_action, - Some(VerificationFailureAction::FallbackWithWarning) - ); +fn then_action_is_fallback(world: &mut ArtefactWorld) -> Result<(), String> { + ensure_eq( + &world.failure_action, + &Some(VerificationFailureAction::FallbackWithWarning), + "failure action", + ) } // --------------------------------------------------------------------------- diff --git a/installer/tests/behaviour_artefact_packaging.rs b/installer/tests/behaviour_artefact_packaging.rs index 861a7c645..189bfa750 100644 --- a/installer/tests/behaviour_artefact_packaging.rs +++ b/installer/tests/behaviour_artefact_packaging.rs @@ -2,22 +2,24 @@ //! //! These scenarios validate the packaging pipeline defined in the //! `artefact::packaging` module against ADR-001 rules. Tests use the -//! rstest-bdd v0.5.0 mutable world pattern. +//! rstest-bdd v0.5.0 mutable world pattern with fallible steps. + +use std::{fs, path::PathBuf}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::fs; -use std::path::PathBuf; use tempfile::TempDir; -use whitaker_installer::artefact::git_sha::GitSha; -use whitaker_installer::artefact::manifest::GeneratedAt; -use whitaker_installer::artefact::naming::ArtefactName; -use whitaker_installer::artefact::packaging::{ - PackageOutput, PackageParams, compute_sha256, generate_manifest_json, package_artefact, +use whitaker_installer::artefact::{ + git_sha::GitSha, + manifest::GeneratedAt, + naming::ArtefactName, + packaging::{ + PackageOutput, PackageParams, compute_sha256, generate_manifest_json, package_artefact, + }, + packaging_error::PackagingError, + target::TargetTriple, + toolchain_channel::ToolchainChannel, }; -use whitaker_installer::artefact::packaging_error::PackagingError; -use whitaker_installer::artefact::target::TargetTriple; -use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; // --------------------------------------------------------------------------- // World types @@ -36,33 +38,51 @@ struct PackagingWorld { archive_sha256: Option, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> PackagingWorld { - PackagingWorld { - temp_dir: Some(TempDir::new().expect("temp dir")), - ..PackagingWorld::default() - } + PackagingWorld::default() } -/// Return the temp directory path, creating one if needed. -fn temp_path(world: &PackagingWorld) -> PathBuf { +/// Return the temp directory path, creating the directory if needed. +fn temp_path(world: &mut PackagingWorld) -> Result { + if world.temp_dir.is_none() { + let dir = TempDir::new().map_err(|e| format!("create temp dir: {e}"))?; + world.temp_dir = Some(dir); + } world .temp_dir .as_ref() - .expect("temp_dir set") - .path() - .to_path_buf() + .map(|dir| dir.path().to_path_buf()) + .ok_or_else(|| String::from("temp_dir set")) +} + +/// Fetch the packaging output, failing if packaging has not run successfully. +fn output_ref(world: &PackagingWorld) -> Result<&PackageOutput, String> { + world + .output + .as_ref() + .ok_or_else(|| String::from("packaging output must be set")) } /// Run the packaging pipeline and store the result in the world. -fn run_packaging(world: &mut PackagingWorld) { - let output_dir = temp_path(world).join("dist"); - fs::create_dir_all(&output_dir).expect("mkdir dist"); +fn run_packaging(world: &mut PackagingWorld) -> Result<(), String> { + let output_dir = temp_path(world)?.join("dist"); + fs::create_dir_all(&output_dir).map_err(|e| format!("mkdir dist: {e}"))?; let params = PackageParams { - git_sha: world.git_sha.clone().expect("git_sha set"), - toolchain: world.toolchain.clone().expect("toolchain set"), - target: world.target.clone().expect("target set"), + git_sha: world + .git_sha + .clone() + .ok_or_else(|| String::from("git_sha set"))?, + toolchain: world + .toolchain + .clone() + .ok_or_else(|| String::from("toolchain set"))?, + target: world + .target + .clone() + .ok_or_else(|| String::from("target set"))?, library_files: world.library_files.clone(), output_dir, generated_at: GeneratedAt::new("2026-02-11T00:00:00Z"), @@ -72,6 +92,29 @@ fn run_packaging(world: &mut PackagingWorld) { Ok(output) => world.output = Some(output), Err(e) => world.packaging_error = Some(e), } + Ok(()) +} + +/// Write a fixture library file into the temp directory and register it. +fn add_library_file(world: &mut PackagingWorld, name: &str, content: &[u8]) -> Result<(), String> { + let path = temp_path(world)?.join(name); + fs::write(&path, content).map_err(|e| format!("write {name}: {e}"))?; + world.library_files.push(path); + Ok(()) +} + +/// Populate the world with valid known packaging components. +fn set_known_components(world: &mut PackagingWorld) -> Result<(), String> { + world.git_sha = Some(GitSha::try_from("abc1234").map_err(|e| format!("valid sha: {e}"))?); + world.toolchain = Some( + ToolchainChannel::try_from("nightly-2026-05-28") + .map_err(|e| format!("valid channel: {e}"))?, + ); + world.target = Some( + TargetTriple::try_from("x86_64-unknown-linux-gnu") + .map_err(|e| format!("valid target: {e}"))?, + ); + Ok(()) } // --------------------------------------------------------------------------- @@ -79,54 +122,57 @@ fn run_packaging(world: &mut PackagingWorld) { // --------------------------------------------------------------------------- #[given("a library file \"{name}\"")] -fn given_library_file(world: &mut PackagingWorld, name: String) { - let path = temp_path(world).join(&name); - fs::write(&path, b"fake library content").expect("write lib"); - world.library_files.push(path); +fn given_library_file(world: &mut PackagingWorld, name: String) -> Result<(), String> { + add_library_file(world, &name, b"fake library content") } #[given("a git SHA \"{sha}\"")] -fn given_git_sha(world: &mut PackagingWorld, sha: String) { - world.git_sha = Some(GitSha::try_from(sha).expect("valid SHA")); +fn given_git_sha(world: &mut PackagingWorld, sha: String) -> Result<(), String> { + world.git_sha = Some(GitSha::try_from(sha).map_err(|e| format!("valid SHA: {e}"))?); + Ok(()) } #[given("a toolchain channel \"{channel}\"")] -fn given_toolchain(world: &mut PackagingWorld, channel: String) { - world.toolchain = Some(ToolchainChannel::try_from(channel).expect("valid channel")); +fn given_toolchain(world: &mut PackagingWorld, channel: String) -> Result<(), String> { + world.toolchain = + Some(ToolchainChannel::try_from(channel).map_err(|e| format!("valid channel: {e}"))?); + Ok(()) } #[given("a target triple \"{triple}\"")] -fn given_target(world: &mut PackagingWorld, triple: String) { - world.target = Some(TargetTriple::try_from(triple).expect("valid target")); +fn given_target(world: &mut PackagingWorld, triple: String) -> Result<(), String> { + world.target = Some(TargetTriple::try_from(triple).map_err(|e| format!("valid target: {e}"))?); + Ok(()) } #[when("the artefact is packaged")] -fn when_packaged(world: &mut PackagingWorld) { - run_packaging(world); +fn when_packaged(world: &mut PackagingWorld) -> Result<(), String> { + run_packaging(world) } #[then("the archive exists with the expected ADR-001 filename")] -fn then_archive_exists(world: &mut PackagingWorld) { - let output = world.output.as_ref().expect("output set"); - assert!(output.archive_path.exists(), "archive file must exist"); +fn then_archive_exists(world: &mut PackagingWorld) -> Result<(), String> { + let output = output_ref(world)?; + if !output.archive_path.exists() { + return Err(String::from("archive file must exist")); + } let filename = output .archive_path .file_name() - .expect("filename") + .ok_or_else(|| String::from("archive path must have a filename"))? .to_string_lossy(); - assert!( - filename.starts_with("whitaker-lints-"), - "filename must start with 'whitaker-lints-'" - ); - assert!( - filename.ends_with(".tar.zst"), - "filename must end with '.tar.zst'" - ); + if !filename.starts_with("whitaker-lints-") { + return Err(String::from("filename must start with 'whitaker-lints-'")); + } + if !filename.ends_with(".tar.zst") { + return Err(String::from("filename must end with '.tar.zst'")); + } + Ok(()) } #[then("the archive contains the library file")] -fn then_archive_has_library(world: &mut PackagingWorld) { - let entries = list_archive_entries(world); +fn then_archive_has_library(world: &mut PackagingWorld) -> Result<(), String> { + let entries = list_archive_entries(world)?; let expected: Vec = world .library_files .iter() @@ -134,184 +180,220 @@ fn then_archive_has_library(world: &mut PackagingWorld) { .map(|n| n.to_string_lossy().into_owned()) .collect(); for name in &expected { - assert!( - entries.contains(name), - "archive must contain {name}, got {entries:?}" - ); + if !entries.contains(name) { + return Err(format!("archive must contain {name}, got {entries:?}")); + } } + Ok(()) } #[then("the archive does not contain a manifest")] -fn then_archive_has_no_manifest(world: &mut PackagingWorld) { - let entries = list_archive_entries(world); - assert!( - !entries.contains(&"manifest.json".to_owned()), - "archive must not contain manifest.json" - ); +fn then_archive_has_no_manifest(world: &mut PackagingWorld) -> Result<(), String> { + let entries = list_archive_entries(world)?; + if entries.contains(&"manifest.json".to_owned()) { + return Err(String::from("archive must not contain manifest.json")); + } + Ok(()) } #[given("a packaged artefact")] -fn given_packaged_artefact(world: &mut PackagingWorld) { - let path = temp_path(world).join("libwhitaker_suite.so"); - fs::write(&path, b"fake library").expect("write"); - world.library_files.push(path); - world.git_sha = Some(GitSha::try_from("abc1234").expect("valid")); - world.toolchain = Some(ToolchainChannel::try_from("nightly-2026-05-28").expect("valid")); - world.target = Some(TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid")); - run_packaging(world); +fn given_packaged_artefact(world: &mut PackagingWorld) -> Result<(), String> { + add_library_file(world, "libwhitaker_suite.so", b"fake library")?; + set_known_components(world)?; + run_packaging(world) } #[when("the manifest JSON is generated")] -fn when_manifest_json_generated(world: &mut PackagingWorld) { - let output = world.output.as_ref().expect("output set"); - let json = generate_manifest_json(&output.manifest).expect("serialization"); - world.manifest_json = Some(serde_json::from_str(&json).expect("parse JSON")); +fn when_manifest_json_generated(world: &mut PackagingWorld) -> Result<(), String> { + let output = output_ref(world)?; + let json = + generate_manifest_json(&output.manifest).map_err(|e| format!("serialization: {e}"))?; + world.manifest_json = + Some(serde_json::from_str(&json).map_err(|e| format!("parse JSON: {e}"))?); + Ok(()) } #[then("the manifest contains field \"{field}\"")] -fn then_manifest_has_field(world: &mut PackagingWorld, field: String) { - let json = world.manifest_json.as_ref().expect("manifest_json set"); - let obj = json.as_object().expect("top-level object"); - assert!(obj.contains_key(&field), "missing field: {field}"); +fn then_manifest_has_field(world: &mut PackagingWorld, field: String) -> Result<(), String> { + let json = world + .manifest_json + .as_ref() + .ok_or_else(|| String::from("manifest_json set"))?; + let obj = json + .as_object() + .ok_or_else(|| String::from("manifest JSON must be a top-level object"))?; + if !obj.contains_key(&field) { + return Err(format!("missing field: {field}")); + } + Ok(()) } #[when("the archive SHA-256 is computed")] -fn when_sha256_computed(world: &mut PackagingWorld) { - let output = world.output.as_ref().expect("output set"); - let digest = compute_sha256(&output.archive_path).expect("sha256"); +fn when_sha256_computed(world: &mut PackagingWorld) -> Result<(), String> { + let output = output_ref(world)?; + let digest = compute_sha256(&output.archive_path).map_err(|e| format!("sha256: {e}"))?; world.archive_sha256 = Some(digest.as_str().to_owned()); + Ok(()) } #[then("it matches the manifest sha256")] -fn then_digest_matches_manifest(world: &mut PackagingWorld) { - let archive_hex = world.archive_sha256.as_ref().expect("sha256 set"); - let manifest_hex = world - .output +fn then_digest_matches_manifest(world: &mut PackagingWorld) -> Result<(), String> { + let archive_hex = world + .archive_sha256 .as_ref() - .expect("output set") - .manifest - .sha256() - .as_str(); - assert_eq!( - archive_hex, manifest_hex, - "archive digest must match manifest sha256" - ); + .ok_or_else(|| String::from("sha256 set"))?; + let manifest_hex = output_ref(world)?.manifest.sha256().as_str(); + if archive_hex != manifest_hex { + return Err(format!( + "archive digest {archive_hex} must match manifest sha256 {manifest_hex}" + )); + } + Ok(()) } #[then("it is a valid 64-character hex string")] -fn then_valid_hex(world: &mut PackagingWorld) { - let hex = world.archive_sha256.as_ref().expect("sha256 set"); - assert_eq!(hex.len(), 64, "digest must be 64 characters"); - assert!( - hex.chars().all(|c| c.is_ascii_hexdigit()), - "digest must be hex" - ); +fn then_valid_hex(world: &mut PackagingWorld) -> Result<(), String> { + let hex = world + .archive_sha256 + .as_ref() + .ok_or_else(|| String::from("sha256 set"))?; + if hex.len() != 64 { + return Err(format!("digest must be 64 characters, got {}", hex.len())); + } + if !hex.chars().all(|c| c.is_ascii_hexdigit()) { + return Err(format!("digest must be hex: {hex}")); + } + Ok(()) } #[given("no library files")] -fn given_no_files(world: &mut PackagingWorld) { +fn given_no_files(world: &mut PackagingWorld) -> Result<(), String> { world.library_files.clear(); - world.git_sha = Some(GitSha::try_from("abc1234").expect("valid")); - world.toolchain = Some(ToolchainChannel::try_from("nightly-2026-05-28").expect("valid")); - world.target = Some(TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid")); + set_known_components(world) } #[when("packaging is attempted")] -fn when_packaging_attempted(world: &mut PackagingWorld) { - run_packaging(world); +fn when_packaging_attempted(world: &mut PackagingWorld) -> Result<(), String> { + run_packaging(world) } #[then("a packaging error is returned")] -fn then_packaging_error(world: &mut PackagingWorld) { - assert!( - world.packaging_error.is_some(), - "expected a packaging error" - ); - assert!( - matches!( - world.packaging_error.as_ref().expect("checked above"), - PackagingError::EmptyFileList - ), - "expected EmptyFileList error" - ); +fn then_packaging_error(world: &mut PackagingWorld) -> Result<(), String> { + let error = world + .packaging_error + .as_ref() + .ok_or_else(|| String::from("expected a packaging error"))?; + if matches!(error, PackagingError::EmptyFileList) { + Ok(()) + } else { + Err(format!("expected EmptyFileList error, got {error:?}")) + } } #[given("library files \"{a}\" and \"{b}\" and \"{c}\"")] -fn given_three_library_files(world: &mut PackagingWorld, a: String, b: String, c: String) { +fn given_three_library_files( + world: &mut PackagingWorld, + a: String, + b: String, + c: String, +) -> Result<(), String> { for name in [a, b, c] { - let path = temp_path(world).join(&name); - fs::write(&path, format!("content of {name}")).expect("write"); - world.library_files.push(path); + add_library_file(world, &name, format!("content of {name}").as_bytes())?; } + Ok(()) } #[given("library files \"{a}\" and \"{b}\"")] -fn given_two_library_files(world: &mut PackagingWorld, a: String, b: String) { +fn given_two_library_files(world: &mut PackagingWorld, a: String, b: String) -> Result<(), String> { for name in [a, b] { - let path = temp_path(world).join(&name); - fs::write(&path, format!("content of {name}")).expect("write"); - world.library_files.push(path); + add_library_file(world, &name, format!("content of {name}").as_bytes())?; } + Ok(()) } #[then("the archive contains {count} library files")] -fn then_archive_has_n_libraries(world: &mut PackagingWorld, count: usize) { - let entries = list_archive_entries(world); +fn then_archive_has_n_libraries(world: &mut PackagingWorld, count: usize) -> Result<(), String> { + let entries = list_archive_entries(world)?; let lib_count = entries.iter().filter(|e| *e != "manifest.json").count(); - assert_eq!( - lib_count, count, - "expected {count} library files, got {lib_count}" - ); + if lib_count != count { + return Err(format!("expected {count} library files, got {lib_count}")); + } + Ok(()) } #[then("the manifest files field contains \"{name}\"")] -fn then_manifest_files_contains(world: &mut PackagingWorld, name: String) { - let json = world.manifest_json.as_ref().expect("manifest_json set"); - let files = json["files"].as_array().expect("files is an array"); +fn then_manifest_files_contains(world: &mut PackagingWorld, name: String) -> Result<(), String> { + let json = world + .manifest_json + .as_ref() + .ok_or_else(|| String::from("manifest_json set"))?; + let files = json + .get("files") + .and_then(|v| v.as_array()) + .ok_or_else(|| String::from("files must be an array"))?; let names: Vec<&str> = files.iter().filter_map(|v| v.as_str()).collect(); - assert!( - names.contains(&name.as_str()), - "files field missing {name}: {names:?}" - ); + if !names.contains(&name.as_str()) { + return Err(format!("files field missing {name}: {names:?}")); + } + Ok(()) } #[given("a packaged artefact with known components")] -fn given_packaged_with_known(world: &mut PackagingWorld) { - given_packaged_artefact(world); +fn given_packaged_with_known(world: &mut PackagingWorld) -> Result<(), String> { + given_packaged_artefact(world) } #[then("it matches the ArtefactName string representation")] -fn then_filename_matches_artefact_name(world: &mut PackagingWorld) { - let output = world.output.as_ref().expect("output set"); +fn then_filename_matches_artefact_name(world: &mut PackagingWorld) -> Result<(), String> { let expected = ArtefactName::new( - world.git_sha.clone().expect("sha"), - world.toolchain.clone().expect("toolchain"), - world.target.clone().expect("target"), - ); - assert_eq!( - output - .archive_path - .file_name() - .expect("filename") - .to_string_lossy(), - expected.filename() + world + .git_sha + .clone() + .ok_or_else(|| String::from("sha set"))?, + world + .toolchain + .clone() + .ok_or_else(|| String::from("toolchain set"))?, + world + .target + .clone() + .ok_or_else(|| String::from("target set"))?, ); + let output = output_ref(world)?; + let filename = output + .archive_path + .file_name() + .ok_or_else(|| String::from("archive path must have a filename"))? + .to_string_lossy(); + if filename != expected.filename() { + return Err(format!( + "filename {filename} must match ArtefactName {}", + expected.filename() + )); + } + Ok(()) } /// Extract entry names from a `.tar.zst` archive. -fn list_archive_entries(world: &PackagingWorld) -> Vec { - let output = world.output.as_ref().expect("output set"); - let file = fs::File::open(&output.archive_path).expect("open"); - let decoder = zstd::Decoder::new(file).expect("decode"); +fn list_archive_entries(world: &PackagingWorld) -> Result, String> { + let output = output_ref(world)?; + let file = fs::File::open(&output.archive_path).map_err(|e| format!("open archive: {e}"))?; + let decoder = zstd::Decoder::new(file).map_err(|e| format!("decode archive: {e}"))?; let mut archive = tar::Archive::new(decoder); - archive + let mut names = Vec::new(); + for entry in archive .entries() - .expect("entries") - .map(|e| { - let entry = e.expect("entry"); - entry.path().expect("path").to_string_lossy().into_owned() - }) - .collect() + .map_err(|e| format!("list archive entries: {e}"))? + { + let archive_entry = entry.map_err(|e| format!("read archive entry: {e}"))?; + let path = archive_entry + .path() + .map_err(|e| format!("read entry path: {e}"))? + .to_string_lossy() + .into_owned(); + names.push(path); + } + Ok(names) } // --------------------------------------------------------------------------- diff --git a/installer/tests/behaviour_binstall.rs b/installer/tests/behaviour_binstall.rs index f58c0dabf..2fdcd7c06 100644 --- a/installer/tests/behaviour_binstall.rs +++ b/installer/tests/behaviour_binstall.rs @@ -30,19 +30,60 @@ struct BinstallWorld { expanded_bin_dir: String, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> BinstallWorld { BinstallWorld::default() } +/// Fetch the binstall table, failing if it has not been loaded. +fn binstall_table(world: &BinstallWorld) -> Result<&Table, String> { + world + .binstall_table + .as_ref() + .ok_or_else(|| String::from("binstall table must be loaded")) +} + +/// Read a string-valued key from a TOML table, failing if absent. +fn table_str<'a>(table: &'a Table, key: &str) -> Result<&'a str, String> { + table + .get(key) + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("{key} not found")) +} + +/// Assert that a string-valued binstall key equals `expected`. +/// +/// The mismatch is returned as an `Err` rather than asserted: these steps +/// return `Result`, so a panic here would trip `clippy::panic_in_result_fn`, +/// and returning the message keeps the failure reporting identical to the +/// other steps in this file. +fn assert_binstall_value_equals( + world: &BinstallWorld, + key: &str, + expected: &str, + value_name: &str, +) -> Result<(), String> { + let binstall = binstall_table(world)?; + let actual = table_str(binstall, key)?; + if actual == expected { + Ok(()) + } else { + Err(format!( + "{value_name} mismatch: expected {expected}, got {actual}" + )) + } +} + // --------------------------------------------------------------------------- // Step definitions // --------------------------------------------------------------------------- #[given("the installer Cargo.toml is loaded")] -fn given_cargo_toml_loaded(world: &mut BinstallWorld) { +fn given_cargo_toml_loaded(world: &mut BinstallWorld) -> Result<(), String> { let table = load_cargo_toml(); world.binstall_table = Some(extract_binstall_table(&table)); + Ok(()) } #[given("target \"{target}\" and version \"{version}\"")] @@ -62,13 +103,13 @@ fn when_binstall_inspected(world: &mut BinstallWorld) { } #[when("the binstall overrides are inspected")] -fn when_overrides_inspected(world: &mut BinstallWorld) { +fn when_overrides_inspected(world: &mut BinstallWorld) -> Result<(), String> { // Verify overrides are accessible; the Then step extracts them directly. - let binstall = world.binstall_table.as_ref().expect("binstall table set"); - assert!( - binstall.get("overrides").is_some(), - "overrides table not found" - ); + let binstall = binstall_table(world)?; + if binstall.get("overrides").is_none() { + return Err(String::from("overrides table not found")); + } + Ok(()) } #[when("the pkg-url template is expanded")] @@ -82,48 +123,41 @@ fn when_bin_dir_expanded(world: &mut BinstallWorld) { } #[then("the pkg-url template is present")] -fn then_pkg_url_present(world: &mut BinstallWorld) { - let binstall = world.binstall_table.as_ref().expect("binstall table set"); - let pkg_url = binstall - .get("pkg-url") - .and_then(|v| v.as_str()) - .expect("pkg-url not found"); - assert_eq!(pkg_url, PKG_URL_TEMPLATE); +fn then_pkg_url_present(world: &mut BinstallWorld) -> Result<(), String> { + assert_binstall_value_equals(world, "pkg-url", PKG_URL_TEMPLATE, "pkg-url") } #[then("the bin-dir template is present")] -fn then_bin_dir_present(world: &mut BinstallWorld) { - let binstall = world.binstall_table.as_ref().expect("binstall table set"); - let bin_dir = binstall - .get("bin-dir") - .and_then(|v| v.as_str()) - .expect("bin-dir not found"); - assert_eq!(bin_dir, BIN_DIR_TEMPLATE); +fn then_bin_dir_present(world: &mut BinstallWorld) -> Result<(), String> { + assert_binstall_value_equals(world, "bin-dir", BIN_DIR_TEMPLATE, "bin-dir") } #[then("the default pkg-fmt is \"{expected}\"")] -fn then_default_pkg_fmt(world: &mut BinstallWorld, expected: String) { - let binstall = world.binstall_table.as_ref().expect("binstall table set"); - let pkg_fmt = binstall - .get("pkg-fmt") - .and_then(|v| v.as_str()) - .expect("pkg-fmt not found"); - assert_eq!(pkg_fmt, expected); +fn then_default_pkg_fmt(world: &mut BinstallWorld, expected: String) -> Result<(), String> { + assert_binstall_value_equals(world, "pkg-fmt", &expected, "pkg-fmt") } #[then("the x86_64-pc-windows-msvc override has pkg-fmt \"{expected}\"")] -fn then_windows_override_pkg_fmt(world: &mut BinstallWorld, expected: String) { - let binstall = world.binstall_table.as_ref().expect("binstall table set"); +fn then_windows_override_pkg_fmt( + world: &mut BinstallWorld, + expected: String, +) -> Result<(), String> { + let binstall = binstall_table(world)?; let windows = binstall .get("overrides") .and_then(|o| o.get(WINDOWS_OVERRIDE_TARGET)) .and_then(|w| w.as_table()) - .expect("Windows override not found"); + .ok_or_else(|| String::from("Windows override not found"))?; let pkg_fmt = windows .get("pkg-fmt") .and_then(|v| v.as_str()) - .expect("pkg-fmt not found in Windows override"); - assert_eq!(pkg_fmt, expected); + .ok_or_else(|| String::from("pkg-fmt not found in Windows override"))?; + if pkg_fmt != expected { + return Err(format!( + "Windows override pkg-fmt mismatch: expected {expected}, got {pkg_fmt}" + )); + } + Ok(()) } #[then("the URL ends with \"{suffix}\"")] @@ -155,24 +189,24 @@ fn then_path_ends_with(world: &mut BinstallWorld, suffix: String) { } #[then("no templates contain the placeholder \"{placeholder}\"")] -fn then_no_invalid_placeholder(world: &mut BinstallWorld, placeholder: String) { - let binstall = world.binstall_table.as_ref().expect("binstall table set"); - let pkg_url = binstall - .get("pkg-url") - .and_then(|v| v.as_str()) - .expect("pkg-url not found"); - let bin_dir = binstall - .get("bin-dir") - .and_then(|v| v.as_str()) - .expect("bin-dir not found"); - assert!( - !pkg_url.contains(&placeholder), - "pkg-url contains invalid placeholder '{placeholder}'" - ); - assert!( - !bin_dir.contains(&placeholder), - "bin-dir contains invalid placeholder '{placeholder}'" - ); +fn then_no_invalid_placeholder( + world: &mut BinstallWorld, + placeholder: String, +) -> Result<(), String> { + let binstall = binstall_table(world)?; + let pkg_url = table_str(binstall, "pkg-url")?; + let bin_dir = table_str(binstall, "bin-dir")?; + if pkg_url.contains(&placeholder) { + return Err(format!( + "pkg-url contains invalid placeholder '{placeholder}'" + )); + } + if bin_dir.contains(&placeholder) { + return Err(format!( + "bin-dir contains invalid placeholder '{placeholder}'" + )); + } + Ok(()) } // --------------------------------------------------------------------------- diff --git a/installer/tests/behaviour_cli.rs b/installer/tests/behaviour_cli.rs index 0bd1f57c0..9dc75446c 100644 --- a/installer/tests/behaviour_cli.rs +++ b/installer/tests/behaviour_cli.rs @@ -9,8 +9,9 @@ mod scenarios; #[path = "behaviour_cli/support.rs"] mod support; -use rstest_bdd_macros::{given, then, when}; use std::process::Command; + +use rstest_bdd_macros::{given, then, when}; pub(crate) use support::{CliWorld, cli_world}; use support::{ assert_cli_exits_successfully, assert_cli_exits_with_error, assert_dry_run_output_is_shown, diff --git a/installer/tests/behaviour_cli/scenarios.rs b/installer/tests/behaviour_cli/scenarios.rs index c95291d84..924b4b019 100644 --- a/installer/tests/behaviour_cli/scenarios.rs +++ b/installer/tests/behaviour_cli/scenarios.rs @@ -1,8 +1,9 @@ //! Scenario bindings for CLI behaviour tests. -use super::{CliWorld, cli_world}; use rstest_bdd_macros::scenario; +use super::{CliWorld, cli_world}; + // Do not reorder scenarios in tests/features/installer.feature — bindings are // index-based. #[scenario(path = "tests/features/installer.feature", index = 12)] diff --git a/installer/tests/behaviour_cli/support.rs b/installer/tests/behaviour_cli/support.rs index 1bae4eacd..974e39e3d 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -1,15 +1,17 @@ -//! Shared fixtures, command helpers, and assertions for CLI behaviour tests. +//! Shared fixtures and command helpers for CLI behaviour tests. + +use std::{ + cell::{Cell, Ref, RefCell}, + path::{Path, PathBuf}, + process::{Command, Output}, +}; -use super::prebuilt_markers::PREBUILT_INSTALL_MARKER; use rstest::fixture; -use std::cell::{Cell, Ref, RefCell}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; use tempfile::TempDir; -use whitaker_installer::dirs::SystemBaseDirs; -use whitaker_installer::prebuilt_path::prebuilt_library_dir; -use whitaker_installer::test_support::TEST_STAGE_SUITE_ENV; -use whitaker_installer::toolchain::parse_toolchain_channel; +use whitaker_installer::{ + dirs::SystemBaseDirs, prebuilt_path::prebuilt_library_dir, test_support::TEST_STAGE_SUITE_ENV, + toolchain::parse_toolchain_channel, +}; #[derive(Default)] pub(super) struct CliWorld { @@ -23,50 +25,50 @@ pub(super) struct CliWorld { temp_dir: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] pub(super) fn cli_world() -> CliWorld { CliWorld::default() } pub(super) fn workspace_root() -> PathBuf { - PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("manifest dir should have parent") - .to_owned() + let manifest_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")); + let Some(parent) = manifest_dir.parent() else { + panic!("manifest dir should have parent"); + }; + parent.to_owned() } pub(super) fn pinned_toolchain_channel() -> String { let toolchain_path = workspace_root().join("rust-toolchain.toml"); - let contents = std::fs::read_to_string(&toolchain_path).unwrap_or_else(|err| { + let Ok(contents) = std::fs::read_to_string(&toolchain_path) else { panic!( - "failed to read rust-toolchain.toml at {}: {err}", + "rust-toolchain.toml at {} should be readable", toolchain_path.display() - ) - }); - parse_toolchain_channel(&contents).unwrap_or_else(|err| { + ); + }; + let Ok(channel) = parse_toolchain_channel(&contents) else { panic!( - "failed to parse rust-toolchain.toml at {}: {err}", + "rust-toolchain.toml at {} should declare a channel", toolchain_path.display() - ) - }) + ); + }; + channel } pub(super) fn is_toolchain_installed(channel: &str) -> bool { Command::new("rustup") .args(["run", channel, "rustc", "--version"]) .output() - .map(|output| output.status.success()) - .unwrap_or(false) + .is_ok_and(|output| output.status.success()) } fn skip_scenario_when_toolchain_missing(cli_world: &CliWorld, channel: &str) { if !is_toolchain_installed(channel) { - eprintln!( - "Skipping scenario because rustup toolchain '{channel}' is not installed. Install this toolchain to run these tests." - ); cli_world.skip_assertions.set(true); rstest_bdd::skip!( - "rustup toolchain '{channel}' is not installed. Install this toolchain to run these tests.", + "rustup toolchain '{channel}' is not installed. Install this toolchain to run these \ + tests.", channel = channel ); } @@ -87,7 +89,9 @@ pub(super) fn ensure_required_toolchain_available(cli_world: &CliWorld) -> Optio } pub(super) fn setup_temp_dir(cli_world: &CliWorld) -> String { - let temp_dir = TempDir::new().expect("failed to create temp dir"); + let Ok(temp_dir) = TempDir::new() else { + panic!("temporary directory should be created"); + }; let target_dir = temp_dir.path().to_string_lossy().to_string(); cli_world.temp_dir.replace(Some(temp_dir)); target_dir @@ -111,7 +115,7 @@ fn expected_prebuilt_target_dir(toolchain: &str) -> Option { let host_target = detect_host_target()?; prebuilt_library_dir(&dirs, toolchain, &host_target) .ok() - .map(|path| path.into_string()) + .map(camino::Utf8PathBuf::into_string) } fn matching_files(dir: &Path, substring: &str) -> Vec { @@ -122,7 +126,7 @@ fn matching_files(dir: &Path, substring: &str) -> Vec { }; entries .map(|entry| match entry { - Ok(entry) => entry.file_name().to_string_lossy().to_string(), + Ok(dir_entry) => dir_entry.file_name().to_string_lossy().to_string(), Err(error) => panic!("failed to read entry in {}: {error}", dir.display()), }) .filter(|name| name.contains(substring)) @@ -206,180 +210,28 @@ pub(super) fn run_installer_cli(cli_world: &CliWorld) { command.env(TEST_STAGE_SUITE_ENV, "1"); } - let output = command.output().expect("failed to run whitaker-installer"); + let Ok(output) = command.output() else { + panic!("whitaker-installer should run"); + }; cli_world.output.replace(Some(output)); } pub(super) fn get_output(cli_world: &CliWorld) -> Ref<'_, Output> { - let output = cli_world.output.borrow(); - Ref::map(output, |opt| opt.as_ref().expect("output not set")) -} - -fn assert_exit_status(cli_world: &CliWorld, expected_success: bool) { - if cli_world.skip_assertions.get() { - return; - } - - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); - assert_eq!( - output.status.success(), - expected_success, - "expected success={expected_success}, stdout={}, stderr={stderr}", - String::from_utf8_lossy(&output.stdout), - ); -} - -pub(super) fn assert_cli_exits_successfully(cli_world: &CliWorld) { - assert_exit_status(cli_world, true); -} - -pub(super) fn assert_dry_run_output_is_shown(cli_world: &CliWorld) { - if cli_world.skip_assertions.get() { - return; - } - - let toolchain = cli_world.toolchain.borrow(); - let toolchain = toolchain.as_ref().expect("toolchain not set"); - - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!(stderr.contains("Dry run - no files will be modified")); - assert!(stderr.contains(&format!("Toolchain: {toolchain}"))); - assert!(stderr.contains("Crates to build:")); - assert!(stderr.contains("whitaker_suite")); - assert!( - !stderr.contains("module_max_lines"), - "individual lint crate should not appear in suite-only mode, stderr: {stderr}" - ); - - let temp_dir = cli_world.temp_dir.borrow(); - let temp_dir = temp_dir.as_ref().expect("temp dir not set"); - let target_dir = temp_dir.path().to_string_lossy(); - let expected_target_dir = - expected_prebuilt_target_dir(toolchain).unwrap_or_else(|| target_dir.into_owned()); - assert!(stderr.contains(&format!("Target directory: {expected_target_dir}"))); -} - -pub(super) fn assert_cli_exits_with_error(cli_world: &CliWorld) { - assert_exit_status(cli_world, false); -} - -pub(super) fn assert_unknown_lint_message_is_shown(cli_world: &CliWorld) { - if cli_world.skip_assertions.get() { - return; - } - - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - !stderr.contains("Dry run - no files will be modified"), - "dry-run configuration output should not be printed on unknown-lint error, stderr: {stderr}" - ); - assert!( - !stderr.contains("Crates to build:"), - "dry-run configuration output should not be printed on unknown-lint error, stderr: {stderr}" - ); - assert!( - stderr.contains("lint crate nonexistent_lint not found"), - "unexpected stderr: {stderr}" - ); -} - -pub(super) fn assert_experimental_lint_opt_in_message_is_shown(cli_world: &CliWorld) { - if cli_world.skip_assertions.get() { - return; - } - - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - !stderr.contains("Dry run - no files will be modified"), - "dry-run configuration output should not be printed on experimental-lint error, stderr: {stderr}" - ); - assert!( - !stderr.contains("Crates to build:"), - "dry-run configuration output should not be printed on experimental-lint error, stderr: {stderr}" - ); - assert!( - stderr.contains( - "experimental lint crate rstest_helper_should_be_fixture requires --experimental" - ), - "unexpected stderr: {stderr}" - ); -} - -pub(super) fn assert_experimental_lint_dry_run_output_is_shown(cli_world: &CliWorld) { - if cli_world.skip_assertions.get() { - return; - } - - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!(stderr.contains("Dry run - no files will be modified")); - assert!(stderr.contains("Crates to build:")); - assert!(stderr.contains("rstest_helper_should_be_fixture")); - assert!( - !stderr.contains( - "experimental lint crate rstest_helper_should_be_fixture requires --experimental" - ), - "experimental opt-in error should not be printed when --experimental is set, stderr: {stderr}" - ); -} - -pub(super) fn assert_installation_succeeds_or_is_skipped(cli_world: &CliWorld) { - if cli_world.skip_assertions.get() { - return; - } - - let output = get_output(cli_world); - assert!( - output.status.success(), - "installation failed: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output_slot = cli_world.output.borrow(); + Ref::map(output_slot, |opt| { + let Some(output) = opt.as_ref() else { + panic!("output not set"); + }; + output + }) } -pub(super) fn assert_suite_library_is_staged(cli_world: &CliWorld) { - if cli_world.skip_assertions.get() { - return; - } - - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); - let channel = cli_world.toolchain.borrow(); - let channel = channel.as_ref().expect("toolchain not set"); - let needle = format!("whitaker_suite@{channel}"); - - if stderr.contains(PREBUILT_INSTALL_MARKER) - && let Some(dir) = expected_prebuilt_target_dir(channel) - { - let prebuilt_path = PathBuf::from(&dir); - let matches = matching_files(&prebuilt_path, &needle); - assert!( - !matches.is_empty(), - "prebuilt marker found in stderr but no library matching \ - '{needle}' in {prebuilt_path:?}, entries={:?}", - matching_files(&prebuilt_path, ""), - ); - return; - } - - let temp_dir = cli_world.temp_dir.borrow(); - let temp_dir = temp_dir.as_ref().expect("temp dir not set"); - let staging_dir = temp_dir.path().join(channel).join("release"); - let matches = matching_files(&staging_dir, &needle); +#[path = "support_assertions.rs"] +mod assertions; - assert!( - matches.len() == 1, - "expected exactly one suite library matching '{needle}' in \ - {staging_dir:?}, matches={matches:?}, entries={:?}, \ - stdout={}, stderr={stderr}", - matching_files(&staging_dir, ""), - String::from_utf8_lossy(&output.stdout), - ); -} +pub(super) use assertions::{ + assert_cli_exits_successfully, assert_cli_exits_with_error, assert_dry_run_output_is_shown, + assert_experimental_lint_dry_run_output_is_shown, + assert_experimental_lint_opt_in_message_is_shown, assert_installation_succeeds_or_is_skipped, + assert_suite_library_is_staged, assert_unknown_lint_message_is_shown, +}; diff --git a/installer/tests/behaviour_cli/support_assertions.rs b/installer/tests/behaviour_cli/support_assertions.rs new file mode 100644 index 000000000..e5eb895d7 --- /dev/null +++ b/installer/tests/behaviour_cli/support_assertions.rs @@ -0,0 +1,203 @@ +//! Assertions over the installer CLI's observed output for behaviour tests. + +use std::path::PathBuf; + +use super::{CliWorld, expected_prebuilt_target_dir, get_output, matching_files}; +use crate::prebuilt_markers::PREBUILT_INSTALL_MARKER; + +fn assert_exit_status(cli_world: &CliWorld, expected_success: bool) { + if cli_world.skip_assertions.get() { + return; + } + + let output = get_output(cli_world); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_eq!( + output.status.success(), + expected_success, + "expected success={expected_success}, stdout={}, stderr={stderr}", + String::from_utf8_lossy(&output.stdout), + ); +} + +fn assert_error_output_is_shown(cli_world: &CliWorld, error_kind: &str, expected_error: &str) { + if cli_world.skip_assertions.get() { + return; + } + + let output = get_output(cli_world); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + !stderr.contains("Dry run - no files will be modified"), + "dry-run configuration output should not be printed on {error_kind} error, stderr: \ + {stderr}" + ); + assert!( + !stderr.contains("Crates to build:"), + "dry-run configuration output should not be printed on {error_kind} error, stderr: \ + {stderr}" + ); + assert!( + stderr.contains(expected_error), + "unexpected stderr: {stderr}" + ); +} + +pub(crate) fn assert_cli_exits_successfully(cli_world: &CliWorld) { + assert_exit_status(cli_world, true); +} + +pub(crate) fn assert_dry_run_output_is_shown(cli_world: &CliWorld) { + if cli_world.skip_assertions.get() { + return; + } + + let toolchain_slot = cli_world.toolchain.borrow(); + let Some(toolchain) = toolchain_slot.as_ref() else { + panic!("toolchain not set"); + }; + + let output = get_output(cli_world); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + stderr.contains("Dry run - no files will be modified"), + "expected dry-run banner in stderr: {stderr}" + ); + assert!( + stderr.contains(&format!("Toolchain: {toolchain}")), + "expected toolchain line in stderr: {stderr}" + ); + assert!( + stderr.contains("Crates to build:"), + "expected crate list in stderr: {stderr}" + ); + assert!( + stderr.contains("whitaker_suite"), + "expected whitaker_suite in stderr: {stderr}" + ); + assert!( + !stderr.contains("module_max_lines"), + "individual lint crate should not appear in suite-only mode, stderr: {stderr}" + ); + + let temp_dir_slot = cli_world.temp_dir.borrow(); + let Some(temp_dir) = temp_dir_slot.as_ref() else { + panic!("temp dir not set"); + }; + let target_dir = temp_dir.path().to_string_lossy(); + let expected_target_dir = + expected_prebuilt_target_dir(toolchain).unwrap_or_else(|| target_dir.into_owned()); + assert!( + stderr.contains(&format!("Target directory: {expected_target_dir}")), + "expected target directory line in stderr: {stderr}" + ); +} + +pub(crate) fn assert_cli_exits_with_error(cli_world: &CliWorld) { + assert_exit_status(cli_world, false); +} + +pub(crate) fn assert_unknown_lint_message_is_shown(cli_world: &CliWorld) { + assert_error_output_is_shown( + cli_world, + "unknown-lint", + "lint crate nonexistent_lint not found", + ); +} + +pub(crate) fn assert_experimental_lint_opt_in_message_is_shown(cli_world: &CliWorld) { + assert_error_output_is_shown( + cli_world, + "experimental-lint", + "experimental lint crate rstest_helper_should_be_fixture requires --experimental", + ); +} + +pub(crate) fn assert_experimental_lint_dry_run_output_is_shown(cli_world: &CliWorld) { + if cli_world.skip_assertions.get() { + return; + } + + let output = get_output(cli_world); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + stderr.contains("Dry run - no files will be modified"), + "expected dry-run banner in stderr: {stderr}" + ); + assert!( + stderr.contains("Crates to build:"), + "expected crate list in stderr: {stderr}" + ); + assert!( + stderr.contains("rstest_helper_should_be_fixture"), + "expected experimental lint crate in stderr: {stderr}" + ); + assert!( + !stderr.contains( + "experimental lint crate rstest_helper_should_be_fixture requires --experimental" + ), + "experimental opt-in error should not be printed when --experimental is set, stderr: \ + {stderr}" + ); +} + +pub(crate) fn assert_installation_succeeds_or_is_skipped(cli_world: &CliWorld) { + if cli_world.skip_assertions.get() { + return; + } + + let output = get_output(cli_world); + assert!( + output.status.success(), + "installation failed: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +pub(crate) fn assert_suite_library_is_staged(cli_world: &CliWorld) { + if cli_world.skip_assertions.get() { + return; + } + + let output = get_output(cli_world); + let stderr = String::from_utf8_lossy(&output.stderr); + let channel_slot = cli_world.toolchain.borrow(); + let Some(channel) = channel_slot.as_ref() else { + panic!("toolchain not set"); + }; + let needle = format!("whitaker_suite@{channel}"); + + if stderr.contains(PREBUILT_INSTALL_MARKER) + && let Some(dir) = expected_prebuilt_target_dir(channel) + { + let prebuilt_path = PathBuf::from(&dir); + let matches = matching_files(&prebuilt_path, &needle); + assert!( + !matches.is_empty(), + "prebuilt marker found in stderr but no library matching '{needle}' in {}, \ + entries={:?}", + prebuilt_path.display(), + matching_files(&prebuilt_path, ""), + ); + return; + } + + let temp_dir_slot = cli_world.temp_dir.borrow(); + let Some(temp_dir) = temp_dir_slot.as_ref() else { + panic!("temp dir not set"); + }; + let staging_dir = temp_dir.path().join(channel).join("release"); + let matches = matching_files(&staging_dir, &needle); + + assert!( + matches.len() == 1, + "expected exactly one suite library matching '{needle}' in {}, matches={matches:?}, \ + entries={:?}, stdout={}, stderr={stderr}", + staging_dir.display(), + matching_files(&staging_dir, ""), + String::from_utf8_lossy(&output.stdout), + ); +} diff --git a/installer/tests/behaviour_core.rs b/installer/tests/behaviour_core.rs index 40f24f403..d1feef1c3 100644 --- a/installer/tests/behaviour_core.rs +++ b/installer/tests/behaviour_core.rs @@ -3,17 +3,20 @@ //! These scenarios validate crate resolution, crate name validation, toolchain //! parsing, and shell snippet generation using rstest-bdd. +use std::cell::{Cell, RefCell}; + use camino::Utf8PathBuf; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; -use whitaker_installer::crate_name::CrateName; -use whitaker_installer::output::ShellSnippet; -use whitaker_installer::resolution::{ - CrateResolutionOptions, EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE, resolve_crates, - validate_crate_names, +use whitaker_installer::{ + crate_name::CrateName, + output::ShellSnippet, + resolution::{ + CrateResolutionOptions, EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE, resolve_crates, + validate_crate_names, + }, + toolchain::parse_toolchain_channel, }; -use whitaker_installer::toolchain::parse_toolchain_channel; // --------------------------------------------------------------------------- // Crate resolution world @@ -27,6 +30,7 @@ struct CrateResolutionWorld { resolved: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn crate_world() -> CrateResolutionWorld { CrateResolutionWorld::default() @@ -134,6 +138,7 @@ struct ValidationWorld { error_message: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn validation_world() -> ValidationWorld { ValidationWorld::default() @@ -197,8 +202,10 @@ fn then_validation_fails(validation_world: &ValidationWorld) { #[then("validation fails with an experimental opt-in error")] fn then_validation_fails_experimental_opt_in(validation_world: &ValidationWorld) { assert_eq!(validation_world.result.get(), Some(false)); - let error_message = validation_world.error_message.borrow(); - let error_message = error_message.as_ref().expect("error message should be set"); + let error_slot = validation_world.error_message.borrow(); + let Some(error_message) = error_slot.as_ref() else { + panic!("error message should be set"); + }; assert!( error_message.contains( "experimental lint crate rstest_helper_should_be_fixture requires --experimental" @@ -218,6 +225,7 @@ struct ToolchainWorld { error: Cell, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn toolchain_world() -> ToolchainWorld { ToolchainWorld::default() @@ -286,6 +294,7 @@ struct SnippetWorld { snippet: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn snippet_world() -> SnippetWorld { SnippetWorld::default() @@ -306,25 +315,39 @@ fn when_snippets_generated(snippet_world: &SnippetWorld) { snippet_world.snippet.replace(Some(snippet)); } -#[then("bash snippet uses export syntax")] -fn then_bash_export(snippet_world: &SnippetWorld) { +/// Check a shell snippet field against an expected prefix. +fn ensure_snippet_prefix( + snippet_world: &SnippetWorld, + field: impl FnOnce(&ShellSnippet) -> &str, + prefix: &str, +) -> Result<(), String> { let snippet = snippet_world.snippet.borrow(); - let s = snippet.as_ref().expect("snippet should exist"); - assert!(s.bash.starts_with("export ")); + let s = snippet + .as_ref() + .ok_or_else(|| String::from("snippet should exist"))?; + let value = field(s); + if value.starts_with(prefix) { + Ok(()) + } else { + Err(format!( + "expected snippet to start with '{prefix}': {value}" + )) + } +} + +#[then("bash snippet uses export syntax")] +fn then_bash_export(snippet_world: &SnippetWorld) -> Result<(), String> { + ensure_snippet_prefix(snippet_world, |s| &s.bash, "export ") } #[then("fish snippet uses set -gx syntax")] -fn then_fish_set(snippet_world: &SnippetWorld) { - let snippet = snippet_world.snippet.borrow(); - let s = snippet.as_ref().expect("snippet should exist"); - assert!(s.fish.starts_with("set -gx ")); +fn then_fish_set(snippet_world: &SnippetWorld) -> Result<(), String> { + ensure_snippet_prefix(snippet_world, |s| &s.fish, "set -gx ") } #[then("PowerShell snippet uses $env syntax")] -fn then_powershell_env(snippet_world: &SnippetWorld) { - let snippet = snippet_world.snippet.borrow(); - let s = snippet.as_ref().expect("snippet should exist"); - assert!(s.powershell.starts_with("$env:")); +fn then_powershell_env(snippet_world: &SnippetWorld) -> Result<(), String> { + ensure_snippet_prefix(snippet_world, |s| &s.powershell, "$env:") } // --------------------------------------------------------------------------- diff --git a/installer/tests/behaviour_dependency_binaries.rs b/installer/tests/behaviour_dependency_binaries.rs index e9281be5d..e0cc9219a 100644 --- a/installer/tests/behaviour_dependency_binaries.rs +++ b/installer/tests/behaviour_dependency_binaries.rs @@ -1,34 +1,48 @@ //! Behaviour tests for dependency-binary installation and provenance output. +use std::path::{Path, PathBuf}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::path::Path; -use std::path::PathBuf; use temp_env::with_var; -use whitaker_installer::dependency_binaries::{ - DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller, - required_dependency_binaries, -}; -use whitaker_installer::dependency_packaging::render_provenance_markdown; -use whitaker_installer::deps::{ - DependencyInstallOptions, DylintToolStatus, install_dylint_tools_with_options, -}; -use whitaker_installer::dirs::BaseDirs; -use whitaker_installer::installer_packaging::TargetTriple; -use whitaker_installer::test_support::env_test_guard; -use whitaker_installer::test_utils::{ - StubDirs, StubExecutor, - dependency_binary_helpers::{ - ExpectedCallConfig, expected_calls, path_binary_location, write_fake_binary, +use whitaker_installer::{ + dependency_binaries::{ + DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller, + required_dependency_binaries, + }, + dependency_packaging::render_provenance_markdown, + deps::{DependencyInstallOptions, DylintToolStatus, install_dylint_tools_with_options}, + dirs::BaseDirs, + installer_packaging::TargetTriple, + test_support::env_test_guard, + test_utils::{ + StubDirs, StubExecutor, + dependency_binary_helpers::{ + ExpectedCallConfig, RepositoryVerification, expected_calls, path_binary_location, + write_fake_binary, + }, }, }; +/// Outcome the stubbed repository installer should simulate. enum RepositoryInstallerBehaviour { + /// Installation succeeds and the staged binary verifies. Success, + /// Installation succeeds but verification of the staged binary fails. + SuccessWithFailedVerification, + /// The release asset is absent from the repository. NotFound, + /// Installation fails with the given message. Failure(String), } +impl RepositoryInstallerBehaviour { + /// Whether the stub stages a runnable binary for this behaviour. + const fn installs_binary(&self) -> bool { + matches!(*self, Self::Success | Self::SuccessWithFailedVerification) + } +} + struct StubRepositoryInstaller { behaviour: RepositoryInstallerBehaviour, } @@ -41,20 +55,23 @@ impl DependencyBinaryInstaller for StubRepositoryInstaller { dirs: &dyn BaseDirs, ) -> std::result::Result { match &self.behaviour { - RepositoryInstallerBehaviour::Success => dirs.bin_dir().map_or_else( - || Err(DependencyBinaryInstallError::MissingBinDir), - |bin_dir| { - // Stage a runnable fake at the returned path: dylint-link - // verification probes the extracted binary directly. The - // platform suffix keeps the fake executable on Windows. - let installed_path = path_binary_location( - &bin_dir, - &format!("{}-{}", dependency.package(), target), - ); - write_fake_binary(&installed_path, true); - Ok(installed_path) - }, - ), + RepositoryInstallerBehaviour::Success + | RepositoryInstallerBehaviour::SuccessWithFailedVerification => { + dirs.bin_dir().map_or_else( + || Err(DependencyBinaryInstallError::MissingBinDir), + |bin_dir| { + // Stage a runnable fake at the returned path: dylint-link + // verification probes the extracted binary directly. The + // platform suffix keeps the fake executable on Windows. + let installed_path = path_binary_location( + &bin_dir, + &format!("{}-{}", dependency.package(), target), + ); + write_fake_binary(&installed_path, true); + Ok(installed_path) + }, + ) + } RepositoryInstallerBehaviour::NotFound => Err(DependencyBinaryInstallError::NotFound { url: format!( "{}/releases/download/v{}/{}", @@ -77,7 +94,6 @@ impl DependencyBinaryInstaller for StubRepositoryInstaller { struct DependencyBinaryWorld { missing_tool: Option, repository_behaviour: Option, - should_repository_verification_fail: bool, expect_missing_dylint_link: bool, is_binstall_available: bool, cargo_binstall_failure: Option, @@ -89,6 +105,7 @@ struct DependencyBinaryWorld { dependencies: Vec, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> DependencyBinaryWorld { DependencyBinaryWorld::default() @@ -115,8 +132,7 @@ fn given_repository_failure(world: &mut DependencyBinaryWorld, message: String) #[given("the repository installer succeeds but verification fails")] fn given_repository_verification_failure(world: &mut DependencyBinaryWorld) { - world.repository_behaviour = Some(RepositoryInstallerBehaviour::Success); - world.should_repository_verification_fail = true; + world.repository_behaviour = Some(RepositoryInstallerBehaviour::SuccessWithFailedVerification); } #[given("dylint-link is missing from PATH after installation")] @@ -150,10 +166,11 @@ fn given_unsupported_target(world: &mut DependencyBinaryWorld) { } #[given("the dependency manifest is loaded")] -fn given_manifest_loaded(world: &mut DependencyBinaryWorld) { +fn given_manifest_loaded(world: &mut DependencyBinaryWorld) -> Result<(), String> { world.dependencies = required_dependency_binaries() - .expect("dependency manifest should load") + .map_err(|error| format!("dependency manifest should load: {error}"))? .to_vec(); + Ok(()) } fn build_stub_executor(world: &DependencyBinaryWorld, tool: &str) -> StubExecutor { @@ -161,51 +178,63 @@ fn build_stub_executor(world: &DependencyBinaryWorld, tool: &str) -> StubExecuto world.repository_behaviour, Some(RepositoryInstallerBehaviour::NotFound) ); - let expect_repository_verification = matches!( + let expect_repository_verification = world + .repository_behaviour + .as_ref() + .is_some_and(RepositoryInstallerBehaviour::installs_binary) + && !world.is_unsupported_target; + let should_verification_fail = matches!( world.repository_behaviour, - Some(RepositoryInstallerBehaviour::Success) - ) && !world.is_unsupported_target; + Some(RepositoryInstallerBehaviour::SuccessWithFailedVerification) + ); + let repository_verification = match (expect_repository_verification, should_verification_fail) { + (false, _) => RepositoryVerification::Skip, + (true, true) => RepositoryVerification::Fails, + (true, false) => RepositoryVerification::Succeeds, + }; StubExecutor::new(expected_calls( tool, - ExpectedCallConfig { + &ExpectedCallConfig { is_binstall_available: world.is_binstall_available, has_repository_context: !world.is_unsupported_target, is_repository_asset_missing, - should_verify_repository_install: expect_repository_verification, - is_repository_verification_failing: world.should_repository_verification_fail, + repository_verification, cargo_binstall_failure: world.cargo_binstall_failure.as_deref(), cargo_install_failure: world.cargo_install_failure.as_deref(), }, )) } +/// Stages a runnable `dylint-link` fake in the executables directory so PATH +/// lookups succeed. +fn stage_dylint_link_on_path(bin_dir: &Path) -> Result<(), String> { + #[cfg(windows)] + let dylint_link_path = bin_dir.join("dylint-link.cmd"); + #[cfg(not(windows))] + let dylint_link_path = bin_dir.join("dylint-link"); + write_fake_binary(&dylint_link_path, true); + Ok(()) +} + fn run_install_with_dylint_link_on_path( - expect_missing_dylint_link: bool, bin_dir: &Path, run_install: impl FnOnce() -> std::result::Result<(), whitaker_installer::error::InstallerError>, ) -> std::result::Result<(), whitaker_installer::error::InstallerError> { let _guard = env_test_guard(); - if !expect_missing_dylint_link { - #[cfg(windows)] - let dylint_link_path = bin_dir.join("dylint-link.cmd"); - #[cfg(not(windows))] - let dylint_link_path = bin_dir.join("dylint-link"); - write_fake_binary(&dylint_link_path, true); - } with_var("PATH", Some(bin_dir), run_install) } #[when("dependency installation runs")] -fn when_dependency_installation_runs(world: &mut DependencyBinaryWorld) { +fn when_dependency_installation_runs(world: &mut DependencyBinaryWorld) -> Result<(), String> { let tool = world .missing_tool .clone() - .expect("missing tool should be configured"); + .ok_or_else(|| String::from("missing tool should be configured"))?; let executor = build_stub_executor(world, &tool); let repository_installer = StubRepositoryInstaller { - behaviour: world.repository_behaviour.take().unwrap_or( - RepositoryInstallerBehaviour::Failure("missing repository".to_owned()), - ), + behaviour: world.repository_behaviour.take().unwrap_or_else(|| { + RepositoryInstallerBehaviour::Failure("missing repository".to_owned()) + }), }; let status = DylintToolStatus { cargo_dylint: tool != "cargo-dylint", @@ -215,13 +244,21 @@ fn when_dependency_installation_runs(world: &mut DependencyBinaryWorld) { let target = if world.is_unsupported_target { None } else { - Some(TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target")) + Some( + TargetTriple::try_from("x86_64-unknown-linux-gnu") + .map_err(|error| format!("valid target: {error}"))?, + ) }; - let bin_dir_temp = tempfile::tempdir().expect("bin dir tempdir should be created"); + let bin_dir_temp = tempfile::tempdir() + .map_err(|error| format!("bin dir tempdir should be created: {error}"))?; let bin_dir = bin_dir_temp.path().to_path_buf(); let dirs = StubDirs { bin_dir: Some(bin_dir.clone()), }; + let is_dylint_link = tool == "dylint-link"; + if is_dylint_link && !world.expect_missing_dylint_link { + stage_dylint_link_on_path(&bin_dir)?; + } let run_install = || { install_dylint_tools_with_options( &executor, @@ -235,16 +272,13 @@ fn when_dependency_installation_runs(world: &mut DependencyBinaryWorld) { }, ) }; - world.install_result = Some(if tool == "dylint-link" { - run_install_with_dylint_link_on_path( - world.expect_missing_dylint_link, - &bin_dir, - run_install, - ) + world.install_result = Some(if is_dylint_link { + run_install_with_dylint_link_on_path(&bin_dir, run_install) } else { run_install() }); executor.assert_finished(); + Ok(()) } #[when("provenance markdown is rendered")] @@ -252,22 +286,36 @@ fn when_provenance_markdown_rendered(world: &mut DependencyBinaryWorld) { world.provenance = Some(render_provenance_markdown(&world.dependencies)); } -#[then("the install succeeds")] -fn then_install_succeeds(world: &mut DependencyBinaryWorld) { - let result = world +/// Borrows the recorded install outcome, failing when the When step has not run. +fn install_result( + world: &DependencyBinaryWorld, +) -> Result<&std::result::Result<(), whitaker_installer::error::InstallerError>, String> { + world .install_result .as_ref() - .expect("install result should exist"); - assert!(result.is_ok(), "expected success, got {result:?}"); + .ok_or_else(|| String::from("install result should exist")) +} + +#[then("the install succeeds")] +fn then_install_succeeds(world: &mut DependencyBinaryWorld) -> Result<(), String> { + let result = install_result(world)?; + match result { + Ok(()) => Ok(()), + Err(error) => Err(format!("expected success, got {error:?}")), + } } #[then("stderr contains \"{expected}\"")] -fn then_stderr_contains(world: &mut DependencyBinaryWorld, expected: String) { - let stderr = String::from_utf8(world.stderr.clone()).expect("stderr should be UTF-8"); - assert!( - stderr.contains(&expected), - "expected stderr to contain {expected:?}, got {stderr:?}" - ); +fn then_stderr_contains(world: &mut DependencyBinaryWorld, expected: String) -> Result<(), String> { + let stderr = String::from_utf8(world.stderr.clone()) + .map_err(|error| format!("stderr should be UTF-8: {error}"))?; + if stderr.contains(&expected) { + Ok(()) + } else { + Err(format!( + "expected stderr to contain {expected:?}, got {stderr:?}" + )) + } } #[then("the install fails for \"{tool}\" with message containing \"{expected}\"")] @@ -275,36 +323,45 @@ fn then_install_fails_with_message( world: &mut DependencyBinaryWorld, tool: String, expected: String, -) { - let result = world - .install_result - .as_ref() - .expect("install result should exist"); - match result { - Err(whitaker_installer::error::InstallerError::DependencyInstall { - tool: actual_tool, - message, - }) => { - assert_eq!(actual_tool, &tool); - assert!( - message.contains(&expected), - "expected error message to contain {expected:?}, got {message:?}" - ); - } - other => panic!("expected dependency install error, got {other:?}"), +) -> Result<(), String> { + let result = install_result(world)?; + let Err(whitaker_installer::error::InstallerError::DependencyInstall { + tool: actual_tool, + message, + }) = result + else { + return Err(format!("expected dependency install error, got {result:?}")); + }; + if actual_tool != &tool { + return Err(format!( + "expected failure for {tool:?}, got {actual_tool:?}" + )); + } + if message.contains(&expected) { + Ok(()) + } else { + Err(format!( + "expected error message to contain {expected:?}, got {message:?}" + )) } } #[then("the provenance contains \"{expected}\"")] -fn then_provenance_contains(world: &mut DependencyBinaryWorld, expected: String) { +fn then_provenance_contains( + world: &mut DependencyBinaryWorld, + expected: String, +) -> Result<(), String> { let provenance = world .provenance .as_ref() - .expect("provenance should have been rendered"); - assert!( - provenance.contains(&expected), - "expected provenance to contain {expected:?}, got {provenance:?}" - ); + .ok_or_else(|| String::from("provenance should have been rendered"))?; + if provenance.contains(&expected) { + Ok(()) + } else { + Err(format!( + "expected provenance to contain {expected:?}, got {provenance:?}" + )) + } } #[scenario(path = "tests/features/dependency_binaries.feature", index = 0)] diff --git a/installer/tests/behaviour_docs.rs b/installer/tests/behaviour_docs.rs index d5e86c313..838dee9ea 100644 --- a/installer/tests/behaviour_docs.rs +++ b/installer/tests/behaviour_docs.rs @@ -6,10 +6,11 @@ mod doc_extraction; +use std::cell::{Ref, RefCell}; + use doc_extraction::extraction::{DOC_TOML_BLOCKS, find_block_containing}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; use toml::Table; // --------------------------------------------------------------------------- @@ -24,6 +25,7 @@ struct TomlWorld { error: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn toml_world() -> TomlWorld { TomlWorld::default() @@ -46,7 +48,7 @@ fn given_suite_only_metadata(toml_world: &TomlWorld) { } #[given("a workspace metadata example for individual crates")] -fn given_individual_crates_metadata(toml_world: &TomlWorld) { +fn given_individual_crates_metadata(toml_world: &TomlWorld) -> Result<(), String> { // Matches the individual crates example showing explicit lint patterns let block = DOC_TOML_BLOCKS .iter() @@ -55,9 +57,10 @@ fn given_individual_crates_metadata(toml_world: &TomlWorld) { && !b.contains("tag =") && !b.contains("rev =") }) - .expect("no individual crates TOML block found") + .ok_or_else(|| String::from("no individual crates TOML block found"))? .clone(); set_toml_content(toml_world, &block); + Ok(()) } #[given("a workspace metadata example with tag pinning")] @@ -111,8 +114,14 @@ fn when_toml_parsed(toml_world: &TomlWorld) { // Helper functions for TOML navigation // --------------------------------------------------------------------------- -/// Get a reference to the first library entry in workspace.metadata.dylint.libraries. -fn get_first_library(table: &Table) -> &toml::Value { +/// Borrows the parsed TOML table, failing when the When step has not run. +fn parsed_table(toml_world: &TomlWorld) -> Result, String> { + Ref::filter_map(toml_world.parsed.borrow(), Option::as_ref) + .map_err(|_| String::from("expected parsed TOML")) +} + +/// Get a reference to the first library entry in `workspace.metadata.dylint.libraries`. +fn get_first_library(table: &Table) -> Result<&toml::Value, String> { table .get("workspace") .and_then(|w| w.get("metadata")) @@ -120,24 +129,37 @@ fn get_first_library(table: &Table) -> &toml::Value { .and_then(|d| d.get("libraries")) .and_then(|l| l.as_array()) .and_then(|arr| arr.first()) - .expect("expected workspace.metadata.dylint.libraries[0]") + .ok_or_else(|| String::from("expected workspace.metadata.dylint.libraries[0]")) } /// Get a string field from the first library entry. -fn get_library_string_field<'a>(table: &'a Table, field: &str) -> &'a str { - get_first_library(table) +fn get_library_string_field<'a>(table: &'a Table, field: &str) -> Result<&'a str, String> { + get_first_library(table)? .get(field) .and_then(|v| v.as_str()) - .unwrap_or_else(|| panic!("expected libraries[0].{field} to be a string")) + .ok_or_else(|| format!("expected libraries[0].{field} to be a string")) } /// Get an integer configuration value from a nested table. -fn get_config_integer(table: &Table, section: &str, key: &str) -> i64 { +fn get_config_integer(table: &Table, section: &str, key: &str) -> Result { table .get(section) .and_then(|s| s.get(key)) - .and_then(|v| v.as_integer()) - .unwrap_or_else(|| panic!("expected {section}.{key} to be an integer")) + .and_then(toml::Value::as_integer) + .ok_or_else(|| format!("expected {section}.{key} to be an integer")) +} + +/// Compare two values for equality, reporting a mismatch as an error. +fn ensure_eq(actual: &T, expected: &U, context: &str) -> Result<(), String> +where + T: PartialEq + std::fmt::Debug + ?Sized, + U: std::fmt::Debug + ?Sized, +{ + if actual == expected { + Ok(()) + } else { + Err(format!("{context}: expected {expected:?}, got {actual:?}")) + } } // --------------------------------------------------------------------------- @@ -145,147 +167,143 @@ fn get_config_integer(table: &Table, section: &str, key: &str) -> i64 { // --------------------------------------------------------------------------- #[then("parsing succeeds")] -fn then_parsing_succeeds(toml_world: &TomlWorld) { +fn then_parsing_succeeds(toml_world: &TomlWorld) -> Result<(), String> { let error = toml_world.error.borrow(); - assert!( - error.is_none(), - "expected TOML to parse successfully, but got error: {:?}", - error - ); + error.as_ref().map_or(Ok(()), |message| { + Err(format!( + "expected TOML to parse successfully, but got error: {message}" + )) + }) } #[then("the libraries pattern is {expected}")] -fn then_libraries_pattern_is(toml_world: &TomlWorld, expected: String) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); - - let pattern = get_library_string_field(table, "pattern"); - - assert_eq!(pattern, expected); +fn then_libraries_pattern_is(toml_world: &TomlWorld, expected: String) -> Result<(), String> { + let table = parsed_table(toml_world)?; + let pattern = get_library_string_field(&table, "pattern")?; + ensure_eq(pattern, expected.as_str(), "libraries[0].pattern") } #[then("the libraries pattern starts with {prefix}")] -fn then_libraries_pattern_starts_with(toml_world: &TomlWorld, prefix: String) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); - - let pattern = get_library_string_field(table, "pattern"); - - assert!( - pattern.starts_with(&prefix), - "expected pattern to start with '{prefix}', got '{pattern}'" - ); +fn then_libraries_pattern_starts_with( + toml_world: &TomlWorld, + prefix: String, +) -> Result<(), String> { + let table = parsed_table(toml_world)?; + let pattern = get_library_string_field(&table, "pattern")?; + if pattern.starts_with(&prefix) { + Ok(()) + } else { + Err(format!( + "expected pattern to start with '{prefix}', got '{pattern}'" + )) + } } #[then("the tag field is present")] -fn then_tag_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); - - let tag = get_library_string_field(table, "tag"); - - assert_eq!(tag, "v0.1.0", "expected tag == \"v0.1.0\""); +fn then_tag_present(toml_world: &TomlWorld) -> Result<(), String> { + let table = parsed_table(toml_world)?; + let tag = get_library_string_field(&table, "tag")?; + ensure_eq(tag, "v0.1.0", "libraries[0].tag") } #[then("the revision field is present")] -fn then_revision_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); - - let rev = get_library_string_field(table, "rev"); - - assert_eq!(rev, "abc123def456", "expected rev == \"abc123def456\""); +fn then_revision_present(toml_world: &TomlWorld) -> Result<(), String> { + let table = parsed_table(toml_world)?; + let rev = get_library_string_field(&table, "rev")?; + ensure_eq(rev, "abc123def456", "libraries[0].rev") } #[then("the path field is present")] -fn then_path_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); - - let path = get_library_string_field(table, "path"); - - assert!( - path.contains("/whitaker/lints/") - && path.contains("/nightly-") - && path.contains("/x86_64-unknown-linux-gnu/lib"), - "expected path to contain prebuilt lints layout, got: {path}" - ); +fn then_path_present(toml_world: &TomlWorld) -> Result<(), String> { + let table = parsed_table(toml_world)?; + let path = get_library_string_field(&table, "path")?; + let has_prebuilt_layout = path.contains("/whitaker/lints/") + && path.contains("/nightly-") + && path.contains("/x86_64-unknown-linux-gnu/lib"); + if has_prebuilt_layout { + Ok(()) + } else { + Err(format!( + "expected path to contain prebuilt lints layout, got: {path}" + )) + } } #[then("module_max_lines configuration is present")] -fn then_module_max_lines_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); - - let max_lines = get_config_integer(table, "module_max_lines", "max_lines"); - - assert_eq!(max_lines, 500); +fn then_module_max_lines_present(toml_world: &TomlWorld) -> Result<(), String> { + let table = parsed_table(toml_world)?; + let max_lines = get_config_integer(&table, "module_max_lines", "max_lines")?; + ensure_eq(&max_lines, &500, "module_max_lines.max_lines") } #[then("conditional_max_n_branches configuration is present")] -fn then_conditional_max_branches_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); - - let max_branches = get_config_integer(table, "conditional_max_n_branches", "max_branches"); - - assert_eq!(max_branches, 3); +fn then_conditional_max_branches_present(toml_world: &TomlWorld) -> Result<(), String> { + let table = parsed_table(toml_world)?; + let max_branches = get_config_integer(&table, "conditional_max_n_branches", "max_branches")?; + ensure_eq(&max_branches, &3, "conditional_max_n_branches.max_branches") } #[then("no_expect_outside_tests additional_test_attributes configuration is present")] -fn then_no_expect_outside_tests_additional_test_attributes_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); +fn then_no_expect_outside_tests_additional_test_attributes_present( + toml_world: &TomlWorld, +) -> Result<(), String> { + let table = parsed_table(toml_world)?; let attributes = table .get("no_expect_outside_tests") .and_then(|t| t.get("additional_test_attributes")) .and_then(|a| a.as_array()) - .expect("expected no_expect_outside_tests.additional_test_attributes array"); + .ok_or_else(|| { + String::from("expected no_expect_outside_tests.additional_test_attributes array") + })?; - let values: Vec<_> = attributes + let values = attributes .iter() .map(|v| { - v.as_str() - .expect("expected additional_test_attributes entries to be strings") + v.as_str().ok_or_else(|| { + String::from("expected additional_test_attributes entries to be strings") + }) }) - .collect(); + .collect::, _>>()?; - assert_eq!( - values, - vec!["my_framework::test", "wasm_bindgen_test"], - "unexpected no_expect_outside_tests.additional_test_attributes" - ); + ensure_eq( + values.as_slice(), + ["my_framework::test", "wasm_bindgen_test"].as_slice(), + "no_expect_outside_tests.additional_test_attributes", + ) } #[then("no_unwrap_or_else_panic allow_in_main configuration is present")] -fn then_no_unwrap_or_else_panic_allow_in_main_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); +fn then_no_unwrap_or_else_panic_allow_in_main_present( + toml_world: &TomlWorld, +) -> Result<(), String> { + let table = parsed_table(toml_world)?; let allow_in_main = table .get("no_unwrap_or_else_panic") .and_then(|t| t.get("allow_in_main")) - .and_then(|v| v.as_bool()) - .expect("expected no_unwrap_or_else_panic.allow_in_main boolean"); - - assert!( - allow_in_main, - "expected no_unwrap_or_else_panic.allow_in_main to be true" - ); + .and_then(toml::Value::as_bool) + .ok_or_else(|| String::from("expected no_unwrap_or_else_panic.allow_in_main boolean"))?; + + if allow_in_main { + Ok(()) + } else { + Err(String::from( + "expected no_unwrap_or_else_panic.allow_in_main to be true", + )) + } } #[then("locale configuration is present")] -fn then_locale_configuration_present(toml_world: &TomlWorld) { - let parsed = toml_world.parsed.borrow(); - let table = parsed.as_ref().expect("expected parsed TOML"); +fn then_locale_configuration_present(toml_world: &TomlWorld) -> Result<(), String> { + let table = parsed_table(toml_world)?; let locale = table .get("locale") .and_then(|v| v.as_str()) - .expect("expected locale string"); + .ok_or_else(|| String::from("expected locale string"))?; - assert_eq!(locale, "cy", "expected locale == \"cy\""); + ensure_eq(locale, "cy", "locale") } // --------------------------------------------------------------------------- diff --git a/installer/tests/behaviour_install_metrics.rs b/installer/tests/behaviour_install_metrics.rs index 940dbd016..3749f88db 100644 --- a/installer/tests/behaviour_install_metrics.rs +++ b/installer/tests/behaviour_install_metrics.rs @@ -1,19 +1,21 @@ //! Behaviour tests for installer metrics recording. +use std::{path::PathBuf, time::Duration}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::path::PathBuf; -use std::time::Duration; use tempfile::TempDir; use whitaker_installer::install_metrics::{ InstallMetrics, InstallMode, RecordOutcome, record_install_at_path, }; +/// Tolerance for comparing floating-point install rates. const FLOAT_RATE_TOLERANCE: f64 = 1e-6; #[derive(Default)] struct InstallMetricsWorld { - _temp_dir: Option, + /// Owns the scenario's temporary metrics directory so it outlives the run. + temp_dir: Option, metrics_path: Option, outcome: Option, last_error: Option, @@ -21,13 +23,47 @@ struct InstallMetricsWorld { summary_line: Option, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> InstallMetricsWorld { InstallMetricsWorld::default() } -fn record_mode(world: &mut InstallMetricsWorld, mode: InstallMode, millis: u64) { - let path = world.metrics_path.as_deref().expect("metrics path set"); +/// Compare two values for equality, reporting a mismatch as an error. +fn ensure_eq(actual: &T, expected: &U, context: &str) -> Result<(), String> +where + T: PartialEq + std::fmt::Debug + ?Sized, + U: std::fmt::Debug + ?Sized, +{ + if actual == expected { + Ok(()) + } else { + Err(format!("{context}: expected {expected:?}, got {actual:?}")) + } +} + +/// Borrow the configured metrics path, failing when no Given step has run. +fn metrics_path(world: &InstallMetricsWorld) -> Result<&std::path::Path, String> { + world + .metrics_path + .as_deref() + .ok_or_else(|| String::from("metrics path set")) +} + +/// Borrow the aggregated metrics, failing when nothing has been recorded. +fn metrics(world: &InstallMetricsWorld) -> Result<&InstallMetrics, String> { + world + .in_memory_metrics + .as_ref() + .ok_or_else(|| String::from("metrics available")) +} + +fn record_mode( + world: &mut InstallMetricsWorld, + mode: InstallMode, + millis: u64, +) -> Result<(), String> { + let path = metrics_path(world)?; let result = record_install_at_path(path, mode, Duration::from_millis(millis)); match result { Ok(outcome) => { @@ -44,37 +80,42 @@ fn record_mode(world: &mut InstallMetricsWorld, mode: InstallMode, millis: u64) world.in_memory_metrics = None; } } + Ok(()) } #[given("an empty install metrics store")] -fn given_empty_store(world: &mut InstallMetricsWorld) { - let temp_dir = tempfile::tempdir().expect("create temp dir"); +fn given_empty_store(world: &mut InstallMetricsWorld) -> Result<(), String> { + let temp_dir = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?; world.metrics_path = Some(temp_dir.path().join("metrics").join("install_metrics.json")); - world._temp_dir = Some(temp_dir); + world.temp_dir = Some(temp_dir); world.outcome = None; world.last_error = None; world.in_memory_metrics = None; world.summary_line = None; + Ok(()) } #[given("a corrupt install metrics store")] -fn given_corrupt_store(world: &mut InstallMetricsWorld) { - given_empty_store(world); - let path = world.metrics_path.as_deref().expect("metrics path set"); - std::fs::create_dir_all(path.parent().expect("metrics parent exists")).expect("create parent"); - std::fs::write(path, "{not valid json").expect("write corrupt file"); +fn given_corrupt_store(world: &mut InstallMetricsWorld) -> Result<(), String> { + given_empty_store(world)?; + let path = metrics_path(world)?; + let parent = path + .parent() + .ok_or_else(|| String::from("metrics parent exists"))?; + std::fs::create_dir_all(parent).map_err(|error| format!("create parent: {error}"))?; + std::fs::write(path, "{not valid json").map_err(|error| format!("write corrupt file: {error}")) } #[given("a blocked install metrics path")] -fn given_blocked_path(world: &mut InstallMetricsWorld) { - given_empty_store(world); - let path = world.metrics_path.as_deref().expect("metrics path set"); - std::fs::create_dir_all(path).expect("create blocking directory"); +fn given_blocked_path(world: &mut InstallMetricsWorld) -> Result<(), String> { + given_empty_store(world)?; + let path = metrics_path(world)?; + std::fs::create_dir_all(path).map_err(|error| format!("create blocking directory: {error}")) } #[given("a download install of {millis:u64} milliseconds is recorded")] -fn given_download_recorded(world: &mut InstallMetricsWorld, millis: u64) { - record_mode(world, InstallMode::Download, millis); +fn given_download_recorded(world: &mut InstallMetricsWorld, millis: u64) -> Result<(), String> { + record_mode(world, InstallMode::Download, millis) } #[given("an in-memory zero metrics aggregate")] @@ -83,13 +124,13 @@ fn given_zero_metrics(world: &mut InstallMetricsWorld) { } #[when("a download install of {millis:u64} milliseconds is recorded")] -fn when_download_recorded(world: &mut InstallMetricsWorld, millis: u64) { - record_mode(world, InstallMode::Download, millis); +fn when_download_recorded(world: &mut InstallMetricsWorld, millis: u64) -> Result<(), String> { + record_mode(world, InstallMode::Download, millis) } #[when("a build install of {millis:u64} milliseconds is recorded")] -fn when_build_recorded(world: &mut InstallMetricsWorld, millis: u64) { - record_mode(world, InstallMode::Build, millis); +fn when_build_recorded(world: &mut InstallMetricsWorld, millis: u64) -> Result<(), String> { + record_mode(world, InstallMode::Build, millis) } #[when("download and build rates are calculated")] @@ -98,91 +139,124 @@ fn when_rates_calculated(world: &mut InstallMetricsWorld) { } #[then("total installs is {expected:u64}")] -fn then_total_installs(world: &mut InstallMetricsWorld, expected: u64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert_eq!(metrics.total_installs(), expected); +fn then_total_installs(world: &mut InstallMetricsWorld, expected: u64) -> Result<(), String> { + ensure_eq( + &metrics(world)?.total_installs(), + &expected, + "total installs", + ) } #[then("download installs is {expected:u64}")] -fn then_download_installs(world: &mut InstallMetricsWorld, expected: u64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert_eq!(metrics.download_installs(), expected); +fn then_download_installs(world: &mut InstallMetricsWorld, expected: u64) -> Result<(), String> { + ensure_eq( + &metrics(world)?.download_installs(), + &expected, + "download installs", + ) } #[then("build installs is {expected:u64}")] -fn then_build_installs(world: &mut InstallMetricsWorld, expected: u64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert_eq!(metrics.build_installs(), expected); +fn then_build_installs(world: &mut InstallMetricsWorld, expected: u64) -> Result<(), String> { + ensure_eq( + &metrics(world)?.build_installs(), + &expected, + "build installs", + ) +} + +/// Compares a floating-point rate against its expected value with tolerance. +fn ensure_rate(actual: f64, expected: f64, context: &str) -> Result<(), String> { + if (actual - expected).abs() < FLOAT_RATE_TOLERANCE { + Ok(()) + } else { + Err(format!("{context}: expected {expected}, got {actual}")) + } } #[then("download rate is {expected:f64}")] -fn then_download_rate(world: &mut InstallMetricsWorld, expected: f64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert!( - (metrics.download_rate() - expected).abs() < FLOAT_RATE_TOLERANCE, - "expected {}, got {}", - expected, - metrics.download_rate() - ); +fn then_download_rate(world: &mut InstallMetricsWorld, expected: f64) -> Result<(), String> { + ensure_rate(metrics(world)?.download_rate(), expected, "download rate") } #[then("build rate is {expected:f64}")] -fn then_build_rate(world: &mut InstallMetricsWorld, expected: f64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert!( - (metrics.build_rate() - expected).abs() < FLOAT_RATE_TOLERANCE, - "expected {}, got {}", - expected, - metrics.build_rate() - ); +fn then_build_rate(world: &mut InstallMetricsWorld, expected: f64) -> Result<(), String> { + ensure_rate(metrics(world)?.build_rate(), expected, "build rate") } #[then("total installation time is {expected:u64} milliseconds")] -fn then_total_installation_time(world: &mut InstallMetricsWorld, expected: u64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert_eq!( - metrics.total_install_duration(), - Duration::from_millis(expected) - ); +fn then_total_installation_time( + world: &mut InstallMetricsWorld, + expected: u64, +) -> Result<(), String> { + ensure_eq( + &metrics(world)?.total_install_duration(), + &Duration::from_millis(expected), + "total installation time", + ) } #[then("metrics recovery from corrupt file is true")] -fn then_recovered(world: &mut InstallMetricsWorld) { - let outcome = world.outcome.as_ref().expect("recording outcome available"); - assert!(outcome.recovered_from_corrupt_file()); +fn then_recovered(world: &mut InstallMetricsWorld) -> Result<(), String> { + let outcome = world + .outcome + .as_ref() + .ok_or_else(|| String::from("recording outcome available"))?; + if outcome.recovered_from_corrupt_file() { + Ok(()) + } else { + Err(String::from( + "expected recovery from a corrupt metrics file", + )) + } } #[then("metrics recording fails")] -fn then_recording_fails(world: &mut InstallMetricsWorld) { - assert!( - world.last_error.is_some(), - "expected recording to fail, got success outcome" - ); +fn then_recording_fails(world: &mut InstallMetricsWorld) -> Result<(), String> { + if world.last_error.is_some() { + Ok(()) + } else { + Err(String::from( + "expected recording to fail, got success outcome", + )) + } } #[then("summary line contains \"{expected}\"")] -fn then_summary_line_contains(world: &mut InstallMetricsWorld, expected: String) { +fn then_summary_line_contains( + world: &mut InstallMetricsWorld, + expected: String, +) -> Result<(), String> { let summary = world .summary_line .as_deref() - .expect("summary line is available"); - assert!( - summary.contains(&expected), - "expected summary line to contain {expected:?}, got {summary:?}" - ); + .ok_or_else(|| String::from("summary line is available"))?; + if summary.contains(&expected) { + Ok(()) + } else { + Err(format!( + "expected summary line to contain {expected:?}, got {summary:?}" + )) + } } #[then("warning text contains \"{expected}\"")] -fn then_warning_text_contains(world: &mut InstallMetricsWorld, expected: String) { +fn then_warning_text_contains( + world: &mut InstallMetricsWorld, + expected: String, +) -> Result<(), String> { let error = world .last_error .as_deref() - .expect("metrics recording error should be available"); + .ok_or_else(|| String::from("metrics recording error should be available"))?; let warning_text = format!("Warning: could not record install metrics: {error}"); - assert!( - warning_text.contains(&expected), - "expected warning text to contain {expected:?}, got {warning_text:?}" - ); + if warning_text.contains(&expected) { + Ok(()) + } else { + Err(format!( + "expected warning text to contain {expected:?}, got {warning_text:?}" + )) + } } #[scenario( diff --git a/installer/tests/behaviour_installer_release.rs b/installer/tests/behaviour_installer_release.rs index 83af635f5..4b6b36c6b 100644 --- a/installer/tests/behaviour_installer_release.rs +++ b/installer/tests/behaviour_installer_release.rs @@ -4,13 +4,16 @@ //! archives matching the binstall `pkg-url` and `bin-dir` templates for //! all supported targets, and that error paths are handled correctly. +use std::path::PathBuf; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::path::PathBuf; -use whitaker_installer::binstall_metadata; -use whitaker_installer::installer_packaging::{ - self, ArchiveFormat, InstallerPackageOutput, InstallerPackageParams, InstallerPackagingError, - TargetTriple, Version, +use whitaker_installer::{ + binstall_metadata, + installer_packaging::{ + self, ArchiveFormat, InstallerPackageOutput, InstallerPackageParams, + InstallerPackagingError, TargetTriple, Version, + }, }; // --------------------------------------------------------------------------- @@ -29,6 +32,7 @@ struct InstallerReleaseWorld { packaging_error: Option, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> InstallerReleaseWorld { InstallerReleaseWorld::default() @@ -44,41 +48,54 @@ fn given_version_and_target(world: &mut InstallerReleaseWorld, version: String, world.target = target; } +/// Parse the world's target string, reporting a descriptive error on failure. +fn world_target(world: &InstallerReleaseWorld) -> Result { + TargetTriple::try_from(world.target.as_str()) + .map_err(|error| format!("invalid target '{}': {error}", world.target)) +} + #[given("a fake installer binary exists")] -fn given_fake_binary_exists(world: &mut InstallerReleaseWorld) { - let temp = tempfile::tempdir().expect("temp dir"); - let bin_name = installer_packaging::binary_filename( - &TargetTriple::try_from(world.target.as_str()).expect("valid target"), - ); +fn given_fake_binary_exists(world: &mut InstallerReleaseWorld) -> Result<(), String> { + let temp = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?; + let bin_name = installer_packaging::binary_filename(&world_target(world)?); let binary_path = temp.path().join(&bin_name); - std::fs::write(&binary_path, b"fake-binary").expect("write fake binary"); + std::fs::write(&binary_path, b"fake-binary") + .map_err(|error| format!("write fake binary: {error}"))?; world.binary_path = Some(binary_path); world.temp_dir = Some(temp); + Ok(()) } #[given("the binary path does not exist")] -fn given_binary_missing(world: &mut InstallerReleaseWorld) { - let temp = tempfile::tempdir().expect("temp dir"); +fn given_binary_missing(world: &mut InstallerReleaseWorld) -> Result<(), String> { + let temp = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?; world.binary_path = Some(temp.path().join("does-not-exist")); world.temp_dir = Some(temp); + Ok(()) } #[when("the archive filename is computed")] -fn when_archive_filename_computed(world: &mut InstallerReleaseWorld) { - world.computed_filename = installer_packaging::archive_filename( - &Version::new(&world.version), - &TargetTriple::try_from(world.target.as_str()).expect("valid target"), - ); +fn when_archive_filename_computed(world: &mut InstallerReleaseWorld) -> Result<(), String> { + world.computed_filename = + installer_packaging::archive_filename(&Version::new(&world.version), &world_target(world)?); + Ok(()) } /// Run the packaging pipeline and store the result in the world. -fn attempt_packaging(world: &mut InstallerReleaseWorld) { - let temp_dir = world.temp_dir.as_ref().expect("temp dir set"); - let binary_path = world.binary_path.as_ref().expect("binary path set"); +fn attempt_packaging(world: &mut InstallerReleaseWorld) -> Result<(), String> { + let target = world_target(world)?; + let temp_dir = world + .temp_dir + .as_ref() + .ok_or_else(|| String::from("temp dir set"))?; + let binary_path = world + .binary_path + .as_ref() + .ok_or_else(|| String::from("binary path set"))?; let params = InstallerPackageParams { version: Version::new(&world.version), - target: TargetTriple::try_from(world.target.as_str()).expect("valid target"), + target, binary_path: binary_path.clone(), output_dir: temp_dir.path().to_path_buf(), }; @@ -87,16 +104,17 @@ fn attempt_packaging(world: &mut InstallerReleaseWorld) { Ok(output) => world.package_output = Some(output), Err(e) => world.packaging_error = Some(e), } + Ok(()) } #[when("the installer is packaged")] -fn when_installer_packaged(world: &mut InstallerReleaseWorld) { - attempt_packaging(world); +fn when_installer_packaged(world: &mut InstallerReleaseWorld) -> Result<(), String> { + attempt_packaging(world) } #[when("packaging is attempted")] -fn when_packaging_attempted(world: &mut InstallerReleaseWorld) { - attempt_packaging(world); +fn when_packaging_attempted(world: &mut InstallerReleaseWorld) -> Result<(), String> { + attempt_packaging(world) } #[then("the archive filename is \"{expected}\"")] @@ -108,21 +126,25 @@ fn then_archive_filename_is(world: &mut InstallerReleaseWorld, expected: String) } #[then("the archive contains \"{expected_path}\"")] -fn then_archive_contains(world: &mut InstallerReleaseWorld, expected_path: String) { +fn then_archive_contains( + world: &mut InstallerReleaseWorld, + expected_path: String, +) -> Result<(), String> { + let format = installer_packaging::archive_format(&world_target(world)?); let output = world .package_output .as_ref() - .expect("package output should be set"); + .ok_or_else(|| String::from("package output should be set"))?; - let format = installer_packaging::archive_format( - &TargetTriple::try_from(world.target.as_str()).expect("valid target"), - ); - let entries = read_archive_entries(&output.archive_path, format); + let entries = read_archive_entries(&output.archive_path, format)?; - assert!( - entries.contains(&expected_path), - "expected archive to contain '{expected_path}', found: {entries:?}" - ); + if entries.contains(&expected_path) { + Ok(()) + } else { + Err(format!( + "expected archive to contain '{expected_path}', found: {entries:?}" + )) + } } #[then("the binstall pkg-url ends with the archive filename")] @@ -136,15 +158,16 @@ fn then_binstall_url_ends_with_filename(world: &mut InstallerReleaseWorld) { } #[then("a packaging error is returned")] -fn then_packaging_error_returned(world: &mut InstallerReleaseWorld) { +fn then_packaging_error_returned(world: &mut InstallerReleaseWorld) -> Result<(), String> { let err = world .packaging_error .as_ref() - .expect("expected packaging to fail, but it succeeded"); - assert!( - matches!(err, InstallerPackagingError::BinaryNotFound(_)), - "expected BinaryNotFound, got {err:?}" - ); + .ok_or_else(|| String::from("expected packaging to fail, but it succeeded"))?; + if matches!(err, InstallerPackagingError::BinaryNotFound(_)) { + Ok(()) + } else { + Err(format!("expected BinaryNotFound, got {err:?}")) + } } // --------------------------------------------------------------------------- @@ -152,7 +175,14 @@ fn then_packaging_error_returned(world: &mut InstallerReleaseWorld) { // --------------------------------------------------------------------------- /// Read entry paths from an archive file. -fn read_archive_entries(path: &std::path::Path, format: ArchiveFormat) -> Vec { +/// +/// # Errors +/// +/// Returns an error when the archive cannot be opened or its entries read. +fn read_archive_entries( + path: &std::path::Path, + format: ArchiveFormat, +) -> Result, String> { match format { ArchiveFormat::Tgz => read_tgz_entries(path), ArchiveFormat::Zip => read_zip_entries(path), @@ -160,32 +190,34 @@ fn read_archive_entries(path: &std::path::Path, format: ArchiveFormat) -> Vec Vec { - let file = std::fs::File::open(path).expect("open tgz"); +fn read_tgz_entries(path: &std::path::Path) -> Result, String> { + let file = std::fs::File::open(path).map_err(|error| format!("open tgz: {error}"))?; let gz = flate2::read::GzDecoder::new(file); let mut archive = tar::Archive::new(gz); archive .entries() - .expect("entries") - .map(|e| { - let entry = e.expect("valid tar entry"); - entry + .map_err(|error| format!("list tgz entries: {error}"))? + .map(|entry| { + let archive_entry = entry.map_err(|error| format!("read tar entry: {error}"))?; + let entry_path = archive_entry .path() - .expect("valid entry path") - .to_string_lossy() - .into_owned() + .map_err(|error| format!("read tar entry path: {error}"))?; + Ok(entry_path.to_string_lossy().into_owned()) }) .collect() } /// Read entry paths from a `.zip` archive. -fn read_zip_entries(path: &std::path::Path) -> Vec { - let file = std::fs::File::open(path).expect("open zip"); - let archive = zip::ZipArchive::new(file).expect("open zip archive"); +fn read_zip_entries(path: &std::path::Path) -> Result, String> { + let file = std::fs::File::open(path).map_err(|error| format!("open zip: {error}"))?; + let archive = + zip::ZipArchive::new(file).map_err(|error| format!("open zip archive: {error}"))?; (0..archive.len()) - .map(|i| { - let entry = archive.name_for_index(i).expect("entry name"); - entry.to_owned() + .map(|index| { + archive + .name_for_index(index) + .map(ToOwned::to_owned) + .ok_or_else(|| format!("zip entry {index} has no name")) }) .collect() } diff --git a/installer/tests/behaviour_prebuilt.rs b/installer/tests/behaviour_prebuilt.rs index b51185f81..f02181c69 100644 --- a/installer/tests/behaviour_prebuilt.rs +++ b/installer/tests/behaviour_prebuilt.rs @@ -1,17 +1,24 @@ //! BDD tests for the prebuilt artefact download and verification workflow. +use std::{ + path::Path, + sync::{Mutex, PoisonError}, +}; + use camino::Utf8PathBuf; use clap::Parser; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::path::Path; -use std::sync::Mutex; -use whitaker_installer::artefact::download::{ArtefactDownloader, DownloadError}; -use whitaker_installer::artefact::extraction::{ArtefactExtractor, ExtractionError}; -use whitaker_installer::cli::{Cli, InstallArgs}; -use whitaker_installer::prebuilt::{PrebuiltConfig, PrebuiltResult, attempt_prebuilt_with}; -use whitaker_installer::resolution::{CrateResolutionOptions, resolve_crates}; -use whitaker_installer::test_utils::{prebuilt_manifest_json, sha256_hex}; +use whitaker_installer::{ + artefact::{ + download::{ArtefactDownloader, DownloadError}, + extraction::{ArtefactExtractor, ExtractionError}, + }, + cli::{Cli, InstallArgs}, + prebuilt::{PrebuiltConfig, PrebuiltResult, attempt_prebuilt_with}, + resolution::{CrateResolutionOptions, resolve_crates}, + test_utils::{prebuilt_manifest_json, sha256_hex}, +}; const FAKE_ARCHIVE: &[u8] = b"fake archive content"; const DEFAULT_TARGET: &str = "x86_64-unknown-linux-gnu"; @@ -44,7 +51,7 @@ struct StubDownloader { } impl StubDownloader { - fn new(manifest: ManifestBehaviour, archive: ArchiveBehaviour) -> Self { + const fn new(manifest: ManifestBehaviour, archive: ArchiveBehaviour) -> Self { Self { manifest: Mutex::new(Some(manifest)), archive: Mutex::new(Some(archive)), @@ -57,9 +64,13 @@ impl ArtefactDownloader for StubDownloader { let behaviour = self .manifest .lock() - .expect("lock") + .unwrap_or_else(PoisonError::into_inner) .take() - .expect("manifest behaviour not set"); + .ok_or_else(|| { + DownloadError::Io(std::io::Error::other( + "stub manifest behaviour was not configured", + )) + })?; match behaviour { ManifestBehaviour::Ok(json) => Ok(json), ManifestBehaviour::HttpError { url, reason } => { @@ -73,9 +84,9 @@ impl ArtefactDownloader for StubDownloader { let behaviour = self .archive .lock() - .expect("lock") + .unwrap_or_else(PoisonError::into_inner) .take() - .unwrap_or(ArchiveBehaviour::CorrectChecksum); + .unwrap_or_default(); match behaviour { ArchiveBehaviour::CorrectChecksum => { std::fs::write(dest, FAKE_ARCHIVE).map_err(DownloadError::Io) @@ -117,10 +128,16 @@ struct PrebuiltWorld { attempted_destination: Option, } +// A fixture cannot report failure to the scenario binding, so setup problems +// abort the scenario with a descriptive panic. #[fixture] fn world() -> PrebuiltWorld { - let temp_dir = tempfile::tempdir().expect("temp dir"); - let staging_root = Utf8PathBuf::try_from(temp_dir.path().to_path_buf()).expect("UTF-8 path"); + let Ok(temp_dir) = tempfile::tempdir() else { + panic!("failed to create staging temp dir"); + }; + let Ok(staging_root) = Utf8PathBuf::try_from(temp_dir.path().to_path_buf()) else { + panic!("staging temp dir path is not valid UTF-8"); + }; PrebuiltWorld { _temp_dir: Some(temp_dir), staging_root: Some(staging_root), @@ -205,16 +222,20 @@ fn given_destination_path_conflict(world: &mut PrebuiltWorld) { } #[when("prebuilt download is attempted")] -fn when_prebuilt_attempted(world: &mut PrebuiltWorld) { +fn when_prebuilt_attempted(world: &mut PrebuiltWorld) -> Result<(), String> { let toolchain = world .expected_toolchain .as_deref() .unwrap_or(DEFAULT_TOOLCHAIN); let target = world.requested_target.as_deref().unwrap_or(DEFAULT_TARGET); - let staging_root = world.staging_root.as_ref().expect("staging_root set"); + let staging_root = world + .staging_root + .as_ref() + .ok_or_else(|| String::from("staging_root set"))?; let destination_dir = if world.force_destination_conflict { let occupied = staging_root.join("occupied"); - std::fs::write(occupied.as_std_path(), b"occupied file").expect("write occupied file"); + std::fs::write(occupied.as_std_path(), b"occupied file") + .map_err(|error| format!("write occupied file: {error}"))?; occupied.join("child").join("lib") } else { staging_root @@ -234,7 +255,7 @@ fn when_prebuilt_attempted(world: &mut PrebuiltWorld) { let manifest_behaviour = world .manifest_behaviour .take() - .expect("manifest_behaviour set"); + .ok_or_else(|| String::from("manifest_behaviour set"))?; let archive_behaviour = world .archive_behaviour .take() @@ -246,6 +267,7 @@ fn when_prebuilt_attempted(world: &mut PrebuiltWorld) { let mut stderr = Vec::new(); let result = attempt_prebuilt_with(&config, &downloader, &extractor, &mut stderr); world.result = Some(result); + Ok(()) } #[when("the install configuration is checked")] @@ -259,77 +281,96 @@ fn when_install_config_checked(world: &mut PrebuiltWorld) { world.should_attempt_prebuilt = Some(install_args.should_attempt_prebuilt(&requested_crates)); } +/// Borrow the recorded prebuilt outcome, failing when the When step has not run. +fn prebuilt_result(world: &PrebuiltWorld) -> Result<&PrebuiltResult, String> { + world + .result + .as_ref() + .ok_or_else(|| String::from("result set")) +} + #[then("the prebuilt result is success")] -fn then_result_is_success(world: &mut PrebuiltWorld) { - let result = world.result.as_ref().expect("result set"); - assert!( - matches!(result, PrebuiltResult::Success { .. }), - "expected Success, got {result:?}" - ); +fn then_result_is_success(world: &mut PrebuiltWorld) -> Result<(), String> { + let result = prebuilt_result(world)?; + if matches!(result, PrebuiltResult::Success { .. }) { + Ok(()) + } else { + Err(format!("expected Success, got {result:?}")) + } } #[then("the staging path uses toolchain, target, and lib directories")] -fn then_staging_path_uses_expected_layout(world: &mut PrebuiltWorld) { - let result = world.result.as_ref().expect("result set"); - if let PrebuiltResult::Success { staging_path } = result { - let toolchain = world - .expected_toolchain - .as_deref() - .unwrap_or(DEFAULT_TOOLCHAIN); - let target = world.requested_target.as_deref().unwrap_or(DEFAULT_TARGET); - let expected_suffix = format!("{toolchain}/{target}/lib"); - assert!( - staging_path.ends_with(&expected_suffix), - "staging path {staging_path} does not end with {expected_suffix}" - ); +fn then_staging_path_uses_expected_layout(world: &mut PrebuiltWorld) -> Result<(), String> { + let result = prebuilt_result(world)?; + let PrebuiltResult::Success { staging_path } = result else { + return Err(format!("expected Success, got {result:?}")); + }; + let toolchain = world + .expected_toolchain + .as_deref() + .unwrap_or(DEFAULT_TOOLCHAIN); + let target = world.requested_target.as_deref().unwrap_or(DEFAULT_TARGET); + let expected_suffix = format!("{toolchain}/{target}/lib"); + if staging_path.ends_with(&expected_suffix) { + Ok(()) } else { - panic!("expected Success, got {result:?}"); + Err(format!( + "staging path {staging_path} does not end with {expected_suffix}" + )) } } #[then("the prebuilt result is fallback")] -fn then_result_is_fallback(world: &mut PrebuiltWorld) { - let result = world.result.as_ref().expect("result set"); - assert!( - matches!(result, PrebuiltResult::Fallback { .. }), - "expected Fallback, got {result:?}" - ); +fn then_result_is_fallback(world: &mut PrebuiltWorld) -> Result<(), String> { + let result = prebuilt_result(world)?; + if matches!(result, PrebuiltResult::Fallback { .. }) { + Ok(()) + } else { + Err(format!("expected Fallback, got {result:?}")) + } } #[then("the fallback reason mentions \"{keyword}\"")] -fn then_fallback_reason_mentions(world: &mut PrebuiltWorld, keyword: String) { - let result = world.result.as_ref().expect("result set"); - match result { - PrebuiltResult::Fallback { reason } => { - let lower_reason = reason.to_lowercase(); - let lower_keyword = keyword.to_lowercase(); - assert!( - lower_reason.contains(&lower_keyword), - "expected reason to contain '{keyword}', got: {reason}" - ); - } - other => panic!("expected Fallback, got {other:?}"), +fn then_fallback_reason_mentions(world: &mut PrebuiltWorld, keyword: String) -> Result<(), String> { + let result = prebuilt_result(world)?; + let PrebuiltResult::Fallback { reason } = result else { + return Err(format!("expected Fallback, got {result:?}")); + }; + let lower_reason = reason.to_lowercase(); + let lower_keyword = keyword.to_lowercase(); + if lower_reason.contains(&lower_keyword) { + Ok(()) + } else { + Err(format!( + "expected reason to contain '{keyword}', got: {reason}" + )) } } #[then("no prebuilt download is attempted")] -fn then_no_prebuilt_attempted(world: &mut PrebuiltWorld) { - assert!( - world.should_attempt_prebuilt == Some(false), - "expected no prebuilt download attempt when --build-only is set" - ); +fn then_no_prebuilt_attempted(world: &mut PrebuiltWorld) -> Result<(), String> { + if world.should_attempt_prebuilt == Some(false) { + Ok(()) + } else { + Err(String::from( + "expected no prebuilt download attempt when --build-only is set", + )) + } } #[then("the destination directory is not created")] -fn then_destination_is_not_created(world: &mut PrebuiltWorld) { +fn then_destination_is_not_created(world: &mut PrebuiltWorld) -> Result<(), String> { let destination = world .attempted_destination .as_ref() - .expect("attempted destination should be set"); - assert!( - !destination.exists(), - "destination directory should not exist: {destination}" - ); + .ok_or_else(|| String::from("attempted destination should be set"))?; + if destination.exists() { + Err(format!( + "destination directory should not exist: {destination}" + )) + } else { + Ok(()) + } } #[scenario( diff --git a/installer/tests/behaviour_staging.rs b/installer/tests/behaviour_staging.rs index 4736bb947..2f27601e6 100644 --- a/installer/tests/behaviour_staging.rs +++ b/installer/tests/behaviour_staging.rs @@ -3,15 +3,15 @@ //! These scenarios cover staged filename conventions and non-writable target //! handling. -use camino::Utf8PathBuf; -use rstest::fixture; -use rstest_bdd_macros::{given, scenario, then, when}; #[cfg(unix)] use std::cell::Cell; use std::cell::RefCell; + +use camino::Utf8PathBuf; +use rstest::fixture; +use rstest_bdd_macros::{given, scenario, then, when}; use tempfile::TempDir; -use whitaker_installer::builder::CrateName; -use whitaker_installer::stager::Stager; +use whitaker_installer::{builder::CrateName, stager::Stager}; // --------------------------------------------------------------------------- // Staging world @@ -24,6 +24,7 @@ struct StagingWorld { staged_name: RefCell, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn staging_world() -> StagingWorld { StagingWorld::default() @@ -44,19 +45,22 @@ fn given_staging_dir(staging_world: &StagingWorld) { } #[when("the library is staged")] -fn when_library_staged(staging_world: &StagingWorld) { - let crate_name = staging_world.crate_name.borrow(); - let crate_name = crate_name.as_ref().expect("crate name not set"); +fn when_library_staged(staging_world: &StagingWorld) -> Result<(), String> { + let crate_name_slot = staging_world.crate_name.borrow(); + let crate_name = crate_name_slot + .as_ref() + .ok_or_else(|| String::from("crate name not set"))?; let toolchain = staging_world.toolchain.borrow(); // Use the production Stager to compute the filename. - let temp_dir = TempDir::new().expect("failed to create temp dir"); - let utf8_path = - Utf8PathBuf::try_from(temp_dir.path().to_path_buf()).expect("temp dir path not UTF-8"); + let temp_dir = TempDir::new().map_err(|error| format!("failed to create temp dir: {error}"))?; + let utf8_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf()) + .map_err(|error| format!("temp dir path not UTF-8: {error}"))?; let stager = Stager::new(utf8_path, toolchain.as_str()); let staged_name = stager.staged_filename(crate_name); staging_world.staged_name.replace(staged_name); + Ok(()) } #[then("the staged filename includes the toolchain")] @@ -77,18 +81,21 @@ use staging_failure::staging_failure_world; #[cfg(unix)] mod staging_failure { - use super::*; - use std::fs; - use std::os::unix::fs::PermissionsExt; + //! Unix-only world and steps for staging permission failures. + + use std::{fs, os::unix::fs::PermissionsExt}; + use tempfile::TempDir; use whitaker_installer::error::InstallerError; + use super::*; + pub struct StagingFailureWorld { stager: RefCell>, result: RefCell>>, skip_assertions: Cell, - // Keep temp_dir alive for the lifetime of the test. - _temp_dir: RefCell>, + // Keep the temporary directory alive for the lifetime of the test. + temp_dir: RefCell>, } impl Default for StagingFailureWorld { @@ -97,73 +104,89 @@ mod staging_failure { stager: RefCell::new(None), result: RefCell::new(None), skip_assertions: Cell::new(false), - _temp_dir: RefCell::new(None), + temp_dir: RefCell::new(None), } } } + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] pub fn staging_failure_world() -> StagingFailureWorld { StagingFailureWorld::default() } #[given("a non-writable staging directory")] - pub fn given_non_writable_dir(staging_failure_world: &StagingFailureWorld) { + pub fn given_non_writable_dir( + staging_failure_world: &StagingFailureWorld, + ) -> Result<(), String> { // Create a temp directory and make it read-only. - let temp_dir = TempDir::new().expect("failed to create temp dir"); + let temp_dir = + TempDir::new().map_err(|error| format!("failed to create temp dir: {error}"))?; let dir_path = temp_dir.path(); // Create the nested staging path structure that Stager expects. let staging_path = dir_path.join("nightly-2026-05-28").join("release"); - fs::create_dir_all(&staging_path).expect("failed to create staging path"); + fs::create_dir_all(&staging_path) + .map_err(|error| format!("failed to create staging path: {error}"))?; // Make the directory read-only (no write permission). let mut perms = fs::metadata(&staging_path) - .expect("failed to get metadata") + .map_err(|error| format!("failed to get metadata: {error}"))? .permissions(); perms.set_mode(0o555); // readable/traversable, not writable - fs::set_permissions(&staging_path, perms).expect("failed to set permissions"); + fs::set_permissions(&staging_path, perms) + .map_err(|error| format!("failed to set permissions: {error}"))?; - let utf8_path = - Utf8PathBuf::try_from(dir_path.to_path_buf()).expect("temp dir path not UTF-8"); + let utf8_path = Utf8PathBuf::try_from(dir_path.to_path_buf()) + .map_err(|error| format!("temp dir path not UTF-8: {error}"))?; let stager = Stager::new(utf8_path, "nightly-2026-05-28"); staging_failure_world.stager.replace(Some(stager)); - staging_failure_world._temp_dir.replace(Some(temp_dir)); + staging_failure_world.temp_dir.replace(Some(temp_dir)); + Ok(()) } #[when("the staging directory is prepared")] - pub fn when_staging_prepared(staging_failure_world: &StagingFailureWorld) { - let stager = staging_failure_world.stager.borrow(); - let stager = stager.as_ref().expect("stager not set"); + pub fn when_staging_prepared( + staging_failure_world: &StagingFailureWorld, + ) -> Result<(), String> { + let stager_slot = staging_failure_world.stager.borrow(); + let stager = stager_slot + .as_ref() + .ok_or_else(|| String::from("stager not set"))?; // Best-effort probe to avoid flakes on filesystems that ignore directory // permissions. If we can unexpectedly create a file in the staging // directory, mark assertions as skipped for this scenario. let probe_path = stager.staging_path().as_std_path().join("write-probe"); - match std::fs::OpenOptions::new() + if let Ok(file) = std::fs::OpenOptions::new() .create_new(true) .write(true) .open(&probe_path) { - Ok(file) => { - drop(file); - let _ = std::fs::remove_file(&probe_path); - staging_failure_world.skip_assertions.set(true); - } - Err(_) => { - // Expected: directory is not writable, continue. - } + drop(file); + std::fs::remove_file(&probe_path).map_err(|error| { + format!( + "failed to remove write probe {}: {error}", + probe_path.display() + ) + })?; + staging_failure_world.skip_assertions.set(true); + } else { + // Expected: directory is not writable, continue. } let result = stager.prepare(); staging_failure_world.result.replace(Some(result)); + Ok(()) } #[then("staging fails with a target not writable error")] - pub fn then_staging_fails_not_writable(staging_failure_world: &StagingFailureWorld) { + pub fn then_staging_fails_not_writable( + staging_failure_world: &StagingFailureWorld, + ) -> Result<(), String> { if staging_failure_world.skip_assertions.get() { - return; + return Ok(()); } // Skip this assertion when running as root (uid 0) since root can bypass @@ -171,15 +194,18 @@ mod staging_failure { // SAFETY: `libc::geteuid()` is a simple FFI call with no preconditions; // it returns the effective user ID without modifying any state. if unsafe { libc::geteuid() } == 0 { - return; + return Ok(()); } - let result = staging_failure_world.result.borrow(); - let result = result.as_ref().expect("result not set"); - assert!( - matches!(result, Err(InstallerError::TargetNotWritable { .. })), - "expected TargetNotWritable error, got {result:?}" - ); + let result_slot = staging_failure_world.result.borrow(); + let result = result_slot + .as_ref() + .ok_or_else(|| String::from("result not set"))?; + if matches!(result, Err(InstallerError::TargetNotWritable { .. })) { + Ok(()) + } else { + Err(format!("expected TargetNotWritable error, got {result:?}")) + } } } diff --git a/installer/tests/behaviour_toolchain.rs b/installer/tests/behaviour_toolchain.rs index 02da80e65..a4a67caed 100644 --- a/installer/tests/behaviour_toolchain.rs +++ b/installer/tests/behaviour_toolchain.rs @@ -5,8 +5,8 @@ //! //! The tests include: //! - Dry-run scenarios that test toolchain detection (skipped if toolchain missing) -//! - Install scenarios that exercise auto-install using an isolated rustup -//! environment (RUSTUP_HOME/CARGO_HOME set to temp directories) +//! - Install scenarios that exercise auto-install using an isolated rustup environment +//! (`RUSTUP_HOME/CARGO_HOME` set to temp directories) //! - Failure scenarios that test error handling with a non-existent toolchain mod prebuilt_markers; @@ -15,11 +15,13 @@ mod toolchain_steps; use rstest_bdd_macros::scenario; use toolchain_steps::{ToolchainWorld, world}; - // Import step definitions so rstest-bdd's scenario macro can discover them. // These imports appear unused to clippy because they're consumed by macro // expansion, not direct source-level calls. -#[allow(unused_imports)] +#[expect( + unused_imports, + reason = "step definitions are consumed by rstest-bdd macro expansion, not by direct calls" +)] use toolchain_steps::{ given_auto_detect_toolchain, given_auto_detect_toolchain_install, given_auto_detect_toolchain_quiet, given_isolated_rustup_auto_install, diff --git a/installer/tests/behaviour_workflows.rs b/installer/tests/behaviour_workflows.rs index 1644bf56b..93296b2f5 100644 --- a/installer/tests/behaviour_workflows.rs +++ b/installer/tests/behaviour_workflows.rs @@ -3,14 +3,17 @@ //! These scenarios test the --skip-deps, --no-update, and --skip-wrapper flags //! added to support standalone installation without a pre-cloned repository. +use std::{ + cell::{Cell, RefCell}, + io::Write as _, + path::{Path, PathBuf}, + process::{Command, Output}, +}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::{Cell, RefCell}; -use std::path::PathBuf; -use std::process::{Command, Output}; use tempfile::TempDir; -use whitaker_installer::test_support::TEST_STAGE_SUITE_ENV; -use whitaker_installer::toolchain::parse_toolchain_channel; +use whitaker_installer::{test_support::TEST_STAGE_SUITE_ENV, toolchain::parse_toolchain_channel}; #[derive(Default)] struct WorkflowWorld { @@ -19,31 +22,33 @@ struct WorkflowWorld { skip_assertions: Cell, requires_toolchain: Cell, use_test_staged_suite: Cell, - _temp_dir: RefCell>, + /// Owns the scenario's temporary target directory so it outlives the run. + temp_dir: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> WorkflowWorld { WorkflowWorld::default() } -fn workspace_root() -> PathBuf { +fn workspace_root() -> Result { PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) .parent() - .expect("manifest dir should have parent") - .to_owned() + .map(Path::to_path_buf) + .ok_or_else(|| String::from("manifest dir should have parent")) } -fn pinned_toolchain_channel() -> String { - let toolchain_path = workspace_root().join("rust-toolchain.toml"); - let contents = std::fs::read_to_string(&toolchain_path).unwrap_or_else(|err| { - panic!( +fn pinned_toolchain_channel() -> Result { + let toolchain_path = workspace_root()?.join("rust-toolchain.toml"); + let contents = std::fs::read_to_string(&toolchain_path).map_err(|err| { + format!( "failed to read rust-toolchain.toml at {}: {err}", toolchain_path.display() ) - }); - parse_toolchain_channel(&contents).unwrap_or_else(|err| { - panic!( + })?; + parse_toolchain_channel(&contents).map_err(|err| { + format!( "failed to parse rust-toolchain.toml at {}: {err}", toolchain_path.display() ) @@ -54,64 +59,100 @@ fn is_toolchain_installed(channel: &str) -> bool { Command::new("rustup") .args(["run", channel, "rustc", "--version"]) .output() - .map(|o| o.status.success()) - .unwrap_or(false) + .is_ok_and(|o| o.status.success()) +} + +/// Reports a skipped scenario on stderr without tripping `print_stderr`. +fn report_skip(reason: &str) -> Result<(), String> { + writeln!(std::io::stderr(), "{reason}") + .map_err(|error| format!("failed to report skipped scenario: {error}")) } -fn skip_scenario_when_toolchain_missing(world: &WorkflowWorld, channel: &str) { +fn skip_scenario_when_toolchain_missing( + world: &WorkflowWorld, + channel: &str, +) -> Result<(), String> { if !is_toolchain_installed(channel) { - eprintln!( - "Skipping scenario because rustup toolchain '{}' is not installed.", - channel - ); + report_skip(&format!( + "Skipping scenario because rustup toolchain '{channel}' is not installed." + ))?; world.skip_assertions.set(true); rstest_bdd::skip!( "rustup toolchain '{channel}' is not installed.", channel = channel ); } + Ok(()) } -fn ensure_required_toolchain_available(world: &WorkflowWorld) -> Option { - let channel = pinned_toolchain_channel(); +fn ensure_required_toolchain_available(world: &WorkflowWorld) -> Result, String> { + let channel = pinned_toolchain_channel()?; world.requires_toolchain.set(true); - skip_scenario_when_toolchain_missing(world, &channel); + skip_scenario_when_toolchain_missing(world, &channel)?; - if world.skip_assertions.get() { - None - } else { - Some(channel) - } + Ok((!world.skip_assertions.get()).then_some(channel)) } macro_rules! skip_if_needed { ($world:expr) => { if $world.skip_assertions.get() { - return; + return Ok(()); } }; } -fn setup_temp_dir(world: &WorkflowWorld) -> String { - let temp_dir = TempDir::new().expect("failed to create temp dir"); +fn setup_temp_dir(world: &WorkflowWorld) -> Result { + let temp_dir = TempDir::new().map_err(|error| format!("failed to create temp dir: {error}"))?; let target_dir = temp_dir.path().to_string_lossy().to_string(); - world._temp_dir.replace(Some(temp_dir)); - target_dir + world.temp_dir.replace(Some(temp_dir)); + Ok(target_dir) } -fn get_output(world: &WorkflowWorld) -> std::cell::Ref<'_, Output> { +/// Borrows the captured CLI output, failing when no command has run yet. +fn get_output(world: &WorkflowWorld) -> Result, String> { let output = world.output.borrow(); - std::cell::Ref::map(output, |opt| opt.as_ref().expect("output not set")) + std::cell::Ref::filter_map(output, Option::as_ref) + .map_err(|_| String::from("CLI output not set; run the installer step first")) +} + +fn require_successful_output(world: &WorkflowWorld, failure_prefix: &str) -> Result<(), String> { + skip_if_needed!(world); + + let output = get_output(world)?; + if output.status.success() { + Ok(()) + } else { + Err(format!( + "{failure_prefix}, stderr: {}", + String::from_utf8_lossy(&output.stderr) + )) + } +} + +fn require_stderr_contains( + world: &WorkflowWorld, + expected: &str, + failure_prefix: &str, +) -> Result<(), String> { + skip_if_needed!(world); + + let output = get_output(world)?; + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains(expected) { + Ok(()) + } else { + Err(format!("{failure_prefix}, stderr: {stderr}")) + } } // --------------------------------------------------------------------------- // Step definitions // --------------------------------------------------------------------------- -fn given_dry_run_with_flag(world: &WorkflowWorld, flag: &str) { - let Some(channel) = ensure_required_toolchain_available(world) else { - return; +fn given_dry_run_with_flag(world: &WorkflowWorld, flag: &str) -> Result<(), String> { + let Some(channel) = ensure_required_toolchain_available(world)? else { + return Ok(()); }; world.args.replace(vec![ @@ -120,30 +161,31 @@ fn given_dry_run_with_flag(world: &WorkflowWorld, flag: &str) { channel, flag.to_owned(), ]); + Ok(()) } #[given("the installer is invoked with dry-run and skip-deps")] -fn given_dry_run_skip_deps(world: &WorkflowWorld) { - given_dry_run_with_flag(world, "--skip-deps"); +fn given_dry_run_skip_deps(world: &WorkflowWorld) -> Result<(), String> { + given_dry_run_with_flag(world, "--skip-deps") } #[given("the installer is invoked with dry-run and no-update")] -fn given_dry_run_no_update(world: &WorkflowWorld) { - given_dry_run_with_flag(world, "--no-update"); +fn given_dry_run_no_update(world: &WorkflowWorld) -> Result<(), String> { + given_dry_run_with_flag(world, "--no-update") } #[given("the installer is invoked with dry-run and skip-wrapper")] -fn given_dry_run_skip_wrapper(world: &WorkflowWorld) { - given_dry_run_with_flag(world, "--skip-wrapper"); +fn given_dry_run_skip_wrapper(world: &WorkflowWorld) -> Result<(), String> { + given_dry_run_with_flag(world, "--skip-wrapper") } #[given("the installer is invoked with skip-wrapper to a temporary directory")] -fn given_skip_wrapper_install(world: &WorkflowWorld) { - let Some(_channel) = ensure_required_toolchain_available(world) else { - return; - }; +fn given_skip_wrapper_install(world: &WorkflowWorld) -> Result<(), String> { + if ensure_required_toolchain_available(world)?.is_none() { + return Ok(()); + } - let target_dir = setup_temp_dir(world); + let target_dir = setup_temp_dir(world)?; world.use_test_staged_suite.set(true); // The behavioural test sets a dedicated env var so the installer stages a @@ -155,98 +197,72 @@ fn given_skip_wrapper_install(world: &WorkflowWorld) { "--skip-wrapper".to_owned(), "--skip-deps".to_owned(), ]); + Ok(()) } #[when("the installer CLI is run")] -fn when_installer_cli_run(world: &WorkflowWorld) { +fn when_installer_cli_run(world: &WorkflowWorld) -> Result<(), String> { skip_if_needed!(world); let args = world.args.borrow(); let mut cmd = Command::new(env!("CARGO_BIN_EXE_whitaker-installer")); cmd.args(args.iter()); - cmd.current_dir(workspace_root()); + cmd.current_dir(workspace_root()?); if world.use_test_staged_suite.get() { cmd.env(TEST_STAGE_SUITE_ENV, "1"); } - let output = cmd.output().expect("failed to run whitaker-installer"); + let output = cmd + .output() + .map_err(|error| format!("failed to run whitaker-installer: {error}"))?; world.output.replace(Some(output)); + Ok(()) } #[then("the CLI exits successfully")] -fn then_cli_exits_successfully(world: &WorkflowWorld) { - skip_if_needed!(world); - - let output = get_output(world); - assert!( - output.status.success(), - "expected success, stderr: {}", - String::from_utf8_lossy(&output.stderr) - ); +fn then_cli_exits_successfully(world: &WorkflowWorld) -> Result<(), String> { + require_successful_output(world, "expected success") } #[then("installation succeeds or is skipped")] -fn then_installation_succeeds_or_is_skipped(world: &WorkflowWorld) { - skip_if_needed!(world); - - let output = get_output(world); - assert!( - output.status.success(), - "installation failed: {}", - String::from_utf8_lossy(&output.stderr) - ); +fn then_installation_succeeds_or_is_skipped(world: &WorkflowWorld) -> Result<(), String> { + require_successful_output(world, "installation failed") } #[then("dry-run output shows skip_deps is true")] -fn then_skip_deps_is_true(world: &WorkflowWorld) { - skip_if_needed!(world); - - let output = get_output(world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - stderr.contains("Skip deps: true"), - "expected skip_deps to be true in output, stderr: {stderr}" - ); +fn then_skip_deps_is_true(world: &WorkflowWorld) -> Result<(), String> { + require_stderr_contains( + world, + "Skip deps: true", + "expected skip_deps to be true in output", + ) } #[then("dry-run output shows no_update is true")] -fn then_no_update_is_true(world: &WorkflowWorld) { - skip_if_needed!(world); - - let output = get_output(world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - stderr.contains("No update: true"), - "expected no_update to be true in output, stderr: {stderr}" - ); +fn then_no_update_is_true(world: &WorkflowWorld) -> Result<(), String> { + require_stderr_contains( + world, + "No update: true", + "expected no_update to be true in output", + ) } #[then("dry-run output shows skip_wrapper is true")] -fn then_skip_wrapper_is_true(world: &WorkflowWorld) { - skip_if_needed!(world); - - let output = get_output(world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - stderr.contains("Skip wrapper: true"), - "expected skip_wrapper to be true in output, stderr: {stderr}" - ); +fn then_skip_wrapper_is_true(world: &WorkflowWorld) -> Result<(), String> { + require_stderr_contains( + world, + "Skip wrapper: true", + "expected skip_wrapper to be true in output", + ) } #[then("output includes DYLINT_LIBRARY_PATH instructions")] -fn then_output_includes_library_path_instructions(world: &WorkflowWorld) { - skip_if_needed!(world); - - let output = get_output(world); - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!( - stderr.contains("DYLINT_LIBRARY_PATH"), - "expected DYLINT_LIBRARY_PATH instructions in output, stderr: {stderr}" - ); +fn then_output_includes_library_path_instructions(world: &WorkflowWorld) -> Result<(), String> { + require_stderr_contains( + world, + "DYLINT_LIBRARY_PATH", + "expected DYLINT_LIBRARY_PATH instructions in output", + ) } // --------------------------------------------------------------------------- diff --git a/installer/tests/doc_extraction/extraction.rs b/installer/tests/doc_extraction/extraction.rs index 575c65bba..f4bf8596a 100644 --- a/installer/tests/doc_extraction/extraction.rs +++ b/installer/tests/doc_extraction/extraction.rs @@ -12,15 +12,16 @@ const DOC_PATHS: &[&str] = &["docs/users-guide.md", "docs/developers-guide.md"]; /// Extracted TOML code blocks from documentation, loaded once at test startup. pub static DOC_TOML_BLOCKS: LazyLock> = LazyLock::new(|| { let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let workspace_root = std::path::Path::new(manifest_dir) - .parent() - .expect("installer crate should be in workspace"); + let Some(workspace_root) = std::path::Path::new(manifest_dir).parent() else { + panic!("installer crate should be in workspace"); + }; let mut all_blocks = Vec::new(); for path in DOC_PATHS { let guide_path = workspace_root.join(path); - let content = std::fs::read_to_string(&guide_path) - .unwrap_or_else(|_| panic!("failed to read {path}")); + let Ok(content) = std::fs::read_to_string(&guide_path) else { + panic!("documentation file {path} should be readable"); + }; all_blocks.extend(extract_toml_blocks(&content)); } all_blocks @@ -52,8 +53,8 @@ fn accumulate_toml_line(line: &str, current_block: &mut String) { /// key = "value" /// ``` /// "#; -/// let blocks = extract_toml_blocks(markdown); -/// assert_eq!(blocks.len(), 1); +/// let blocks = `extract_toml_blocks(markdown)`; +/// `assert_eq!(blocks.len()`, 1); /// assert!(!blocks[0].contains("# A comment")); /// ``` pub fn extract_toml_blocks(markdown: &str) -> Vec { @@ -86,15 +87,16 @@ pub fn extract_toml_blocks(markdown: &str) -> Vec { /// Find a TOML block containing the specified marker text. pub fn find_block_containing(marker: &str) -> String { - DOC_TOML_BLOCKS - .iter() - .find(|block| block.contains(marker)) - .unwrap_or_else(|| panic!("no TOML block containing '{marker}' found in documentation")) - .clone() + let Some(block) = DOC_TOML_BLOCKS.iter().find(|block| block.contains(marker)) else { + panic!("no TOML block containing '{marker}' found in documentation"); + }; + block.clone() } #[cfg(test)] mod tests { + //! Tests for extracting TOML blocks from documentation. + use super::*; #[test] @@ -116,8 +118,10 @@ other = true let blocks = extract_toml_blocks(markdown); assert_eq!(blocks.len(), 2); - assert!(blocks[0].contains("key = \"value\"")); - assert!(blocks[1].contains("other = true")); + let first = blocks.first().expect("first TOML block should be present"); + let second = blocks.get(1).expect("second TOML block should be present"); + assert!(first.contains("key = \"value\"")); + assert!(second.contains("other = true")); } #[test] @@ -132,11 +136,12 @@ key = "value" let blocks = extract_toml_blocks(markdown); assert_eq!(blocks.len(), 1); + let block = blocks.first().expect("TOML block should be present"); assert!( - !blocks[0].contains("# This is a comment"), + !block.contains("# This is a comment"), "expected comment to be skipped" ); - assert!(blocks[0].contains("key = \"value\"")); + assert!(block.contains("key = \"value\"")); } #[test] diff --git a/installer/tests/support/mod.rs b/installer/tests/support/mod.rs index fbd8d8747..c16c923e3 100644 --- a/installer/tests/support/mod.rs +++ b/installer/tests/support/mod.rs @@ -4,39 +4,50 @@ //! including workspace path resolution, toolchain detection, and isolated rustup //! environment setup. -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + use tempfile::TempDir; use whitaker_installer::toolchain::parse_toolchain_channel; /// Returns the workspace root directory (parent of the installer crate). -pub fn workspace_root() -> PathBuf { +/// +/// # Errors +/// +/// Returns an error when the installer manifest directory has no parent. +pub fn workspace_root() -> Result { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() - .expect("installer crate is not at workspace root") - .to_path_buf() + .map(Path::to_path_buf) + .ok_or_else(|| String::from("installer crate is not at workspace root")) } /// Parses and returns the toolchain channel from rust-toolchain.toml. -pub fn pinned_toolchain_channel() -> String { - let toolchain_path = workspace_root().join("rust-toolchain.toml"); - let contents = - std::fs::read_to_string(&toolchain_path).expect("failed to read rust-toolchain.toml"); - parse_toolchain_channel(&contents).expect("failed to parse rust-toolchain.toml") +/// +/// # Errors +/// +/// Returns an error when rust-toolchain.toml cannot be read or parsed. +pub fn pinned_toolchain_channel() -> Result { + let toolchain_path = workspace_root()?.join("rust-toolchain.toml"); + let contents = std::fs::read_to_string(&toolchain_path) + .map_err(|error| format!("failed to read {}: {error}", toolchain_path.display()))?; + parse_toolchain_channel(&contents) + .map_err(|error| format!("failed to parse {}: {error}", toolchain_path.display())) } /// Checks if a toolchain is installed on the host system. /// -/// Sanitizes rustup environment by always setting RUSTUP_AUTO_INSTALL=0 and -/// RUSTUP_TOOLCHAIN to prevent host settings from leaking into tests. +/// Sanitizes rustup environment by always setting `RUSTUP_AUTO_INSTALL=0` and +/// `RUSTUP_TOOLCHAIN` to prevent host settings from leaking into tests. pub fn is_toolchain_installed(channel: &str) -> bool { Command::new("rustup") .args(["run", channel, "rustc", "--version"]) .env("RUSTUP_AUTO_INSTALL", "0") .env_remove("RUSTUP_TOOLCHAIN") .output() - .map(|o| o.status.success()) - .unwrap_or(false) + .is_ok_and(|o| o.status.success()) } /// Checks if a toolchain is installed in an isolated rustup environment. @@ -55,8 +66,7 @@ pub fn is_toolchain_installed_in_env( .env("RUSTUP_AUTO_INSTALL", "0") .env_remove("RUSTUP_TOOLCHAIN") .output() - .map(|o| o.status.success()) - .unwrap_or(false) + .is_ok_and(|o| o.status.success()) } /// Result of setting up an isolated rustup environment. @@ -68,13 +78,17 @@ pub struct IsolatedRustupEnv { /// Initializes an isolated rustup environment by running `rustup show`. /// /// This creates necessary settings files that rustup expects to exist. -/// The function sets RUSTUP_AUTO_INSTALL=0 to prevent auto-installing any -/// toolchain during initialization, clears RUSTUP_TOOLCHAIN to avoid +/// The function sets `RUSTUP_AUTO_INSTALL=0` to prevent auto-installing any +/// toolchain during initialization, clears `RUSTUP_TOOLCHAIN` to avoid /// rust-toolchain.toml files affecting initialization, and runs from -/// rustup_home as a current directory to prevent rustup from walking up +/// `rustup_home` as a current directory to prevent rustup from walking up /// to the workspace and discovering a project's rust-toolchain.toml /// (which would affect toolchain selection). -fn init_isolated_rustup(rustup_home: &Path, cargo_home: &Path) { +/// +/// # Errors +/// +/// Returns an error when rustup cannot be run or reports a failure. +fn init_isolated_rustup(rustup_home: &Path, cargo_home: &Path) -> Result<(), String> { let init_output = Command::new("rustup") .arg("show") .current_dir(rustup_home) // Prevent rustup from discovering workspace rust-toolchain.toml @@ -83,13 +97,14 @@ fn init_isolated_rustup(rustup_home: &Path, cargo_home: &Path) { .env("RUSTUP_AUTO_INSTALL", "0") .env_remove("RUSTUP_TOOLCHAIN") .output() - .expect("failed to initialise isolated rustup environment"); + .map_err(|error| format!("failed to initialize isolated rustup environment: {error}"))?; - assert!( - init_output.status.success(), - "failed to initialise isolated rustup: {}", - String::from_utf8_lossy(&init_output.stderr) - ); + if !init_output.status.success() { + return Err(format!( + "failed to initialize isolated rustup: {}", + String::from_utf8_lossy(&init_output.stderr) + )); + } let self_update_output = Command::new("rustup") .args(["set", "auto-self-update", "disable"]) @@ -99,80 +114,101 @@ fn init_isolated_rustup(rustup_home: &Path, cargo_home: &Path) { .env("RUSTUP_AUTO_INSTALL", "0") .env_remove("RUSTUP_TOOLCHAIN") .output() - .expect("failed to disable rustup self-update in isolated environment"); + .map_err(|error| { + format!("failed to disable rustup self-update in isolated environment: {error}") + })?; + + if !self_update_output.status.success() { + return Err(format!( + "failed to disable rustup self-update: {}", + String::from_utf8_lossy(&self_update_output.stderr) + )); + } - assert!( - self_update_output.status.success(), - "failed to disable rustup self-update: {}", - String::from_utf8_lossy(&self_update_output.stderr) - ); + Ok(()) } /// Parses the output of a command that locates rustup, extracting the first path. -fn parse_rustup_location_output(output: &std::process::Output) -> String { +/// +/// # Errors +/// +/// Returns an error when the command produced no output lines. +fn parse_rustup_location_output(output: &std::process::Output) -> Result { String::from_utf8_lossy(&output.stdout) .lines() .next() - .expect("rustup not found in PATH") - .trim() - .to_string() + .map(|line| line.trim().to_owned()) + .ok_or_else(|| String::from("rustup not found in PATH")) } /// Locates the system rustup binary path. +/// +/// # Errors +/// +/// Returns an error when the lookup command cannot run or rustup is absent. #[cfg(unix)] -fn find_system_rustup() -> String { +fn find_system_rustup() -> Result { let output = Command::new("which") .arg("rustup") .output() - .expect("failed to run which rustup"); + .map_err(|error| format!("failed to run which rustup: {error}"))?; parse_rustup_location_output(&output) } #[cfg(windows)] -fn find_system_rustup() -> String { +fn find_system_rustup() -> Result { let output = Command::new("where") .arg("rustup") .output() - .expect("failed to run where rustup"); + .map_err(|error| format!("failed to run where rustup: {error}"))?; parse_rustup_location_output(&output) } -/// Installs rustup into the isolated cargo_bin directory. +/// Installs rustup into the isolated `cargo_bin` directory. +/// +/// # Errors +/// +/// Returns an error when rustup cannot be linked or copied into `cargo_bin`. #[cfg(unix)] -fn install_rustup_to_cargo_bin(rustup_path: &str, cargo_bin: &Path) { +fn install_rustup_to_cargo_bin(rustup_path: &str, cargo_bin: &Path) -> Result<(), String> { std::os::unix::fs::symlink(rustup_path, cargo_bin.join("rustup")) - .expect("failed to symlink rustup to CARGO_HOME/bin"); + .map_err(|error| format!("failed to symlink rustup to CARGO_HOME/bin: {error}")) } #[cfg(windows)] -fn install_rustup_to_cargo_bin(rustup_path: &str, cargo_bin: &Path) { +fn install_rustup_to_cargo_bin(rustup_path: &str, cargo_bin: &Path) -> Result<(), String> { std::fs::copy(rustup_path, cargo_bin.join("rustup.exe")) - .expect("failed to copy rustup to CARGO_HOME/bin"); + .map(|_| ()) + .map_err(|error| format!("failed to copy rustup to CARGO_HOME/bin: {error}")) } -/// Sets up isolated RUSTUP_HOME and CARGO_HOME directories for testing. +/// Sets up isolated `RUSTUP_HOME` and `CARGO_HOME` directories for testing. /// /// This ensures the auto-install code path is exercised regardless of host state. /// The function initializes rustup in the isolated environment and makes the system /// rustup binary available (via symlink on Unix, copy on Windows). /// -/// # Panics +/// # Errors /// -/// Panics if the isolated environment cannot be created or initialized. -pub fn setup_isolated_rustup() -> IsolatedRustupEnv { - let rustup_home = TempDir::new().expect("failed to create RUSTUP_HOME temp dir"); - let cargo_home = TempDir::new().expect("failed to create CARGO_HOME temp dir"); +/// Returns an error if the isolated environment cannot be created or +/// initialized. +pub fn setup_isolated_rustup() -> Result { + let rustup_home = TempDir::new() + .map_err(|error| format!("failed to create RUSTUP_HOME temp dir: {error}"))?; + let cargo_home = + TempDir::new().map_err(|error| format!("failed to create CARGO_HOME temp dir: {error}"))?; - init_isolated_rustup(rustup_home.path(), cargo_home.path()); + init_isolated_rustup(rustup_home.path(), cargo_home.path())?; let cargo_bin = cargo_home.path().join("bin"); - std::fs::create_dir_all(&cargo_bin).expect("failed to create CARGO_HOME/bin"); + std::fs::create_dir_all(&cargo_bin) + .map_err(|error| format!("failed to create CARGO_HOME/bin: {error}"))?; - let rustup_path = find_system_rustup(); - install_rustup_to_cargo_bin(&rustup_path, &cargo_bin); + let rustup_path = find_system_rustup()?; + install_rustup_to_cargo_bin(&rustup_path, &cargo_bin)?; - IsolatedRustupEnv { + Ok(IsolatedRustupEnv { rustup_home, cargo_home, - } + }) } diff --git a/installer/tests/toolchain_steps/mod.rs b/installer/tests/toolchain_steps/mod.rs index 3a220dc77..9808b3856 100644 --- a/installer/tests/toolchain_steps/mod.rs +++ b/installer/tests/toolchain_steps/mod.rs @@ -3,20 +3,19 @@ //! These step implementations are used by the scenarios in //! `behaviour_toolchain.rs` via rstest-bdd macros. +mod scenario_setup; + +use std::process::Command; + use rstest::fixture; use rstest_bdd_macros::{given, then, when}; -use std::cell::{Cell, RefCell}; -use std::process::{Command, Output}; -use tempfile::TempDir; - -use super::prebuilt_markers::PREBUILT_INSTALL_MARKER; -use super::support::{ - is_toolchain_installed, is_toolchain_installed_in_env, pinned_toolchain_channel, - setup_isolated_rustup, workspace_root, +pub use scenario_setup::{FAKE_TOOLCHAIN, ToolchainWorld, setup_install_scenario}; +use scenario_setup::{ + ensure_toolchain_installed_in_isolated_env, get_combined_output_string, get_output, + get_stderr_string, setup_auto_install_scenario, setup_dry_run_scenario, setup_failure_scenario, }; -/// Non-existent toolchain channel used to exercise auto-install failure paths. -pub const FAKE_TOOLCHAIN: &str = "nonexistent-nightly-2024-01-01"; +use super::{prebuilt_markers::PREBUILT_INSTALL_MARKER, support::workspace_root}; /// Output marker indicating successful library staging (build-from-source path). const STAGING_OUTPUT_MARKER: &str = "Staging libraries to"; @@ -30,126 +29,62 @@ const TOOLCHAIN_ERROR_MARKER: &str = "installation failed"; /// Maximum output lines expected in quiet mode error scenarios. const QUIET_MODE_MAX_LINES: usize = 5; -#[derive(Default)] -pub struct ToolchainWorld { - pub args: RefCell>, - pub output: RefCell>, - pub should_skip_assertions: Cell, - pub temp_dir: RefCell>, - pub rustup_home: RefCell>, - pub cargo_home: RefCell>, - pub pinned_channel: RefCell, -} - -fn get_output(world: &ToolchainWorld) -> std::cell::Ref<'_, Output> { - let output = world.output.borrow(); - std::cell::Ref::map(output, |opt| opt.as_ref().expect("output not set")) -} - -fn get_combined_output_string(world: &ToolchainWorld) -> String { - let output = get_output(world); - format!( - "{}\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ) -} - -fn get_stderr_string(world: &ToolchainWorld) -> String { - let output = get_output(world); - String::from_utf8_lossy(&output.stderr).to_string() -} - macro_rules! skip_if_needed { ($world:expr) => { if $world.should_skip_assertions.get() { - return; + return Ok(()); } }; } -fn skip_scenario_when_toolchain_missing(world: &ToolchainWorld, channel: &str) { - if !is_toolchain_installed(channel) { - eprintln!("Skipping scenario: toolchain '{channel}' not installed."); - world.should_skip_assertions.set(true); - rstest_bdd::skip!("toolchain '{channel}' is not installed.", channel = channel); - } -} - -fn setup_temp_dir(world: &ToolchainWorld) -> String { - let temp_dir = TempDir::new().expect("failed to create temp dir"); - let target_dir = temp_dir.path().to_string_lossy().to_string(); - world.temp_dir.replace(Some(temp_dir)); - target_dir -} - -fn setup_dry_run_scenario(world: &ToolchainWorld, extra_args: &[&str]) { - let channel = pinned_toolchain_channel(); - skip_scenario_when_toolchain_missing(world, &channel); - world.pinned_channel.replace(channel.clone()); - - let target_dir = setup_temp_dir(world); - let mut args: Vec = extra_args.iter().map(|s| (*s).to_owned()).collect(); - args.extend(["--target-dir".to_owned(), target_dir]); - world.args.replace(args); -} - -pub fn setup_install_scenario(world: &ToolchainWorld, extra_args: &[&str]) { - let env = setup_isolated_rustup(); - world.rustup_home.replace(Some(env.rustup_home)); - world.cargo_home.replace(Some(env.cargo_home)); - world.pinned_channel.replace(pinned_toolchain_channel()); +fn assert_toolchain_install_message_presence( + world: &ToolchainWorld, + expected_presence: bool, +) -> Result<(), String> { + skip_if_needed!(world); - let target_dir = setup_temp_dir(world); - let mut args: Vec = extra_args.iter().map(|s| (*s).to_owned()).collect(); - args.extend(["--target-dir".to_owned(), target_dir]); - world.args.replace(args); + let output = get_combined_output_string(world)?; + let channel = world.pinned_channel.borrow().clone(); + let output_lowercase = output.to_lowercase(); + let channel_lowercase = channel.to_lowercase(); + let expected_message = format!("toolchain {channel} installed successfully").to_lowercase(); + let has_install_message = output_lowercase.contains(&expected_message) + || output_lowercase.contains(&channel_lowercase) + && output_lowercase.contains(TOOLCHAIN_INSTALLED_MARKER); + + if has_install_message == expected_presence { + Ok(()) + } else if expected_presence { + Err(format!( + "expected success marker for channel '{channel}' in output, got:\n{output}" + )) + } else { + Err(format!( + "expected no installation message for channel '{channel}' in output, got:\n{output}" + )) + } } -fn setup_failure_scenario(world: &ToolchainWorld, extra_args: &[&str]) { - // Use isolated rustup environment so the install failure path is exercised - // without affecting the host system. - let env = setup_isolated_rustup(); - world.rustup_home.replace(Some(env.rustup_home)); - world.cargo_home.replace(Some(env.cargo_home)); - - let target_dir = setup_temp_dir(world); - // Filter out --dry-run to exercise the real install path - let mut args: Vec = extra_args - .iter() - .filter(|s| **s != "--dry-run") - .map(|s| (*s).to_owned()) - .collect(); - args.extend([ - "--toolchain".to_owned(), - FAKE_TOOLCHAIN.to_owned(), - "--target-dir".to_owned(), - target_dir, - "--skip-deps".to_owned(), - ]); - world.args.replace(args); -} +fn assert_stderr_contains( + world: &ToolchainWorld, + expected: &str, + failure_message: impl FnOnce(&str) -> String, +) -> Result<(), String> { + skip_if_needed!(world); -fn assert_toolchain_installed_in_isolated_env(world: &ToolchainWorld) { - let rustup_home = world.rustup_home.borrow(); - let cargo_home = world.cargo_home.borrow(); - assert!( - rustup_home.is_some() && cargo_home.is_some(), - "isolated rustup environment must be configured for install scenario" - ); - let rustup = rustup_home.as_ref().expect("rustup_home"); - let cargo = cargo_home.as_ref().expect("cargo_home"); - let channel = pinned_toolchain_channel(); - assert!( - is_toolchain_installed_in_env(&channel, rustup, cargo), - "toolchain '{channel}' was not installed in isolated environment" - ); + let stderr = get_stderr_string(world)?; + if stderr.contains(expected) { + Ok(()) + } else { + Err(failure_message(&stderr)) + } } // --------------------------------------------------------------------------- // Fixture // --------------------------------------------------------------------------- +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] pub fn world() -> ToolchainWorld { ToolchainWorld::default() @@ -160,55 +95,42 @@ pub fn world() -> ToolchainWorld { // --------------------------------------------------------------------------- #[given("the installer is invoked with auto-detect toolchain")] -pub fn given_auto_detect_toolchain(world: &ToolchainWorld) { - setup_dry_run_scenario(world, &["--dry-run"]); +pub fn given_auto_detect_toolchain(world: &ToolchainWorld) -> Result<(), String> { + setup_dry_run_scenario(world, &["--dry-run"]) } #[given("the installer is invoked with auto-detect toolchain in quiet mode")] -pub fn given_auto_detect_toolchain_quiet(world: &ToolchainWorld) { - setup_dry_run_scenario(world, &["--dry-run", "--quiet"]); -} - -fn setup_auto_install_scenario(world: &ToolchainWorld) { - // Skip auto-install tests on Windows - toolchain downloads are extremely slow - // due to Windows Defender scanning and larger binaries. The code path is - // identical to Linux; we're testing rustup behaviour rather than installer logic. - if cfg!(windows) { - eprintln!("Skipping auto-install scenario on Windows (toolchain downloads too slow)."); - world.should_skip_assertions.set(true); - rstest_bdd::skip!("auto-install tests skipped on Windows"); - } - // Use --skip-wrapper to prevent writing to the user's real ~/.local/bin. - setup_install_scenario(world, &["--jobs", "1", "--skip-deps", "--skip-wrapper"]); +pub fn given_auto_detect_toolchain_quiet(world: &ToolchainWorld) -> Result<(), String> { + setup_dry_run_scenario(world, &["--dry-run", "--quiet"]) } #[given("the installer is invoked with auto-detect toolchain to a temporary directory")] -pub fn given_auto_detect_toolchain_install(world: &ToolchainWorld) { - setup_auto_install_scenario(world); +pub fn given_auto_detect_toolchain_install(world: &ToolchainWorld) -> Result<(), String> { + setup_auto_install_scenario(world) } #[given("the installer is invoked with isolated rustup to force auto-install")] -pub fn given_isolated_rustup_auto_install(world: &ToolchainWorld) { - setup_auto_install_scenario(world); +pub fn given_isolated_rustup_auto_install(world: &ToolchainWorld) -> Result<(), String> { + setup_auto_install_scenario(world) } #[given("the installer is invoked with isolated rustup in quiet mode")] -pub fn given_isolated_rustup_quiet(world: &ToolchainWorld) { +pub fn given_isolated_rustup_quiet(world: &ToolchainWorld) -> Result<(), String> { // Use --skip-wrapper to prevent writing to the user's real ~/.local/bin. setup_install_scenario( world, &["--jobs", "1", "--quiet", "--skip-deps", "--skip-wrapper"], - ); + ) } #[given("the installer is invoked with a non-existent toolchain")] -pub fn given_nonexistent_toolchain(world: &ToolchainWorld) { - setup_failure_scenario(world, &[]); +pub fn given_nonexistent_toolchain(world: &ToolchainWorld) -> Result<(), String> { + setup_failure_scenario(world, &[]) } #[given("the installer is invoked with a non-existent toolchain in quiet mode")] -pub fn given_nonexistent_toolchain_quiet(world: &ToolchainWorld) { - setup_failure_scenario(world, &["--quiet"]); +pub fn given_nonexistent_toolchain_quiet(world: &ToolchainWorld) -> Result<(), String> { + setup_failure_scenario(world, &["--quiet"]) } // --------------------------------------------------------------------------- @@ -216,13 +138,13 @@ pub fn given_nonexistent_toolchain_quiet(world: &ToolchainWorld) { // --------------------------------------------------------------------------- #[when("the installer CLI is run")] -pub fn when_installer_cli_run(world: &ToolchainWorld) { +pub fn when_installer_cli_run(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); let args = world.args.borrow(); let mut cmd = Command::new(env!("CARGO_BIN_EXE_whitaker-installer")); cmd.args(args.iter()); - cmd.current_dir(workspace_root()); + cmd.current_dir(workspace_root()?); // Sanitize rustup environment to prevent host settings from leaking // into tests: always disable auto-install and remove toolchain overrides @@ -236,8 +158,11 @@ pub fn when_installer_cli_run(world: &ToolchainWorld) { cmd.env("CARGO_HOME", cargo_home.path()); } - let output = cmd.output().expect("failed to run whitaker-installer"); + let output = cmd + .output() + .map_err(|error| format!("failed to run whitaker-installer: {error}"))?; world.output.replace(Some(output)); + Ok(()) } // --------------------------------------------------------------------------- @@ -245,131 +170,123 @@ pub fn when_installer_cli_run(world: &ToolchainWorld) { // --------------------------------------------------------------------------- #[then("the CLI exits successfully")] -pub fn then_cli_exits_successfully(world: &ToolchainWorld) { +pub fn then_cli_exits_successfully(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(world); - assert!( - output.status.success(), - "expected success, stderr: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output = get_output(world)?; + if output.status.success() { + Ok(()) + } else { + Err(format!( + "expected success, stderr: {}", + String::from_utf8_lossy(&output.stderr) + )) + } } #[then("dry-run output shows the detected toolchain")] -pub fn then_dry_run_shows_toolchain(world: &ToolchainWorld) { +pub fn then_dry_run_shows_toolchain(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(world); + let output = get_output(world)?; let stderr = String::from_utf8_lossy(&output.stderr); let expected_channel = world.pinned_channel.borrow().clone(); - assert!( - stderr.contains(&expected_channel), - "expected toolchain '{expected_channel}' in output, stderr: {stderr}" - ); + if stderr.contains(&expected_channel) { + Ok(()) + } else { + Err(format!( + "expected toolchain '{expected_channel}' in output, stderr: {stderr}" + )) + } } #[then("no toolchain installation message is shown")] -pub fn then_no_install_message(world: &ToolchainWorld) { - skip_if_needed!(world); - let out = get_combined_output_string(world); - let channel = world.pinned_channel.borrow().clone(); - let out_lc = out.to_lowercase(); - let needle = format!("toolchain {channel} installed successfully").to_lowercase(); - assert!( - !(out_lc.contains(&needle) - || out_lc.contains(&channel.to_lowercase()) - && out_lc.contains(TOOLCHAIN_INSTALLED_MARKER)), - "expected no installation message for channel '{channel}' in output, got:\n{out}" - ); +pub fn then_no_install_message(world: &ToolchainWorld) -> Result<(), String> { + assert_toolchain_install_message_presence(world, false) } #[then("the toolchain installation message is shown")] -pub fn then_install_message_shown(world: &ToolchainWorld) { - skip_if_needed!(world); - let out = get_combined_output_string(world); - let channel = world.pinned_channel.borrow().clone(); - let out_lc = out.to_lowercase(); - let needle = format!("toolchain {channel} installed successfully").to_lowercase(); - let ok = out_lc.contains(&needle) - || (out_lc.contains("installed successfully") && out_lc.contains(&channel.to_lowercase())); - assert!( - ok, - "expected success marker for channel '{channel}' in output, got:\n{out}" - ); +pub fn then_install_message_shown(world: &ToolchainWorld) -> Result<(), String> { + assert_toolchain_install_message_presence(world, true) } #[then("installation succeeds or is skipped")] -pub fn then_installation_succeeds_or_is_skipped(world: &ToolchainWorld) { +pub fn then_installation_succeeds_or_is_skipped(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(world); - assert!( - output.status.success(), - "installation failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - assert_toolchain_installed_in_isolated_env(world); + { + let output = get_output(world)?; + if !output.status.success() { + return Err(format!( + "installation failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + } + ensure_toolchain_installed_in_isolated_env(world) } #[then("the toolchain is installed in the isolated environment")] -pub fn then_toolchain_installed_in_isolated_env(world: &ToolchainWorld) { +pub fn then_toolchain_installed_in_isolated_env(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - assert_toolchain_installed_in_isolated_env(world); + ensure_toolchain_installed_in_isolated_env(world) } #[then("the suite library is staged")] -pub fn then_suite_library_is_staged(world: &ToolchainWorld) { +pub fn then_suite_library_is_staged(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(world); + let output = get_output(world)?; let stderr = String::from_utf8_lossy(&output.stderr); // Accept either the build-from-source staging marker or the prebuilt // success marker — when prebuilt artefacts are available the installer // downloads them instead of building locally. let has_local_staging_marker = stderr.contains(STAGING_OUTPUT_MARKER); let has_prebuilt_staging_marker = stderr.contains(PREBUILT_INSTALL_MARKER); - assert!( - has_local_staging_marker || has_prebuilt_staging_marker, - "expected '{STAGING_OUTPUT_MARKER}' or '{PREBUILT_INSTALL_MARKER}' in staging output, stderr: {stderr}" - ); + if has_local_staging_marker || has_prebuilt_staging_marker { + Ok(()) + } else { + Err(format!( + "expected '{STAGING_OUTPUT_MARKER}' or '{PREBUILT_INSTALL_MARKER}' in staging output, \ + stderr: {stderr}" + )) + } } #[then("the CLI exits with an error")] -pub fn then_cli_exits_with_error(world: &ToolchainWorld) { +pub fn then_cli_exits_with_error(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(world); - assert!( - !output.status.success(), - "expected failure exit code, but command succeeded" - ); + let output = get_output(world)?; + if output.status.success() { + return Err(String::from( + "expected failure exit code, but command succeeded", + )); + } + Ok(()) } #[then("the error mentions toolchain installation failure")] -pub fn then_error_mentions_install_failure(world: &ToolchainWorld) { - skip_if_needed!(world); - let stderr = get_stderr_string(world); - assert!( - stderr.contains(TOOLCHAIN_ERROR_MARKER), - "expected '{}' in stderr: {stderr}", - TOOLCHAIN_ERROR_MARKER - ); +pub fn then_error_mentions_install_failure(world: &ToolchainWorld) -> Result<(), String> { + assert_stderr_contains(world, TOOLCHAIN_ERROR_MARKER, |stderr| { + format!("expected '{TOOLCHAIN_ERROR_MARKER}' in stderr: {stderr}") + }) } #[then("the error includes the toolchain name")] -pub fn then_error_includes_toolchain_name(world: &ToolchainWorld) { - skip_if_needed!(world); - let stderr = get_stderr_string(world); - assert!( - stderr.contains(FAKE_TOOLCHAIN), - "expected toolchain name '{FAKE_TOOLCHAIN}' in error output, stderr: {stderr}" - ); +pub fn then_error_includes_toolchain_name(world: &ToolchainWorld) -> Result<(), String> { + assert_stderr_contains(world, FAKE_TOOLCHAIN, |stderr| { + format!("expected toolchain name '{FAKE_TOOLCHAIN}' in error output, stderr: {stderr}") + }) } #[then("the error output is minimal")] -pub fn then_error_output_is_minimal(world: &ToolchainWorld) { +pub fn then_error_output_is_minimal(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(world); + let output = get_output(world)?; let stderr = String::from_utf8_lossy(&output.stderr); let line_count = stderr.lines().count(); - assert!( - line_count <= QUIET_MODE_MAX_LINES, - "expected at most {QUIET_MODE_MAX_LINES} lines in quiet mode, got {line_count}: {stderr}" - ); + if line_count <= QUIET_MODE_MAX_LINES { + Ok(()) + } else { + Err(format!( + "expected at most {QUIET_MODE_MAX_LINES} lines in quiet mode, got {line_count}: \ + {stderr}" + )) + } } diff --git a/installer/tests/toolchain_steps/scenario_setup.rs b/installer/tests/toolchain_steps/scenario_setup.rs new file mode 100644 index 000000000..872d2e526 --- /dev/null +++ b/installer/tests/toolchain_steps/scenario_setup.rs @@ -0,0 +1,174 @@ +//! Scenario setup helpers and shared world state for toolchain behaviour tests. +//! +//! The step definitions in the parent module drive these helpers; keeping them +//! here keeps each module within the repository's size budget. + +use std::{ + cell::{Cell, RefCell}, + io::Write as _, + process::Output, +}; + +use tempfile::TempDir; + +use crate::support::{ + is_toolchain_installed, is_toolchain_installed_in_env, pinned_toolchain_channel, + setup_isolated_rustup, +}; + +/// Non-existent toolchain channel used to exercise auto-install failure paths. +pub const FAKE_TOOLCHAIN: &str = "nonexistent-nightly-2024-01-01"; + +#[derive(Default)] +pub struct ToolchainWorld { + pub args: RefCell>, + pub output: RefCell>, + pub should_skip_assertions: Cell, + pub temp_dir: RefCell>, + pub rustup_home: RefCell>, + pub cargo_home: RefCell>, + pub pinned_channel: RefCell, +} + +/// Borrows the captured CLI output, failing when no command has run yet. +pub(super) fn get_output(world: &ToolchainWorld) -> Result, String> { + let output = world.output.borrow(); + std::cell::Ref::filter_map(output, Option::as_ref) + .map_err(|_| String::from("CLI output not set; run the installer step first")) +} + +pub(super) fn get_combined_output_string(world: &ToolchainWorld) -> Result { + let output = get_output(world)?; + Ok(format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + )) +} + +pub(super) fn get_stderr_string(world: &ToolchainWorld) -> Result { + let output = get_output(world)?; + Ok(String::from_utf8_lossy(&output.stderr).to_string()) +} + +/// Reports a skipped scenario on stderr without tripping `print_stderr`. +fn report_skip(reason: &str) -> Result<(), String> { + writeln!(std::io::stderr(), "{reason}") + .map_err(|error| format!("failed to report skipped scenario: {error}")) +} + +fn skip_scenario_when_toolchain_missing( + world: &ToolchainWorld, + channel: &str, +) -> Result<(), String> { + if !is_toolchain_installed(channel) { + report_skip(&format!( + "Skipping scenario: toolchain '{channel}' not installed." + ))?; + world.should_skip_assertions.set(true); + rstest_bdd::skip!("toolchain '{channel}' is not installed.", channel = channel); + } + Ok(()) +} + +fn setup_temp_dir(world: &ToolchainWorld) -> Result { + let temp_dir = TempDir::new().map_err(|error| format!("failed to create temp dir: {error}"))?; + let target_dir = temp_dir.path().to_string_lossy().to_string(); + world.temp_dir.replace(Some(temp_dir)); + Ok(target_dir) +} + +pub(super) fn setup_dry_run_scenario( + world: &ToolchainWorld, + extra_args: &[&str], +) -> Result<(), String> { + let channel = pinned_toolchain_channel()?; + skip_scenario_when_toolchain_missing(world, &channel)?; + world.pinned_channel.replace(channel.clone()); + + let target_dir = setup_temp_dir(world)?; + let mut args: Vec = extra_args.iter().map(|s| (*s).to_owned()).collect(); + args.extend(["--target-dir".to_owned(), target_dir]); + world.args.replace(args); + Ok(()) +} + +/// Prepares an isolated rustup environment and CLI arguments for an install run. +/// +/// # Errors +/// +/// Returns an error when the isolated environment or temporary directory +/// cannot be created, or the pinned toolchain cannot be resolved. +pub fn setup_install_scenario(world: &ToolchainWorld, extra_args: &[&str]) -> Result<(), String> { + let env = setup_isolated_rustup()?; + world.rustup_home.replace(Some(env.rustup_home)); + world.cargo_home.replace(Some(env.cargo_home)); + world.pinned_channel.replace(pinned_toolchain_channel()?); + + let target_dir = setup_temp_dir(world)?; + let mut args: Vec = extra_args.iter().map(|s| (*s).to_owned()).collect(); + args.extend(["--target-dir".to_owned(), target_dir]); + world.args.replace(args); + Ok(()) +} + +pub(super) fn setup_failure_scenario( + world: &ToolchainWorld, + extra_args: &[&str], +) -> Result<(), String> { + // Use isolated rustup environment so the install failure path is exercised + // without affecting the host system. + let env = setup_isolated_rustup()?; + world.rustup_home.replace(Some(env.rustup_home)); + world.cargo_home.replace(Some(env.cargo_home)); + + let target_dir = setup_temp_dir(world)?; + // Filter out --dry-run to exercise the real install path + let mut args: Vec = extra_args + .iter() + .filter(|s| **s != "--dry-run") + .map(|s| (*s).to_owned()) + .collect(); + args.extend([ + "--toolchain".to_owned(), + FAKE_TOOLCHAIN.to_owned(), + "--target-dir".to_owned(), + target_dir, + "--skip-deps".to_owned(), + ]); + world.args.replace(args); + Ok(()) +} + +pub(super) fn ensure_toolchain_installed_in_isolated_env( + world: &ToolchainWorld, +) -> Result<(), String> { + let rustup_home = world.rustup_home.borrow(); + let cargo_home = world.cargo_home.borrow(); + let (Some(rustup), Some(cargo)) = (rustup_home.as_ref(), cargo_home.as_ref()) else { + return Err(String::from( + "isolated rustup environment must be configured for install scenario", + )); + }; + let channel = pinned_toolchain_channel()?; + if is_toolchain_installed_in_env(&channel, rustup, cargo) { + Ok(()) + } else { + Err(format!( + "toolchain '{channel}' was not installed in isolated environment" + )) + } +} + +pub(super) fn setup_auto_install_scenario(world: &ToolchainWorld) -> Result<(), String> { + // Skip auto-install tests on Windows - toolchain downloads are extremely slow + // due to Windows Defender scanning and larger binaries. The code path is + // identical to Linux; we're testing rustup behaviour rather than installer logic. + if cfg!(windows) { + report_skip("Skipping auto-install scenario on Windows (toolchain downloads too slow).")?; + world.should_skip_assertions.set(true); + rstest_bdd::skip!("auto-install tests skipped on Windows"); + } + // Use --skip-wrapper to prevent writing to the user's real ~/.local/bin. + setup_install_scenario(world, &["--jobs", "1", "--skip-deps", "--skip-wrapper"]) +} diff --git a/installer/tests/ui.rs b/installer/tests/ui.rs index dc04ac2ec..5bf326536 100644 --- a/installer/tests/ui.rs +++ b/installer/tests/ui.rs @@ -3,11 +3,10 @@ //! `sha2` 0.11 made two source-breaking changes that the installer had to work //! around: //! -//! - `Sha256::finalize()` / `Sha256::digest()` now return -//! `hybrid_array::Array`, which does not implement -//! [`core::fmt::LowerHex`], so `format!("{:x}", digest)` no longer compiles. -//! - `Sha256` no longer implements [`std::io::Write`], so -//! `io::copy(reader, &mut hasher)` no longer compiles. +//! - `Sha256::finalize()` / `Sha256::digest()` now return `hybrid_array::Array`, which does +//! not implement [`core::fmt::LowerHex`], so `format!("{:x}", digest)` no longer compiles. +//! - `Sha256` no longer implements [`std::io::Write`], so `io::copy(reader, &mut hasher)` no longer +//! compiles. //! //! These `trybuild` cases pin those breaks: if a future change reintroduces the //! pre-0.11 pattern — for example by downgrading `sha2` back to 0.10 — the diff --git a/suite/Cargo.toml b/suite/Cargo.toml index 780108db7..5623c642d 100644 --- a/suite/Cargo.toml +++ b/suite/Cargo.toml @@ -49,6 +49,7 @@ bumpy_road_function = { path = "../crates/bumpy_road_function", optional = true, rstest_helper_should_be_fixture = { path = "../crates/rstest_helper_should_be_fixture", optional = true, features = ["dylint-driver", "constituent"] } [dev-dependencies] +whitaker_test_macros = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } diff --git a/suite/tests/registration.rs b/suite/tests/registration.rs index d137100a3..2c1f78865 100644 --- a/suite/tests/registration.rs +++ b/suite/tests/registration.rs @@ -1,12 +1,15 @@ +//! Behaviour-driven tests for the suite registration wiring. #![feature(rustc_private)] #![cfg(feature = "dylint-driver")] -//! Behaviour-driven tests for the suite registration wiring. + +use std::{ + cell::RefCell, + panic::{AssertUnwindSafe, catch_unwind}, +}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use rustc_lint::LintStore; -use std::cell::RefCell; -use std::panic::{AssertUnwindSafe, catch_unwind}; use whitaker_suite::{register_suite_lints, suite_lint_decls, suite_lint_names}; struct RegistrationWorld { @@ -28,6 +31,7 @@ impl RegistrationWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> RegistrationWorld { RegistrationWorld::new() @@ -49,15 +53,16 @@ fn when_register_suite(world: &RegistrationWorld) { let registration = catch_unwind(AssertUnwindSafe(|| { register_suite_lints(&mut world.store.borrow_mut()); })) - .map(|_| ()) .map_err(|panic| { - if let Some(message) = panic.downcast_ref::<&str>() { - (*message).to_string() - } else if let Some(message) = panic.downcast_ref::() { - message.clone() - } else { - "registration panicked with a non-string payload".to_string() - } + panic.downcast_ref::<&str>().map_or_else( + || { + panic.downcast_ref::().map_or_else( + || "registration panicked with a non-string payload".to_owned(), + Clone::clone, + ) + }, + |message| (*message).to_owned(), + ) }); *world.result.borrow_mut() = registration; @@ -83,7 +88,7 @@ fn then_names_match(world: &RegistrationWorld) { .iter() .map(|lint| lint.name_lower()) .collect(); - let expected: Vec = suite_lint_names().map(str::to_string).collect(); + let expected: Vec = suite_lint_names().map(str::to_owned).collect(); assert_eq!(registered, expected); } @@ -94,7 +99,7 @@ fn then_decls_align() { .iter() .map(|lint| lint.name_lower()) .collect(); - let expected: Vec = suite_lint_names().map(str::to_string).collect(); + let expected: Vec = suite_lint_names().map(str::to_owned).collect(); assert_eq!(declared, expected); } From c15cc2e7c7da892456f72e12afdd671251f8696f Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 29 Aug 2026 00:28:37 +0200 Subject: [PATCH 2/9] Apply sweep findings across tests and assertion helpers Follow-up pass over the base sweep, driven by a seven-category audit: - whitaker_sarif: collapse ~18 near-identical serde match-panic round-trip blocks into shared test-support helpers (JSON round-trip and serialize-then-check), keeping match-panic internally so the helpers stay clean under a future no-expect-outside-tests gate. - config_loading behaviour steps: extract the repeated shared-config lookup into one fallible helper feeding five thin then-steps. - behaviour_cli support assertions: decompose the monolithic assertion functions into fallible lookups, pure queries, and thin wrappers, following the toolchain_steps exemplar. - whitaker_clones_core: split assert_region's fallible computation from its equality assertion, and convert ten ast tests whose Result returns were decorative (every `?` on always-valid literal constructors) into plain asserting tests with descriptive expects. --- crates/whitaker_clones_core/src/ast/tests.rs | 72 +++--- crates/whitaker_clones_core/src/run0/tests.rs | 24 +- crates/whitaker_sarif/src/model/descriptor.rs | 45 ++-- crates/whitaker_sarif/src/model/location.rs | 39 +--- crates/whitaker_sarif/src/model/log.rs | 31 +-- crates/whitaker_sarif/src/model/result.rs | 72 +++--- crates/whitaker_sarif/src/model/run.rs | 41 ++-- crates/whitaker_sarif/src/test_support.rs | 39 ++++ .../whitaker_sarif/src/whitaker_properties.rs | 11 +- installer/tests/behaviour_cli/support.rs | 12 +- .../tests/behaviour_cli/support_assertions.rs | 207 ++++++++++++------ tests/config_loading.rs | 90 ++++---- 12 files changed, 375 insertions(+), 308 deletions(-) diff --git a/crates/whitaker_clones_core/src/ast/tests.rs b/crates/whitaker_clones_core/src/ast/tests.rs index 5560ed1fc..12ebcbb2c 100644 --- a/crates/whitaker_clones_core/src/ast/tests.rs +++ b/crates/whitaker_clones_core/src/ast/tests.rs @@ -10,15 +10,15 @@ use rstest::rstest; #[cfg(not(feature = "parser"))] #[rstest] -fn parser_free_lowering_reports_parser_unavailable() -> AstResult<()> { +fn parser_free_lowering_reports_parser_unavailable() { let source = "fn f() {}"; - let span = ByteSpan::new(source, 0, source.len() as u32)?; + let span = ByteSpan::new(source, 0, source.len() as u32) + .expect("span covering the full source literal should be valid"); assert_eq!( super::lower_span(source, span), Err(super::AstError::ParserUnavailable) ); - Ok(()) } #[rstest] @@ -57,18 +57,18 @@ fn equal_width_covering_candidates_select_the_first() { } #[rstest] -fn deeply_nested_trees_extract_features_and_hash_without_recursion() -> AstResult<()> { - let tree = tree_with_root(deep_chain(2_048))?; +fn deeply_nested_trees_extract_features_and_hash_without_recursion() { + let tree = + tree_with_root(deep_chain(2_048)).expect("deep chain tree with static span should build"); assert_eq!(kind_counts(&tree).iter().count(), 2_049); assert_eq!(production_multiset(&tree).bigrams().count(), 1); assert_eq!(canonical_hash(&tree).to_hex().len(), 16); - Ok(()) } #[rstest] -fn kind_counts_record_depth_resolved_counts() -> AstResult<()> { - let counts = kind_counts(&feature_tree()?); +fn kind_counts_record_depth_resolved_counts() { + let counts = kind_counts(&feature_tree().expect("feature tree with static span should build")); let expected = [ (KindId::new(1), Depth::root(), 1), @@ -79,13 +79,12 @@ fn kind_counts_record_depth_resolved_counts() -> AstResult<()> { ]; assert_eq!(counts.iter().collect::>(), expected); - - Ok(()) } #[rstest] -fn weighted_histogram_applies_dyadic_depth_weights() -> AstResult<()> { - let histogram = kind_histogram(&feature_tree()?); +fn weighted_histogram_applies_dyadic_depth_weights() { + let histogram = + kind_histogram(&feature_tree().expect("feature tree with static span should build")); assert_eq!( histogram.get(KindId::new(1)).map(KindWeight::get), @@ -99,29 +98,28 @@ fn weighted_histogram_applies_dyadic_depth_weights() -> AstResult<()> { histogram.get(KindId::new(4)).map(KindWeight::get), Some(KindWeight::SCALE >> 2) ); - - Ok(()) } #[rstest] -fn weighted_histogram_accumulates_four_equal_depth_one_kinds() -> AstResult<()> { +fn weighted_histogram_accumulates_four_equal_depth_one_kinds() { let kind = KindId::new(9); let tree = tree_with_root(NormalizedNode::new( KindId::new(1), None, (0..4).map(|_| ident(kind)).collect(), - ))?; + )) + .expect("four-leaf tree with static span should build"); assert_eq!( kind_histogram(&tree).get(kind).map(KindWeight::get), Some(4 * (KindWeight::SCALE >> 1)) ); - Ok(()) } #[rstest] -fn production_multiset_records_bigrams_and_trigrams() -> AstResult<()> { - let productions = production_multiset(&feature_tree()?); +fn production_multiset_records_bigrams_and_trigrams() { + let productions = + production_multiset(&feature_tree().expect("feature tree with static span should build")); assert_eq!( productions.count(Production::Bigram(KindId::new(1), KindId::new(2))), @@ -147,38 +145,32 @@ fn production_multiset_records_bigrams_and_trigrams() -> AstResult<()> { )), 0 ); - - Ok(()) } #[rstest] -fn canonical_hash_is_stable_for_equivalent_trees() -> AstResult<()> { +fn canonical_hash_is_stable_for_equivalent_trees() { assert_eq!( - canonical_hash(&feature_tree()?), - canonical_hash(&feature_tree()?) + canonical_hash(&feature_tree().expect("feature tree with static span should build")), + canonical_hash(&feature_tree().expect("feature tree with static span should build")) ); - - Ok(()) } #[rstest] -fn canonical_hash_is_sensitive_to_child_order() -> AstResult<()> { +fn canonical_hash_is_sensitive_to_child_order() { assert_ne!( - canonical_hash(&feature_tree()?), - canonical_hash(&reordered_tree()?) + canonical_hash(&feature_tree().expect("feature tree with static span should build")), + canonical_hash(&reordered_tree().expect("reordered tree with static span should build")) ); - - Ok(()) } #[rstest] -fn canonical_hash_is_sensitive_to_leaf_class() -> AstResult<()> { +fn canonical_hash_is_sensitive_to_leaf_class() { assert_ne!( - canonical_hash(&feature_tree()?), - canonical_hash(&different_leaf_tree()?) + canonical_hash(&feature_tree().expect("feature tree with static span should build")), + canonical_hash( + &different_leaf_tree().expect("different-leaf tree with static span should build") + ) ); - - Ok(()) } fn feature_tree() -> AstResult { @@ -233,13 +225,14 @@ fn literal(kind: KindId) -> NormalizedNode { } #[rstest] -fn feature_functions_reflect_tree_contents() -> AstResult<()> { - let expected = feature_tree()?; +fn feature_functions_reflect_tree_contents() { + let expected = feature_tree().expect("feature tree with static span should build"); let distinct = tree_with_root(NormalizedNode::new( KindId::new(9), None, vec![literal(KindId::new(8))], - ))?; + )) + .expect("distinct tree with static span should build"); assert_ne!(kind_counts(&expected), kind_counts(&distinct)); assert_ne!(kind_histogram(&expected), kind_histogram(&distinct)); @@ -248,7 +241,6 @@ fn feature_functions_reflect_tree_contents() -> AstResult<()> { production_multiset(&distinct) ); assert_ne!(canonical_hash(&expected), canonical_hash(&distinct)); - Ok(()) } proptest! { diff --git a/crates/whitaker_clones_core/src/run0/tests.rs b/crates/whitaker_clones_core/src/run0/tests.rs index f0c8d3dff..cf5ed63fb 100644 --- a/crates/whitaker_clones_core/src/run0/tests.rs +++ b/crates/whitaker_clones_core/src/run0/tests.rs @@ -31,10 +31,12 @@ fn assert_single_accepted( assert_eq!(accepted[0].score(), expected_score); } -fn assert_region(id: &str, source: &str, range: std::ops::Range, expected: Region) { - let region = region_for_range(id, source, range) - .unwrap_or_else(|error| panic!("unexpected region error: {error}")); - assert_eq!(region, expected); +/// Resolves the region for `range`, panicking with context on failure. +fn resolved_region(id: &str, source: &str, range: std::ops::Range) -> Region { + match region_for_range(id, source, range) { + Ok(region) => region, + Err(error) => panic!("unexpected region error for fragment '{id}': {error}"), + } } #[test] @@ -115,10 +117,9 @@ fn just_below_threshold_is_rejected() { #[test] fn single_line_region_uses_one_based_columns() { - assert_region( - "alpha", - "fn a() {}\n", - 0..8, + let region = resolved_region("alpha", "fn a() {}\n", 0..8); + assert_eq!( + region, Region { start_line: 1, start_column: Some(1), @@ -132,10 +133,9 @@ fn single_line_region_uses_one_based_columns() { #[test] fn multi_line_region_tracks_trailing_newline() { - assert_region( - "alpha", - "fn alpha() {\n value();\n}\n", - 13..27, + let region = resolved_region("alpha", "fn alpha() {\n value();\n}\n", 13..27); + assert_eq!( + region, Region { start_line: 2, start_column: Some(1), diff --git a/crates/whitaker_sarif/src/model/descriptor.rs b/crates/whitaker_sarif/src/model/descriptor.rs index 1f64460f1..7898ee845 100644 --- a/crates/whitaker_sarif/src/model/descriptor.rs +++ b/crates/whitaker_sarif/src/model/descriptor.rs @@ -61,6 +61,7 @@ pub struct MultiformatMessageString { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{assert_json_round_trip, assert_serialized_json}; #[test] fn descriptor_round_trip() { @@ -72,13 +73,7 @@ mod tests { }), help_uri: Some("https://example.com".into()), }; - match serde_json::to_string(&desc) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(desc, parsed), - Err(e) => panic!("failed to deserialize: {e}"), - }, - Err(e) => panic!("failed to serialize: {e}"), - } + assert_json_round_trip(&desc); } #[test] @@ -89,14 +84,17 @@ mod tests { short_description: None, help_uri: None, }; - match serde_json::to_string(&desc) { - Ok(json) => { - assert!(!json.contains("\"name\"")); - assert!(!json.contains("\"shortDescription\"")); - assert!(!json.contains("\"helpUri\"")); - } - Err(e) => panic!("failed to serialize: {e}"), - } + assert_serialized_json(&desc, |json| { + assert!(!json.contains("\"name\""), "unset name present: {json}"); + assert!( + !json.contains("\"shortDescription\""), + "unset shortDescription present: {json}" + ); + assert!( + !json.contains("\"helpUri\""), + "unset helpUri present: {json}" + ); + }); } #[test] @@ -109,12 +107,15 @@ mod tests { }), help_uri: Some("https://example.com".into()), }; - match serde_json::to_string(&desc) { - Ok(json) => { - assert!(json.contains("\"shortDescription\"")); - assert!(json.contains("\"helpUri\"")); - } - Err(e) => panic!("failed to serialize: {e}"), - } + assert_serialized_json(&desc, |json| { + assert!( + json.contains("\"shortDescription\""), + "camelCase shortDescription missing: {json}" + ); + assert!( + json.contains("\"helpUri\""), + "camelCase helpUri missing: {json}" + ); + }); } } diff --git a/crates/whitaker_sarif/src/model/location.rs b/crates/whitaker_sarif/src/model/location.rs index c3cd50d4a..b762f17ee 100644 --- a/crates/whitaker_sarif/src/model/location.rs +++ b/crates/whitaker_sarif/src/model/location.rs @@ -129,6 +129,7 @@ pub struct RelatedLocation { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{assert_json_round_trip, assert_serialized_json}; #[test] fn region_round_trip() { @@ -140,13 +141,7 @@ mod tests { byte_offset: None, byte_length: None, }; - match serde_json::to_string(®ion) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(region, parsed), - Err(e) => panic!("failed to deserialize: {e}"), - }, - Err(e) => panic!("failed to serialize: {e}"), - } + assert_json_round_trip(®ion); } #[test] @@ -159,13 +154,13 @@ mod tests { byte_offset: None, byte_length: None, }; - match serde_json::to_string(®ion) { - Ok(json) => { - assert!(!json.contains("startColumn")); - assert!(!json.contains("endLine")); - } - Err(e) => panic!("failed to serialize: {e}"), - } + assert_serialized_json(®ion, |json| { + assert!( + !json.contains("startColumn"), + "unset startColumn present: {json}" + ); + assert!(!json.contains("endLine"), "unset endLine present: {json}"); + }); } #[test] @@ -186,13 +181,7 @@ mod tests { }), }, }; - match serde_json::to_string(&loc) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(loc, parsed), - Err(e) => panic!("failed to deserialize: {e}"), - }, - Err(e) => panic!("failed to serialize: {e}"), - } + assert_json_round_trip(&loc); } #[test] @@ -210,12 +199,6 @@ mod tests { region: None, }, }; - match serde_json::to_string(&rl) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(rl, parsed), - Err(e) => panic!("failed to deserialize: {e}"), - }, - Err(e) => panic!("failed to serialize: {e}"), - } + assert_json_round_trip(&rl); } } diff --git a/crates/whitaker_sarif/src/model/log.rs b/crates/whitaker_sarif/src/model/log.rs index 1df6dcbd7..382912816 100644 --- a/crates/whitaker_sarif/src/model/log.rs +++ b/crates/whitaker_sarif/src/model/log.rs @@ -55,6 +55,7 @@ impl Default for SarifLog { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{assert_json_round_trip, assert_serialized_json}; #[test] fn default_log_has_correct_version() { @@ -70,33 +71,23 @@ mod tests { #[test] fn round_trip_empty_log() { - let log = SarifLog::default(); - match serde_json::to_string(&log) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(log, parsed), - Err(e) => panic!("deserialization failed: {e}"), - }, - Err(e) => panic!("serialization failed: {e}"), - } + assert_json_round_trip(&SarifLog::default()); } #[test] fn schema_field_serializes_as_dollar_schema() { - let log = SarifLog::default(); - match serde_json::to_string(&log) { - Ok(json) => assert!(json.contains("\"$schema\"")), - Err(e) => panic!("serialization failed: {e}"), - } + assert_serialized_json(&SarifLog::default(), |json| { + assert!(json.contains("\"$schema\""), "missing $schema key: {json}"); + }); } #[test] fn empty_runs_present_in_json() { - let log = SarifLog::default(); - match serde_json::to_string(&log) { - Ok(json) => { - assert!(json.contains("\"runs\":[]") || json.contains("\"runs\": []")); - } - Err(e) => panic!("serialization failed: {e}"), - } + assert_serialized_json(&SarifLog::default(), |json| { + assert!( + json.contains("\"runs\":[]") || json.contains("\"runs\": []"), + "empty runs array missing: {json}" + ); + }); } } diff --git a/crates/whitaker_sarif/src/model/result.rs b/crates/whitaker_sarif/src/model/result.rs index 3d07164d7..1d7044e85 100644 --- a/crates/whitaker_sarif/src/model/result.rs +++ b/crates/whitaker_sarif/src/model/result.rs @@ -115,24 +115,19 @@ pub struct SarifResult { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{assert_json_round_trip, assert_serialized_json}; #[test] fn level_serializes_lowercase() { - match serde_json::to_string(&Level::Warning) { - Ok(json) => assert_eq!(json, "\"warning\""), - Err(e) => panic!("failed to serialize Warning: {e}"), - } - match serde_json::to_string(&Level::Note) { - Ok(json) => assert_eq!(json, "\"note\""), - Err(e) => panic!("failed to serialize Note: {e}"), - } - match serde_json::to_string(&Level::Error) { - Ok(json) => assert_eq!(json, "\"error\""), - Err(e) => panic!("failed to serialize Error: {e}"), - } - match serde_json::to_string(&Level::None) { - Ok(json) => assert_eq!(json, "\"none\""), - Err(e) => panic!("failed to serialize None: {e}"), + for (level, expected) in [ + (Level::Warning, "\"warning\""), + (Level::Note, "\"note\""), + (Level::Error, "\"error\""), + (Level::None, "\"none\""), + ] { + assert_serialized_json(&level, |json| { + assert_eq!(json, expected, "unexpected serialization of {level:?}"); + }); } } @@ -155,13 +150,7 @@ mod tests { properties: None, baseline_state: None, }; - match serde_json::to_string(&result) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(result, parsed), - Err(e) => panic!("failed to deserialize: {e}"), - }, - Err(e) => panic!("failed to serialize: {e}"), - } + assert_json_round_trip(&result); } #[test] @@ -176,14 +165,20 @@ mod tests { properties: None, baseline_state: None, }; - match serde_json::to_string(&result) { - Ok(json) => { - assert!(!json.contains("\"locations\"")); - assert!(!json.contains("\"relatedLocations\"")); - assert!(!json.contains("\"partialFingerprints\"")); - } - Err(e) => panic!("failed to serialize: {e}"), - } + assert_serialized_json(&result, |json| { + assert!( + !json.contains("\"locations\""), + "empty locations present: {json}" + ); + assert!( + !json.contains("\"relatedLocations\""), + "empty relatedLocations present: {json}" + ); + assert!( + !json.contains("\"partialFingerprints\""), + "empty partialFingerprints present: {json}" + ); + }); } #[test] @@ -200,12 +195,15 @@ mod tests { properties: None, baseline_state: None, }; - match serde_json::to_string(&result) { - Ok(json) => { - assert!(json.contains("\"partialFingerprints\"")); - assert!(json.contains("\"whitakerFragment\"")); - } - Err(e) => panic!("failed to serialize: {e}"), - } + assert_serialized_json(&result, |json| { + assert!( + json.contains("\"partialFingerprints\""), + "partialFingerprints missing: {json}" + ); + assert!( + json.contains("\"whitakerFragment\""), + "whitakerFragment key missing: {json}" + ); + }); } } diff --git a/crates/whitaker_sarif/src/model/run.rs b/crates/whitaker_sarif/src/model/run.rs index 48a7f9ec7..f5d47225f 100644 --- a/crates/whitaker_sarif/src/model/run.rs +++ b/crates/whitaker_sarif/src/model/run.rs @@ -148,6 +148,7 @@ pub struct Artifact { mod tests { use super::*; use crate::model::location::ArtifactLocation; + use crate::test_support::{assert_json_round_trip, assert_serialized_json}; #[test] fn run_round_trip() { @@ -167,13 +168,7 @@ mod tests { results: Vec::new(), artifacts: Vec::new(), }; - match serde_json::to_string(&run) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(run, parsed), - Err(e) => panic!("failed to deserialize: {e}"), - }, - Err(e) => panic!("failed to serialize: {e}"), - } + assert_json_round_trip(&run); } #[test] @@ -191,15 +186,21 @@ mod tests { results: Vec::new(), artifacts: Vec::new(), }; - match serde_json::to_string(&run) { - Ok(json) => { - assert!(!json.contains("\"invocations\"")); - assert!(!json.contains("\"results\"")); - assert!(!json.contains("\"artifacts\"")); - assert!(!json.contains("\"rules\"")); - } - Err(e) => panic!("failed to serialize: {e}"), - } + assert_serialized_json(&run, |json| { + assert!( + !json.contains("\"invocations\""), + "empty invocations present: {json}" + ); + assert!( + !json.contains("\"results\""), + "empty results present: {json}" + ); + assert!( + !json.contains("\"artifacts\""), + "empty artifacts present: {json}" + ); + assert!(!json.contains("\"rules\""), "empty rules present: {json}"); + }); } #[test] @@ -211,12 +212,6 @@ mod tests { }, mime_type: Some("text/x-rust".into()), }; - match serde_json::to_string(&artifact) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(artifact, parsed), - Err(e) => panic!("failed to deserialize: {e}"), - }, - Err(e) => panic!("failed to serialize: {e}"), - } + assert_json_round_trip(&artifact); } } diff --git a/crates/whitaker_sarif/src/test_support.rs b/crates/whitaker_sarif/src/test_support.rs index 311d631f9..f941d6a44 100644 --- a/crates/whitaker_sarif/src/test_support.rs +++ b/crates/whitaker_sarif/src/test_support.rs @@ -4,10 +4,49 @@ //! It exists solely to avoid duplicating test helper logic between the //! `merge::tests` unit tests and the `tests/` integration tests. +use core::fmt::Debug; + +use serde::Serialize; +use serde::de::DeserializeOwned; + use crate::builders::{LocationBuilder, RegionBuilder, ResultBuilder}; use crate::merge::WHITAKER_FRAGMENT_KEY; use crate::model::result::{Level, SarifResult}; +/// Asserts that `value` survives a JSON serialize/deserialize round trip +/// unchanged. +/// +/// Panics with the type name and underlying serde error on failure; intended +/// only for test code. +pub fn assert_json_round_trip(value: &T) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let type_name = core::any::type_name::(); + let json = match serde_json::to_string(value) { + Ok(json) => json, + Err(e) => panic!("failed to serialize {type_name}: {e}"), + }; + match serde_json::from_str::(&json) { + Ok(parsed) => assert_eq!( + *value, parsed, + "JSON round trip changed {type_name} (json: {json})" + ), + Err(e) => panic!("failed to deserialize {type_name} from {json}: {e}"), + } +} + +/// Serializes `value` to JSON and passes the resulting string to `check`. +/// +/// Panics with the underlying serde error if serialization fails; intended +/// only for test code. +pub fn assert_serialized_json(value: &impl Serialize, check: impl FnOnce(&str)) { + match serde_json::to_string(value) { + Ok(json) => check(&json), + Err(e) => panic!("failed to serialize value to JSON: {e}"), + } +} + /// Builds a [`SarifResult`] with a fingerprint, location, and region. /// /// Panics on builder failure; intended only for test code. diff --git a/crates/whitaker_sarif/src/whitaker_properties.rs b/crates/whitaker_sarif/src/whitaker_properties.rs index 90260386f..48176c7fd 100644 --- a/crates/whitaker_sarif/src/whitaker_properties.rs +++ b/crates/whitaker_sarif/src/whitaker_properties.rs @@ -287,16 +287,7 @@ mod tests { .with_cosine(0.90) .build(); match props { - Ok(props) => { - let json = serde_json::to_string(&props); - match json { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(props, parsed), - Err(e) => panic!("deserialization failed: {e}"), - }, - Err(e) => panic!("serialization failed: {e}"), - } - } + Ok(props) => crate::test_support::assert_json_round_trip(&props), Err(e) => panic!("unexpected build error: {e}"), } } diff --git a/installer/tests/behaviour_cli/support.rs b/installer/tests/behaviour_cli/support.rs index 974e39e3d..d38b8efb1 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -1,7 +1,7 @@ //! Shared fixtures and command helpers for CLI behaviour tests. use std::{ - cell::{Cell, Ref, RefCell}, + cell::{Cell, RefCell}, path::{Path, PathBuf}, process::{Command, Output}, }; @@ -216,16 +216,6 @@ pub(super) fn run_installer_cli(cli_world: &CliWorld) { cli_world.output.replace(Some(output)); } -pub(super) fn get_output(cli_world: &CliWorld) -> Ref<'_, Output> { - let output_slot = cli_world.output.borrow(); - Ref::map(output_slot, |opt| { - let Some(output) = opt.as_ref() else { - panic!("output not set"); - }; - output - }) -} - #[path = "support_assertions.rs"] mod assertions; diff --git a/installer/tests/behaviour_cli/support_assertions.rs b/installer/tests/behaviour_cli/support_assertions.rs index e5eb895d7..60e80c218 100644 --- a/installer/tests/behaviour_cli/support_assertions.rs +++ b/installer/tests/behaviour_cli/support_assertions.rs @@ -1,22 +1,83 @@ //! Assertions over the installer CLI's observed output for behaviour tests. +//! +//! Helpers are layered following the toolchain step conventions: fallible +//! lookups return `Result` with a diagnostic message, queries are pure, and +//! the public assertion wrappers stay thin. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -use super::{CliWorld, expected_prebuilt_target_dir, get_output, matching_files}; +use super::{CliWorld, expected_prebuilt_target_dir, matching_files}; use crate::prebuilt_markers::PREBUILT_INSTALL_MARKER; +/// Banner printed when the installer runs in dry-run mode. +const DRY_RUN_BANNER: &str = "Dry run - no files will be modified"; + +/// Header preceding the crate list in dry-run output. +const CRATE_LIST_MARKER: &str = "Crates to build:"; + +/// Owned snapshot of the captured CLI process output. +struct CapturedOutput { + success: bool, + stdout: String, + stderr: String, +} + +/// Snapshots the captured CLI output, or reports that the CLI has not run. +fn captured_output(cli_world: &CliWorld) -> Result { + let output_slot = cli_world.output.borrow(); + let output = output_slot + .as_ref() + .ok_or_else(|| "output not set; was the installer CLI run?".to_owned())?; + Ok(CapturedOutput { + success: output.status.success(), + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }) +} + +/// Clones the configured toolchain channel, or reports that none is set. +fn configured_toolchain(cli_world: &CliWorld) -> Result { + cli_world + .toolchain + .borrow() + .clone() + .ok_or_else(|| "toolchain not set; was the scenario configured?".to_owned()) +} + +/// Clones the temporary target directory path, or reports that none is set. +fn temp_target_path(cli_world: &CliWorld) -> Result { + cli_world + .temp_dir + .borrow() + .as_ref() + .map(|temp_dir| temp_dir.path().to_owned()) + .ok_or_else(|| "temp dir not set; was the scenario configured?".to_owned()) +} + +/// Reports whether the stderr contains dry-run configuration output. +fn contains_dry_run_configuration(stderr: &str) -> bool { + stderr.contains(DRY_RUN_BANNER) || stderr.contains(CRATE_LIST_MARKER) +} + +/// Resolves the target directory the dry-run summary should report. +fn expected_dry_run_target_dir(toolchain: &str, fallback: &Path) -> String { + expected_prebuilt_target_dir(toolchain) + .unwrap_or_else(|| fallback.to_string_lossy().into_owned()) +} + fn assert_exit_status(cli_world: &CliWorld, expected_success: bool) { if cli_world.skip_assertions.get() { return; } - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); + let output = match captured_output(cli_world) { + Ok(output) => output, + Err(message) => panic!("{message}"), + }; assert_eq!( - output.status.success(), - expected_success, - "expected success={expected_success}, stdout={}, stderr={stderr}", - String::from_utf8_lossy(&output.stdout), + output.success, expected_success, + "expected success={expected_success}, stdout={}, stderr={}", + output.stdout, output.stderr, ); } @@ -25,16 +86,14 @@ fn assert_error_output_is_shown(cli_world: &CliWorld, error_kind: &str, expected return; } - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); + let output = match captured_output(cli_world) { + Ok(output) => output, + Err(message) => panic!("{message}"), + }; + let stderr = output.stderr; assert!( - !stderr.contains("Dry run - no files will be modified"), - "dry-run configuration output should not be printed on {error_kind} error, stderr: \ - {stderr}" - ); - assert!( - !stderr.contains("Crates to build:"), + !contains_dry_run_configuration(&stderr), "dry-run configuration output should not be printed on {error_kind} error, stderr: \ {stderr}" ); @@ -53,16 +112,18 @@ pub(crate) fn assert_dry_run_output_is_shown(cli_world: &CliWorld) { return; } - let toolchain_slot = cli_world.toolchain.borrow(); - let Some(toolchain) = toolchain_slot.as_ref() else { - panic!("toolchain not set"); + let toolchain = match configured_toolchain(cli_world) { + Ok(toolchain) => toolchain, + Err(message) => panic!("{message}"), }; - - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); + let output = match captured_output(cli_world) { + Ok(output) => output, + Err(message) => panic!("{message}"), + }; + let stderr = output.stderr; assert!( - stderr.contains("Dry run - no files will be modified"), + stderr.contains(DRY_RUN_BANNER), "expected dry-run banner in stderr: {stderr}" ); assert!( @@ -70,7 +131,7 @@ pub(crate) fn assert_dry_run_output_is_shown(cli_world: &CliWorld) { "expected toolchain line in stderr: {stderr}" ); assert!( - stderr.contains("Crates to build:"), + stderr.contains(CRATE_LIST_MARKER), "expected crate list in stderr: {stderr}" ); assert!( @@ -82,13 +143,11 @@ pub(crate) fn assert_dry_run_output_is_shown(cli_world: &CliWorld) { "individual lint crate should not appear in suite-only mode, stderr: {stderr}" ); - let temp_dir_slot = cli_world.temp_dir.borrow(); - let Some(temp_dir) = temp_dir_slot.as_ref() else { - panic!("temp dir not set"); + let target_dir = match temp_target_path(cli_world) { + Ok(path) => path, + Err(message) => panic!("{message}"), }; - let target_dir = temp_dir.path().to_string_lossy(); - let expected_target_dir = - expected_prebuilt_target_dir(toolchain).unwrap_or_else(|| target_dir.into_owned()); + let expected_target_dir = expected_dry_run_target_dir(&toolchain, &target_dir); assert!( stderr.contains(&format!("Target directory: {expected_target_dir}")), "expected target directory line in stderr: {stderr}" @@ -120,15 +179,18 @@ pub(crate) fn assert_experimental_lint_dry_run_output_is_shown(cli_world: &CliWo return; } - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); + let output = match captured_output(cli_world) { + Ok(output) => output, + Err(message) => panic!("{message}"), + }; + let stderr = output.stderr; assert!( - stderr.contains("Dry run - no files will be modified"), + stderr.contains(DRY_RUN_BANNER), "expected dry-run banner in stderr: {stderr}" ); assert!( - stderr.contains("Crates to build:"), + stderr.contains(CRATE_LIST_MARKER), "expected crate list in stderr: {stderr}" ); assert!( @@ -149,11 +211,35 @@ pub(crate) fn assert_installation_succeeds_or_is_skipped(cli_world: &CliWorld) { return; } - let output = get_output(cli_world); + let output = match captured_output(cli_world) { + Ok(output) => output, + Err(message) => panic!("{message}"), + }; + assert!(output.success, "installation failed: {}", output.stderr); +} + +/// Asserts the prebuilt directory holds at least one library matching `needle`. +fn assert_prebuilt_library_present(prebuilt_path: &Path, needle: &str) { + let matches = matching_files(prebuilt_path, needle); + assert!( + !matches.is_empty(), + "prebuilt marker found in stderr but no library matching '{needle}' in {}, entries={:?}", + prebuilt_path.display(), + matching_files(prebuilt_path, ""), + ); +} + +/// Asserts the staging directory holds exactly one library matching `needle`. +fn assert_staged_library_unique(staging_dir: &Path, needle: &str, output: &CapturedOutput) { + let matches = matching_files(staging_dir, needle); assert!( - output.status.success(), - "installation failed: {}", - String::from_utf8_lossy(&output.stderr) + matches.len() == 1, + "expected exactly one suite library matching '{needle}' in {}, matches={matches:?}, \ + entries={:?}, stdout={}, stderr={}", + staging_dir.display(), + matching_files(staging_dir, ""), + output.stdout, + output.stderr, ); } @@ -162,42 +248,27 @@ pub(crate) fn assert_suite_library_is_staged(cli_world: &CliWorld) { return; } - let output = get_output(cli_world); - let stderr = String::from_utf8_lossy(&output.stderr); - let channel_slot = cli_world.toolchain.borrow(); - let Some(channel) = channel_slot.as_ref() else { - panic!("toolchain not set"); + let channel = match configured_toolchain(cli_world) { + Ok(channel) => channel, + Err(message) => panic!("{message}"), + }; + let output = match captured_output(cli_world) { + Ok(output) => output, + Err(message) => panic!("{message}"), }; let needle = format!("whitaker_suite@{channel}"); - if stderr.contains(PREBUILT_INSTALL_MARKER) - && let Some(dir) = expected_prebuilt_target_dir(channel) + if output.stderr.contains(PREBUILT_INSTALL_MARKER) + && let Some(dir) = expected_prebuilt_target_dir(&channel) { - let prebuilt_path = PathBuf::from(&dir); - let matches = matching_files(&prebuilt_path, &needle); - assert!( - !matches.is_empty(), - "prebuilt marker found in stderr but no library matching '{needle}' in {}, \ - entries={:?}", - prebuilt_path.display(), - matching_files(&prebuilt_path, ""), - ); + assert_prebuilt_library_present(&PathBuf::from(dir), &needle); return; } - let temp_dir_slot = cli_world.temp_dir.borrow(); - let Some(temp_dir) = temp_dir_slot.as_ref() else { - panic!("temp dir not set"); + let target_dir = match temp_target_path(cli_world) { + Ok(path) => path, + Err(message) => panic!("{message}"), }; - let staging_dir = temp_dir.path().join(channel).join("release"); - let matches = matching_files(&staging_dir, &needle); - - assert!( - matches.len() == 1, - "expected exactly one suite library matching '{needle}' in {}, matches={matches:?}, \ - entries={:?}, stdout={}, stderr={stderr}", - staging_dir.display(), - matching_files(&staging_dir, ""), - String::from_utf8_lossy(&output.stdout), - ); + let staging_dir = target_dir.join(&channel).join("release"); + assert_staged_library_unique(&staging_dir, &needle, &output); } diff --git a/tests/config_loading.rs b/tests/config_loading.rs index b763a1b1c..2c8a660db 100644 --- a/tests/config_loading.rs +++ b/tests/config_loading.rs @@ -136,13 +136,41 @@ fn load_config( .replace(outcome.map_err(panic_message)); } +/// Clones the successfully loaded configuration from the step state. +/// +/// Returns an error when loading has not run yet or reported a failure. +fn loaded_config( + load_result: &RefCell>>, +) -> Result { + match load_result.borrow().as_ref() { + Some(Ok(config)) => Ok(config.clone()), + Some(Err(error)) => Err(format!( + "expected configuration loading to succeed: {error}" + )), + None => Err("configuration should be loaded".to_owned()), + } +} + +/// Clones the loading error from the step state. +/// +/// Returns an error when loading has not run yet or unexpectedly succeeded. +fn load_error( + load_result: &RefCell>>, +) -> Result { + match load_result.borrow().as_ref() { + Some(Err(error)) => Ok(error.clone()), + Some(Ok(config)) => Err(format!( + "expected configuration loading to fail but succeeded with {config:?}" + )), + None => Err("configuration should be loaded".to_owned()), + } +} + #[then("the module max line limit is {expected}")] fn assert_max_lines(load_result: &RefCell>>, expected: usize) { - let borrow = load_result.borrow(); - let config = match borrow.as_ref() { - Some(Ok(config)) => config, - Some(Err(error)) => panic!("expected configuration loading to succeed: {error}"), - None => panic!("configuration should be loaded"), + let config = match loaded_config(load_result) { + Ok(config) => config, + Err(message) => panic!("{message}"), }; assert_eq!(config.module_max_lines.max_lines, expected); @@ -154,13 +182,12 @@ fn assert_locale( expected: StepLocale, ) { let raw = expected.into_inner(); - let expected_value = normalise_locale(Some(raw.as_str())) - .unwrap_or_else(|| panic!("expected the step to provide a locale value")); - let borrow = load_result.borrow(); - let config = match borrow.as_ref() { - Some(Ok(config)) => config, - Some(Err(error)) => panic!("expected configuration loading to succeed: {error}"), - None => panic!("configuration should be loaded"), + let Some(expected_value) = normalise_locale(Some(raw.as_str())) else { + panic!("expected the step to provide a locale value"); + }; + let config = match loaded_config(load_result) { + Ok(config) => config, + Err(message) => panic!("{message}"), }; assert_eq!(config.locale(), Some(expected_value)); @@ -168,11 +195,9 @@ fn assert_locale( #[then("no locale override is configured")] fn assert_no_locale(load_result: &RefCell>>) { - let borrow = load_result.borrow(); - let config = match borrow.as_ref() { - Some(Ok(config)) => config, - Some(Err(error)) => panic!("expected configuration loading to succeed: {error}"), - None => panic!("configuration should be loaded"), + let config = match loaded_config(load_result) { + Ok(config) => config, + Err(message) => panic!("{message}"), }; assert!( @@ -184,13 +209,8 @@ fn assert_no_locale(load_result: &RefCell>>) #[then("a configuration error is reported")] fn assert_error(load_result: &RefCell>>) { - let borrow = load_result.borrow(); - match borrow.as_ref() { - Some(Err(_)) => {} - Some(Ok(config)) => { - panic!("expected configuration loading to fail but succeeded with {config:?}") - } - None => panic!("configuration should be loaded"), + if let Err(message) = load_error(load_result) { + panic!("{message}"); } } @@ -200,19 +220,15 @@ fn assert_error_with_snippet( snippet: ErrorSnippet, ) { let snippet_value = snippet.into_inner(); - let borrow = load_result.borrow(); - match borrow.as_ref() { - Some(Err(error)) => { - assert!( - error.contains(snippet_value.as_str()), - "expected error '{error}' to mention '{snippet_value}'", - ); - } - Some(Ok(config)) => { - panic!("expected configuration loading to fail but succeeded with {config:?}") - } - None => panic!("configuration should be loaded"), - } + let error = match load_error(load_result) { + Ok(error) => error, + Err(message) => panic!("{message}"), + }; + + assert!( + error.contains(snippet_value.as_str()), + "expected error '{error}' to mention '{snippet_value}'", + ); } #[scenario("tests/features/config_loading.feature", index = 0)] From 95f63f342776902cdc2b5f8b3acfcc729e147f86 Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 29 Aug 2026 00:55:17 +0200 Subject: [PATCH 3/9] Correct en-GB-oxendict spellings in code symbols and prose Rename code symbols and prose to Oxford spelling (-ize/-yze for the -ise pairs; analyse-family words correctly keep -yse): normalize, localize, summarize, recognized, serialize, and the SARIF domain terms Artifact -> Artefact across whitaker_sarif and its consumers. The SARIF 2.1.0 wire format spells its properties `artifactLocation` and `artifacts`, so those fields carry explicit `#[serde(rename)]` pins with doc comments explaining that the wire name is fixed by the schema rather than derived from the field name. External identifiers (`CompilerArtifact`, cargo metadata types, GitHub Actions flags) keep the external system's spelling. Feature files and step definitions are renamed together, and the typos configuration is pruned from 255 ignore patterns to the residual set the corrected tree still needs, so the spelling gate now enforces the convention. A handful of self-named module files move to mod.rs form here because the renames were entangled with those moves. --- .config/nextest.toml | 4 +- Cargo.lock | 4 + Cargo.toml | 1 + common/locales/cy/common.ftl | 2 +- .../locales/en-GB/no_expect_outside_tests.ftl | 2 +- common/src/brain_trait_metrics/tests.rs | 4 +- common/src/complexity_signal.rs | 9 - common/src/i18n/mod.rs | 2 +- common/src/i18n/selection.rs | 8 +- common/src/lib.rs | 2 +- crates/bumpy_road_function/src/analysis.rs | 20 +- .../bumpy_road_function/src/driver/config.rs | 2 +- .../src/driver/diagnostic.rs | 17 +- .../src/{driver.rs => driver/mod.rs} | 6 +- .../src/driver/segment_builder.rs | 15 +- .../tests/analysis_behaviour.rs | 8 +- .../conditional_max_n_branches/src/driver.rs | 46 ++-- .../function_attrs_follow_docs/src/driver.rs | 2 +- .../src/tests/localization.rs | 6 +- crates/module_max_lines/Cargo.toml | 1 + crates/module_max_lines/src/driver.rs | 1 + crates/module_max_lines/src/lib.rs | 5 + .../src/{driver.rs => driver/mod.rs} | 8 +- ...il_expect_in_rstest_non_test_module.stderr | 2 +- .../fail_expect_in_tokio_crate_non_test_fn.rs | 2 +- ...l_expect_in_tokio_crate_non_test_fn.stderr | 2 +- .../no_expect_outside_tests/src/behaviour.rs | 8 +- .../src/{context.rs => context/mod.rs} | 4 +- .../src/diagnostics.rs | 4 +- .../no_expect_outside_tests/src/driver/mod.rs | 4 +- crates/no_expect_outside_tests/src/tests.rs | 14 +- .../src/tests/localization.rs | 4 +- .../src/tests/receiver_type_edge_cases.rs | 4 +- .../tests/features/context_summary.feature | 10 +- ...l_expect_in_file_backed_non_test_fn.stderr | 2 +- .../ui/fail_expect_in_fn.stderr | 4 +- .../ui/fail_expect_with_cfg_attr.stderr | 2 +- crates/no_std_fs_operations/Cargo.toml | 1 + crates/no_std_fs_operations/src/behaviour.rs | 10 +- .../no_std_fs_operations/src/diagnostics.rs | 2 +- .../src/exclusion_behaviour.rs | 1 + .../src/{usage.rs => usage/mod.rs} | 0 .../no_std_fs_operations/src/usage/tests.rs | 2 +- .../tests/features/localization.feature | 12 +- .../fail_unwrap_in_rstest_non_test_module.rs | 2 +- .../src/{context.rs => context/mod.rs} | 2 +- crates/no_unwrap_or_else_panic/src/driver.rs | 6 +- crates/test_must_not_have_example/Cargo.toml | 1 + .../src/behaviour.rs | 1 + .../whitaker_clones_core/src/index/tests.rs | 4 + crates/whitaker_clones_core/src/run0/emit.rs | 2 +- crates/whitaker_clones_core/src/run0/tests.rs | 4 +- crates/whitaker_clones_core/src/run0/types.rs | 2 +- .../tests/run0_sarif_behaviour.rs | 2 +- .../src/builders/location_builder.rs | 14 +- .../src/builders/log_builder.rs | 2 +- .../src/builders/run_builder.rs | 22 +- crates/whitaker_sarif/src/lib.rs | 2 +- crates/whitaker_sarif/src/merge.rs | 10 +- crates/whitaker_sarif/src/model/location.rs | 36 ++-- crates/whitaker_sarif/src/model/mod.rs | 8 +- crates/whitaker_sarif/src/model/run.rs | 49 +++-- docs/developers-guide.md | 4 +- .../2-2-5-test-must-not-have-example-lint.md | 2 +- .../3-4-4-install-prebuilt-artefacts.md | 2 +- ...4.6. Record download-versus-build rates.md | 2 +- docs/execplans/7-1-1-whitaker-sarif-crate.md | 4 +- ...ication-of-bounded-lsh-index-invariants.md | 2 +- ...e-spans-and-extract-ast-feature-vectors.md | 4 +- ...2-1-create-the-rstest-helper-lint-crate.md | 2 +- ...-2-call-site-collection-in-rstest-tests.md | 4 +- .../installer-does-not-install-toolchain.md | 2 +- ...e-110-ambiguous-name-reported-by-dylint.md | 2 +- .../issue-94-dual-ownership-of-binary.md | 2 +- ...n-of-github-actions-with-act-and-pytest.md | 20 +- docs/rstest-bdd-users-guide.md | 2 +- docs/scripting-standards.md | 2 +- docs/whitaker-clone-detector-design.md | 2 +- docs/whitaker-dylint-suite-design.md | 8 +- installer/src/artefact/extraction.rs | 8 +- installer/src/artefact/schema_version.rs | 2 +- installer/src/artefact/sha256_digest.rs | 1 + .../dependency_binaries/install/checksum.rs | 7 +- .../dependency_binaries/install/downloader.rs | 7 +- .../install/http_test_server.rs | 62 ++++-- installer/src/{deps.rs => deps/mod.rs} | 0 installer/src/output.rs | 3 +- src/config.rs | 10 +- src/lints/template/validation.rs | 12 +- tests/config_loading.rs | 4 +- tests/locale_resolution.rs | 5 +- tests/ui_harness.rs | 1 + typos.local.toml | 204 +----------------- typos.toml | 200 +---------------- 94 files changed, 353 insertions(+), 677 deletions(-) rename crates/bumpy_road_function/src/{driver.rs => driver/mod.rs} (97%) rename crates/module_must_have_inner_docs/src/{driver.rs => driver/mod.rs} (98%) rename crates/no_expect_outside_tests/src/{context.rs => context/mod.rs} (99%) rename crates/no_std_fs_operations/src/{usage.rs => usage/mod.rs} (100%) rename crates/no_unwrap_or_else_panic/src/{context.rs => context/mod.rs} (99%) rename installer/src/{deps.rs => deps/mod.rs} (100%) diff --git a/.config/nextest.toml b/.config/nextest.toml index 5eceddc50..827acc4fc 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -13,7 +13,7 @@ default-filter = "not (binary(behaviour_toolchain) | binary(behaviour_cli) | kin [profile.ci] default-filter = "not kind(example)" -# Serialise dylint UI tests that build lint libraries and use +# Serialize dylint UI tests that build lint libraries and use # `#[serial]`. nextest runs each test in its own process, so the # `serial_test` crate's in-process mutex has no effect. A test-group # with max-threads = 1 prevents concurrent lint-library builds from @@ -49,7 +49,7 @@ retries = { backoff = "exponential", count = 2, delay = "5s" } slow-timeout = { period = "10m", terminate-after = 1 } [[profile.default.overrides]] -# Serialise ignored exclusion integration tests when they are explicitly run. +# Serialize ignored exclusion integration tests when they are explicitly run. # They build and stage the lint library before invoking `cargo dylint`, so they # share the same target-directory race risk as the UI harnesses above. filter = "binary(integration_exclusion)" diff --git a/Cargo.lock b/Cargo.lock index 63add6fad..577a87cc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1836,6 +1836,7 @@ dependencies = [ "tempfile", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1918,6 +1919,7 @@ dependencies = [ "toml 1.1.3+spec-1.1.0", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -3077,6 +3079,7 @@ dependencies = [ "serde", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -3638,6 +3641,7 @@ dependencies = [ "toml 1.1.3+spec-1.1.0", "whitaker-common", "whitaker-installer", + "whitaker_test_macros", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c22cf16d2..27733f1f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,7 @@ rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } dylint_testing = { workspace = true } +whitaker_test_macros = { workspace = true } [lints.clippy] pedantic = { level = "warn", priority = -1 } diff --git a/common/locales/cy/common.ftl b/common/locales/cy/common.ftl index d5f35a3f0..4f996c64d 100644 --- a/common/locales/cy/common.ftl +++ b/common/locales/cy/common.ftl @@ -6,7 +6,7 @@ -term-branch = cangen -term-test-coverage = cwmpas profion -# Borrowed English nouns typically pluralise with -iau (Modern Welsh, Gareth +# Borrowed English nouns typically pluralize with -iau (Modern Welsh, Gareth # King §2.8), so we render "lint" as "lintiau" in aggregated messaging. common-lint-count = { $lint -> diff --git a/common/locales/en-GB/no_expect_outside_tests.ftl b/common/locales/en-GB/no_expect_outside_tests.ftl index 29ef45aa1..3d478e5f8 100644 --- a/common/locales/en-GB/no_expect_outside_tests.ftl +++ b/common/locales/en-GB/no_expect_outside_tests.ftl @@ -1,7 +1,7 @@ ## Restrict expect calls outside test contexts. no_expect_outside_tests = Avoid calling expect on { $receiver } outside test-only code. - .note = The call originates within { $context } which is not recognised as a test. + .note = The call originates within { $context } which is not recognized as a test. .help = { $handling -> [option] Handle the `None` variant of { $receiver } or move the code into a test. [result] Handle the `Err` variant of { $receiver } or move the code into a test. diff --git a/common/src/brain_trait_metrics/tests.rs b/common/src/brain_trait_metrics/tests.rs index e5e03328c..e0e5965b2 100644 --- a/common/src/brain_trait_metrics/tests.rs +++ b/common/src/brain_trait_metrics/tests.rs @@ -96,7 +96,7 @@ fn default_method_cc_sum_aggregates_only_default_methods() { let items = vec![ TraitItemMetrics::required_method("parse"), TraitItemMetrics::default_method("render", 12), - TraitItemMetrics::default_method("serialise", 8), + TraitItemMetrics::default_method("serialize", 8), TraitItemMetrics::associated_type("Output"), ]; @@ -191,7 +191,7 @@ fn implementor_burden_equals_required_method_count() { let mut builder = TraitMetricsBuilder::new("Transformer"); builder.add_required_method("parse"); builder.add_required_method("validate"); - builder.add_default_method("normalise", 7, false); + builder.add_default_method("normalize", 7, false); builder.add_associated_type("Output"); let metrics = builder.build(); diff --git a/common/src/complexity_signal.rs b/common/src/complexity_signal.rs index d71e903c6..29d6caffd 100644 --- a/common/src/complexity_signal.rs +++ b/common/src/complexity_signal.rs @@ -225,15 +225,6 @@ pub fn rasterize_signal( Ok(accumulate_signal_from_diff(diff.as_slice(), len)) } -#[deprecated(note = "Use rasterize_signal instead.")] -#[must_use = "Inspect the signal build result to handle invalid ranges"] -pub fn rasterise_signal( - function_lines: RangeInclusive, - segments: &[LineSegment], -) -> Result, SignalBuildError> { - rasterize_signal(function_lines, segments) -} - /// Errors emitted when smoothing a signal. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum SmoothingError { diff --git a/common/src/i18n/mod.rs b/common/src/i18n/mod.rs index 383a426c2..300db0588 100644 --- a/common/src/i18n/mod.rs +++ b/common/src/i18n/mod.rs @@ -78,7 +78,7 @@ pub use helpers::{ }; pub use loader::{Arguments, I18nError, Localizer}; pub use locales::{available_locales, supports_locale}; -pub use selection::{LocaleSelection, LocaleSource, normalise_locale, resolve_localizer}; +pub use selection::{LocaleSelection, LocaleSource, normalize_locale, resolve_localizer}; #[cfg(test)] mod tests; diff --git a/common/src/i18n/selection.rs b/common/src/i18n/selection.rs index ce7b02434..dbea901b2 100644 --- a/common/src/i18n/selection.rs +++ b/common/src/i18n/selection.rs @@ -97,7 +97,7 @@ impl LocaleSelection { /// Attempt to resolve a locale candidate from the given source. fn try_resolve_candidate(source: LocaleSource, raw: Option<&str>) -> Option { - let candidate = normalise_locale(raw)?; + let candidate = normalize_locale(raw)?; if supports_locale(candidate) { return Some(LocaleSelection::new( @@ -143,7 +143,7 @@ pub fn resolve_localizer( /// Trim whitespace and discard empty locale candidates. #[must_use] -pub fn normalise_locale(input: Option<&str>) -> Option<&str> { +pub fn normalize_locale(input: Option<&str>) -> Option<&str> { input.map(str::trim).filter(|value| !value.is_empty()) } @@ -223,7 +223,7 @@ mod tests { #[case(Some(" "), None)] #[case(Some("cy"), Some("cy"))] #[case(Some(" cy "), Some("cy"))] - fn normalises_candidates(#[case] input: Option<&str>, #[case] expected: Option<&str>) { - assert_eq!(normalise_locale(input), expected); + fn normalizes_candidates(#[case] input: Option<&str>, #[case] expected: Option<&str>) { + assert_eq!(normalize_locale(input), expected); } } diff --git a/common/src/lib.rs b/common/src/lib.rs index bb7214a0e..bf7007b63 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -50,7 +50,7 @@ pub use expr::{Expr, def_id_of_expr_callee, is_path_to, recv_is_option_or_result pub use i18n::{ Arguments, FALLBACK_LOCALE, I18nError, LocaleSelection, LocaleSource, Localizer, MessageResolution, available_locales, branch_phrase, get_localizer_for_lint, noop_reporter, - normalise_locale, resolve_localizer, safe_resolve_message_set, supports_locale, + normalize_locale, resolve_localizer, safe_resolve_message_set, supports_locale, }; pub use lcom4::{MethodInfo, MethodInfoBuilder, cohesion_components, collect_method_infos}; pub use path::SimplePath; diff --git a/crates/bumpy_road_function/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index 72aa96d0c..15e9401b9 100644 --- a/crates/bumpy_road_function/src/analysis.rs +++ b/crates/bumpy_road_function/src/analysis.rs @@ -69,7 +69,7 @@ impl Default for Settings { /// # Examples /// /// ``` -/// use bumpy_road_function::analysis::{normalise_settings, Settings, Weights}; +/// use bumpy_road_function::analysis::{normalize_settings, Settings, Weights}; /// /// let settings = Settings { /// threshold: -1.0, @@ -82,14 +82,14 @@ impl Default for Settings { /// ..Settings::default() /// }; /// -/// let normalised = normalise_settings(settings); -/// assert_eq!(normalised.threshold, Settings::default().threshold); -/// assert_eq!(normalised.window, Settings::default().window); -/// assert_eq!(normalised.weights, Settings::default().weights); +/// let normalized = normalize_settings(settings); +/// assert_eq!(normalized.threshold, Settings::default().threshold); +/// assert_eq!(normalized.window, Settings::default().window); +/// assert_eq!(normalized.weights, Settings::default().weights); /// ``` #[must_use] -pub fn normalise_settings(settings: Settings) -> Settings { - fn normalise_weight(candidate: f64, fallback: f64) -> f64 { +pub fn normalize_settings(settings: Settings) -> Settings { + fn normalize_weight(candidate: f64, fallback: f64) -> f64 { if candidate.is_finite() && candidate >= 0.0 { candidate } else { @@ -116,9 +116,9 @@ pub fn normalise_settings(settings: Settings) -> Settings { let min_bump_lines = settings.min_bump_lines.max(1); let weights = Weights { - depth: normalise_weight(settings.weights.depth, defaults.weights.depth), - predicate: normalise_weight(settings.weights.predicate, defaults.weights.predicate), - flow: normalise_weight(settings.weights.flow, defaults.weights.flow), + depth: normalize_weight(settings.weights.depth, defaults.weights.depth), + predicate: normalize_weight(settings.weights.predicate, defaults.weights.predicate), + flow: normalize_weight(settings.weights.flow, defaults.weights.flow), }; Settings { diff --git a/crates/bumpy_road_function/src/driver/config.rs b/crates/bumpy_road_function/src/driver/config.rs index 4381ab7a9..4388791bb 100644 --- a/crates/bumpy_road_function/src/driver/config.rs +++ b/crates/bumpy_road_function/src/driver/config.rs @@ -1,7 +1,7 @@ //! Configuration parsing and loading for the bumpy road lint. //! //! The lint reads optional configuration from `dylint.toml`, applies defaults, -//! and relies on `analysis::normalise_settings` to clamp invalid values. +//! and relies on `analysis::normalize_settings` to clamp invalid values. use crate::analysis::{Settings, Weights}; use log::debug; diff --git a/crates/bumpy_road_function/src/driver/diagnostic.rs b/crates/bumpy_road_function/src/driver/diagnostic.rs index 467e43263..265065111 100644 --- a/crates/bumpy_road_function/src/driver/diagnostic.rs +++ b/crates/bumpy_road_function/src/driver/diagnostic.rs @@ -36,7 +36,7 @@ pub(super) fn emit_diagnostic( args.insert(Cow::Borrowed("name"), FluentValue::from(input.name)); args.insert( Cow::Borrowed("count"), - FluentValue::from(input.bumps.len() as i64), + FluentValue::from(i64::try_from(input.bumps.len()).unwrap_or(i64::MAX)), ); args.insert( Cow::Borrowed("threshold"), @@ -66,8 +66,11 @@ pub(super) fn emit_diagnostic( let Some(span) = bump_spans.get(ordinal).copied().flatten() else { continue; }; - let label = - resolve_bump_label(localizer, (ordinal + 1) as i64, interval.len() as i64); + let label = resolve_bump_label( + localizer, + i64::try_from(ordinal + 1).unwrap_or(i64::MAX), + i64::try_from(interval.len()).unwrap_or(i64::MAX), + ); lint.span_label(span, label); } @@ -150,10 +153,10 @@ impl LineSpanMapper { .unwrap_or(self.snippet_len); let base = self.base_span.shrink_to_lo(); - // `BytePos` is `u32`-backed; the snippet length is expected to fit in - // 4 GiB for any reasonable Rust source file. - let lo = base.lo() + BytePos(start_offset as u32); - let mut hi = base.lo() + BytePos(end_offset as u32); + // `BytePos` is `u32`-backed; offsets beyond 4 GiB cannot be represented + // as spans, so such (pathological) snippets yield no span. + let lo = base.lo() + BytePos(u32::try_from(start_offset).ok()?); + let mut hi = base.lo() + BytePos(u32::try_from(end_offset).ok()?); if hi <= lo { hi = lo + BytePos(1); } diff --git a/crates/bumpy_road_function/src/driver.rs b/crates/bumpy_road_function/src/driver/mod.rs similarity index 97% rename from crates/bumpy_road_function/src/driver.rs rename to crates/bumpy_road_function/src/driver/mod.rs index 41ab1c795..e3afd6857 100644 --- a/crates/bumpy_road_function/src/driver.rs +++ b/crates/bumpy_road_function/src/driver/mod.rs @@ -5,7 +5,7 @@ //! more separated bumps above a configurable threshold. The warning highlights //! the two largest bump intervals with labelled spans. -use crate::analysis::{Settings, detect_bumps, normalise_settings}; +use crate::analysis::{Settings, detect_bumps, normalize_settings}; use rustc_hir as hir; use rustc_hir::ExprKind; use rustc_lint::{LateContext, LateLintPass}; @@ -52,7 +52,7 @@ impl Default for BumpyRoadFunction { impl<'tcx> LateLintPass<'tcx> for BumpyRoadFunction { fn check_crate(&mut self, _cx: &LateContext<'tcx>) { - self.settings = normalise_settings(load_configuration().into_settings()); + self.settings = normalize_settings(load_configuration().into_settings()); let shared_config = SharedConfig::load(); self.localizer = get_localizer_for_lint(LINT_NAME, shared_config.locale()); } @@ -196,7 +196,7 @@ fn analyse_body( Err(error) => { cx.tcx.sess.dcx().span_delayed_bug( body_span, - format!("bumpy-road signal rasterisation failed: {error}"), + format!("bumpy-road signal rasterization failed: {error}"), ); return; } diff --git a/crates/bumpy_road_function/src/driver/segment_builder.rs b/crates/bumpy_road_function/src/driver/segment_builder.rs index 27992e200..2b7f7ee44 100644 --- a/crates/bumpy_road_function/src/driver/segment_builder.rs +++ b/crates/bumpy_road_function/src/driver/segment_builder.rs @@ -139,7 +139,7 @@ impl<'a, 'tcx> SegmentBuilder<'a, 'tcx> { return; } - let branches = count_branches(expr) as f64; + let branches = f64::from(u32::try_from(count_branches(expr)).unwrap_or(u32::MAX)); let value = branches * self.settings.weights.predicate; self.push_segment(expr.span, value); } @@ -211,12 +211,8 @@ fn count_branches(expr: &hir::Expr<'_>) -> usize { ExprKind::Binary(op, lhs, rhs) if matches!(op.node, BinOpKind::And | BinOpKind::Or) => { count_branches(lhs) + count_branches(rhs) } - ExprKind::Unary(UnOp::Not, inner) => count_branches(inner), - ExprKind::DropTemps(inner) => count_branches(inner), - ExprKind::Block(block, _) => match block.expr { - Some(inner) => count_branches(inner), - None => 1, - }, + ExprKind::Unary(UnOp::Not, inner) | ExprKind::DropTemps(inner) => count_branches(inner), + ExprKind::Block(block, _) => block.expr.map_or(1, count_branches), ExprKind::If(cond, ..) => count_branches(cond), _ => 1, } @@ -229,8 +225,9 @@ pub(super) fn span_line_range(source_map: &SourceMap, span: Span) -> Option LateLintPass<'tcx> for ConditionalMaxNBranches { fn check_crate(&mut self, _cx: &LateContext<'tcx>) { self.max_branches = load_configuration().max_branches.max(1); @@ -187,12 +200,8 @@ fn count_branches(expr: &hir::Expr<'_>) -> usize { ExprKind::Binary(op, lhs, rhs) if matches!(op.node, BinOpKind::And | BinOpKind::Or) => { count_branches(lhs) + count_branches(rhs) } - ExprKind::Unary(UnOp::Not, inner) => count_branches(inner), - ExprKind::DropTemps(inner) => count_branches(inner), - ExprKind::Block(block, _) => match block.expr { - Some(inner) => count_branches(inner), - None => 1, - }, + ExprKind::Unary(UnOp::Not, inner) | ExprKind::DropTemps(inner) => count_branches(inner), + ExprKind::Block(block, _) => block.expr.map_or(1, count_branches), ExprKind::If(cond, ..) => count_branches(cond), _ => 1, } @@ -211,9 +220,12 @@ fn emit_diagnostic( ); args.insert( Cow::Borrowed("branches"), - FluentValue::from(metadata.branches as i64), + FluentValue::from(i64::try_from(metadata.branches).unwrap_or(i64::MAX)), + ); + args.insert( + Cow::Borrowed("limit"), + FluentValue::from(i64::try_from(limit).unwrap_or(i64::MAX)), ); - args.insert(Cow::Borrowed("limit"), FluentValue::from(limit as i64)); let branch_phrase_text = branch_phrase(localizer.locale(), metadata.branches); args.insert( Cow::Borrowed("branch_phrase"), @@ -234,9 +246,9 @@ fn emit_diagnostic( fallback_messages(metadata.kind, metadata.branches, limit) }); - let primary = normalise_isolation_marks(messages.primary()); - let note = normalise_isolation_marks(messages.note()); - let help = normalise_isolation_marks(messages.help()); + let primary = normalize_isolation_marks(messages.primary()); + let note = normalize_isolation_marks(messages.note()); + let help = normalize_isolation_marks(messages.help()); cx.emit_span_lint( CONDITIONAL_MAX_N_BRANCHES, @@ -249,7 +261,7 @@ fn emit_diagnostic( ); } -fn normalise_isolation_marks(text: &str) -> String { +fn normalize_isolation_marks(text: &str) -> String { if text .chars() .any(|character| matches!(character, '\u{2068}' | '\u{2069}' | '\u{FFFD}')) diff --git a/crates/function_attrs_follow_docs/src/driver.rs b/crates/function_attrs_follow_docs/src/driver.rs index 1235e3d4b..9f5ef801a 100644 --- a/crates/function_attrs_follow_docs/src/driver.rs +++ b/crates/function_attrs_follow_docs/src/driver.rs @@ -332,7 +332,7 @@ const MESSAGE_KEY: MessageKey<'static> = MessageKey::new("function_attrs_follow_ type FunctionAttrsMessages = DiagnosticMessageSet; #[cfg(test)] -fn localised_messages( +fn localized_messages( lookup: &impl BundleLookup, kind: FunctionKind, attribute: &str, diff --git a/crates/function_attrs_follow_docs/src/tests/localization.rs b/crates/function_attrs_follow_docs/src/tests/localization.rs index 7afb76f67..45d24741a 100644 --- a/crates/function_attrs_follow_docs/src/tests/localization.rs +++ b/crates/function_attrs_follow_docs/src/tests/localization.rs @@ -11,7 +11,7 @@ use whitaker_common::i18n::{I18nError, testing::FailingLookup}; use super::{ FunctionAttrsMessages, FunctionKind, Localizer, MESSAGE_KEY, attribute_fallback, - localised_messages, + localized_messages, }; #[derive(Default)] @@ -119,9 +119,9 @@ fn resolve_localization( ) -> Result { if failing { let lookup = failing_lookup(); - localised_messages(&lookup, kind, attribute) + localized_messages(&lookup, kind, attribute) } else { - world.with_localizer(|localizer| localised_messages(localizer, kind, attribute)) + world.with_localizer(|localizer| localized_messages(localizer, kind, attribute)) } } diff --git a/crates/module_max_lines/Cargo.toml b/crates/module_max_lines/Cargo.toml index 7001aca96..5589aed53 100644 --- a/crates/module_max_lines/Cargo.toml +++ b/crates/module_max_lines/Cargo.toml @@ -34,6 +34,7 @@ whitaker = { workspace = true, features = ["dylint-driver"], optional = true } fluent-templates = { workspace = true, optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } camino = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } diff --git a/crates/module_max_lines/src/driver.rs b/crates/module_max_lines/src/driver.rs index c67dd8522..3eda99591 100644 --- a/crates/module_max_lines/src/driver.rs +++ b/crates/module_max_lines/src/driver.rs @@ -250,6 +250,7 @@ mod behaviour { } } + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> ModuleWorld { ModuleWorld::default() diff --git a/crates/module_max_lines/src/lib.rs b/crates/module_max_lines/src/lib.rs index 3fbf5a1a9..840470ad6 100644 --- a/crates/module_max_lines/src/lib.rs +++ b/crates/module_max_lines/src/lib.rs @@ -1,3 +1,8 @@ +//! Dylint lint that flags modules exceeding the configured line budget. +//! +//! The lint drives contributors toward smaller, reviewable modules; the +//! `dylint-driver` feature gates the rustc-facing implementation so the crate +//! also builds as an ordinary library. #![cfg_attr(feature = "dylint-driver", feature(rustc_private))] #[cfg(feature = "dylint-driver")] diff --git a/crates/module_must_have_inner_docs/src/driver.rs b/crates/module_must_have_inner_docs/src/driver/mod.rs similarity index 98% rename from crates/module_must_have_inner_docs/src/driver.rs rename to crates/module_must_have_inner_docs/src/driver/mod.rs index 2bba08980..ef19d43ba 100644 --- a/crates/module_must_have_inner_docs/src/driver.rs +++ b/crates/module_must_have_inner_docs/src/driver/mod.rs @@ -326,17 +326,17 @@ fn fallback_messages(module: ModuleName<'_>) -> ModuleDocMessages { } #[cfg(test)] -#[path = "tests/behaviour.rs"] +#[path = "../tests/behaviour.rs"] mod behaviour; #[cfg(test)] -#[path = "tests/ui.rs"] +#[path = "../tests/ui.rs"] mod ui; #[cfg(test)] -#[path = "tests/classifier.rs"] +#[path = "../tests/classifier.rs"] mod classifier; #[cfg(test)] -#[path = "tests/span_to_snippet.rs"] +#[path = "../tests/span_to_snippet.rs"] mod span_to_snippet; diff --git a/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.stderr b/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.stderr index 17b1568df..a9beb0365 100644 --- a/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.stderr +++ b/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.stderr @@ -4,7 +4,7 @@ error: Avoid calling expect on `std::option::Option<&str>` outside test-only cod LL | let _ = parsed.expect("ordinary code must not inherit rstest harness status"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: The call originates within function `parse` which is not recognised as a test. + = note: The call originates within function `parse` which is not recognized as a test. = help: Handle the `None` variant of `std::option::Option<&str>` or move the code into a test. = note: requested on the command line with `-D no-expect-outside-tests` diff --git a/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.rs b/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.rs index 4da9179f3..97f6f537d 100644 --- a/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.rs +++ b/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.rs @@ -1,7 +1,7 @@ //! Negative regression ensuring Tokio-specific test configuration does not //! leak into ordinary code. //! -//! The fixture config recognises `#[tokio::test]`, but `parse_config` remains +//! The fixture config recognizes `#[tokio::test]`, but `parse_config` remains //! production code and must still trigger the lint even in the same crate. fn parse_config() { diff --git a/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.stderr b/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.stderr index add3a1f4c..1aece484b 100644 --- a/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.stderr +++ b/crates/no_expect_outside_tests/examples/fail_expect_in_tokio_crate_non_test_fn.stderr @@ -4,7 +4,7 @@ error: Avoid calling expect on `std::option::Option<&str>` outside test-only cod LL | let _ = parsed.expect("ordinary code in a Tokio crate must still lint"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: The call originates within function `parse_config` which is not recognised as a test. + = note: The call originates within function `parse_config` which is not recognized as a test. = help: Handle the `None` variant of `std::option::Option<&str>` or move the code into a test. = note: requested on the command line with `-D no-expect-outside-tests` diff --git a/crates/no_expect_outside_tests/src/behaviour.rs b/crates/no_expect_outside_tests/src/behaviour.rs index 2c94e0084..1f903e09a 100644 --- a/crates/no_expect_outside_tests/src/behaviour.rs +++ b/crates/no_expect_outside_tests/src/behaviour.rs @@ -1,7 +1,7 @@ //! Behaviour-driven tests covering context summarization for the lint's context //! world and BDD steps. -use crate::context::{ContextSummary, summarise_context}; +use crate::context::{ContextSummary, summarize_context}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use std::cell::RefCell; @@ -55,7 +55,7 @@ impl ContextWorld { fn evaluate(&self) { let entries = self.entries.borrow(); - let summary = summarise_context( + let summary = summarize_context( entries.as_slice(), *self.cfg_test.borrow(), self.additional.borrow().as_slice(), @@ -115,8 +115,8 @@ fn given_doctest(world: &ContextWorld) { world.mark_doctest(); } -#[when("I summarise the context")] -fn when_summarise(world: &ContextWorld) { +#[when("I summarize the context")] +fn when_summarize(world: &ContextWorld) { world.evaluate(); } diff --git a/crates/no_expect_outside_tests/src/context.rs b/crates/no_expect_outside_tests/src/context/mod.rs similarity index 99% rename from crates/no_expect_outside_tests/src/context.rs rename to crates/no_expect_outside_tests/src/context/mod.rs index 59b8fc2dd..d9a3f5181 100644 --- a/crates/no_expect_outside_tests/src/context.rs +++ b/crates/no_expect_outside_tests/src/context/mod.rs @@ -109,7 +109,7 @@ fn has_test_ancestry( /// # Examples /// /// ```ignore -/// let summary = summarise_context( +/// let summary = summarize_context( /// &entries, /// has_test_context_ancestry, /// additional_test_attributes, @@ -118,7 +118,7 @@ fn has_test_ancestry( /// // `.expect()` is allowed in this context. /// } /// ``` -pub(crate) fn summarise_context( +pub(crate) fn summarize_context( entries: &[ContextEntry], has_test_context_ancestry: bool, additional_test_attributes: &[AttributePath], diff --git a/crates/no_expect_outside_tests/src/diagnostics.rs b/crates/no_expect_outside_tests/src/diagnostics.rs index 48d582e5a..8f4535251 100644 --- a/crates/no_expect_outside_tests/src/diagnostics.rs +++ b/crates/no_expect_outside_tests/src/diagnostics.rs @@ -195,7 +195,7 @@ const MESSAGE_KEY: MessageKey<'static> = MessageKey::new("no_expect_outside_test type NoExpectMessages = DiagnosticMessageSet; #[cfg(test)] -fn localised_messages( +fn localized_messages( lookup: &impl BundleLookup, receiver: &ReceiverLabel, context: &ContextLabel, @@ -224,7 +224,7 @@ fn fallback_messages( category: ReceiverCategory, ) -> NoExpectMessages { let primary = format!("Avoid calling expect on {receiver} outside test-only code."); - let note = format!("The call originates within {context} which is not recognised as a test.",); + let note = format!("The call originates within {context} which is not recognized as a test.",); let help = category.fallback_help(receiver); NoExpectMessages::new(primary, note, help) diff --git a/crates/no_expect_outside_tests/src/driver/mod.rs b/crates/no_expect_outside_tests/src/driver/mod.rs index 25d1485d4..5ecdbfb9a 100644 --- a/crates/no_expect_outside_tests/src/driver/mod.rs +++ b/crates/no_expect_outside_tests/src/driver/mod.rs @@ -23,7 +23,7 @@ use whitaker::SharedConfig; use whitaker::hir::has_test_like_hir_attributes; use whitaker_common::{AttributePath, Localizer, get_localizer_for_lint}; -use crate::context::{collect_context, is_cfg_test_attribute, summarise_context}; +use crate::context::{collect_context, is_cfg_test_attribute, summarize_context}; use crate::diagnostics::{DiagnosticContext, emit_diagnostic}; dylint_linting::impl_late_lint! { @@ -122,7 +122,7 @@ impl<'tcx> LateLintPass<'tcx> for NoExpectOutsideTests { let additional = self.additional_test_attributes.as_slice(); let (entries, has_test_context_ancestry) = collect_context(cx, expr.hir_id, additional); - let summary = summarise_context(entries.as_slice(), has_test_context_ancestry, additional); + let summary = summarize_context(entries.as_slice(), has_test_context_ancestry, additional); if summary.is_test { return; diff --git a/crates/no_expect_outside_tests/src/tests.rs b/crates/no_expect_outside_tests/src/tests.rs index f569b8b4c..a74000480 100644 --- a/crates/no_expect_outside_tests/src/tests.rs +++ b/crates/no_expect_outside_tests/src/tests.rs @@ -1,7 +1,7 @@ //! Unit tests validating context summarization outcomes across default and //! configured test attributes. -use crate::context::summarise_context; +use crate::context::summarize_context; use rstest::rstest; use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; use whitaker_common::{ContextEntry, ContextKind}; @@ -19,18 +19,18 @@ fn test_attribute() -> Attribute { } #[rstest] -fn summarises_plain_context() { +fn summarizes_plain_context() { let entries = vec![function_entry("handler", Vec::new())]; - let summary = summarise_context(&entries, false, &[]); + let summary = summarize_context(&entries, false, &[]); assert!(!summary.is_test); assert_eq!(summary.function_name.as_deref(), Some("handler")); } #[rstest] -fn recognises_test_attribute() { +fn recognizes_test_attribute() { let entries = vec![function_entry("test_case", vec![test_attribute()])]; - let summary = summarise_context(&entries, false, &[]); + let summary = summarize_context(&entries, false, &[]); assert!(summary.is_test); assert_eq!(summary.function_name.as_deref(), Some("test_case")); @@ -39,7 +39,7 @@ fn recognises_test_attribute() { #[rstest] fn honours_cfg_test() { let entries = vec![module_entry("tests", Vec::new())]; - let summary = summarise_context(&entries, true, &[]); + let summary = summarize_context(&entries, true, &[]); assert!(summary.is_test); assert_eq!(summary.function_name, None); @@ -55,7 +55,7 @@ fn honours_additional_attributes() { )], )]; let additional = vec![AttributePath::from("custom::test")]; - let summary = summarise_context(&entries, false, additional.as_slice()); + let summary = summarize_context(&entries, false, additional.as_slice()); assert!(summary.is_test); assert_eq!(summary.function_name.as_deref(), Some("custom")); diff --git a/crates/no_expect_outside_tests/src/tests/localization.rs b/crates/no_expect_outside_tests/src/tests/localization.rs index 5890b799f..3bfbc6ee1 100644 --- a/crates/no_expect_outside_tests/src/tests/localization.rs +++ b/crates/no_expect_outside_tests/src/tests/localization.rs @@ -13,7 +13,7 @@ use whitaker_common::i18n::{BundleLookup, testing::FailingLookup}; use super::{ I18nError, Localizer, MESSAGE_KEY, NoExpectMessages, ReceiverCategory, ReceiverLabel, - context_label, fallback_messages, localised_messages, + context_label, fallback_messages, localized_messages, }; use crate::context::ContextSummary; @@ -267,7 +267,7 @@ fn execute_localization( ) -> Result { let context = context_label(summary); let category = ReceiverCategory::for_label(receiver); - localised_messages(lookup, receiver, &context, category) + localized_messages(lookup, receiver, &context, category) } fn failing_lookup() -> FailingLookup { diff --git a/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs b/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs index b4d40b19f..769ab60af 100644 --- a/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs +++ b/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs @@ -3,7 +3,7 @@ use rstest::rstest; use super::{ - ContextLabel, Localizer, NoExpectMessages, ReceiverCategory, ReceiverLabel, localised_messages, + ContextLabel, Localizer, NoExpectMessages, ReceiverCategory, ReceiverLabel, localized_messages, }; #[rstest] @@ -27,7 +27,7 @@ fn handles_receiver_type_edge_cases( let receiver_label = ReceiverLabel::new(receiver); let context_label = ContextLabel::new(context); let category = ReceiverCategory::for_label(&receiver_label); - let messages = localised_messages(&lookup, &receiver_label, &context_label, category) + let messages = localized_messages(&lookup, &receiver_label, &context_label, category) .expect("localization succeeds"); assert!( assertion(&messages), diff --git a/crates/no_expect_outside_tests/tests/features/context_summary.feature b/crates/no_expect_outside_tests/tests/features/context_summary.feature index ae6b995a9..14469084e 100644 --- a/crates/no_expect_outside_tests/tests/features/context_summary.feature +++ b/crates/no_expect_outside_tests/tests/features/context_summary.feature @@ -2,32 +2,32 @@ Feature: Summarize traversal context for `.expect(..)` linting Scenario: Plain function without test attributes Given a non-test function named handler - When I summarise the context + When I summarize the context Then the context is marked as production And the function name is handler Scenario: Function marked as a test Given a test function named works - When I summarise the context + When I summarize the context Then the context is marked as test And the function name is works Scenario: Module guarded by cfg(test) Given a module with cfg(test) - When I summarise the context + When I summarize the context Then the context is marked as test And no function name is recorded Scenario: Function recognized via configured attribute Given an additional test attribute custom::test is configured And a function annotated with the additional attribute custom::test - When I summarise the context + When I summarize the context Then the context is marked as test And the function name is custom Scenario: Doctest crate bypasses linting Given a non-test function named handler And the lint is running within a doctest - When I summarise the context + When I summarize the context Then the lint is skipped And the function name is handler diff --git a/crates/no_expect_outside_tests/ui/fail_expect_in_file_backed_non_test_fn.stderr b/crates/no_expect_outside_tests/ui/fail_expect_in_file_backed_non_test_fn.stderr index 85082bd0d..2778af4f8 100644 --- a/crates/no_expect_outside_tests/ui/fail_expect_in_file_backed_non_test_fn.stderr +++ b/crates/no_expect_outside_tests/ui/fail_expect_in_file_backed_non_test_fn.stderr @@ -4,7 +4,7 @@ error: Avoid calling expect on `std::option::Option<&str>` outside test-only cod LL | let _ = value.expect("file-backed cfg(test) ancestry must not leak into main"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: The call originates within function `main` which is not recognised as a test. + = note: The call originates within function `main` which is not recognized as a test. = help: Handle the `None` variant of `std::option::Option<&str>` or move the code into a test. note: the lint level is defined here --> $DIR/fail_expect_in_file_backed_non_test_fn.rs:6:9 diff --git a/crates/no_expect_outside_tests/ui/fail_expect_in_fn.stderr b/crates/no_expect_outside_tests/ui/fail_expect_in_fn.stderr index 688473104..609da9dc5 100644 --- a/crates/no_expect_outside_tests/ui/fail_expect_in_fn.stderr +++ b/crates/no_expect_outside_tests/ui/fail_expect_in_fn.stderr @@ -4,7 +4,7 @@ error: Avoid calling expect on `std::option::Option` outside test-only code LL | let _result = value.expect("value should exist"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: The call originates within function `process` which is not recognised as a test. + = note: The call originates within function `process` which is not recognized as a test. = help: Handle the `None` variant of `std::option::Option` or move the code into a test. note: the lint level is defined here --> $DIR/fail_expect_in_fn.rs:2:9 @@ -18,7 +18,7 @@ error: Avoid calling expect on `std::result::Result<(), &str>` outside test-only LL | let _ = result.expect("result should be ok"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: The call originates within function `fail_result` which is not recognised as a test. + = note: The call originates within function `fail_result` which is not recognized as a test. = help: Handle the `Err` variant of `std::result::Result<(), &str>` or move the code into a test. error: aborting due to 2 previous errors diff --git a/crates/no_expect_outside_tests/ui/fail_expect_with_cfg_attr.stderr b/crates/no_expect_outside_tests/ui/fail_expect_with_cfg_attr.stderr index e5d6c5184..aa650a667 100644 --- a/crates/no_expect_outside_tests/ui/fail_expect_with_cfg_attr.stderr +++ b/crates/no_expect_outside_tests/ui/fail_expect_with_cfg_attr.stderr @@ -4,7 +4,7 @@ error: Avoid calling expect on `std::option::Option` outside test-only code LL | let _ = value.expect("handler should not ignore errors"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: The call originates within function `handler` which is not recognised as a test. + = note: The call originates within function `handler` which is not recognized as a test. = help: Handle the `None` variant of `std::option::Option` or move the code into a test. note: the lint level is defined here --> $DIR/fail_expect_with_cfg_attr.rs:1:9 diff --git a/crates/no_std_fs_operations/Cargo.toml b/crates/no_std_fs_operations/Cargo.toml index 08f46a848..b4f77c88d 100644 --- a/crates/no_std_fs_operations/Cargo.toml +++ b/crates/no_std_fs_operations/Cargo.toml @@ -35,6 +35,7 @@ serde = { workspace = true, optional = true } whitaker = { workspace = true, features = ["dylint-driver"], optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } anyhow = "1.0" cargo_metadata = { workspace = true } insta = { workspace = true } diff --git a/crates/no_std_fs_operations/src/behaviour.rs b/crates/no_std_fs_operations/src/behaviour.rs index 4e3cb6174..d3b848271 100644 --- a/crates/no_std_fs_operations/src/behaviour.rs +++ b/crates/no_std_fs_operations/src/behaviour.rs @@ -1,6 +1,6 @@ //! Behaviour-driven localization tests for the `no_std_fs_operations` lint. -use crate::diagnostics::{StdFsMessages, localised_messages}; +use crate::diagnostics::{StdFsMessages, localized_messages}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use std::cell::RefCell; @@ -31,10 +31,10 @@ impl LocalizationWorld { fn resolve(&mut self) { let op = self.operation.clone(); let result = if self.failing { - localised_messages(&FailingLookup::new("no_std_fs_operations"), &op) + localized_messages(&FailingLookup::new("no_std_fs_operations"), &op) } else { let localizer = self.localizer.as_ref().expect("a locale must be selected"); - localised_messages(localizer, &op) + localized_messages(localizer, &op) }; self.result = Some(result); } @@ -83,8 +83,8 @@ fn given_failure(world: &WorldCell) { world.borrow_mut().mark_failure(); } -#[when("I localise the std::fs diagnostic")] -fn when_localise(world: &WorldCell) { +#[when("I localize the std::fs diagnostic")] +fn when_localize(world: &WorldCell) { world.borrow_mut().resolve(); } diff --git a/crates/no_std_fs_operations/src/diagnostics.rs b/crates/no_std_fs_operations/src/diagnostics.rs index 6ba5a22df..77c77cb31 100644 --- a/crates/no_std_fs_operations/src/diagnostics.rs +++ b/crates/no_std_fs_operations/src/diagnostics.rs @@ -75,7 +75,7 @@ fn sanitize_message(text: String) -> String { } #[cfg(test)] -pub(crate) fn localised_messages( +pub(crate) fn localized_messages( lookup: &impl BundleLookup, operation: &str, ) -> Result { diff --git a/crates/no_std_fs_operations/src/exclusion_behaviour.rs b/crates/no_std_fs_operations/src/exclusion_behaviour.rs index e77e9ab8d..db1e41d1f 100644 --- a/crates/no_std_fs_operations/src/exclusion_behaviour.rs +++ b/crates/no_std_fs_operations/src/exclusion_behaviour.rs @@ -34,6 +34,7 @@ impl ExclusionWorld { type WorldCell = RefCell; +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> WorldCell { RefCell::new(ExclusionWorld::default()) diff --git a/crates/no_std_fs_operations/src/usage.rs b/crates/no_std_fs_operations/src/usage/mod.rs similarity index 100% rename from crates/no_std_fs_operations/src/usage.rs rename to crates/no_std_fs_operations/src/usage/mod.rs diff --git a/crates/no_std_fs_operations/src/usage/tests.rs b/crates/no_std_fs_operations/src/usage/tests.rs index d7c9b7947..25114112b 100644 --- a/crates/no_std_fs_operations/src/usage/tests.rs +++ b/crates/no_std_fs_operations/src/usage/tests.rs @@ -21,7 +21,7 @@ use rstest::rstest; #[case("fs::std", false)] #[case("std::", false)] #[case("std::filesystem", false)] -fn recognises_std_fs_paths(#[case] path: &str, #[case] expected: bool) { +fn recognizes_std_fs_paths(#[case] path: &str, #[case] expected: bool) { assert_eq!(label_is_std_fs(path), expected); } diff --git a/crates/no_std_fs_operations/tests/features/localization.feature b/crates/no_std_fs_operations/tests/features/localization.feature index 41ac1d823..de9376544 100644 --- a/crates/no_std_fs_operations/tests/features/localization.feature +++ b/crates/no_std_fs_operations/tests/features/localization.feature @@ -1,9 +1,9 @@ -Feature: Localised diagnostics for std::fs usage +Feature: Localized diagnostics for std::fs usage Scenario: English messaging encourages cap-std Given the locale "en-GB" is selected And the operation is "std::fs::read_to_string" - When I localise the std::fs diagnostic + When I localize the std::fs diagnostic Then the primary mentions "std::fs::read_to_string" And the note references "cap_std::fs::Dir" And the help references "camino::Utf8Path" @@ -11,26 +11,26 @@ Feature: Localised diagnostics for std::fs usage Scenario: Welsh messaging reflects the capability note Given the locale "cy" is selected And the operation is "std::fs::remove_file" - When I localise the std::fs diagnostic + When I localize the std::fs diagnostic Then the primary mentions "std::fs::remove_file" And the note references "cyfeiriadur" Scenario: Scottish Gaelic messaging mirrors the policy Given the locale "gd" is selected And the operation is "std::fs::metadata" - When I localise the std::fs diagnostic + When I localize the std::fs diagnostic Then the primary mentions "std::fs::metadata" And the note references "neach-gairm" Scenario: Unsupported locale falls back to English Given the locale "zz" is selected And the operation is "std::fs::read_dir" - When I localise the std::fs diagnostic + When I localize the std::fs diagnostic Then the primary mentions "std::fs::read_dir" And the help references "cap_std::fs::Dir" Scenario: Localization failure surfaces the missing key Given localization fails And the operation is "std::fs::canonicalize" - When I localise the std::fs diagnostic + When I localize the std::fs diagnostic Then localization fails for "no_std_fs_operations" diff --git a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_rstest_non_test_module.rs b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_rstest_non_test_module.rs index 36fd16361..8315dc7d1 100644 --- a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_rstest_non_test_module.rs +++ b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_rstest_non_test_module.rs @@ -35,7 +35,7 @@ fn parse() { /// Const-only sibling module used as a fixture. /// /// Verifies that `collect_rstest_companion_test_functions` does not mistake an -/// arbitrary `const`-only module for a synthesised rstest harness descriptor. +/// arbitrary `const`-only module for a synthesized rstest harness descriptor. /// A genuine rstest companion module contains `#[test]` functions generated /// by the proc-macro; a module containing only `pub const` items must never /// be treated as one. diff --git a/crates/no_unwrap_or_else_panic/src/context.rs b/crates/no_unwrap_or_else_panic/src/context/mod.rs similarity index 99% rename from crates/no_unwrap_or_else_panic/src/context.rs rename to crates/no_unwrap_or_else_panic/src/context/mod.rs index ffc03bd60..58426ee47 100644 --- a/crates/no_unwrap_or_else_panic/src/context.rs +++ b/crates/no_unwrap_or_else_panic/src/context/mod.rs @@ -31,7 +31,7 @@ use rustc_span::sym; /// Summarize the context for a given HIR node. #[cfg(feature = "dylint-driver")] -pub(crate) fn summarise_context<'tcx>( +pub(crate) fn summarize_context<'tcx>( cx: &LateContext<'tcx>, hir_id: hir::HirId, ) -> ContextSummary { diff --git a/crates/no_unwrap_or_else_panic/src/driver.rs b/crates/no_unwrap_or_else_panic/src/driver.rs index 2260f891a..39b4e1ed0 100644 --- a/crates/no_unwrap_or_else_panic/src/driver.rs +++ b/crates/no_unwrap_or_else_panic/src/driver.rs @@ -98,7 +98,7 @@ impl<'tcx> LateLintPass<'tcx> for NoUnwrapOrElsePanic { return; }; - let summary = summarise_context_with_harness( + let summary = summarize_context_with_harness( cx, expr.hir_id, self.is_test_harness, @@ -134,13 +134,13 @@ fn is_inside_harness_test_function<'tcx>( /// Summarizes the lint context for an expression, merging attribute-based and /// harness-based test detection into a single immutable result. -fn summarise_context_with_harness<'tcx>( +fn summarize_context_with_harness<'tcx>( cx: &LateContext<'tcx>, hir_id: hir::HirId, is_test_harness: bool, harness_test_functions: &HashSet, ) -> ContextSummary { - let mut summary = crate::context::summarise_context(cx, hir_id); + let mut summary = crate::context::summarize_context(cx, hir_id); if !summary.is_test && is_test_harness { summary.is_test = is_inside_harness_test_function(cx, hir_id, harness_test_functions); } diff --git a/crates/test_must_not_have_example/Cargo.toml b/crates/test_must_not_have_example/Cargo.toml index 18605b91a..f935597bd 100644 --- a/crates/test_must_not_have_example/Cargo.toml +++ b/crates/test_must_not_have_example/Cargo.toml @@ -34,6 +34,7 @@ log = { workspace = true, optional = true } whitaker = { version = "0.2.7", path = "../../", features = ["dylint-driver"], optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } diff --git a/crates/test_must_not_have_example/src/behaviour.rs b/crates/test_must_not_have_example/src/behaviour.rs index 522268c86..52535f429 100644 --- a/crates/test_must_not_have_example/src/behaviour.rs +++ b/crates/test_must_not_have_example/src/behaviour.rs @@ -26,6 +26,7 @@ impl DocumentationWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> DocumentationWorld { DocumentationWorld::default() diff --git a/crates/whitaker_clones_core/src/index/tests.rs b/crates/whitaker_clones_core/src/index/tests.rs index 2f9871c5d..fd99e7044 100644 --- a/crates/whitaker_clones_core/src/index/tests.rs +++ b/crates/whitaker_clones_core/src/index/tests.rs @@ -1,6 +1,7 @@ //! Unit tests for MinHash and LSH candidate generation. use rstest::{fixture, rstest}; +use whitaker_test_macros::allow_fixture_expansion_lints; use crate::token::Fingerprint; @@ -33,16 +34,19 @@ fn multi_band_config() -> LshConfig { LshConfig::new(32, 4).expect("multi-band config should validate") } +#[allow_fixture_expansion_lints] #[fixture] fn shared_signature() -> MinHashSignature { sketch(&[1, 2, 3, 4]) } +#[allow_fixture_expansion_lints] #[fixture] fn distinct_signature() -> MinHashSignature { sketch(&[8, 9, 10, 11]) } +#[allow_fixture_expansion_lints] #[fixture] fn identical_signature() -> MinHashSignature { sketch(&[5, 7, 11, 13]) diff --git a/crates/whitaker_clones_core/src/run0/emit.rs b/crates/whitaker_clones_core/src/run0/emit.rs index 9c9ece949..b1d747e9f 100644 --- a/crates/whitaker_clones_core/src/run0/emit.rs +++ b/crates/whitaker_clones_core/src/run0/emit.rs @@ -160,7 +160,7 @@ fn build_result( text: format!("Peer clone fragment: {}", peer.id().as_str()), }), physical_location: whitaker_sarif::PhysicalLocation { - artifact_location: whitaker_sarif::ArtifactLocation { + artefact_location: whitaker_sarif::ArtefactLocation { uri: peer.file_uri().to_owned(), uri_base_id: None, }, diff --git a/crates/whitaker_clones_core/src/run0/tests.rs b/crates/whitaker_clones_core/src/run0/tests.rs index cf5ed63fb..58e5f7f03 100644 --- a/crates/whitaker_clones_core/src/run0/tests.rs +++ b/crates/whitaker_clones_core/src/run0/tests.rs @@ -181,13 +181,13 @@ fn emit_run0_uses_primary_and_related_locations() { assert_eq!(result.locations.len(), 1); assert_eq!(result.related_locations.len(), 1); assert_eq!( - result.locations[0].physical_location.artifact_location.uri, + result.locations[0].physical_location.artefact_location.uri, "src/a.rs" ); assert_eq!( result.related_locations[0] .physical_location - .artifact_location + .artefact_location .uri, "src/b.rs" ); diff --git a/crates/whitaker_clones_core/src/run0/types.rs b/crates/whitaker_clones_core/src/run0/types.rs index e11af4c5e..8ee0a8229 100644 --- a/crates/whitaker_clones_core/src/run0/types.rs +++ b/crates/whitaker_clones_core/src/run0/types.rs @@ -71,7 +71,7 @@ impl TokenFragment { self.profile } - /// Returns the source artifact URI used in SARIF output. + /// Returns the source artefact URI used in SARIF output. #[must_use] pub fn file_uri(&self) -> &str { self.file_uri.as_str() diff --git a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs index 698f84990..cb0ce7010 100644 --- a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs +++ b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs @@ -314,7 +314,7 @@ fn then_primary_file_is(world: &Run0World, file_uri: String) { panic!("a primary location must be present"); }; assert_eq!( - location.physical_location.artifact_location.uri, file_uri, + location.physical_location.artefact_location.uri, file_uri, "primary file URI must match the scenario expectation" ); }); diff --git a/crates/whitaker_sarif/src/builders/location_builder.rs b/crates/whitaker_sarif/src/builders/location_builder.rs index e3fc7febd..ff5b161a5 100644 --- a/crates/whitaker_sarif/src/builders/location_builder.rs +++ b/crates/whitaker_sarif/src/builders/location_builder.rs @@ -1,7 +1,7 @@ //! Builders for [`Location`] and [`Region`] objects. use crate::error::SarifError; -use crate::model::location::{ArtifactLocation, Location, PhysicalLocation, Region}; +use crate::model::location::{ArtefactLocation, Location, PhysicalLocation, Region}; /// Fluent builder for constructing a [`Region`]. /// @@ -64,7 +64,7 @@ impl RegionBuilder { self } - /// Sets the byte offset from the start of the artifact. + /// Sets the byte offset from the start of the artefact. #[must_use] pub fn with_byte_offset(mut self, offset: usize) -> Self { self.byte_offset = Some(offset); @@ -163,7 +163,7 @@ impl RegionBuilder { /// let loc = LocationBuilder::new("src/main.rs") /// .with_region(RegionBuilder::new(10).with_end_line(15).build().expect("valid region")) /// .build(); -/// assert_eq!(loc.physical_location.artifact_location.uri, "src/main.rs"); +/// assert_eq!(loc.physical_location.artefact_location.uri, "src/main.rs"); /// ``` #[derive(Debug, Clone)] pub struct LocationBuilder { @@ -190,7 +190,7 @@ impl LocationBuilder { self } - /// Sets the region within the artifact. + /// Sets the region within the artefact. #[must_use] pub fn with_region(mut self, region: Region) -> Self { self.region = Some(region); @@ -202,7 +202,7 @@ impl LocationBuilder { pub fn build(self) -> Location { Location { physical_location: PhysicalLocation { - artifact_location: ArtifactLocation { + artefact_location: ArtefactLocation { uri: self.uri, uri_base_id: self.uri_base_id, }, @@ -255,7 +255,7 @@ mod tests { #[test] fn location_builder_minimal() { let loc = LocationBuilder::new("src/main.rs").build(); - assert_eq!(loc.physical_location.artifact_location.uri, "src/main.rs"); + assert_eq!(loc.physical_location.artefact_location.uri, "src/main.rs"); assert!(loc.physical_location.region.is_none()); } @@ -281,7 +281,7 @@ mod tests { .build(); assert_eq!( loc.physical_location - .artifact_location + .artefact_location .uri_base_id .as_deref(), Some("%SRCROOT%") diff --git a/crates/whitaker_sarif/src/builders/log_builder.rs b/crates/whitaker_sarif/src/builders/log_builder.rs index 76911f6a5..53a25b8c8 100644 --- a/crates/whitaker_sarif/src/builders/log_builder.rs +++ b/crates/whitaker_sarif/src/builders/log_builder.rs @@ -111,7 +111,7 @@ mod tests { }, invocations: Vec::new(), results: Vec::new(), - artifacts: Vec::new(), + artefacts: Vec::new(), }; let log = SarifLogBuilder::new().with_run(run).build(); assert_eq!(log.runs.len(), 1); diff --git a/crates/whitaker_sarif/src/builders/run_builder.rs b/crates/whitaker_sarif/src/builders/run_builder.rs index e8d1b944c..a436eb3b2 100644 --- a/crates/whitaker_sarif/src/builders/run_builder.rs +++ b/crates/whitaker_sarif/src/builders/run_builder.rs @@ -2,7 +2,7 @@ use crate::model::descriptor::ReportingDescriptor; use crate::model::result::SarifResult; -use crate::model::run::{Artifact, Invocation, Run, Tool, ToolComponent}; +use crate::model::run::{Artefact, Invocation, Run, Tool, ToolComponent}; /// Fluent builder for constructing a [`Run`]. /// @@ -22,7 +22,7 @@ pub struct RunBuilder { rules: Vec, invocations: Vec, results: Vec, - artifacts: Vec, + artefacts: Vec, } impl RunBuilder { @@ -36,7 +36,7 @@ impl RunBuilder { rules: Vec::new(), invocations: Vec::new(), results: Vec::new(), - artifacts: Vec::new(), + artefacts: Vec::new(), } } @@ -117,27 +117,27 @@ impl RunBuilder { self } - /// Appends an artifact reference. + /// Appends an artefact reference. /// /// # Examples /// /// ``` - /// use whitaker_sarif::{RunBuilder, Artifact, ArtifactLocation}; + /// use whitaker_sarif::{RunBuilder, Artefact, ArtefactLocation}; /// /// let run = RunBuilder::new("tool", "1.0") - /// .with_artifact(Artifact { - /// location: ArtifactLocation { + /// .with_artefact(Artefact { + /// location: ArtefactLocation { /// uri: "src/main.rs".into(), /// uri_base_id: None, /// }, /// mime_type: Some("text/x-rust".into()), /// }) /// .build(); - /// assert_eq!(run.artifacts.len(), 1); + /// assert_eq!(run.artefacts.len(), 1); /// ``` #[must_use] - pub fn with_artifact(mut self, artifact: Artifact) -> Self { - self.artifacts.push(artifact); + pub fn with_artefact(mut self, artefact: Artefact) -> Self { + self.artefacts.push(artefact); self } @@ -166,7 +166,7 @@ impl RunBuilder { }, invocations: self.invocations, results: self.results, - artifacts: self.artifacts, + artefacts: self.artefacts, } } } diff --git a/crates/whitaker_sarif/src/lib.rs b/crates/whitaker_sarif/src/lib.rs index bf4cde4a9..63514b6ba 100644 --- a/crates/whitaker_sarif/src/lib.rs +++ b/crates/whitaker_sarif/src/lib.rs @@ -27,7 +27,7 @@ pub use error::{Result, SarifError}; // Model types pub use model::{ - Artifact, ArtifactLocation, Invocation, Level, Location, Message, MultiformatMessageString, + Artefact, ArtefactLocation, Invocation, Level, Location, Message, MultiformatMessageString, PhysicalLocation, Region, RelatedLocation, ReportingDescriptor, Run, SarifLog, SarifResult, Tool, ToolComponent, }; diff --git a/crates/whitaker_sarif/src/merge.rs b/crates/whitaker_sarif/src/merge.rs index 3e76a0116..e16bf4331 100644 --- a/crates/whitaker_sarif/src/merge.rs +++ b/crates/whitaker_sarif/src/merge.rs @@ -66,7 +66,7 @@ fn extract_key(result: &SarifResult) -> Option { .clone(); let location = result.locations.first()?; - let file = location.physical_location.artifact_location.uri.clone(); + let file = location.physical_location.artefact_location.uri.clone(); let region = location.physical_location.region.as_ref()?; Some(ResultKey { @@ -127,7 +127,7 @@ pub fn deduplicate_results(results: &[SarifResult]) -> Vec { /// /// The tool metadata is taken from the first run. Rules are unioned across all /// runs by `id` (first occurrence wins). Results are collected from all runs -/// and deduplicated. Artifacts and invocations are concatenated. +/// and deduplicated. Artefacts and invocations are concatenated. /// /// # Errors /// @@ -152,12 +152,12 @@ pub fn merge_runs(runs: &[Run]) -> Result { tool.driver.rules = union_rules(runs); let mut all_results = Vec::new(); - let mut all_artifacts = Vec::new(); + let mut all_artefacts = Vec::new(); let mut all_invocations = Vec::new(); for run in runs { all_results.extend(run.results.clone()); - all_artifacts.extend(run.artifacts.clone()); + all_artefacts.extend(run.artefacts.clone()); all_invocations.extend(run.invocations.clone()); } @@ -167,7 +167,7 @@ pub fn merge_runs(runs: &[Run]) -> Result { tool, invocations: all_invocations, results, - artifacts: all_artifacts, + artefacts: all_artefacts, }) } diff --git a/crates/whitaker_sarif/src/model/location.rs b/crates/whitaker_sarif/src/model/location.rs index b762f17ee..04c8908b4 100644 --- a/crates/whitaker_sarif/src/model/location.rs +++ b/crates/whitaker_sarif/src/model/location.rs @@ -2,21 +2,21 @@ //! //! These types describe where a result was found in source code. A //! [`Location`] wraps a [`PhysicalLocation`] which combines an -//! [`ArtifactLocation`] (file URI) with an optional [`Region`] (line and +//! [`ArtefactLocation`] (file URI) with an optional [`Region`] (line and //! column spans). use serde::{Deserialize, Serialize}; -/// A location within an artifact (source file). +/// A location within an artefact (source file). /// /// # Examples /// /// ``` -/// use whitaker_sarif::{Location, PhysicalLocation, ArtifactLocation, Region}; +/// use whitaker_sarif::{Location, PhysicalLocation, ArtefactLocation, Region}; /// /// let loc = Location { /// physical_location: PhysicalLocation { -/// artifact_location: ArtifactLocation { +/// artefact_location: ArtefactLocation { /// uri: "src/main.rs".into(), /// uri_base_id: None, /// }, @@ -30,7 +30,7 @@ use serde::{Deserialize, Serialize}; /// }), /// }, /// }; -/// assert_eq!(loc.physical_location.artifact_location.uri, "src/main.rs"); +/// assert_eq!(loc.physical_location.artefact_location.uri, "src/main.rs"); /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -43,19 +43,23 @@ pub struct Location { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PhysicalLocation { - /// Identifies the artifact (file). - pub artifact_location: ArtifactLocation, - - /// Optional region within the artifact. + /// Identifies the artefact (file). + /// + /// The SARIF 2.1.0 schema spells this property `artifactLocation`, so the + /// wire name is pinned here rather than derived from the field name. + #[serde(rename = "artifactLocation")] + pub artefact_location: ArtefactLocation, + + /// Optional region within the artefact. #[serde(default, skip_serializing_if = "Option::is_none")] pub region: Option, } -/// A reference to an artifact by URI. +/// A reference to an artefact by URI. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ArtifactLocation { - /// Relative or absolute URI of the artifact. +pub struct ArtefactLocation { + /// Relative or absolute URI of the artefact. pub uri: String, /// Base identifier for resolving relative URIs. @@ -63,7 +67,7 @@ pub struct ArtifactLocation { pub uri_base_id: Option, } -/// A region within an artifact, identified by line and column numbers. +/// A region within an artefact, identified by line and column numbers. /// /// `start_line` is always required (1-based). All other fields are optional. /// @@ -100,7 +104,7 @@ pub struct Region { #[serde(default, skip_serializing_if = "Option::is_none")] pub end_column: Option, - /// Optional byte offset from the start of the artifact. + /// Optional byte offset from the start of the artefact. #[serde(default, skip_serializing_if = "Option::is_none")] pub byte_offset: Option, @@ -167,7 +171,7 @@ mod tests { fn location_round_trip() { let loc = Location { physical_location: PhysicalLocation { - artifact_location: ArtifactLocation { + artefact_location: ArtefactLocation { uri: "src/lib.rs".into(), uri_base_id: Some("%SRCROOT%".into()), }, @@ -192,7 +196,7 @@ mod tests { text: "peer fragment".into(), }), physical_location: PhysicalLocation { - artifact_location: ArtifactLocation { + artefact_location: ArtefactLocation { uri: "src/other.rs".into(), uri_base_id: None, }, diff --git a/crates/whitaker_sarif/src/model/mod.rs b/crates/whitaker_sarif/src/model/mod.rs index a38b1493c..7869f6e4d 100644 --- a/crates/whitaker_sarif/src/model/mod.rs +++ b/crates/whitaker_sarif/src/model/mod.rs @@ -5,10 +5,10 @@ //! //! - [`log`] — top-level [`SarifLog`] container. //! - [`run`] — [`Run`], [`Tool`], [`ToolComponent`], [`Invocation`], and -//! [`Artifact`]. +//! [`Artefact`]. //! - [`result`] — [`SarifResult`], [`Level`], and [`Message`]. //! - [`location`] — [`Location`], [`PhysicalLocation`], -//! [`ArtifactLocation`], [`Region`], and [`RelatedLocation`]. +//! [`ArtefactLocation`], [`Region`], and [`RelatedLocation`]. //! - [`descriptor`] — [`ReportingDescriptor`] and //! [`MultiformatMessageString`]. //! @@ -22,7 +22,7 @@ pub mod result; pub mod run; pub use descriptor::{MultiformatMessageString, ReportingDescriptor}; -pub use location::{ArtifactLocation, Location, PhysicalLocation, Region, RelatedLocation}; +pub use location::{ArtefactLocation, Location, PhysicalLocation, Region, RelatedLocation}; pub use log::SarifLog; pub use result::{Level, Message, SarifResult}; -pub use run::{Artifact, Invocation, Run, Tool, ToolComponent}; +pub use run::{Artefact, Invocation, Run, Tool, ToolComponent}; diff --git a/crates/whitaker_sarif/src/model/run.rs b/crates/whitaker_sarif/src/model/run.rs index f5d47225f..bba772b96 100644 --- a/crates/whitaker_sarif/src/model/run.rs +++ b/crates/whitaker_sarif/src/model/run.rs @@ -1,9 +1,9 @@ -//! SARIF run, tool, invocation, and artifact types. +//! SARIF run, tool, invocation, and artefact types. //! //! A [`Run`] represents a single execution of an analysis tool. It contains //! the [`Tool`] that produced the results, optional [`Invocation`] metadata, //! the [`SarifResult`] findings, and any referenced -//! [`Artifact`]s. +//! [`Artefact`]s. use serde::{Deserialize, Serialize}; @@ -28,7 +28,7 @@ use super::result::SarifResult; /// }, /// invocations: Vec::new(), /// results: Vec::new(), -/// artifacts: Vec::new(), +/// artefacts: Vec::new(), /// }; /// assert_eq!(run.tool.driver.name, "whitaker_clones_cli"); /// ``` @@ -46,9 +46,12 @@ pub struct Run { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub results: Vec, - /// Referenced source artifacts. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub artifacts: Vec, + /// Referenced source artefacts. + /// + /// The SARIF 2.1.0 schema spells this property `artifacts`, so the wire + /// name is pinned here rather than derived from the field name. + #[serde(default, rename = "artifacts", skip_serializing_if = "Vec::is_empty")] + pub artefacts: Vec, } /// The analysis tool that produced a run. @@ -117,27 +120,27 @@ pub struct Invocation { pub command_line: Option, } -/// A source artifact referenced by results. +/// A source artefact referenced by results. /// /// # Examples /// /// ``` -/// use whitaker_sarif::{Artifact, ArtifactLocation}; +/// use whitaker_sarif::{Artefact, ArtefactLocation}; /// -/// let artifact = Artifact { -/// location: ArtifactLocation { +/// let artefact = Artefact { +/// location: ArtefactLocation { /// uri: "src/main.rs".into(), /// uri_base_id: None, /// }, /// mime_type: Some("text/x-rust".into()), /// }; -/// assert_eq!(artifact.location.uri, "src/main.rs"); +/// assert_eq!(artefact.location.uri, "src/main.rs"); /// ``` #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Artifact { - /// Location of the artifact. - pub location: super::location::ArtifactLocation, +pub struct Artefact { + /// Location of the artefact. + pub location: super::location::ArtefactLocation, /// Optional MIME type. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -147,7 +150,7 @@ pub struct Artifact { #[cfg(test)] mod tests { use super::*; - use crate::model::location::ArtifactLocation; + use crate::model::location::ArtefactLocation; use crate::test_support::{assert_json_round_trip, assert_serialized_json}; #[test] @@ -166,7 +169,7 @@ mod tests { command_line: None, }], results: Vec::new(), - artifacts: Vec::new(), + artefacts: Vec::new(), }; assert_json_round_trip(&run); } @@ -184,7 +187,7 @@ mod tests { }, invocations: Vec::new(), results: Vec::new(), - artifacts: Vec::new(), + artefacts: Vec::new(), }; assert_serialized_json(&run, |json| { assert!( @@ -196,22 +199,22 @@ mod tests { "empty results present: {json}" ); assert!( - !json.contains("\"artifacts\""), - "empty artifacts present: {json}" + !json.contains("\"artefacts\""), + "empty artefacts present: {json}" ); assert!(!json.contains("\"rules\""), "empty rules present: {json}"); }); } #[test] - fn artifact_round_trip() { - let artifact = Artifact { - location: ArtifactLocation { + fn artefact_round_trip() { + let artefact = Artefact { + location: ArtefactLocation { uri: "src/lib.rs".into(), uri_base_id: Some("%SRCROOT%".into()), }, mime_type: Some("text/x-rust".into()), }; - assert_json_round_trip(&artifact); + assert_json_round_trip(&artefact); } } diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 92b1938d3..a15b1fd17 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -1541,7 +1541,7 @@ context by combining a HIR ancestry walk with attribute-shape matching. `ContextEntry` items for modules, functions, impls, and blocks, and carries a boolean `has_test_context_ancestry` alongside that list. On each step, `has_test_ancestry` updates that boolean so the test-only decision can -propagate from outer ancestors into nested helper code. `summarise_context` +propagate from outer ancestors into nested helper code. `summarize_context` then combines the accumulated entries, the propagated boolean, and `in_test_like_context_with(additional_test_attributes)` to produce the final `ContextSummary.is_test` result. This pattern matters because user-configured @@ -1555,7 +1555,7 @@ not just the immediately enclosing function. `is_cfg_test_attribute` - the current ancestor is a function item whose attributes match Whitaker's built-in test list or `additional_test_attributes` -- `summarise_context` merges that ancestry flag with the collected +- `summarize_context` merges that ancestry flag with the collected `ContextEntry` values to derive the final `ContextSummary.is_test` decision. Real `rstest` case expansion adds a second `--test` harness shape that the diff --git a/docs/execplans/2-2-5-test-must-not-have-example-lint.md b/docs/execplans/2-2-5-test-must-not-have-example-lint.md index a85ff270e..a6c6e7e20 100644 --- a/docs/execplans/2-2-5-test-must-not-have-example-lint.md +++ b/docs/execplans/2-2-5-test-must-not-have-example-lint.md @@ -211,7 +211,7 @@ Current repository state relevant to this task: - Existing reusable detection helpers already exist: - `common::context::{is_test_fn_with, in_test_like_context_with}`; - `crates/no_expect_outside_tests/src/context.rs` for HIR ancestor collection - and `cfg(test)` handling (`collect_context`, `summarise_context`). + and `cfg(test)` handling (`collect_context`, `summarize_context`). - Existing BDD tests use `rstest_bdd_macros::{given, when, then, scenario}` and are executed under `cargo test`. diff --git a/docs/execplans/3-4-4-install-prebuilt-artefacts.md b/docs/execplans/3-4-4-install-prebuilt-artefacts.md index f3252a249..568a849fe 100644 --- a/docs/execplans/3-4-4-install-prebuilt-artefacts.md +++ b/docs/execplans/3-4-4-install-prebuilt-artefacts.md @@ -558,7 +558,7 @@ All phases produce additive changes. If a phase fails partway through, fix the issue and re-run the quality gates. The `tempfile` crate ensures downloaded archives are cleaned up on failure. -## Artifacts and notes +## Artefacts and notes Key files created: diff --git a/docs/execplans/3.4.6. Record download-versus-build rates.md b/docs/execplans/3.4.6. Record download-versus-build rates.md index 05af2d05d..2c300c785 100644 --- a/docs/execplans/3.4.6. Record download-versus-build rates.md +++ b/docs/execplans/3.4.6. Record download-versus-build rates.md @@ -356,7 +356,7 @@ Expected observable checks after implementation: - If metrics write fails, continue installation and log a warning; retry on next installation. -## Artifacts and notes +## Artefacts and notes During implementation, capture concise evidence snippets in this section: diff --git a/docs/execplans/7-1-1-whitaker-sarif-crate.md b/docs/execplans/7-1-1-whitaker-sarif-crate.md index ac6ed9665..44cd6e7c9 100644 --- a/docs/execplans/7-1-1-whitaker-sarif-crate.md +++ b/docs/execplans/7-1-1-whitaker-sarif-crate.md @@ -263,9 +263,9 @@ field. Convenience type alias `Result`. ### Stage D: Implement SARIF model types Split into `model/` directory: `mod.rs`, `log.rs` (`SarifLog`), `run.rs` (`Run`, -`Tool`, `ToolComponent`, `Invocation`, `Artifact`), `result.rs` (`SarifResult`, +`Tool`, `ToolComponent`, `Invocation`, `Artefact`), `result.rs` (`SarifResult`, `Level`, `Message`), `location.rs` (`Location`, `PhysicalLocation`, -`ArtifactLocation`, `Region`, `RelatedLocation`), `descriptor.rs` +`ArtefactLocation`, `Region`, `RelatedLocation`), `descriptor.rs` (`ReportingDescriptor`, `MultiformatMessageString`). All types derive `Debug`, `Clone`, `PartialEq`, `Serialize`, `Deserialize` with `#[serde(rename_all = "camelCase")]`. diff --git a/docs/execplans/7-2-8-kani-verification-of-bounded-lsh-index-invariants.md b/docs/execplans/7-2-8-kani-verification-of-bounded-lsh-index-invariants.md index d72b85813..8477738ea 100644 --- a/docs/execplans/7-2-8-kani-verification-of-bounded-lsh-index-invariants.md +++ b/docs/execplans/7-2-8-kani-verification-of-bounded-lsh-index-invariants.md @@ -679,7 +679,7 @@ If a quality gate fails because of unrelated main-branch drift or another agent's changes, stop, record the evidence in this plan, and ask for direction. Do not revert unrelated work. -## Artifacts and notes +## Artefacts and notes Wyvern planning agents reported three useful facts: diff --git a/docs/execplans/7-3-1-map-candidate-spans-and-extract-ast-feature-vectors.md b/docs/execplans/7-3-1-map-candidate-spans-and-extract-ast-feature-vectors.md index ae2e52dde..bf8e223f8 100644 --- a/docs/execplans/7-3-1-map-candidate-spans-and-extract-ast-feature-vectors.md +++ b/docs/execplans/7-3-1-map-candidate-spans-and-extract-ast-feature-vectors.md @@ -77,7 +77,7 @@ escalation, not a workaround. bare `ra_ap_syntax::`/`rowan::` path appears outside comments, with the forbidden-crate list as a `const`. - **No persisted `KindId` from 7.3.1.** Only `AstHash` (which is seeded with - `PARSER_SCHEMA_VERSION`) is hashable/serialisable in this item. `KindId` is + `PARSER_SCHEMA_VERSION`) is hashable/serializable in this item. `KindId` is an in-memory opaque token and must not be persisted, so a future cache (7.6.x) cannot accidentally compare raw discriminants across parser pins. - **Bounded per-candidate cost.** Lowering touches one candidate subtree; the @@ -617,7 +617,7 @@ Decisions already taken while drafting this plan: corruption, no crash). Seeding makes every hash change on a bump, so any cross-pin cache compare fails closed. An `insta` snapshot of `PARSER_SCHEMA_VERSION` forces any bump to be reviewed. `KindId` itself is - **not** persisted by 7.3.1 (only `AstHash` is hashable/serialisable here); + **not** persisted by 7.3.1 (only `AstHash` is hashable/serializable here); this is stated as a Constraint so 7.6.x inherits the rule. Date/Author: 2026-06-09, Doggylump (review panel). - Decision: **Promote the FNV-1a constants and byte-mixing step from diff --git a/docs/execplans/8-2-1-create-the-rstest-helper-lint-crate.md b/docs/execplans/8-2-1-create-the-rstest-helper-lint-crate.md index b168c6d52..770a2066a 100644 --- a/docs/execplans/8-2-1-create-the-rstest-helper-lint-crate.md +++ b/docs/execplans/8-2-1-create-the-rstest-helper-lint-crate.md @@ -730,7 +730,7 @@ If validation fails, inspect the matching changing code. Record persistent failures in `Surprises & Discoveries` or `Decision Log` with the command and log path. -## Artifacts and notes +## Artefacts and notes Wyvern repository-pattern findings: diff --git a/docs/execplans/8-2-2-call-site-collection-in-rstest-tests.md b/docs/execplans/8-2-2-call-site-collection-in-rstest-tests.md index 84b0648d9..a0ae48ce2 100644 --- a/docs/execplans/8-2-2-call-site-collection-in-rstest-tests.md +++ b/docs/execplans/8-2-2-call-site-collection-in-rstest-tests.md @@ -892,13 +892,13 @@ If validation fails, inspect the matching changing code. Record persistent failures under "Surprises & discoveries" or "Decision log" with the command and log path. -## Artifacts and notes +## Artefacts and notes Wyvern repository-pattern findings (summary): ```plaintext - No existing Whitaker lint uses check_crate_post. The current lints - initialise per-crate config in check_crate and emit diagnostics + initialize per-crate config in check_crate and emit diagnostics expression-locally through check_expr / check_fn / check_item / check_impl_item. - Callee resolution is consistently done via cx.qpath_res for Call and diff --git a/docs/execplans/installer-does-not-install-toolchain.md b/docs/execplans/installer-does-not-install-toolchain.md index fc33b4e21..2c0ed3775 100644 --- a/docs/execplans/installer-does-not-install-toolchain.md +++ b/docs/execplans/installer-does-not-install-toolchain.md @@ -186,7 +186,7 @@ already installed, no rustup install command should run. If rustup fails, the installer should return an error and can be re-run once the environment recovers. -## Artifacts and Notes +## Artefacts and Notes Example log (expected after change): diff --git a/docs/execplans/issue-110-ambiguous-name-reported-by-dylint.md b/docs/execplans/issue-110-ambiguous-name-reported-by-dylint.md index a0df36c8d..0e6eef526 100644 --- a/docs/execplans/issue-110-ambiguous-name-reported-by-dylint.md +++ b/docs/execplans/issue-110-ambiguous-name-reported-by-dylint.md @@ -216,7 +216,7 @@ Edits are safe to re-run. If a step fails, revert file changes with Git and re-apply. Validation commands are safe to repeat. If validation fails twice, stop and escalate with the captured logs. -## Artifacts and Notes +## Artefacts and Notes Keep the validation logs from `/tmp/whitaker-*.log` as evidence for each quality gate. Record any output from `cargo dylint list` that shows the updated diff --git a/docs/execplans/issue-94-dual-ownership-of-binary.md b/docs/execplans/issue-94-dual-ownership-of-binary.md index 158b01e30..0c1b8b19b 100644 --- a/docs/execplans/issue-94-dual-ownership-of-binary.md +++ b/docs/execplans/issue-94-dual-ownership-of-binary.md @@ -186,7 +186,7 @@ Edits are safe to re-run. If a step fails, revert the file changes with Git and re-apply. The `install-smoke` target uses a temporary directory and is safe to repeat. If validation fails twice, stop and escalate with the captured logs. -## Artifacts and Notes +## Artefacts and Notes Capture key outputs in the log files listed in Concrete Steps. These logs are sufficient evidence for validation and troubleshooting. diff --git a/docs/local-validation-of-github-actions-with-act-and-pytest.md b/docs/local-validation-of-github-actions-with-act-and-pytest.md index 0b6494adf..be7114f65 100644 --- a/docs/local-validation-of-github-actions-with-act-and-pytest.md +++ b/docs/local-validation-of-github-actions-with-act-and-pytest.md @@ -121,9 +121,9 @@ def run_act( job: str = "selftest", event_path: Path = EVENT, *, - artifact_dir: Path, + artefact_dir: Path, ) -> tuple[int, Path, str]: - artifact_dir.mkdir(parents=True, exist_ok=True) + artefact_dir.mkdir(parents=True, exist_ok=True) cmd = [ "act", "pull_request", @@ -134,18 +134,18 @@ def run_act( "-P", "ubuntu-latest=catthehacker/ubuntu:act-latest", "--artifact-server-path", - str(artifact_dir), + str(artefact_dir), "--json", # machine-parseable log stream "-b", # bind-mount repo as workspace (preserves side effects) ] completed = subprocess.run(cmd, text=True, capture_output=True) logs = completed.stdout + "\n" + completed.stderr - return completed.returncode, artifact_dir, logs + return completed.returncode, artefact_dir, logs def test_workflow_produces_expected_artefact_and_logs(tmp_path: Path) -> None: - artifact_dir = tmp_path / "act-artifacts" - code, artdir, logs = run_act(artifact_dir=artifact_dir) + artefact_dir = tmp_path / "act-artifacts" + code, artdir, logs = run_act(artefact_dir=artefact_dir) assert code == 0, f"act failed:\n{logs}" # Assert artefact presence and contents @@ -214,11 +214,11 @@ loop: from cmd_mox import CmdMox def test_record(tmp_path: Path) -> None: - artifact_dir = tmp_path / "act-artifacts" + artefact_dir = tmp_path / "act-artifacts" with CmdMox() as mox: gh = mox.spy("gh").passthrough() mox.replay() - code, _, logs = run_act(artifact_dir=artifact_dir) + code, _, logs = run_act(artefact_dir=artefact_dir) assert code == 0, logs mox.verify() assert gh.call_count == 1 @@ -229,7 +229,7 @@ loop: ```python def test_replay(tmp_path: Path, cmd_mox) -> None: - artifact_dir = tmp_path / "act-artifacts" + artefact_dir = tmp_path / "act-artifacts" cmd_mox.mock("gh").with_args( "release", "view", @@ -237,7 +237,7 @@ loop: "tagName", ).returns(stdout='{"tagName":"v9.9.9"}\n') cmd_mox.replay() - code, _, logs = run_act(artifact_dir=artifact_dir) + code, _, logs = run_act(artefact_dir=artefact_dir) assert code == 0, logs cmd_mox.verify() ``` diff --git a/docs/rstest-bdd-users-guide.md b/docs/rstest-bdd-users-guide.md index 67db84097..9e2ad2c76 100644 --- a/docs/rstest-bdd-users-guide.md +++ b/docs/rstest-bdd-users-guide.md @@ -988,7 +988,7 @@ assert_eq!(row_number, 2); assert_eq!(column_index, 2); assert!(err .to_string() - .contains("unrecognised boolean value 'maybe'")); + .contains("unrecognized boolean value 'maybe'")); ``` [`DataTableError`]: crate::datatable::DataTableError diff --git a/docs/scripting-standards.md b/docs/scripting-standards.md index 288fc5ff8..15b042b04 100644 --- a/docs/scripting-standards.md +++ b/docs/scripting-standards.md @@ -241,7 +241,7 @@ from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] DIST = PROJECT_ROOT / "dist" -(DIST / "artifacts").mkdir(parents=True, exist_ok=True) +(DIST / "artefacts").mkdir(parents=True, exist_ok=True) # Portable joins and normalization cfg = PROJECT_ROOT.joinpath("config", "release.toml").resolve() diff --git a/docs/whitaker-clone-detector-design.md b/docs/whitaker-clone-detector-design.md index fd05d25f8..3fb1366a2 100644 --- a/docs/whitaker-clone-detector-design.md +++ b/docs/whitaker-clone-detector-design.md @@ -61,7 +61,7 @@ passes. Use `runs[0]` for the token pass and `runs[1]` for the AST pass. Each - `serde` models of SARIF 2.1.0 (subset plus extensions) with `From` and `Into` helpers. -- Helpers to build rules, results, locations, artifacts, and invocations. +- Helpers to build rules, results, locations, artefacts, and invocations. - Stable file layout: `target/whitaker/clones.{pass}.sarif` and `target/whitaker/clones.refined.sarif`. - Merge logic: combine runs and deduplicate results by diff --git a/docs/whitaker-dylint-suite-design.md b/docs/whitaker-dylint-suite-design.md index 6745627d2..25bd591c5 100644 --- a/docs/whitaker-dylint-suite-design.md +++ b/docs/whitaker-dylint-suite-design.md @@ -1237,7 +1237,7 @@ and predicate complexity. Collect segments `(start_line, end_line, value)` using `SourceMap` mapping and accumulate contributions with weights (`wD = 1.0`, `wP = 0.5`, `wK = 0.5`). -Rasterise once per function to produce a per-line signal `C[line]` representing +Rasterize once per function to produce a per-line signal `C[line]` representing local complexity. ```rust @@ -1295,7 +1295,7 @@ highlight the top two intervals in the diagnostic. 1. Walk the function HIR, updating depth and collecting segments for blocks, branches, and predicate spans. -2. Rasterise segments to per-line values, then smooth with the configured +2. Rasterize segments to per-line values, then smooth with the configured window. 3. Detect bumps where the smoothed value meets or exceeds `threshold`. 4. Emit a diagnostic on the function name span when bumps ≥ 2, attaching labels @@ -1321,7 +1321,7 @@ functions typically remain below the threshold after smoothing. Deep single nests fall under other lints such as `excessive_nesting`. **Performance.** The pass is linear in the size of each function’s HIR. Segment -rasterisation touches at most the number of lines in the function, keeping the +rasterization touches at most the number of lines in the function, keeping the overhead negligible for typical Rust code. **Test plan.** Provide UI cases covering two separated nested blocks, @@ -1366,7 +1366,7 @@ function and is suitable as a stable Dylint rule in the default suite. ### Phase 0 — Repo scaffolding -- Initialise workspace +- Initialize workspace - Create `Cargo.toml` with `[workspace]`, resolver = 2, members = `crates/*`, `common`, `suite`, `installer`. - Add `rust-toolchain.toml` (pin nightly) and `rustfmt.toml`. diff --git a/installer/src/artefact/extraction.rs b/installer/src/artefact/extraction.rs index fdb6f812c..0c7e3af3a 100644 --- a/installer/src/artefact/extraction.rs +++ b/installer/src/artefact/extraction.rs @@ -137,8 +137,8 @@ mod tests { builder .append_path_with_name(&source_file, "hello.txt") .expect("append"); - let encoder = builder.into_inner().expect("tar finish"); - encoder.finish().expect("zstd finish"); + let finished_encoder = builder.into_inner().expect("tar finish"); + finished_encoder.finish().expect("zstd finish"); let extractor = ZstdExtractor; let files = extractor @@ -184,8 +184,8 @@ mod tests { let output_file = std::fs::File::create(&archive_path).expect("create"); let encoder = zstd::Encoder::new(output_file, 0).expect("zstd"); let builder = tar::Builder::new(encoder); - let encoder = builder.into_inner().expect("tar finish"); - encoder.finish().expect("zstd finish"); + let finished_encoder = builder.into_inner().expect("tar finish"); + finished_encoder.finish().expect("zstd finish"); let extractor = ZstdExtractor; let result = extractor.extract(&archive_path, &dest_dir); diff --git a/installer/src/artefact/schema_version.rs b/installer/src/artefact/schema_version.rs index 2f0b101a8..2631d8501 100644 --- a/installer/src/artefact/schema_version.rs +++ b/installer/src/artefact/schema_version.rs @@ -100,7 +100,7 @@ mod tests { fn rejects_version_zero() { let result = SchemaVersion::try_from(0_u32); assert!(result.is_err()); - let err = result.unwrap_err(); + let err = result.expect_err("schema version zero must be rejected"); assert!(matches!( err, ArtefactError::UnsupportedSchemaVersion { value: 0, max: 1 } diff --git a/installer/src/artefact/sha256_digest.rs b/installer/src/artefact/sha256_digest.rs index c64cfc143..1332219f4 100644 --- a/installer/src/artefact/sha256_digest.rs +++ b/installer/src/artefact/sha256_digest.rs @@ -129,6 +129,7 @@ mod tests { use super::*; use rstest::{fixture, rstest}; + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn valid_digest() -> String { "a".repeat(64) diff --git a/installer/src/dependency_binaries/install/checksum.rs b/installer/src/dependency_binaries/install/checksum.rs index 066adb126..ed7ab3347 100644 --- a/installer/src/dependency_binaries/install/checksum.rs +++ b/installer/src/dependency_binaries/install/checksum.rs @@ -260,8 +260,11 @@ mod tests { return Err(io::Error::new(io::ErrorKind::Interrupted, "interrupted")); } let take = self.data.len().min(self.chunk).min(buf.len()); - buf[..take].copy_from_slice(&self.data[..take]); - self.data = &self.data[take..]; + let (to_copy, rest) = self.data.split_at(take); + buf.get_mut(..take) + .expect("take is bounded by buf.len()") + .copy_from_slice(to_copy); + self.data = rest; Ok(take) } } diff --git a/installer/src/dependency_binaries/install/downloader.rs b/installer/src/dependency_binaries/install/downloader.rs index 2b5d4d709..13923ac85 100644 --- a/installer/src/dependency_binaries/install/downloader.rs +++ b/installer/src/dependency_binaries/install/downloader.rs @@ -333,9 +333,10 @@ mod tests { impl Drop for ProbeCleanup { fn drop(&mut self) { - // A correct run never writes the probe into this directory, so a - // missing file is the expected case. - let _ = self.dir.remove_file(&self.name); + if self.dir.remove_file(&self.name).is_err() { + // A correct run never writes the probe into this directory, so + // a missing file is the expected case. + } } } diff --git a/installer/src/dependency_binaries/install/http_test_server.rs b/installer/src/dependency_binaries/install/http_test_server.rs index 17bd35bd1..46f486c23 100644 --- a/installer/src/dependency_binaries/install/http_test_server.rs +++ b/installer/src/dependency_binaries/install/http_test_server.rs @@ -61,9 +61,11 @@ impl LocalServer { let requested = Arc::new(Mutex::new(Vec::new())); let stop = Arc::new(AtomicBool::new(false)); let handle = { - let requested = Arc::clone(&requested); - let stop = Arc::clone(&stop); - thread::spawn(move || run_server(&listener, &routes, &requested, &stop)) + let requested_for_thread = Arc::clone(&requested); + let stop_for_thread = Arc::clone(&stop); + thread::spawn(move || { + run_server(&listener, &routes, &requested_for_thread, &stop_for_thread); + }) }; Self { base_url: format!("http://127.0.0.1:{port}"), @@ -85,8 +87,11 @@ impl LocalServer { impl Drop for LocalServer { fn drop(&mut self) { self.stop.store(true, Ordering::Relaxed); - if let Some(handle) = self.handle.take() { - let _ = handle.join(); + if let Some(handle) = self.handle.take() + && handle.join().is_err() + { + // The server thread panicked; tests observe failures through the + // requests they make, so nothing further can be reported here. } } } @@ -120,9 +125,17 @@ fn serve_connection( // Restore blocking mode and bound reads/writes on the accepted connection // (the listener is non-blocking only so the accept loop can poll for // shutdown). `try_clone` shares the socket, so `peer` inherits these. - let _ = stream.set_nonblocking(false); - let _ = stream.set_read_timeout(Some(Duration::from_secs(5))); - let _ = stream.set_write_timeout(Some(Duration::from_secs(5))); + // Drop the connection if the socket cannot be configured. + if stream.set_nonblocking(false).is_err() + || stream + .set_read_timeout(Some(Duration::from_secs(5))) + .is_err() + || stream + .set_write_timeout(Some(Duration::from_secs(5))) + .is_err() + { + return; + } let Ok(peer) = stream.try_clone() else { return; }; @@ -139,19 +152,24 @@ fn serve_connection( // Drain the remaining request headers up to the blank line. loop { let mut line = String::new(); - match reader.read_line(&mut line) { - Ok(0) => break, - Ok(_) if line == "\r\n" || line == "\n" => break, - Ok(_) => {} - Err(_) => break, + let bytes_read = reader.read_line(&mut line).unwrap_or(0); + if bytes_read == 0 || line == "\r\n" || line == "\n" { + break; } } // Resolve the route first — the returned references borrow `routes`, not // `path` — so the owned `path` can then move into the request log. - let (status_line, body, declared_len): (&str, &[u8], usize) = match routes.get(&path) { - Some(response) => (response.status_line, &response.body, response.declared_len), - None => ("404 Not Found", b"not found", b"not found".len()), - }; + let not_found: &[u8] = b"not found"; + let (status_line, body, declared_len) = routes.get(&path).map_or_else( + || ("404 Not Found", not_found, not_found.len()), + |response| { + ( + response.status_line, + response.body.as_slice(), + response.declared_len, + ) + }, + ); requested.lock().expect("lock requested paths").push(path); let header = format!( concat!( @@ -162,7 +180,11 @@ fn serve_connection( ), status_line, declared_len, ); - let _ = stream.write_all(header.as_bytes()); - let _ = stream.write_all(body); - let _ = stream.flush(); + let write_result = stream + .write_all(header.as_bytes()) + .and_then(|()| stream.write_all(body)) + .and_then(|()| stream.flush()); + if write_result.is_err() { + // The client disconnected early; there is nothing further to serve. + } } diff --git a/installer/src/deps.rs b/installer/src/deps/mod.rs similarity index 100% rename from installer/src/deps.rs rename to installer/src/deps/mod.rs diff --git a/installer/src/output.rs b/installer/src/output.rs index 53bc593f9..e8ac48bd8 100644 --- a/installer/src/output.rs +++ b/installer/src/output.rs @@ -178,6 +178,7 @@ mod tests { } /// Shared fixture providing a shell snippet for the test path. + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn test_snippet(test_path: Utf8PathBuf) -> ShellSnippet { ShellSnippet::new(&test_path) @@ -214,7 +215,7 @@ mod tests { #[rstest] #[case::singular(1, "1 lint library")] #[case::plural(5, "5 lint libraries")] - fn success_message_pluralises_correctly(#[case] count: usize, #[case] expected: &str) { + fn success_message_pluralizes_correctly(#[case] count: usize, #[case] expected: &str) { let path = Utf8PathBuf::from("/tmp"); let msg = success_message(count, &path); assert!(msg.contains(expected)); diff --git a/src/config.rs b/src/config.rs index fe7955f76..d223dfdcd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,7 @@ //! `dylint.toml` when present and fall back to sensible defaults otherwise. use serde::Deserialize; -use whitaker_common::i18n::normalise_locale; +use whitaker_common::i18n::normalize_locale; /// Shared configuration for the workspace-level crate. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] @@ -92,7 +92,7 @@ impl SharedConfig { /// behaviour when `dylint.toml` is templated or patched. #[must_use] pub fn locale(&self) -> Option<&str> { - normalise_locale(self.locale.as_deref()) + normalize_locale(self.locale.as_deref()) } } @@ -133,7 +133,7 @@ mod tests { } #[rstest] - fn deserialises_overrides_from_toml() { + fn deserializes_overrides_from_toml() { let source = "[module_max_lines]\nmax_lines = 120\n"; // Panic with the TOML parser's message so broken overrides are easy to debug. @@ -144,7 +144,7 @@ mod tests { } #[rstest] - fn deserialises_locale_override() { + fn deserializes_locale_override() { let source = "locale = \"cy\"\n"; let config = toml::from_str::(source) @@ -164,7 +164,7 @@ mod tests { } #[rstest] - fn propagates_deserialisation_failures() { + fn propagates_deserialization_failures() { let source = "[module_max_lines]\nmax_lines = \"a lot\"\n"; let outcome: Result = toml::from_str(source); diff --git a/src/lints/template/validation.rs b/src/lints/template/validation.rs index 04f6a4ccd..66abaf442 100644 --- a/src/lints/template/validation.rs +++ b/src/lints/template/validation.rs @@ -97,23 +97,23 @@ pub(crate) fn lint_constant(crate_name: &str) -> String { .collect() } -fn capitalise_segment(segment: &str) -> Option { +fn capitalize_segment(segment: &str) -> Option { let mut characters = segment.chars(); let first = characters.next()?; - let mut capitalised = String::with_capacity(segment.len()); - capitalised.push(first.to_ascii_uppercase()); + let mut capitalized = String::with_capacity(segment.len()); + capitalized.push(first.to_ascii_uppercase()); for character in characters { - capitalised.push(character.to_ascii_lowercase()); + capitalized.push(character.to_ascii_lowercase()); } - Some(capitalised) + Some(capitalized) } pub(crate) fn pass_struct_name(crate_name: &str) -> String { crate_name .split(['-', '_']) - .filter_map(capitalise_segment) + .filter_map(capitalize_segment) .collect() } diff --git a/tests/config_loading.rs b/tests/config_loading.rs index 2c8a660db..832df279a 100644 --- a/tests/config_loading.rs +++ b/tests/config_loading.rs @@ -12,7 +12,7 @@ use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use support::locale::StepLocale; use whitaker::SharedConfig; -use whitaker_common::i18n::normalise_locale; +use whitaker_common::i18n::normalize_locale; #[fixture] fn config_source() -> RefCell> { @@ -182,7 +182,7 @@ fn assert_locale( expected: StepLocale, ) { let raw = expected.into_inner(); - let Some(expected_value) = normalise_locale(Some(raw.as_str())) else { + let Some(expected_value) = normalize_locale(Some(raw.as_str())) else { panic!("expected the step to provide a locale value"); }; let config = match loaded_config(load_result) { diff --git a/tests/locale_resolution.rs b/tests/locale_resolution.rs index 6b07dd7e1..b06c12cfc 100644 --- a/tests/locale_resolution.rs +++ b/tests/locale_resolution.rs @@ -8,7 +8,7 @@ mod support; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use support::locale::StepLocale; -use whitaker_common::i18n::{LocaleSelection, LocaleSource, normalise_locale, resolve_localizer}; +use whitaker_common::i18n::{LocaleSelection, LocaleSource, normalize_locale, resolve_localizer}; #[derive(Default)] struct LocaleWorld { @@ -18,6 +18,7 @@ struct LocaleWorld { resolution: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> LocaleWorld { LocaleWorld::default() @@ -105,7 +106,7 @@ fn assert_source(world: &LocaleWorld, source: StepSource) { fn assert_locale(world: &LocaleWorld, value: StepLocale) { let resolution = resolved(world); let raw = value.into_inner(); - let expected = normalise_locale(Some(raw.as_str())) + let expected = normalize_locale(Some(raw.as_str())) .unwrap_or_else(|| panic!("expected the step to provide a locale value")); assert_eq!(resolution.locale(), expected); diff --git a/tests/ui_harness.rs b/tests/ui_harness.rs index a0e688d67..b442a3f71 100644 --- a/tests/ui_harness.rs +++ b/tests/ui_harness.rs @@ -57,6 +57,7 @@ impl Default for HarnessWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn harness_world() -> HarnessWorld { HarnessWorld::default() diff --git a/typos.local.toml b/typos.local.toml index 94f5978bc..25588330c 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -12,88 +12,21 @@ accepted = [] [patterns] ignore = [ - "(?m)^ \\.context\\(\"failed to serialise diagnostic for snapshot\"\\),$", - "(?m)^ Ok\\(parsed\\) => assert_eq!\\(artifact, parsed\\),$", - "(?m)^ \\.artifact_location$", - "(?m)^ artifact_location: ArtifactLocation \\{$", - "(?m)^ artifact_location: whitaker_sarif::ArtifactLocation \\{$", - "(?m)^ assert!\\(!json\\.contains\\(\"\\\\\"artifacts\\\\\"\"\\)\\);$", - "(?m)^ format!\\(\"bumpy\\-road signal rasterisation failed: \\{error\\}\"\\),$", - "(?m)^ Ok\\(json\\) => match serde_json::from_str::\\(&json\\) \\{$", - "(?m)^ \\.artifact_location$", - "(?m)^ \\.expect\\(\"Fluent source should be initialised\"\\);$", - "(?m)^ \\.unwrap_or_else\\(\\|\\| panic!\\(\"localizer should be initialised\"\\)\\)$", - "(?m)^ artifacts: Vec::new\\(\\),$", - "(?m)^ artifacts: self\\.artifacts,$", - "(?m)^ localised_messages\\(&FailingLookup::new\\(\"no_std_fs_operations\"\\), &op\\)$", - "(?m)^ localised_messages\\(localizer, &op\\)$", - "(?m)^ location: ArtifactLocation \\{$", + # SARIF 2.1.0 (OASIS) spells these properties `artifactLocation` and + # `artifacts`. The Rust fields use the repository's Oxford spelling and pin + # the wire names with `serde(rename)`, so these two literals are serialized + # values fixed by the external schema, not prose. + "(?m)^ #\\[serde\\(rename = \"artifactLocation\"\\)\\]$", + "(?m)^ #\\[serde\\(default, rename = \"artifacts\", skip_serializing_if = \"Vec::is_empty\"\\)\\]$", "(?m)^ \"\\-\\-artifact\\-server\\-path\",$", - "(?m)^ \"failed to initialise isolated rustup: \\{\\}\",$", - "(?m)^ TraitItemMetrics::default_method\\(\"serialise\", 8\\),$", - "(?m)^ \\.expect\\(\"failed to initialise isolated rustup environment\"\\);$", - "(?m)^ \\.expect\\(\"settings should be normalised\"\\);$", - "(?m)^ \\.filter_map\\(capitalise_segment\\)$", - "(?m)^ \\.normalised$", - "(?m)^ all_artifacts\\.extend\\(run\\.artifacts\\.clone\\(\\)\\);$", - "(?m)^ artifacts: all_artifacts,$", - "(?m)^ assert_eq!\\(loc\\.physical_location\\.artifact_location\\.uri, \"src/main\\.rs\"\\);$", - "(?m)^ assert_eq!\\(location\\.physical_location\\.artifact_location\\.uri, file_uri\\);$", - "(?m)^ assert_eq!\\(normalise_locale\\(input\\), expected\\);$", - "(?m)^ capitalised\\.push\\(character\\.to_ascii_lowercase\\(\\)\\);$", - "(?m)^ depth: normalise_weight\\(settings\\.weights\\.depth, defaults\\.weights\\.depth\\),$", - "(?m)^ flow: normalise_weight\\(settings\\.weights\\.flow, defaults\\.weights\\.flow\\),$", "(?m)^ let Message::CompilerArtifact\\(artefact\\) = message else \\{$", "(?m)^ let Ok\\(Message::CompilerArtifact\\(artefact\\)\\) = message else \\{$", - "(?m)^ let artifact = Artifact \\{$", - "(?m)^ let summary = summarise_context\\($", - "(?m)^ let summary = summarise_context\\(entries\\.as_slice\\(\\), has_test_context_ancestry, additional\\);$", - "(?m)^ let summary = summarise_context_with_harness\\($", - "(?m)^ localised_messages\\(&lookup, kind, attribute\\)$", - "(?m)^ match serde_json::to_string\\(&artifact\\) \\{$", - "(?m)^ normalise_locale\\(self\\.locale\\.as_deref\\(\\)\\)$", - "(?m)^ predicate: normalise_weight\\(settings\\.weights\\.predicate, defaults\\.weights\\.predicate\\),$", - "(?m)^ result\\.locations\\[0\\]\\.physical_location\\.artifact_location\\.uri,$", - "(?m)^ self\\.artifacts\\.push\\(artifact\\);$", - "(?m)^ self\\.settings = normalise_settings\\(load_configuration\\(\\)\\.into_settings\\(\\)\\);$", - "(?m)^ uses: actions/download\\-artifact@v4$", "(?m)^ uses: actions/upload\\-artifact@v4$", "(?m)^ uses: actions/upload\\-artifact@v7$", - "(?m)^ world\\.with_localizer\\(\\|localizer\\| localised_messages\\(localizer, kind, attribute\\)\\)$", - "(?m)^ artifact_dir = tmp_path / \"act\\-artifacts\"$", "(?m)^ \\- Download all artefacts \\(actions/download\\-artifact@v4,$", "(?m)^ \\- Upload artefact \\(actions/upload\\-artifact@v4,$", - "(?m)^ /// Appends an artifact reference\\.$", "(?m)^ /// Categorises the input with a single cluster of conditional logic\\.$", - "(?m)^ /// Identifies the artifact \\(file\\)\\.$", - "(?m)^ /// Location of the artifact\\.$", - "(?m)^ /// Optional byte offset from the start of the artifact\\.$", - "(?m)^ /// Optional region within the artifact\\.$", - "(?m)^ /// Referenced source artifacts\\.$", - "(?m)^ /// Relative or absolute URI of the artifact\\.$", - "(?m)^ /// Returns the source artifact URI used in SARIF output\\.$", - "(?m)^ /// Sets the byte offset from the start of the artifact\\.$", - "(?m)^ /// Sets the region within the artifact\\.$", - "(?m)^ /// assert_eq!\\(run\\.artifacts\\.len\\(\\), 1\\);$", "(?m)^ /// executed \\(catches mis\\-specified scenarios missing the When step\\)\\.$", - "(?m)^ And I normalise the settings$", - "(?m)^ And a default method normalise with CC 7$", - "(?m)^ Artifact, ArtifactLocation, Invocation, Level, Location, Message, MultiformatMessageString,$", - "(?m)^ ContextLabel, Localizer, NoExpectMessages, ReceiverCategory, ReceiverLabel, localised_messages,$", - "(?m)^ DEFAULT_THRESHOLD, Settings, Weights, detect_bumps, normalise_settings, top_two_bumps,$", - "(?m)^ Given the lint recognises custom::test as a test attribute$", - "(?m)^ Some\\(capitalised\\)$", - "(?m)^ Then the function is recognised as an rstest fixture$", - "(?m)^ Then the function is recognised as an rstest test$", - "(?m)^ Then the function is recognised as not being an rstest test$", - "(?m)^ Then the function is recognised as not test\\-like$", - "(?m)^ Then the function is recognised as test\\-like$", - "(?m)^ When I localise the diagnostic$", - "(?m)^ When I localise the expect diagnostic$", - "(?m)^ When I localise the std::fs diagnostic$", - "(?m)^ When I summarise the context$", - "(?m)^ When the complexity is finalised$", - "(?m)^ \\.contains\\(\"unrecognised boolean value 'maybe'\"\\)\\);$", "(?m)^ \\.help = Cuir beachd doc ris a tha a’ mìneachadh giùlan \\{ \\$function \\}\\.$", "(?m)^ \\.help = Cuir seachad `cap_std::fs::Dir` agus paramadairean `camino::Utf8Path`/`Utf8PathBuf` tron API seach std::fs a ghairm gu dìreach\\.$", "(?m)^ \\.label = Tha bump iom\\-fhillteachd \\{ \\$index \\} a’ leudachadh thairis air \\{ \\$lines \\} \\{ \\$lines \\->$", @@ -105,106 +38,20 @@ ignore = [ "(?m)^ \\.note = Tha mòidealan mòra nas duilghe an ath\\-sgrùdadh\\.$", "(?m)^ \\.note = Tha na docs airson \\{ \\$test \\} a’ gabhail a\\-steach ceann eisimpleirean no bloca còd le feansa\\.$", "(?m)^ \\.note = Tha teachdaireachdan airson \\{ \\$lint \\} ri fhaighinn ann an Gàidhlig\\.$", - "(?m)^ \\.note = The call originates within \\{ \\$context \\} which is not recognised as a test\\.$", "(?m)^ artefact: &cargo_metadata::Artifact,$", - "(?m)^ artifact_dir = tmp_path / \"act\\-artifacts\"$", - "(?m)^ artifacts: Vec,$", - "(?m)^ assert_eq!\\(normalised, expected\\);$", - "(?m)^ builder\\.add_default_method\\(\"normalise\", 7, false\\);$", - "(?m)^ capitalised\\.push\\(first\\.to_ascii_uppercase\\(\\)\\);$", - "(?m)^ context_label, fallback_messages, localised_messages,$", - "(?m)^ fn artifact_round_trip\\(\\) \\{$", - "(?m)^ fn deserialises_locale_override\\(\\) \\{$", - "(?m)^ fn deserialises_overrides_from_toml\\(\\) \\{$", - "(?m)^ fn normalise_weight\\(candidate: f64, fallback: f64\\) \\-> f64 \\{$", - "(?m)^ fn normalises_candidates\\(#\\[case\\] input: Option<&str>, #\\[case\\] expected: Option<&str>\\) \\{$", - "(?m)^ fn propagates_deserialisation_failures\\(\\) \\{$", - "(?m)^ fn success_message_pluralises_correctly\\(#\\[case\\] count: usize, #\\[case\\] expected: &str\\) \\{$", "(?m)^ let _ = fixture::categorise\\(0\\);$", "(?m)^ let _ = fixture::categorise\\(4\\);$", "(?m)^ let _ = fixture::categorise\\(99\\);$", - "(?m)^ let candidate = normalise_locale\\(raw\\)\\?;$", - "(?m)^ let expected = normalise_locale\\(Some\\(raw\\.as_str\\(\\)\\)\\)$", - "(?m)^ let expected_value = normalise_locale\\(Some\\(raw\\.as_str\\(\\)\\)\\)$", - "(?m)^ let file = location\\.physical_location\\.artifact_location\\.uri\\.clone\\(\\);$", - "(?m)^ let help = normalise_isolation_marks\\(messages\\.help\\(\\)\\);$", - "(?m)^ let messages = localised_messages\\(&lookup, &receiver_label, &context_label, category\\)$", - "(?m)^ let mut all_artifacts = Vec::new\\(\\);$", - "(?m)^ let mut capitalised = String::with_capacity\\(segment\\.len\\(\\)\\);$", - "(?m)^ let mut summary = crate::context::summarise_context\\(cx, hir_id\\);$", - "(?m)^ let normalised = normalise_settings\\(settings\\);$", - "(?m)^ let note = format!\\(\"The call originates within \\{context\\} which is not recognised as a test\\.\",\\);$", - "(?m)^ let note = normalise_isolation_marks\\(messages\\.note\\(\\)\\);$", - "(?m)^ let primary = normalise_isolation_marks\\(messages\\.primary\\(\\)\\);$", - "(?m)^ let summary = summarise_context\\(&entries, false, &\\[\\]\\);$", - "(?m)^ let summary = summarise_context\\(&entries, false, additional\\.as_slice\\(\\)\\);$", - "(?m)^ let summary = summarise_context\\(&entries, true, &\\[\\]\\);$", - "(?m)^ localised_messages,$", - "(?m)^ localised_messages\\(lookup, receiver, &context, category\\)$", - "(?m)^ normalise_locale, resolve_localizer, safe_resolve_message_set, supports_locale,$", - "(?m)^ normalised: RefCell>,$", - "(?m)^ pub artifact_location: ArtifactLocation,$", - "(?m)^ pub artifacts: Vec,$", "(?m)^ pub fn categorise\\(input: i32\\) \\-> &'static str \\{$", - "(?m)^ pub fn with_artifact\\(mut self, artifact: Artifact\\) \\-> Self \\{$", - "(?m)^ pub location: super::location::ArtifactLocation,$", "(?m)^ types: \\[opened, reopened, synchronize, labeled, ready_for_review\\]$", - "(?m)^ use crate::model::location::ArtifactLocation;$", - "(?m)^ validate_pluralisation_coverage\\(locale, max_branches\\);$", - "(?m)^ world\\.normalised\\.replace\\(Some\\(normalised\\)\\);$", "(?m)^ = help: Cuir seachad `cap_std::fs::Dir` agus paramadairean `camino::Utf8Path`/`Utf8PathBuf` tron API seach std::fs a ghairm gu dìreach\\.$", - "(?m)^ = note: The call originates within function `fail_result` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `handler` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `main` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `parse_config` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `parse` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `process` which is not recognised as a test\\.$", "(?m)^ CARGO_TERM_COLOR: always$", - "(?m)^ Scenario: Function recognised via configured attribute$", - "(?m)^ Scenario: Recognise configured custom test attribute$", - "(?m)^ Scenario: Recognise rstest decorated functions$", - "(?m)^ Scenario: Recognise tokio::test decorated functions$", - "(?m)^ Scenario: Type methods split into parsing, serialisation, and filesystem groups$", - "(?m)^ Whitaker lints require localised diagnostics with predictable fallbacks\\.$", "(?m)^ `download\\-artifact` merge step would clobber all but the last\\.$", - "(?m)^# Borrowed English nouns typically pluralise with \\-iau \\(Modern Welsh, Gareth$", - "(?m)^# Serialise dylint UI tests that build lint libraries and use$", - "(?m)^# Serialise ignored exclusion integration tests when they are explicitly run\\.$", - "(?m)^## Artifacts and Notes$", - "(?m)^## Artifacts and notes$", + "(?m)^# Borrowed English nouns typically pluralize with \\-iau \\(Modern Welsh, Gareth$", "(?m)^## Cuidhtearan breithneachaidh ga roinn thar linteran Whitaker\\.$", "(?m)^## Seachain `unwrap_or_else` a tha a’ clisgeadh\\.$", "(?m)^## Tha `expect` toirmisgte taobh a\\-muigh deuchainnean\\.$", "(?m)^#\\. Air a shealltainn ann an breithneachaidhean nuair a tha e a’ toirt iomradh air am buadh roimhe\\.$", - "(?m)^#\\[given\\(\"the lint recognises \\{path\\} as a test attribute\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as an rstest fixture\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as an rstest test\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as not being an rstest test\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as not test\\-like\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as test\\-like\"\\)\\]$", - "(?m)^#\\[when\\(\"I localise the diagnostic\"\\)\\]$", - "(?m)^#\\[when\\(\"I localise the expect diagnostic\"\\)\\]$", - "(?m)^#\\[when\\(\"I localise the std::fs diagnostic\"\\)\\]$", - "(?m)^#\\[when\\(\"I normalise the settings\"\\)\\]$", - "(?m)^#\\[when\\(\"I summarise the context\"\\)\\]$", - "(?m)^#\\[when\\(\"the complexity is finalised\"\\)\\]$", - "(?m)^//! SARIF run, tool, invocation, and artifact types\\.$", - "(?m)^//! The fixture config recognises `#\\[tokio::test\\]`, but `parse_config` remains$", - "(?m)^/// artifacts: Vec::new\\(\\),$", - "(?m)^/// A location within an artifact \\(source file\\)\\.$", - "(?m)^/// A reference to an artifact by URI\\.$", - "(?m)^/// A region within an artifact, identified by line and column numbers\\.$", - "(?m)^/// A source artifact referenced by results\\.$", - "(?m)^/// and deduplicated\\. Artifacts and invocations are concatenated\\.$", - "(?m)^/// arbitrary `const`\\-only module for a synthesised rstest harness descriptor\\.$", - "(?m)^/// assert_eq!\\(artifact\\.location\\.uri, \"src/main\\.rs\"\\);$", - "(?m)^/// let artifact = Artifact \\{$", - "(?m)^Feature: Localised diagnostics for expect usage$", - "(?m)^Feature: Localised diagnostics for function attribute ordering$", - "(?m)^Feature: Localised diagnostics for std::fs usage$", - "(?m)^Feature: Localiser helpers$", - "(?m)^Feature: Summarise traversal context for `\\.expect\\(\\.\\.\\)` linting$", - "(?m)^\\- Helpers to build rules, results, locations, artifacts, and invocations\\.$", "(?m)^\\- Lexical segment matching can mis\\-handle renamed imports, glob imports, or$", "(?m)^\\- \\*\\*Checkboxes:\\*\\* Precede task and sub\\-task items with a GitHub Flavored$", "(?m)^\\- `\\-\\-artifact\\-server\\-path `: export uploaded artefacts to a host$", @@ -213,45 +60,8 @@ ignore = [ "(?m)^cargo dylint list \\-\\-color never \\| Where\\-Object \\{\\{$", "(?m)^cargo dylint list \\-\\-color never \\| awk \\-v suite=\"\\{suite_crate\\}\" '\\$0 ~ \"\\^\" suite \"\\(\\[\\[:space:\\]\\]\\|\\$\\)\" \\{\\{ print \\}\\}'$", "(?m)^error: Tha gnìomh std::fs `std::fs::read` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", - "(?m)^fn capitalise_segment\\(segment: &str\\) \\-> Option \\{$", - "(?m)^fn localised_help_attributes_are_complete\\(\\) \\{$", - "(?m)^fn localised_messages\\($", - "(?m)^fn normalise_isolation_marks\\(text: &str\\) \\-> String \\{$", - "(?m)^fn normalise_settings_falls_back_to_defaults\\($", - "(?m)^fn pluralisation_covers_sample_range\\(#\\[case\\] locale: &str, #\\[case\\] max_branches: i64\\) \\{$", - "(?m)^fn recognises_std_fs_paths\\(#\\[case\\] path: &str, #\\[case\\] expected: bool\\) \\{$", - "(?m)^fn recognises_test_attribute\\(\\) \\{$", - "(?m)^fn resolve_additional_components_parametrised\\(#\\[case\\] cranelift: bool, #\\[case\\] expected: &\\[&str\\]\\) \\{$", - "(?m)^fn scenario_recognises_custom\\(function: FunctionFixture, evaluation: Evaluation\\) \\{$", - "(?m)^fn summarise_context_with_harness<'tcx>\\($", - "(?m)^fn summarises_plain_context\\(\\) \\{$", - "(?m)^fn validate_pluralisation_coverage\\(locale: &str, max_branches: i64\\) \\{$", - "(?m)^fn when_finalised\\(world: &CcWorld\\) \\{$", - "(?m)^fn when_localise\\(world: &WorldCell\\) \\{$", - "(?m)^fn when_normalise\\(world: &World\\) \\{$", - "(?m)^fn when_summarise\\(world: &ContextWorld\\) \\{$", "(?m)^module_max_lines = Tha mòideal \\{ \\$module \\} a’ leudachadh gu \\{ \\$lines \\} loidhnichean agus a’ briseadh an crìoch \\{ \\$limit \\}\\.$", "(?m)^no_std_fs_operations = Tha gnìomh std::fs `\\{ \\$operation \\}` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", - "(?m)^pub fn normalise_locale\\(input: Option<&str>\\) \\-> Option<&str> \\{$", - "(?m)^pub fn normalise_settings\\(settings: Settings\\) \\-> Settings \\{$", - "(?m)^pub fn rasterise_signal\\($", - "(?m)^pub struct Artifact \\{$", - "(?m)^pub struct ArtifactLocation \\{$", - "(?m)^pub use location::\\{ArtifactLocation, Location, PhysicalLocation, Region, RelatedLocation\\};$", - "(?m)^pub use run::\\{Artifact, Invocation, Run, Tool, ToolComponent\\};$", - "(?m)^pub use selection::\\{LocaleSelection, LocaleSource, normalise_locale, resolve_localizer\\};$", - "(?m)^pub\\(crate\\) fn localised_messages\\($", - "(?m)^pub\\(crate\\) fn summarise_context<'tcx>\\($", - "(?m)^pub\\(crate\\) fn summarise_context\\($", - "(?m)^use crate::analysis::\\{Settings, detect_bumps, normalise_settings\\};$", - "(?m)^use crate::context::\\{ContextSummary, summarise_context\\};$", - "(?m)^use crate::context::\\{collect_context, is_cfg_test_attribute, summarise_context\\};$", - "(?m)^use crate::context::summarise_context;$", - "(?m)^use crate::diagnostics::\\{StdFsMessages, localised_messages\\};$", - "(?m)^use crate::model::location::\\{ArtifactLocation, Location, PhysicalLocation, Region\\};$", - "(?m)^use crate::model::run::\\{Artifact, Invocation, Run, Tool, ToolComponent\\};$", - "(?m)^use whitaker_common::i18n::\\{LocaleSelection, LocaleSource, normalise_locale, resolve_localizer\\};$", - "(?m)^use whitaker_common::i18n::normalise_locale;$", ] [files] diff --git a/typos.toml b/typos.toml index fdcc50807..0f9af87af 100644 --- a/typos.toml +++ b/typos.toml @@ -34,88 +34,17 @@ extend-exclude = [ [default] locale = "en-gb" extend-ignore-re = [ - "(?m)^ \\.context\\(\"failed to serialise diagnostic for snapshot\"\\),$", - "(?m)^ Ok\\(parsed\\) => assert_eq!\\(artifact, parsed\\),$", - "(?m)^ \\.artifact_location$", - "(?m)^ artifact_location: ArtifactLocation \\{$", - "(?m)^ artifact_location: whitaker_sarif::ArtifactLocation \\{$", - "(?m)^ assert!\\(!json\\.contains\\(\"\\\\\"artifacts\\\\\"\"\\)\\);$", - "(?m)^ format!\\(\"bumpy\\-road signal rasterisation failed: \\{error\\}\"\\),$", - "(?m)^ Ok\\(json\\) => match serde_json::from_str::\\(&json\\) \\{$", - "(?m)^ \\.artifact_location$", - "(?m)^ \\.expect\\(\"Fluent source should be initialised\"\\);$", - "(?m)^ \\.unwrap_or_else\\(\\|\\| panic!\\(\"localizer should be initialised\"\\)\\)$", - "(?m)^ artifacts: Vec::new\\(\\),$", - "(?m)^ artifacts: self\\.artifacts,$", - "(?m)^ localised_messages\\(&FailingLookup::new\\(\"no_std_fs_operations\"\\), &op\\)$", - "(?m)^ localised_messages\\(localizer, &op\\)$", - "(?m)^ location: ArtifactLocation \\{$", "(?m)^ \"\\-\\-artifact\\-server\\-path\",$", - "(?m)^ \"failed to initialise isolated rustup: \\{\\}\",$", - "(?m)^ TraitItemMetrics::default_method\\(\"serialise\", 8\\),$", - "(?m)^ \\.expect\\(\"failed to initialise isolated rustup environment\"\\);$", - "(?m)^ \\.expect\\(\"settings should be normalised\"\\);$", - "(?m)^ \\.filter_map\\(capitalise_segment\\)$", - "(?m)^ \\.normalised$", - "(?m)^ all_artifacts\\.extend\\(run\\.artifacts\\.clone\\(\\)\\);$", - "(?m)^ artifacts: all_artifacts,$", - "(?m)^ assert_eq!\\(loc\\.physical_location\\.artifact_location\\.uri, \"src/main\\.rs\"\\);$", - "(?m)^ assert_eq!\\(location\\.physical_location\\.artifact_location\\.uri, file_uri\\);$", - "(?m)^ assert_eq!\\(normalise_locale\\(input\\), expected\\);$", - "(?m)^ capitalised\\.push\\(character\\.to_ascii_lowercase\\(\\)\\);$", - "(?m)^ depth: normalise_weight\\(settings\\.weights\\.depth, defaults\\.weights\\.depth\\),$", - "(?m)^ flow: normalise_weight\\(settings\\.weights\\.flow, defaults\\.weights\\.flow\\),$", "(?m)^ let Message::CompilerArtifact\\(artefact\\) = message else \\{$", "(?m)^ let Ok\\(Message::CompilerArtifact\\(artefact\\)\\) = message else \\{$", - "(?m)^ let artifact = Artifact \\{$", - "(?m)^ let summary = summarise_context\\($", - "(?m)^ let summary = summarise_context\\(entries\\.as_slice\\(\\), has_test_context_ancestry, additional\\);$", - "(?m)^ let summary = summarise_context_with_harness\\($", - "(?m)^ localised_messages\\(&lookup, kind, attribute\\)$", - "(?m)^ match serde_json::to_string\\(&artifact\\) \\{$", - "(?m)^ normalise_locale\\(self\\.locale\\.as_deref\\(\\)\\)$", - "(?m)^ predicate: normalise_weight\\(settings\\.weights\\.predicate, defaults\\.weights\\.predicate\\),$", - "(?m)^ result\\.locations\\[0\\]\\.physical_location\\.artifact_location\\.uri,$", - "(?m)^ self\\.artifacts\\.push\\(artifact\\);$", - "(?m)^ self\\.settings = normalise_settings\\(load_configuration\\(\\)\\.into_settings\\(\\)\\);$", - "(?m)^ uses: actions/download\\-artifact@v4$", "(?m)^ uses: actions/upload\\-artifact@v4$", "(?m)^ uses: actions/upload\\-artifact@v7$", - "(?m)^ world\\.with_localizer\\(\\|localizer\\| localised_messages\\(localizer, kind, attribute\\)\\)$", - "(?m)^ artifact_dir = tmp_path / \"act\\-artifacts\"$", "(?m)^ \\- Download all artefacts \\(actions/download\\-artifact@v4,$", "(?m)^ \\- Upload artefact \\(actions/upload\\-artifact@v4,$", - "(?m)^ /// Appends an artifact reference\\.$", + "(?m)^ #\\[serde\\(default, rename = \"artifacts\", skip_serializing_if = \"Vec::is_empty\"\\)\\]$", + "(?m)^ #\\[serde\\(rename = \"artifactLocation\"\\)\\]$", "(?m)^ /// Categorises the input with a single cluster of conditional logic\\.$", - "(?m)^ /// Identifies the artifact \\(file\\)\\.$", - "(?m)^ /// Location of the artifact\\.$", - "(?m)^ /// Optional byte offset from the start of the artifact\\.$", - "(?m)^ /// Optional region within the artifact\\.$", - "(?m)^ /// Referenced source artifacts\\.$", - "(?m)^ /// Relative or absolute URI of the artifact\\.$", - "(?m)^ /// Returns the source artifact URI used in SARIF output\\.$", - "(?m)^ /// Sets the byte offset from the start of the artifact\\.$", - "(?m)^ /// Sets the region within the artifact\\.$", - "(?m)^ /// assert_eq!\\(run\\.artifacts\\.len\\(\\), 1\\);$", "(?m)^ /// executed \\(catches mis\\-specified scenarios missing the When step\\)\\.$", - "(?m)^ And I normalise the settings$", - "(?m)^ And a default method normalise with CC 7$", - "(?m)^ Artifact, ArtifactLocation, Invocation, Level, Location, Message, MultiformatMessageString,$", - "(?m)^ ContextLabel, Localizer, NoExpectMessages, ReceiverCategory, ReceiverLabel, localised_messages,$", - "(?m)^ DEFAULT_THRESHOLD, Settings, Weights, detect_bumps, normalise_settings, top_two_bumps,$", - "(?m)^ Given the lint recognises custom::test as a test attribute$", - "(?m)^ Some\\(capitalised\\)$", - "(?m)^ Then the function is recognised as an rstest fixture$", - "(?m)^ Then the function is recognised as an rstest test$", - "(?m)^ Then the function is recognised as not being an rstest test$", - "(?m)^ Then the function is recognised as not test\\-like$", - "(?m)^ Then the function is recognised as test\\-like$", - "(?m)^ When I localise the diagnostic$", - "(?m)^ When I localise the expect diagnostic$", - "(?m)^ When I localise the std::fs diagnostic$", - "(?m)^ When I summarise the context$", - "(?m)^ When the complexity is finalised$", - "(?m)^ \\.contains\\(\"unrecognised boolean value 'maybe'\"\\)\\);$", "(?m)^ \\.help = Cuir beachd doc ris a tha a’ mìneachadh giùlan \\{ \\$function \\}\\.$", "(?m)^ \\.help = Cuir seachad `cap_std::fs::Dir` agus paramadairean `camino::Utf8Path`/`Utf8PathBuf` tron API seach std::fs a ghairm gu dìreach\\.$", "(?m)^ \\.label = Tha bump iom\\-fhillteachd \\{ \\$index \\} a’ leudachadh thairis air \\{ \\$lines \\} \\{ \\$lines \\->$", @@ -127,106 +56,20 @@ extend-ignore-re = [ "(?m)^ \\.note = Tha mòidealan mòra nas duilghe an ath\\-sgrùdadh\\.$", "(?m)^ \\.note = Tha na docs airson \\{ \\$test \\} a’ gabhail a\\-steach ceann eisimpleirean no bloca còd le feansa\\.$", "(?m)^ \\.note = Tha teachdaireachdan airson \\{ \\$lint \\} ri fhaighinn ann an Gàidhlig\\.$", - "(?m)^ \\.note = The call originates within \\{ \\$context \\} which is not recognised as a test\\.$", "(?m)^ artefact: &cargo_metadata::Artifact,$", - "(?m)^ artifact_dir = tmp_path / \"act\\-artifacts\"$", - "(?m)^ artifacts: Vec,$", - "(?m)^ assert_eq!\\(normalised, expected\\);$", - "(?m)^ builder\\.add_default_method\\(\"normalise\", 7, false\\);$", - "(?m)^ capitalised\\.push\\(first\\.to_ascii_uppercase\\(\\)\\);$", - "(?m)^ context_label, fallback_messages, localised_messages,$", - "(?m)^ fn artifact_round_trip\\(\\) \\{$", - "(?m)^ fn deserialises_locale_override\\(\\) \\{$", - "(?m)^ fn deserialises_overrides_from_toml\\(\\) \\{$", - "(?m)^ fn normalise_weight\\(candidate: f64, fallback: f64\\) \\-> f64 \\{$", - "(?m)^ fn normalises_candidates\\(#\\[case\\] input: Option<&str>, #\\[case\\] expected: Option<&str>\\) \\{$", - "(?m)^ fn propagates_deserialisation_failures\\(\\) \\{$", - "(?m)^ fn success_message_pluralises_correctly\\(#\\[case\\] count: usize, #\\[case\\] expected: &str\\) \\{$", "(?m)^ let _ = fixture::categorise\\(0\\);$", "(?m)^ let _ = fixture::categorise\\(4\\);$", "(?m)^ let _ = fixture::categorise\\(99\\);$", - "(?m)^ let candidate = normalise_locale\\(raw\\)\\?;$", - "(?m)^ let expected = normalise_locale\\(Some\\(raw\\.as_str\\(\\)\\)\\)$", - "(?m)^ let expected_value = normalise_locale\\(Some\\(raw\\.as_str\\(\\)\\)\\)$", - "(?m)^ let file = location\\.physical_location\\.artifact_location\\.uri\\.clone\\(\\);$", - "(?m)^ let help = normalise_isolation_marks\\(messages\\.help\\(\\)\\);$", - "(?m)^ let messages = localised_messages\\(&lookup, &receiver_label, &context_label, category\\)$", - "(?m)^ let mut all_artifacts = Vec::new\\(\\);$", - "(?m)^ let mut capitalised = String::with_capacity\\(segment\\.len\\(\\)\\);$", - "(?m)^ let mut summary = crate::context::summarise_context\\(cx, hir_id\\);$", - "(?m)^ let normalised = normalise_settings\\(settings\\);$", - "(?m)^ let note = format!\\(\"The call originates within \\{context\\} which is not recognised as a test\\.\",\\);$", - "(?m)^ let note = normalise_isolation_marks\\(messages\\.note\\(\\)\\);$", - "(?m)^ let primary = normalise_isolation_marks\\(messages\\.primary\\(\\)\\);$", - "(?m)^ let summary = summarise_context\\(&entries, false, &\\[\\]\\);$", - "(?m)^ let summary = summarise_context\\(&entries, false, additional\\.as_slice\\(\\)\\);$", - "(?m)^ let summary = summarise_context\\(&entries, true, &\\[\\]\\);$", - "(?m)^ localised_messages,$", - "(?m)^ localised_messages\\(lookup, receiver, &context, category\\)$", - "(?m)^ normalise_locale, resolve_localizer, safe_resolve_message_set, supports_locale,$", - "(?m)^ normalised: RefCell>,$", - "(?m)^ pub artifact_location: ArtifactLocation,$", - "(?m)^ pub artifacts: Vec,$", "(?m)^ pub fn categorise\\(input: i32\\) \\-> &'static str \\{$", - "(?m)^ pub fn with_artifact\\(mut self, artifact: Artifact\\) \\-> Self \\{$", - "(?m)^ pub location: super::location::ArtifactLocation,$", "(?m)^ types: \\[opened, reopened, synchronize, labeled, ready_for_review\\]$", - "(?m)^ use crate::model::location::ArtifactLocation;$", - "(?m)^ validate_pluralisation_coverage\\(locale, max_branches\\);$", - "(?m)^ world\\.normalised\\.replace\\(Some\\(normalised\\)\\);$", "(?m)^ = help: Cuir seachad `cap_std::fs::Dir` agus paramadairean `camino::Utf8Path`/`Utf8PathBuf` tron API seach std::fs a ghairm gu dìreach\\.$", - "(?m)^ = note: The call originates within function `fail_result` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `handler` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `main` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `parse_config` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `parse` which is not recognised as a test\\.$", - "(?m)^ = note: The call originates within function `process` which is not recognised as a test\\.$", "(?m)^ CARGO_TERM_COLOR: always$", - "(?m)^ Scenario: Function recognised via configured attribute$", - "(?m)^ Scenario: Recognise configured custom test attribute$", - "(?m)^ Scenario: Recognise rstest decorated functions$", - "(?m)^ Scenario: Recognise tokio::test decorated functions$", - "(?m)^ Scenario: Type methods split into parsing, serialisation, and filesystem groups$", - "(?m)^ Whitaker lints require localised diagnostics with predictable fallbacks\\.$", "(?m)^ `download\\-artifact` merge step would clobber all but the last\\.$", - "(?m)^# Borrowed English nouns typically pluralise with \\-iau \\(Modern Welsh, Gareth$", - "(?m)^# Serialise dylint UI tests that build lint libraries and use$", - "(?m)^# Serialise ignored exclusion integration tests when they are explicitly run\\.$", - "(?m)^## Artifacts and Notes$", - "(?m)^## Artifacts and notes$", + "(?m)^# Borrowed English nouns typically pluralize with \\-iau \\(Modern Welsh, Gareth$", "(?m)^## Cuidhtearan breithneachaidh ga roinn thar linteran Whitaker\\.$", "(?m)^## Seachain `unwrap_or_else` a tha a’ clisgeadh\\.$", "(?m)^## Tha `expect` toirmisgte taobh a\\-muigh deuchainnean\\.$", "(?m)^#\\. Air a shealltainn ann an breithneachaidhean nuair a tha e a’ toirt iomradh air am buadh roimhe\\.$", - "(?m)^#\\[given\\(\"the lint recognises \\{path\\} as a test attribute\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as an rstest fixture\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as an rstest test\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as not being an rstest test\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as not test\\-like\"\\)\\]$", - "(?m)^#\\[then\\(\"the function is recognised as test\\-like\"\\)\\]$", - "(?m)^#\\[when\\(\"I localise the diagnostic\"\\)\\]$", - "(?m)^#\\[when\\(\"I localise the expect diagnostic\"\\)\\]$", - "(?m)^#\\[when\\(\"I localise the std::fs diagnostic\"\\)\\]$", - "(?m)^#\\[when\\(\"I normalise the settings\"\\)\\]$", - "(?m)^#\\[when\\(\"I summarise the context\"\\)\\]$", - "(?m)^#\\[when\\(\"the complexity is finalised\"\\)\\]$", - "(?m)^//! SARIF run, tool, invocation, and artifact types\\.$", - "(?m)^//! The fixture config recognises `#\\[tokio::test\\]`, but `parse_config` remains$", - "(?m)^/// artifacts: Vec::new\\(\\),$", - "(?m)^/// A location within an artifact \\(source file\\)\\.$", - "(?m)^/// A reference to an artifact by URI\\.$", - "(?m)^/// A region within an artifact, identified by line and column numbers\\.$", - "(?m)^/// A source artifact referenced by results\\.$", - "(?m)^/// and deduplicated\\. Artifacts and invocations are concatenated\\.$", - "(?m)^/// arbitrary `const`\\-only module for a synthesised rstest harness descriptor\\.$", - "(?m)^/// assert_eq!\\(artifact\\.location\\.uri, \"src/main\\.rs\"\\);$", - "(?m)^/// let artifact = Artifact \\{$", - "(?m)^Feature: Localised diagnostics for expect usage$", - "(?m)^Feature: Localised diagnostics for function attribute ordering$", - "(?m)^Feature: Localised diagnostics for std::fs usage$", - "(?m)^Feature: Localiser helpers$", - "(?m)^Feature: Summarise traversal context for `\\.expect\\(\\.\\.\\)` linting$", - "(?m)^\\- Helpers to build rules, results, locations, artifacts, and invocations\\.$", "(?m)^\\- Lexical segment matching can mis\\-handle renamed imports, glob imports, or$", "(?m)^\\- \\*\\*Checkboxes:\\*\\* Precede task and sub\\-task items with a GitHub Flavored$", "(?m)^\\- `\\-\\-artifact\\-server\\-path `: export uploaded artefacts to a host$", @@ -235,45 +78,8 @@ extend-ignore-re = [ "(?m)^cargo dylint list \\-\\-color never \\| Where\\-Object \\{\\{$", "(?m)^cargo dylint list \\-\\-color never \\| awk \\-v suite=\"\\{suite_crate\\}\" '\\$0 ~ \"\\^\" suite \"\\(\\[\\[:space:\\]\\]\\|\\$\\)\" \\{\\{ print \\}\\}'$", "(?m)^error: Tha gnìomh std::fs `std::fs::read` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", - "(?m)^fn capitalise_segment\\(segment: &str\\) \\-> Option \\{$", - "(?m)^fn localised_help_attributes_are_complete\\(\\) \\{$", - "(?m)^fn localised_messages\\($", - "(?m)^fn normalise_isolation_marks\\(text: &str\\) \\-> String \\{$", - "(?m)^fn normalise_settings_falls_back_to_defaults\\($", - "(?m)^fn pluralisation_covers_sample_range\\(#\\[case\\] locale: &str, #\\[case\\] max_branches: i64\\) \\{$", - "(?m)^fn recognises_std_fs_paths\\(#\\[case\\] path: &str, #\\[case\\] expected: bool\\) \\{$", - "(?m)^fn recognises_test_attribute\\(\\) \\{$", - "(?m)^fn resolve_additional_components_parametrised\\(#\\[case\\] cranelift: bool, #\\[case\\] expected: &\\[&str\\]\\) \\{$", - "(?m)^fn scenario_recognises_custom\\(function: FunctionFixture, evaluation: Evaluation\\) \\{$", - "(?m)^fn summarise_context_with_harness<'tcx>\\($", - "(?m)^fn summarises_plain_context\\(\\) \\{$", - "(?m)^fn validate_pluralisation_coverage\\(locale: &str, max_branches: i64\\) \\{$", - "(?m)^fn when_finalised\\(world: &CcWorld\\) \\{$", - "(?m)^fn when_localise\\(world: &WorldCell\\) \\{$", - "(?m)^fn when_normalise\\(world: &World\\) \\{$", - "(?m)^fn when_summarise\\(world: &ContextWorld\\) \\{$", "(?m)^module_max_lines = Tha mòideal \\{ \\$module \\} a’ leudachadh gu \\{ \\$lines \\} loidhnichean agus a’ briseadh an crìoch \\{ \\$limit \\}\\.$", "(?m)^no_std_fs_operations = Tha gnìomh std::fs `\\{ \\$operation \\}` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", - "(?m)^pub fn normalise_locale\\(input: Option<&str>\\) \\-> Option<&str> \\{$", - "(?m)^pub fn normalise_settings\\(settings: Settings\\) \\-> Settings \\{$", - "(?m)^pub fn rasterise_signal\\($", - "(?m)^pub struct Artifact \\{$", - "(?m)^pub struct ArtifactLocation \\{$", - "(?m)^pub use location::\\{ArtifactLocation, Location, PhysicalLocation, Region, RelatedLocation\\};$", - "(?m)^pub use run::\\{Artifact, Invocation, Run, Tool, ToolComponent\\};$", - "(?m)^pub use selection::\\{LocaleSelection, LocaleSource, normalise_locale, resolve_localizer\\};$", - "(?m)^pub\\(crate\\) fn localised_messages\\($", - "(?m)^pub\\(crate\\) fn summarise_context<'tcx>\\($", - "(?m)^pub\\(crate\\) fn summarise_context\\($", - "(?m)^use crate::analysis::\\{Settings, detect_bumps, normalise_settings\\};$", - "(?m)^use crate::context::\\{ContextSummary, summarise_context\\};$", - "(?m)^use crate::context::\\{collect_context, is_cfg_test_attribute, summarise_context\\};$", - "(?m)^use crate::context::summarise_context;$", - "(?m)^use crate::diagnostics::\\{StdFsMessages, localised_messages\\};$", - "(?m)^use crate::model::location::\\{ArtifactLocation, Location, PhysicalLocation, Region\\};$", - "(?m)^use crate::model::run::\\{Artifact, Invocation, Run, Tool, ToolComponent\\};$", - "(?m)^use whitaker_common::i18n::\\{LocaleSelection, LocaleSource, normalise_locale, resolve_localizer\\};$", - "(?m)^use whitaker_common::i18n::normalise_locale;$", "(?s)```.*?```", "\\brust-analyzer\\b", "`[^`\\n]+`", From 7f0910e10ec3dd4b0751a47d17955fafe5620dd8 Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 29 Aug 2026 01:09:18 +0200 Subject: [PATCH 4/9] Adopt directory-based module structure Move the remaining self-named module files to mod.rs form (install_flow, toolchain) and extract oversized in-src test modules into sibling files (list, workspace, downloader, the non-UTF-8 boundary tests, the dependency-binary fakes, and the deps repository tests), each wired with the repo's `#[cfg(test)] #[path]` convention and opening with an explanatory `//!` line. The moves are content-preserving: extracted test bodies are this tree's code verbatim; only the module structure changes. This keeps every module inside the 400-line budget the lint configuration later in this stack will enforce, without entangling the structural review with lint fixes. --- .../dependency_binaries/install/downloader.rs | 114 +------- .../downloader_boundary_non_utf8_tests.rs | 70 +++++ .../install/downloader_boundary_tests.rs | 76 +----- .../install/downloader_tests.rs | 112 ++++++++ installer/src/deps/tests.rs | 143 +--------- installer/src/deps/tests_repository.rs | 162 +++++++++++ .../{install_flow.rs => install_flow/mod.rs} | 0 installer/src/list.rs | 252 +----------------- installer/src/list_tests.rs | 248 +++++++++++++++++ .../test_utils/dependency_binary_helpers.rs | 119 +-------- .../dependency_binary_helpers_fakes.rs | 108 ++++++++ .../src/{toolchain.rs => toolchain/mod.rs} | 0 installer/src/workspace.rs | 191 +------------ installer/src/workspace_tests.rs | 187 +++++++++++++ 14 files changed, 905 insertions(+), 877 deletions(-) create mode 100644 installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs create mode 100644 installer/src/dependency_binaries/install/downloader_tests.rs create mode 100644 installer/src/deps/tests_repository.rs rename installer/src/{install_flow.rs => install_flow/mod.rs} (100%) create mode 100644 installer/src/list_tests.rs create mode 100644 installer/src/test_utils/dependency_binary_helpers_fakes.rs rename installer/src/{toolchain.rs => toolchain/mod.rs} (100%) create mode 100644 installer/src/workspace_tests.rs diff --git a/installer/src/dependency_binaries/install/downloader.rs b/installer/src/dependency_binaries/install/downloader.rs index 13923ac85..dbd3c15a8 100644 --- a/installer/src/dependency_binaries/install/downloader.rs +++ b/installer/src/dependency_binaries/install/downloader.rs @@ -287,115 +287,5 @@ fn open_destination_dir(destination: &Utf8Path) -> io::Result<(Dir, &str)> { } #[cfg(test)] -mod tests { - //! Tests for downloader error mapping and archive checksum verification. - - use super::*; - use crate::hex::to_lower_hex; - use sha2::{Digest, Sha256}; - use std::io::Write; - use tempfile::TempDir; - - // The under-cap success path is covered end to end by the local-server - // boundary tests; this exercises the over-cap rejection they cannot. - #[test] - fn copy_capped_rejects_a_body_exceeding_the_limit() { - let url = "https://example.test/a.tgz"; - let error = copy_capped(&[0u8; 100][..], &mut Vec::new(), 8, url) - .expect_err("an over-cap body must be rejected"); - assert!( - matches!(&error, DependencyBinaryInstallError::Download { url: u, reason } - if u == url && reason.contains("exceeds the maximum")), - "unexpected error: {error:?}", - ); - } - - #[test] - fn open_destination_dir_rejects_a_path_without_a_file_name() { - // The filesystem root has no file name, so the capability boundary - // cannot derive an archive name and must reject it up front. - let root = Utf8Path::new("/"); - let error = open_destination_dir(root).expect_err("root path has no file name"); - - assert_eq!(error.kind(), io::ErrorKind::InvalidInput); - assert!( - error.to_string().contains("has no file name"), - "error should identify the missing file name, got: {error}", - ); - } - - /// Removes a probe file from a capability-scoped directory when dropped, so - /// a leaked artefact is cleaned up even if an assertion panics first. - struct ProbeCleanup { - dir: Dir, - name: String, - } - - impl Drop for ProbeCleanup { - fn drop(&mut self) { - if self.dir.remove_file(&self.name).is_err() { - // A correct run never writes the probe into this directory, so - // a missing file is the expected case. - } - } - } - - #[test] - fn open_destination_dir_writes_into_the_destination_parent_not_the_cwd() { - let temp = TempDir::new().expect("create temp dir"); - let temp_dir = Utf8Path::from_path(temp.path()).expect("temp path is UTF-8"); - // A unique, test-owned probe name derived from the temp directory, so - // it can never collide with — or delete — a pre-existing file in the - // working directory. - let archive_file = format!( - "{}.tgz", - temp_dir.file_name().expect("temp dir has a file name"), - ); - let destination = temp_dir.join(&archive_file); - - // Open the process working directory as a capability, paired with RAII - // cleanup: if a regression leaks the probe here, it is removed on drop - // even when an assertion below panics first. - let cwd_probe = ProbeCleanup { - dir: Dir::open_ambient_dir(".", ambient_authority()).expect("open cwd capability"), - name: archive_file.clone(), - }; - // An independent capability for the destination's directory, used to - // confirm the archive actually lands there rather than trusting the - // directory handle returned by the code under test. - let destination_dir = Dir::open_ambient_dir(temp_dir, ambient_authority()) - .expect("open destination capability"); - - let (dir, archive_name) = open_destination_dir(&destination).expect("open destination dir"); - assert_eq!(archive_name, archive_file.as_str()); - - let mut file = dir - .create(archive_name) - .expect("create archive via capability"); - file.write_all(b"hello world").expect("write archive"); - drop(file); - - // The capability must write into the destination's parent directory... - assert!( - destination_dir.exists(&archive_file), - "archive must exist at the destination path", - ); - // ...and never into the process working directory. This assertion fails - // if `open_destination_dir` opens `.` for a destination with a real - // parent. - assert!( - !cwd_probe.dir.exists(&archive_file), - "capability must not create the archive in the current working directory", - ); - - // Re-open through the same capability and keep the end-to-end checksum - // assertion. - let expected = to_lower_hex(&Sha256::digest(b"hello world")); - let archive = dir.open(archive_name).expect("open archive via capability"); - assert!(verify_archive_checksum(archive, destination.as_std_path(), &expected).is_ok()); - - // `cwd_probe` drops here (or on any earlier panic), removing a leaked - // probe through its capability. - drop(cwd_probe); - } -} +#[path = "downloader_tests.rs"] +mod tests; diff --git a/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs new file mode 100644 index 000000000..288ced087 --- /dev/null +++ b/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs @@ -0,0 +1,70 @@ +//! Unix-only boundary tests rejecting non-UTF-8 archive destinations. + +use std::io; + +use super::super::downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}; +use super::*; + +#[test] +fn download_from_urls_rejects_a_non_utf8_destination_before_any_request() { + use std::os::unix::ffi::OsStringExt as _; + + // No route is registered; the server exists only to prove it is never + // contacted for an invalid destination. + let server = LocalServer::start(HashMap::new()); + + // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8 + // and must be rejected during validation, before any HTTP request. + let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ + b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', + ])); + + let error = download_from_urls( + &agent(), + &server.url("/archive.tgz"), + &server.url("/archive.tgz.sha256"), + &invalid, + ) + .expect_err("non-UTF-8 destination must be rejected"); + + match error { + DependencyBinaryInstallError::Io(source) => { + assert_eq!(source.kind(), io::ErrorKind::InvalidInput); + } + other => panic!("expected an Io error, got {other:?}"), + } + assert!( + server.requested_paths().is_empty(), + "no HTTP request must be made for an invalid destination", + ); +} + +#[test] +fn download_rejects_a_non_utf8_destination_before_any_network_access() { + use std::os::unix::ffi::OsStringExt as _; + + // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8. + // The production `download` must reject it during path validation, which + // happens before any HTTP call, so the test needs no network. + let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ + b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', + ])); + + let downloader = RepositoryArchiveDownloader; + let error = downloader + .download("whitaker-dependency", &invalid) + .expect_err("non-UTF-8 destination must be rejected"); + + match error { + DependencyBinaryInstallError::Io(source) => { + assert_eq!(source.kind(), io::ErrorKind::InvalidInput); + assert!( + source + .to_string() + .contains("destination archive path is not valid UTF-8"), + "error should identify the non-UTF-8 destination, got: {source}", + ); + } + other => panic!("expected an Io error, got {other:?}"), + } +} diff --git a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs index 81b06990f..5b9c43ff4 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs @@ -338,76 +338,8 @@ fn download_from_urls_rejects_a_malformed_checksum_sidecar( ); } -// The non-UTF-8 destination cases are Unix-only and live in their own inline -// module so their imports stay scoped to the platform that compiles them. +// The non-UTF-8 destination cases are Unix-only and live in their own module so +// this one stays within its size budget. #[cfg(unix)] -mod non_utf8 { - use std::io; - - use super::super::downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}; - use super::*; - - #[test] - fn download_from_urls_rejects_a_non_utf8_destination_before_any_request() { - use std::os::unix::ffi::OsStringExt as _; - - // No route is registered; the server exists only to prove it is never - // contacted for an invalid destination. - let server = LocalServer::start(HashMap::new()); - - // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8 - // and must be rejected during validation, before any HTTP request. - let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ - b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', - ])); - - let error = download_from_urls( - &agent(), - &server.url("/archive.tgz"), - &server.url("/archive.tgz.sha256"), - &invalid, - ) - .expect_err("non-UTF-8 destination must be rejected"); - - match error { - DependencyBinaryInstallError::Io(source) => { - assert_eq!(source.kind(), io::ErrorKind::InvalidInput); - } - other => panic!("expected an Io error, got {other:?}"), - } - assert!( - server.requested_paths().is_empty(), - "no HTTP request must be made for an invalid destination", - ); - } - - #[test] - fn download_rejects_a_non_utf8_destination_before_any_network_access() { - use std::os::unix::ffi::OsStringExt as _; - - // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8. - // The production `download` must reject it during path validation, which - // happens before any HTTP call, so the test needs no network. - let invalid = std::path::PathBuf::from(std::ffi::OsString::from_vec(vec![ - b'/', b't', b'm', b'p', b'/', 0x80, b'.', b't', b'g', b'z', - ])); - - let downloader = RepositoryArchiveDownloader; - let error = downloader - .download("whitaker-dependency", &invalid) - .expect_err("non-UTF-8 destination must be rejected"); - - match error { - DependencyBinaryInstallError::Io(source) => { - assert_eq!(source.kind(), io::ErrorKind::InvalidInput); - assert!( - source - .to_string() - .contains("destination archive path is not valid UTF-8"), - "error should identify the non-UTF-8 destination, got: {source}", - ); - } - other => panic!("expected an Io error, got {other:?}"), - } - } -} +#[path = "downloader_boundary_non_utf8_tests.rs"] +mod non_utf8; diff --git a/installer/src/dependency_binaries/install/downloader_tests.rs b/installer/src/dependency_binaries/install/downloader_tests.rs new file mode 100644 index 000000000..e6c97caa0 --- /dev/null +++ b/installer/src/dependency_binaries/install/downloader_tests.rs @@ -0,0 +1,112 @@ +//! Tests for downloader error mapping and archive checksum verification. + +//! Tests for downloader error mapping and archive checksum verification. + +use super::*; +use crate::hex::to_lower_hex; +use sha2::{Digest, Sha256}; +use std::io::Write; +use tempfile::TempDir; + +// The under-cap success path is covered end to end by the local-server +// boundary tests; this exercises the over-cap rejection they cannot. +#[test] +fn copy_capped_rejects_a_body_exceeding_the_limit() { + let url = "https://example.test/a.tgz"; + let error = copy_capped(&[0u8; 100][..], &mut Vec::new(), 8, url) + .expect_err("an over-cap body must be rejected"); + assert!( + matches!(&error, DependencyBinaryInstallError::Download { url: u, reason } + if u == url && reason.contains("exceeds the maximum")), + "unexpected error: {error:?}", + ); +} + +#[test] +fn open_destination_dir_rejects_a_path_without_a_file_name() { + // The filesystem root has no file name, so the capability boundary + // cannot derive an archive name and must reject it up front. + let root = Utf8Path::new("/"); + let error = open_destination_dir(root).expect_err("root path has no file name"); + + assert_eq!(error.kind(), io::ErrorKind::InvalidInput); + assert!( + error.to_string().contains("has no file name"), + "error should identify the missing file name, got: {error}", + ); +} + +/// Removes a probe file from a capability-scoped directory when dropped, so +/// a leaked artefact is cleaned up even if an assertion panics first. +struct ProbeCleanup { + dir: Dir, + name: String, +} + +impl Drop for ProbeCleanup { + fn drop(&mut self) { + if self.dir.remove_file(&self.name).is_err() { + // A correct run never writes the probe into this directory, so + // a missing file is the expected case. + } + } +} + +#[test] +fn open_destination_dir_writes_into_the_destination_parent_not_the_cwd() { + let temp = TempDir::new().expect("create temp dir"); + let temp_dir = Utf8Path::from_path(temp.path()).expect("temp path is UTF-8"); + // A unique, test-owned probe name derived from the temp directory, so + // it can never collide with — or delete — a pre-existing file in the + // working directory. + let archive_file = format!( + "{}.tgz", + temp_dir.file_name().expect("temp dir has a file name"), + ); + let destination = temp_dir.join(&archive_file); + + // Open the process working directory as a capability, paired with RAII + // cleanup: if a regression leaks the probe here, it is removed on drop + // even when an assertion below panics first. + let cwd_probe = ProbeCleanup { + dir: Dir::open_ambient_dir(".", ambient_authority()).expect("open cwd capability"), + name: archive_file.clone(), + }; + // An independent capability for the destination's directory, used to + // confirm the archive actually lands there rather than trusting the + // directory handle returned by the code under test. + let destination_dir = + Dir::open_ambient_dir(temp_dir, ambient_authority()).expect("open destination capability"); + + let (dir, archive_name) = open_destination_dir(&destination).expect("open destination dir"); + assert_eq!(archive_name, archive_file.as_str()); + + let mut file = dir + .create(archive_name) + .expect("create archive via capability"); + file.write_all(b"hello world").expect("write archive"); + drop(file); + + // The capability must write into the destination's parent directory... + assert!( + destination_dir.exists(&archive_file), + "archive must exist at the destination path", + ); + // ...and never into the process working directory. This assertion fails + // if `open_destination_dir` opens `.` for a destination with a real + // parent. + assert!( + !cwd_probe.dir.exists(&archive_file), + "capability must not create the archive in the current working directory", + ); + + // Re-open through the same capability and keep the end-to-end checksum + // assertion. + let expected = to_lower_hex(&Sha256::digest(b"hello world")); + let archive = dir.open(archive_name).expect("open archive via capability"); + assert!(verify_archive_checksum(archive, destination.as_std_path(), &expected).is_ok()); + + // `cwd_probe` drops here (or on any earlier panic), removing a leaked + // probe through its capability. + drop(cwd_probe); +} diff --git a/installer/src/deps/tests.rs b/installer/src/deps/tests.rs index 6e69ce5fa..d0fe9faff 100644 --- a/installer/src/deps/tests.rs +++ b/installer/src/deps/tests.rs @@ -281,144 +281,5 @@ fn install_dylint_tools_skips_dylint_link_when_cargo_dylint_source_build_install executor.assert_finished(); } -/// Writes a fake `dylint-link` into a temporary directory and returns the -/// directory guard alongside the binary path. -/// -/// The fake is staged with a failing exit status so that any attempt to -/// execute it as a health check would be observable as a verification -/// failure. -fn staged_unrunnable_dylint_link() -> std::io::Result<(tempfile::TempDir, PathBuf)> { - let dir = tempfile::tempdir()?; - let path = crate::test_utils::dependency_binary_helpers::path_binary_location( - dir.path(), - "dylint-link", - ); - crate::test_utils::dependency_binary_helpers::write_fake_binary_with_status(&path, true, 1); - Ok((dir, path)) -} - -#[test] -fn install_dylint_tools_accepts_repository_dylint_link_without_executing_it() { - // `dylint-link` forwards its argument list to the underlying linker and - // has no reliable self-reporting subcommand, so a successful repository - // install is accepted on the strength of the download, checksum, - // extraction, and permission steps alone. Staging a fake that exits - // non-zero proves the binary is never executed as a health check: any - // such probe would reject it and fall back to Cargo. - let (_dir, binary_path) = staged_unrunnable_dylint_link().expect("stage fake dylint-link"); - let mut repository_installer = MockDependencyBinaryInstaller::new(); - repository_installer - .expect_install() - .once() - .returning(move |_, _, _| Ok(binary_path.clone())); - let executor = StubExecutor::new(vec![binstall_version_check_with_result(Ok( - success_output(), - ))]); - let mut stderr = Vec::new(); - - install_dylint_tools_with_options( - &executor, - &DylintToolStatus { - cargo_dylint: true, - dylint_link: false, - }, - &mut stderr, - install_options(&repository_installer, false), - ) - .expect("repository install should satisfy dylint-link"); - - let output = String::from_utf8(stderr).expect("stderr should be UTF-8"); - assert!(output.contains("Installed dylint-link from repository release.")); - assert!(!output.contains("failed verification")); - // No Cargo command beyond the binstall-availability probe may run: a - // source build of dylint-link cannot succeed on toolchains below the - // crate's rustc floor. - executor.assert_finished(); -} - -#[test] -fn install_dylint_tools_falls_back_when_repository_dylint_link_install_fails() { - // Genuine repository failures — missing asset, checksum mismatch, - // extraction failure, or an unwritable executable — must still fall back - // to Cargo. - let mut repository_installer = MockDependencyBinaryInstaller::new(); - repository_installer.expect_install().returning(|_, _, _| { - Err(DependencyBinaryInstallError::Install { - binary: "dylint-link".to_owned(), - reason: "checksum mismatch".to_owned(), - }) - }); - let executor = StubExecutor::new(vec![ - binstall_version_check_with_result(Ok(success_output())), - binstall_install("dylint-link", Ok(success_output())), - // The post-binstall check resolves the PATH binary and then confirms - // the version against Cargo's installed-binary registry. - dylint_link_install_list_check(), - ]); - let mut stderr = Vec::new(); - - with_fake_binary_on_path("dylint-link", || { - install_dylint_tools_with_options( - &executor, - &DylintToolStatus { - cargo_dylint: true, - dylint_link: false, - }, - &mut stderr, - install_options(&repository_installer, false), - ) - .expect("binstall fallback should succeed"); - }); - - let output = String::from_utf8(stderr).expect("stderr should be UTF-8"); - assert!(output.contains("Repository install for dylint-link unavailable")); - assert!(output.contains("Installed dylint-link with cargo binstall.")); - executor.assert_finished(); -} - -#[test] -fn install_tool_errors_when_dependency_manifest_entry_is_missing() { - let missing_tool = DependencyTool { - package: "missing-tool", - command: "missing-tool", - args: &["--version"], - }; - let executor = StubExecutor::new(vec![]); - let mut repository_installer = MockDependencyBinaryInstaller::new(); - repository_installer.expect_install().never(); - let dirs = StubDirs { - bin_dir: Some(PathBuf::from("/tmp/bin")), - }; - let target = TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"); - let mut stderr = Vec::new(); - - let error = install_tool( - &executor, - &missing_tool, - &mut stderr, - &InstallContext { - repo: repository_install_context( - Some(&dirs), - Some(&repository_installer as &dyn DependencyBinaryInstaller), - Some(&target), - ), - cargo_fallback_mode: InstallMode::Binstall, - quiet: false, - }, - ) - .expect_err("missing dependency manifest entry should be an install error"); - - match error { - InstallerError::DependencyInstall { tool, message } => { - assert_eq!(tool, "missing-tool"); - assert_eq!( - message, - "dependency manifest is missing an entry for missing-tool" - ); - } - other => panic!("unexpected error: {other}"), - } - - assert!(stderr.is_empty()); - executor.assert_finished(); -} +#[path = "tests_repository.rs"] +mod repository; diff --git a/installer/src/deps/tests_repository.rs b/installer/src/deps/tests_repository.rs new file mode 100644 index 000000000..9ef30750c --- /dev/null +++ b/installer/src/deps/tests_repository.rs @@ -0,0 +1,162 @@ +//! Tests for repository-release installation of the Dylint dependency tools. + +use std::path::PathBuf; + +use super::{ + DependencyBinaryInstaller, DependencyTool, DylintToolStatus, + install::{InstallContext, InstallMode, install_tool, repository_install_context}, + install_dylint_tools_with_options, install_options, +}; +use crate::dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}; +use crate::error::InstallerError; +use crate::installer_packaging::TargetTriple; +use crate::test_utils::{ + StubDirs, StubExecutor, + dependency_binary_helpers::{ + binstall_install, binstall_version_check_with_result, dylint_link_install_list_check, + with_fake_binary_on_path, + }, + success_output, +}; + +/// Writes a fake `dylint-link` into a temporary directory and returns the +/// directory guard alongside the binary path. +/// +/// The fake is staged with a failing exit status so that any attempt to +/// execute it as a health check would be observable as a verification +/// failure. +fn staged_unrunnable_dylint_link() -> std::io::Result<(tempfile::TempDir, PathBuf)> { + let dir = tempfile::tempdir()?; + let path = crate::test_utils::dependency_binary_helpers::path_binary_location( + dir.path(), + "dylint-link", + ); + crate::test_utils::dependency_binary_helpers::write_fake_binary_with_status(&path, true, 1); + Ok((dir, path)) +} + +#[test] +fn install_dylint_tools_accepts_repository_dylint_link_without_executing_it() { + // `dylint-link` forwards its argument list to the underlying linker and + // has no reliable self-reporting subcommand, so a successful repository + // install is accepted on the strength of the download, checksum, + // extraction, and permission steps alone. Staging a fake that exits + // non-zero proves the binary is never executed as a health check: any + // such probe would reject it and fall back to Cargo. + let (_dir, binary_path) = staged_unrunnable_dylint_link().expect("stage fake dylint-link"); + let mut repository_installer = MockDependencyBinaryInstaller::new(); + repository_installer + .expect_install() + .once() + .returning(move |_, _, _| Ok(binary_path.clone())); + let executor = StubExecutor::new(vec![binstall_version_check_with_result(Ok( + success_output(), + ))]); + let mut stderr = Vec::new(); + + install_dylint_tools_with_options( + &executor, + &DylintToolStatus { + cargo_dylint: true, + dylint_link: false, + }, + &mut stderr, + install_options(&repository_installer, false), + ) + .expect("repository install should satisfy dylint-link"); + + let output = String::from_utf8(stderr).expect("stderr should be UTF-8"); + assert!(output.contains("Installed dylint-link from repository release.")); + assert!(!output.contains("failed verification")); + // No Cargo command beyond the binstall-availability probe may run: a + // source build of dylint-link cannot succeed on toolchains below the + // crate's rustc floor. + executor.assert_finished(); +} + +#[test] +fn install_dylint_tools_falls_back_when_repository_dylint_link_install_fails() { + // Genuine repository failures — missing asset, checksum mismatch, + // extraction failure, or an unwritable executable — must still fall back + // to Cargo. + let mut repository_installer = MockDependencyBinaryInstaller::new(); + repository_installer.expect_install().returning(|_, _, _| { + Err(DependencyBinaryInstallError::Install { + binary: "dylint-link".to_owned(), + reason: "checksum mismatch".to_owned(), + }) + }); + let executor = StubExecutor::new(vec![ + binstall_version_check_with_result(Ok(success_output())), + binstall_install("dylint-link", Ok(success_output())), + // The post-binstall check resolves the PATH binary and then confirms + // the version against Cargo's installed-binary registry. + dylint_link_install_list_check(), + ]); + let mut stderr = Vec::new(); + + with_fake_binary_on_path("dylint-link", || { + install_dylint_tools_with_options( + &executor, + &DylintToolStatus { + cargo_dylint: true, + dylint_link: false, + }, + &mut stderr, + install_options(&repository_installer, false), + ) + .expect("binstall fallback should succeed"); + }); + + let output = String::from_utf8(stderr).expect("stderr should be UTF-8"); + assert!(output.contains("Repository install for dylint-link unavailable")); + assert!(output.contains("Installed dylint-link with cargo binstall.")); + executor.assert_finished(); +} + +#[test] +fn install_tool_errors_when_dependency_manifest_entry_is_missing() { + let missing_tool = DependencyTool { + package: "missing-tool", + command: "missing-tool", + args: &["--version"], + }; + let executor = StubExecutor::new(vec![]); + let mut repository_installer = MockDependencyBinaryInstaller::new(); + repository_installer.expect_install().never(); + let dirs = StubDirs { + bin_dir: Some(PathBuf::from("/tmp/bin")), + }; + let target = TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"); + let mut stderr = Vec::new(); + + let error = install_tool( + &executor, + &missing_tool, + &mut stderr, + &InstallContext { + repo: repository_install_context( + Some(&dirs), + Some(&repository_installer as &dyn DependencyBinaryInstaller), + Some(&target), + ), + cargo_fallback_mode: InstallMode::Binstall, + quiet: false, + }, + ) + .expect_err("missing dependency manifest entry should be an install error"); + + match error { + InstallerError::DependencyInstall { tool, message } => { + assert_eq!(tool, "missing-tool"); + assert_eq!( + message, + "dependency manifest is missing an entry for missing-tool" + ); + } + other => panic!("unexpected error: {other}"), + } + + assert!(stderr.is_empty()); + executor.assert_finished(); +} diff --git a/installer/src/install_flow.rs b/installer/src/install_flow/mod.rs similarity index 100% rename from installer/src/install_flow.rs rename to installer/src/install_flow/mod.rs diff --git a/installer/src/list.rs b/installer/src/list.rs index 0ba73eb98..cfb5bb0c9 100644 --- a/installer/src/list.rs +++ b/installer/src/list.rs @@ -175,253 +175,5 @@ where } #[cfg(test)] -mod tests { - use super::*; - use rstest::{fixture, rstest}; - use std::fs; - use tempfile::TempDir; - - // ------------------------------------------------------------------------- - // Fixtures - // ------------------------------------------------------------------------- - - /// A temporary directory converted to a UTF-8 path for test isolation. - struct TempTarget { - _temp: TempDir, - path: Utf8PathBuf, - } - - #[fixture] - fn temp_target() -> TempTarget { - let temp = TempDir::new().expect("failed to create temp dir"); - let path = Utf8PathBuf::try_from(temp.path().to_owned()).expect("non-UTF8 temp path"); - TempTarget { _temp: temp, path } - } - - /// A Write implementation that always fails, for testing error paths. - struct FailingWriter; - - impl std::io::Write for FailingWriter { - fn write(&mut self, _buf: &[u8]) -> std::io::Result { - Err(std::io::Error::other("simulated write failure")) - } - - fn flush(&mut self) -> std::io::Result<()> { - Err(std::io::Error::other("simulated flush failure")) - } - } - - // ------------------------------------------------------------------------- - // Helpers - // ------------------------------------------------------------------------- - - #[derive(Debug, Clone, Copy)] - enum MockLibraryKind { - Local, - Prebuilt { target: &'static str }, - } - - impl MockLibraryKind { - fn library_dir(&self, target_dir: &Utf8Path, toolchain: &str) -> Utf8PathBuf { - match self { - Self::Local => target_dir.join(toolchain).join("release"), - Self::Prebuilt { target } => target_dir.join(toolchain).join(target).join("lib"), - } - } - - fn content(&self) -> &'static [u8] { - match self { - Self::Local => b"mock library", - Self::Prebuilt { .. } => b"mock prebuilt library", - } - } - } - - fn create_mock_library_internal(target_dir: &Utf8Path, toolchain: &str, kind: MockLibraryKind) { - use crate::builder::{library_extension, library_prefix}; - - let lib_dir = kind.library_dir(target_dir, toolchain); - fs::create_dir_all(&lib_dir).expect("failed to create target library directory"); - - let filename = format!( - "{}whitaker_suite@{toolchain}{}", - library_prefix(), - library_extension() - ); - - let error_msg = match kind { - MockLibraryKind::Local => "failed to create mock library", - MockLibraryKind::Prebuilt { .. } => "failed to create prebuilt mock library", - }; - fs::write(lib_dir.join(filename), kind.content()).expect(error_msg); - } - - /// Helper to create a mock installed library in the target directory for tests. - fn create_mock_library(target_dir: &Utf8Path, toolchain: &str) { - create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Local); - } - - fn create_mock_prebuilt_library(target_dir: &Utf8Path, toolchain: &str, target: &'static str) { - create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Prebuilt { target }); - } - - // ------------------------------------------------------------------------- - // run_list tests - // ------------------------------------------------------------------------- - - #[rstest] - fn run_list_outputs_human_readable_format(temp_target: TempTarget) { - let args = ListArgs { - json: false, - target_dir: Some(temp_target.path.clone()), - }; - let mut stdout = Vec::new(); - - let result = run_list_with(&args, &mut stdout, || None); - - assert!(result.is_ok(), "expected success, got: {result:?}"); - let output = String::from_utf8_lossy(&stdout); - assert!(output.contains("No lints installed"), "got: {output}"); - } - - #[rstest] - #[case::json_format(true, &["toolchains", "\"active\""])] - #[case::human_format(false, &["nightly-2026-05-28", "whitaker_suite"])] - fn run_list_with_installed_library_includes_expected_output( - temp_target: TempTarget, - #[case] json: bool, - #[case] expected: &[&str], - ) { - create_mock_library(&temp_target.path, "nightly-2026-05-28"); - let args = ListArgs { - json, - target_dir: Some(temp_target.path.clone()), - }; - let mut stdout = Vec::new(); - - let result = run_list_with(&args, &mut stdout, || Some("nightly-2026-05-28".to_owned())); - - assert!(result.is_ok(), "expected success, got: {result:?}"); - let output = String::from_utf8_lossy(&stdout); - for needle in expected { - assert!( - output.contains(needle), - "expected '{needle}' in output: {output}" - ); - } - } - - #[rstest] - fn run_list_finds_prebuilt_layout_libraries(temp_target: TempTarget) { - create_mock_prebuilt_library( - &temp_target.path, - "nightly-2026-05-28", - "x86_64-unknown-linux-gnu", - ); - let args = ListArgs { - json: false, - target_dir: Some(temp_target.path.clone()), - }; - let mut stdout = Vec::new(); - - let result = run_list_with(&args, &mut stdout, || Some("nightly-2026-05-28".to_owned())); - - assert!(result.is_ok(), "expected success, got: {result:?}"); - let output = String::from_utf8_lossy(&stdout); - assert!(output.contains("nightly-2026-05-28"), "got: {output}"); - assert!(output.contains("whitaker_suite"), "got: {output}"); - } - - #[rstest] - fn run_list_returns_write_failed_on_stdout_error(temp_target: TempTarget) { - let args = ListArgs { - json: false, - target_dir: Some(temp_target.path.clone()), - }; - let mut failing_stdout = FailingWriter; - - let result = run_list_with(&args, &mut failing_stdout, || None); - - let err = result.expect_err("expected error on write failure"); - assert!( - matches!(err, InstallerError::WriteFailed { .. }), - "expected WriteFailed error, got: {err:?}" - ); - } - - // ------------------------------------------------------------------------- - // detect_active_toolchain_in tests - // ------------------------------------------------------------------------- - - #[rstest] - fn detect_active_toolchain_in_returns_none_when_no_toolchain_file(temp_target: TempTarget) { - let result = detect_active_toolchain_in(&temp_target.path); - assert!( - result.is_none(), - "expected None for directory without rust-toolchain.toml" - ); - } - - #[rstest] - fn detect_active_toolchain_in_returns_channel_when_toolchain_file_exists( - temp_target: TempTarget, - ) { - // Create a rust-toolchain.toml file - let toolchain_content = r#"[toolchain] -channel = "nightly-2026-05-28" -"#; - fs::write( - temp_target.path.join("rust-toolchain.toml"), - toolchain_content, - ) - .expect("failed to write rust-toolchain.toml"); - - let result = detect_active_toolchain_in(&temp_target.path); - - assert_eq!(result, Some("nightly-2026-05-28".to_owned())); - } - - // ------------------------------------------------------------------------- - // determine_target_dir tests - // ------------------------------------------------------------------------- - - #[rstest] - fn determine_target_dir_returns_cli_value_when_provided(temp_target: TempTarget) { - let result = determine_target_dir_with(Some(&temp_target.path), || None); - - assert!(result.is_ok(), "expected success, got: {result:?}"); - assert_eq!(result.expect("already checked"), temp_target.path); - } - - #[rstest] - fn determine_target_dir_falls_back_to_default_when_cli_is_none(temp_target: TempTarget) { - let default_path = temp_target.path.clone(); - - let result = determine_target_dir_with(None, || Some(default_path.clone())); - - assert!(result.is_ok(), "expected success, got: {result:?}"); - assert_eq!(result.expect("already checked"), default_path); - } - - #[test] - fn determine_target_dir_returns_error_when_no_default_available() { - let result = determine_target_dir_with(None, || None); - - let err = result.expect_err("expected error when no default"); - assert!( - matches!(err, InstallerError::StagingFailed { .. }), - "expected StagingFailed error, got: {err:?}" - ); - } - - #[rstest] - fn determine_target_dir_prefers_cli_over_default(temp_target: TempTarget) { - let cli_path = temp_target.path.clone(); - let default_path = temp_target.path.join("should_not_be_used"); - - let result = determine_target_dir_with(Some(&cli_path), || Some(default_path)); - - assert!(result.is_ok(), "expected success, got: {result:?}"); - assert_eq!(result.expect("already checked"), cli_path); - } -} +#[path = "list_tests.rs"] +mod tests; diff --git a/installer/src/list_tests.rs b/installer/src/list_tests.rs new file mode 100644 index 000000000..88d2a871b --- /dev/null +++ b/installer/src/list_tests.rs @@ -0,0 +1,248 @@ +//! Tests for the `list` command handler and its path resolution helpers. + +use super::*; +use rstest::{fixture, rstest}; +use std::fs; +use tempfile::TempDir; + +// ------------------------------------------------------------------------- +// Fixtures +// ------------------------------------------------------------------------- + +/// A temporary directory converted to a UTF-8 path for test isolation. +struct TempTarget { + _temp: TempDir, + path: Utf8PathBuf, +} + +#[fixture] +fn temp_target() -> TempTarget { + let temp = TempDir::new().expect("failed to create temp dir"); + let path = Utf8PathBuf::try_from(temp.path().to_owned()).expect("non-UTF8 temp path"); + TempTarget { _temp: temp, path } +} + +/// A Write implementation that always fails, for testing error paths. +struct FailingWriter; + +impl std::io::Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> std::io::Result { + Err(std::io::Error::other("simulated write failure")) + } + + fn flush(&mut self) -> std::io::Result<()> { + Err(std::io::Error::other("simulated flush failure")) + } +} + +// ------------------------------------------------------------------------- +// Helpers +// ------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy)] +enum MockLibraryKind { + Local, + Prebuilt { target: &'static str }, +} + +impl MockLibraryKind { + fn library_dir(&self, target_dir: &Utf8Path, toolchain: &str) -> Utf8PathBuf { + match self { + Self::Local => target_dir.join(toolchain).join("release"), + Self::Prebuilt { target } => target_dir.join(toolchain).join(target).join("lib"), + } + } + + fn content(&self) -> &'static [u8] { + match self { + Self::Local => b"mock library", + Self::Prebuilt { .. } => b"mock prebuilt library", + } + } +} + +fn create_mock_library_internal(target_dir: &Utf8Path, toolchain: &str, kind: MockLibraryKind) { + use crate::builder::{library_extension, library_prefix}; + + let lib_dir = kind.library_dir(target_dir, toolchain); + fs::create_dir_all(&lib_dir).expect("failed to create target library directory"); + + let filename = format!( + "{}whitaker_suite@{toolchain}{}", + library_prefix(), + library_extension() + ); + + let error_msg = match kind { + MockLibraryKind::Local => "failed to create mock library", + MockLibraryKind::Prebuilt { .. } => "failed to create prebuilt mock library", + }; + fs::write(lib_dir.join(filename), kind.content()).expect(error_msg); +} + +/// Helper to create a mock installed library in the target directory for tests. +fn create_mock_library(target_dir: &Utf8Path, toolchain: &str) { + create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Local); +} + +fn create_mock_prebuilt_library(target_dir: &Utf8Path, toolchain: &str, target: &'static str) { + create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Prebuilt { target }); +} + +// ------------------------------------------------------------------------- +// run_list tests +// ------------------------------------------------------------------------- + +#[rstest] +fn run_list_outputs_human_readable_format(temp_target: TempTarget) { + let args = ListArgs { + json: false, + target_dir: Some(temp_target.path.clone()), + }; + let mut stdout = Vec::new(); + + let result = run_list_with(&args, &mut stdout, || None); + + assert!(result.is_ok(), "expected success, got: {result:?}"); + let output = String::from_utf8_lossy(&stdout); + assert!(output.contains("No lints installed"), "got: {output}"); +} + +#[rstest] +#[case::json_format(true, &["toolchains", "\"active\""])] +#[case::human_format(false, &["nightly-2026-05-28", "whitaker_suite"])] +fn run_list_with_installed_library_includes_expected_output( + temp_target: TempTarget, + #[case] json: bool, + #[case] expected: &[&str], +) { + create_mock_library(&temp_target.path, "nightly-2026-05-28"); + let args = ListArgs { + json, + target_dir: Some(temp_target.path.clone()), + }; + let mut stdout = Vec::new(); + + let result = run_list_with(&args, &mut stdout, || Some("nightly-2026-05-28".to_owned())); + + assert!(result.is_ok(), "expected success, got: {result:?}"); + let output = String::from_utf8_lossy(&stdout); + for needle in expected { + assert!( + output.contains(needle), + "expected '{needle}' in output: {output}" + ); + } +} + +#[rstest] +fn run_list_finds_prebuilt_layout_libraries(temp_target: TempTarget) { + create_mock_prebuilt_library( + &temp_target.path, + "nightly-2026-05-28", + "x86_64-unknown-linux-gnu", + ); + let args = ListArgs { + json: false, + target_dir: Some(temp_target.path.clone()), + }; + let mut stdout = Vec::new(); + + let result = run_list_with(&args, &mut stdout, || Some("nightly-2026-05-28".to_owned())); + + assert!(result.is_ok(), "expected success, got: {result:?}"); + let output = String::from_utf8_lossy(&stdout); + assert!(output.contains("nightly-2026-05-28"), "got: {output}"); + assert!(output.contains("whitaker_suite"), "got: {output}"); +} + +#[rstest] +fn run_list_returns_write_failed_on_stdout_error(temp_target: TempTarget) { + let args = ListArgs { + json: false, + target_dir: Some(temp_target.path.clone()), + }; + let mut failing_stdout = FailingWriter; + + let result = run_list_with(&args, &mut failing_stdout, || None); + + let err = result.expect_err("expected error on write failure"); + assert!( + matches!(err, InstallerError::WriteFailed { .. }), + "expected WriteFailed error, got: {err:?}" + ); +} + +// ------------------------------------------------------------------------- +// detect_active_toolchain_in tests +// ------------------------------------------------------------------------- + +#[rstest] +fn detect_active_toolchain_in_returns_none_when_no_toolchain_file(temp_target: TempTarget) { + let result = detect_active_toolchain_in(&temp_target.path); + assert!( + result.is_none(), + "expected None for directory without rust-toolchain.toml" + ); +} + +#[rstest] +fn detect_active_toolchain_in_returns_channel_when_toolchain_file_exists(temp_target: TempTarget) { + // Create a rust-toolchain.toml file + let toolchain_content = r#"[toolchain] +channel = "nightly-2026-05-28" +"#; + fs::write( + temp_target.path.join("rust-toolchain.toml"), + toolchain_content, + ) + .expect("failed to write rust-toolchain.toml"); + + let result = detect_active_toolchain_in(&temp_target.path); + + assert_eq!(result, Some("nightly-2026-05-28".to_owned())); +} + +// ------------------------------------------------------------------------- +// determine_target_dir tests +// ------------------------------------------------------------------------- + +#[rstest] +fn determine_target_dir_returns_cli_value_when_provided(temp_target: TempTarget) { + let result = determine_target_dir_with(Some(&temp_target.path), || None); + + assert!(result.is_ok(), "expected success, got: {result:?}"); + assert_eq!(result.expect("already checked"), temp_target.path); +} + +#[rstest] +fn determine_target_dir_falls_back_to_default_when_cli_is_none(temp_target: TempTarget) { + let default_path = temp_target.path.clone(); + + let result = determine_target_dir_with(None, || Some(default_path.clone())); + + assert!(result.is_ok(), "expected success, got: {result:?}"); + assert_eq!(result.expect("already checked"), default_path); +} + +#[test] +fn determine_target_dir_returns_error_when_no_default_available() { + let result = determine_target_dir_with(None, || None); + + let err = result.expect_err("expected error when no default"); + assert!( + matches!(err, InstallerError::StagingFailed { .. }), + "expected StagingFailed error, got: {err:?}" + ); +} + +#[rstest] +fn determine_target_dir_prefers_cli_over_default(temp_target: TempTarget) { + let cli_path = temp_target.path.clone(); + let default_path = temp_target.path.join("should_not_be_used"); + + let result = determine_target_dir_with(Some(&cli_path), || Some(default_path)); + + assert!(result.is_ok(), "expected success, got: {result:?}"); + assert_eq!(result.expect("already checked"), cli_path); +} diff --git a/installer/src/test_utils/dependency_binary_helpers.rs b/installer/src/test_utils/dependency_binary_helpers.rs index 193a89d9f..9ce6d9813 100644 --- a/installer/src/test_utils/dependency_binary_helpers.rs +++ b/installer/src/test_utils/dependency_binary_helpers.rs @@ -1,124 +1,17 @@ //! Test helpers for dependency binary installation tests. use crate::dependency_binaries::find_dependency_binary; -#[cfg(any(test, feature = "test-support"))] -use crate::dependency_binaries::{ - DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller, -}; -#[cfg(any(test, feature = "test-support"))] -use crate::dirs::BaseDirs; use crate::error::Result; -#[cfg(any(test, feature = "test-support"))] -use crate::installer_packaging::TargetTriple; -#[cfg(any(test, feature = "test-support"))] -use crate::test_support::env_test_guard; use crate::test_utils::{ExpectedCall, failure_output, stdout_output, success_output}; -#[cfg(any(test, feature = "test-support"))] -use std::fs; -#[cfg(all(any(test, feature = "test-support"), unix))] -use std::os::unix::fs::PermissionsExt; -#[cfg(any(test, feature = "test-support"))] -use std::path::{Path, PathBuf}; use std::process::Output; -/// Repository installer test double that always reports a missing archive. -#[cfg(any(test, feature = "test-support"))] -pub struct AlwaysNotFoundRepositoryInstaller; - -#[cfg(any(test, feature = "test-support"))] -impl DependencyBinaryInstaller for AlwaysNotFoundRepositoryInstaller { - fn install( - &self, - dependency: &DependencyBinary, - target: &TargetTriple, - _dirs: &dyn BaseDirs, - ) -> std::result::Result { - Err(DependencyBinaryInstallError::NotFound { - url: format!( - "https://example.test/{}-{}-v{}.tgz", - dependency.package(), - target, - dependency.version() - ), - }) - } -} - -/// Writes a fake binary at `path` that exits successfully. -#[cfg(any(test, feature = "test-support"))] -pub fn write_fake_binary(path: &Path, is_executable: bool) { - write_fake_binary_with_status(path, is_executable, 0); -} +#[path = "dependency_binary_helpers_fakes.rs"] +mod fakes; -/// Writes a fake binary at `path` that exits with the supplied status code. -#[cfg(any(test, feature = "test-support"))] -pub fn write_fake_binary_with_status(path: &Path, is_executable: bool, exit_code: i32) { - fs::write(path, fake_binary_contents(exit_code)).expect("write fake binary"); - #[cfg(unix)] - { - let mode = if is_executable { 0o755 } else { 0o644 }; - let mut permissions = fs::metadata(path) - .expect("read fake binary metadata") - .permissions(); - permissions.set_mode(mode); - fs::set_permissions(path, permissions).expect("set fake binary permissions"); - } - #[cfg(not(unix))] - let _ = is_executable; -} - -#[cfg(any(test, feature = "test-support"))] -fn fake_binary_contents(exit_code: i32) -> Vec { - #[cfg(windows)] - { - format!("@echo off\r\nexit /b {exit_code}\r\n").into_bytes() - } - #[cfg(not(windows))] - { - format!("#!/bin/sh\nexit {exit_code}\n").into_bytes() - } -} - -/// Runs a closure with `PATH` pointing at one or more fake directories. -#[cfg(any(test, feature = "test-support"))] -pub fn with_fake_path(setup: impl FnOnce(&[PathBuf]), run: impl FnOnce() -> T) -> T { - let _guard = env_test_guard(); - let temp_dirs = [ - tempfile::tempdir().expect("create temp dir"), - tempfile::tempdir().expect("create temp dir"), - ]; - let path_dirs = temp_dirs - .iter() - .map(|dir| dir.path().to_path_buf()) - .collect::>(); - setup(&path_dirs); - let path = std::env::join_paths(path_dirs.iter().map(PathBuf::as_path)) - .expect("join fake PATH directories"); - temp_env::with_var("PATH", Some(path), run) -} - -/// Runs a closure with `PATH` containing a fake executable in the first entry. -#[cfg(any(test, feature = "test-support"))] -pub fn with_fake_binary_on_path(binary_name: &str, run: impl FnOnce() -> T) -> T { - with_fake_path( - |directories| write_fake_binary(&path_binary_location(&directories[0], binary_name), true), - run, - ) -} - -/// Joins `binary_name` onto `directory` with the platform executable suffix, -/// so directly probed fakes are runnable on Windows as well as Unix. -#[cfg(any(test, feature = "test-support"))] -pub fn path_binary_location(directory: &Path, binary_name: &str) -> PathBuf { - #[cfg(windows)] - { - directory.join(format!("{binary_name}.cmd")) - } - #[cfg(not(windows))] - { - directory.join(binary_name) - } -} +pub use fakes::{ + AlwaysNotFoundRepositoryInstaller, path_binary_location, with_fake_binary_on_path, + with_fake_path, write_fake_binary, write_fake_binary_with_status, +}; /// Expected outcome of repository-install verification in a test scenario. /// diff --git a/installer/src/test_utils/dependency_binary_helpers_fakes.rs b/installer/src/test_utils/dependency_binary_helpers_fakes.rs new file mode 100644 index 000000000..ec91be59a --- /dev/null +++ b/installer/src/test_utils/dependency_binary_helpers_fakes.rs @@ -0,0 +1,108 @@ +//! Fake binaries, PATH staging, and repository-installer doubles. +//! +//! These helpers stage executables in temporary directories and point `PATH` +//! at them so dependency-install probes can be exercised without touching a +//! real toolchain. + +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; + +use crate::dependency_binaries::{ + DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller, +}; +use crate::dirs::BaseDirs; +use crate::installer_packaging::TargetTriple; +use crate::test_support::env_test_guard; + +/// Repository installer test double that always reports a missing archive. +pub struct AlwaysNotFoundRepositoryInstaller; + +impl DependencyBinaryInstaller for AlwaysNotFoundRepositoryInstaller { + fn install( + &self, + dependency: &DependencyBinary, + target: &TargetTriple, + _dirs: &dyn BaseDirs, + ) -> std::result::Result { + Err(DependencyBinaryInstallError::NotFound { + url: format!( + "https://example.test/{}-{}-v{}.tgz", + dependency.package(), + target, + dependency.version() + ), + }) + } +} + +/// Writes a fake binary at `path` that exits successfully. +pub fn write_fake_binary(path: &Path, is_executable: bool) { + write_fake_binary_with_status(path, is_executable, 0); +} + +/// Writes a fake binary at `path` that exits with the supplied status code. +pub fn write_fake_binary_with_status(path: &Path, is_executable: bool, exit_code: i32) { + fs::write(path, fake_binary_contents(exit_code)).expect("write fake binary"); + #[cfg(unix)] + { + let mode = if is_executable { 0o755 } else { 0o644 }; + let mut permissions = fs::metadata(path) + .expect("read fake binary metadata") + .permissions(); + permissions.set_mode(mode); + fs::set_permissions(path, permissions).expect("set fake binary permissions"); + } + #[cfg(not(unix))] + let _ = is_executable; +} + +fn fake_binary_contents(exit_code: i32) -> Vec { + #[cfg(windows)] + { + format!("@echo off\r\nexit /b {exit_code}\r\n").into_bytes() + } + #[cfg(not(windows))] + { + format!("#!/bin/sh\nexit {exit_code}\n").into_bytes() + } +} + +/// Runs a closure with `PATH` pointing at one or more fake directories. +pub fn with_fake_path(setup: impl FnOnce(&[PathBuf]), run: impl FnOnce() -> T) -> T { + let _guard = env_test_guard(); + let temp_dirs = [ + tempfile::tempdir().expect("create temp dir"), + tempfile::tempdir().expect("create temp dir"), + ]; + let path_dirs = temp_dirs + .iter() + .map(|dir| dir.path().to_path_buf()) + .collect::>(); + setup(&path_dirs); + let path = std::env::join_paths(path_dirs.iter().map(PathBuf::as_path)) + .expect("join fake PATH directories"); + temp_env::with_var("PATH", Some(path), run) +} + +/// Runs a closure with `PATH` containing a fake executable in the first entry. +pub fn with_fake_binary_on_path(binary_name: &str, run: impl FnOnce() -> T) -> T { + with_fake_path( + |directories| write_fake_binary(&path_binary_location(&directories[0], binary_name), true), + run, + ) +} + +/// Joins `binary_name` onto `directory` with the platform executable suffix, +/// so directly probed fakes are runnable on Windows as well as Unix. +pub fn path_binary_location(directory: &Path, binary_name: &str) -> PathBuf { + #[cfg(windows)] + { + directory.join(format!("{binary_name}.cmd")) + } + #[cfg(not(windows))] + { + directory.join(binary_name) + } +} diff --git a/installer/src/toolchain.rs b/installer/src/toolchain/mod.rs similarity index 100% rename from installer/src/toolchain.rs rename to installer/src/toolchain/mod.rs diff --git a/installer/src/workspace.rs b/installer/src/workspace.rs index 3d06bbf5f..674a07641 100644 --- a/installer/src/workspace.rs +++ b/installer/src/workspace.rs @@ -195,192 +195,5 @@ fn is_cargo_workspace_root(cargo_toml: &Utf8Path) -> Result { } #[cfg(test)] -mod tests { - use super::*; - use crate::dirs::{MockBaseDirs, SystemBaseDirs}; - use rstest::{fixture, rstest}; - use std::fs; - use std::path::PathBuf; - use tempfile::TempDir; - - /// A temporary directory converted to a UTF-8 path for workspace tests. - struct TempWorkspace { - _temp: TempDir, - path: Utf8PathBuf, - } - - #[fixture] - fn temp_workspace() -> TempWorkspace { - let temp = TempDir::new().expect("failed to create temp dir"); - let path = Utf8PathBuf::try_from(temp.path().to_owned()).expect("non-UTF8 temp path"); - TempWorkspace { _temp: temp, path } - } - - fn write_cargo_toml(dir: &Utf8Path, package_name: &str) { - let cargo_toml = dir.join("Cargo.toml"); - fs::write( - cargo_toml, - format!("[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\n"), - ) - .expect("failed to write Cargo.toml"); - } - - #[rstest] - #[case::whitaker_project(Some("whitaker"), true)] - #[case::other_project(Some("other-project"), false)] - #[case::empty_dir(None, false)] - fn is_whitaker_workspace_detection( - temp_workspace: TempWorkspace, - #[case] package_name: Option<&str>, - #[case] expected: bool, - ) { - if let Some(name) = package_name { - write_cargo_toml(&temp_workspace.path, name); - } - assert_eq!(is_whitaker_workspace(&temp_workspace.path), expected); - } - - #[test] - fn clone_directory_returns_some_on_supported_platforms() { - // This test may fail on unsupported platforms, but should pass on - // Linux, macOS, and Windows. - let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs"); - let dir = clone_directory(&dirs); - assert!(dir.is_some(), "expected clone_directory to return Some"); - assert!( - dir.as_ref() - .is_some_and(|p| p.as_str().contains("whitaker")), - "expected path to contain 'whitaker'" - ); - } - - #[rstest] - fn decide_workspace_action_uses_cwd_when_whitaker(temp_workspace: TempWorkspace) { - write_cargo_toml(&temp_workspace.path, "whitaker"); - let clone_dir = Utf8PathBuf::from("/nonexistent/clone/dir"); - - let action = decide_workspace_action(&temp_workspace.path, &clone_dir, true); - - assert_eq!(action, WorkspaceAction::UseCurrentDir(temp_workspace.path)); - } - - #[rstest] - fn decide_workspace_action_clones_when_empty(temp_workspace: TempWorkspace) { - // temp_workspace.path is empty (no Cargo.toml), clone_dir doesn't exist - let clone_dir = temp_workspace.path.join("clone_target"); - - let action = decide_workspace_action(&temp_workspace.path, &clone_dir, true); - - assert_eq!(action, WorkspaceAction::CloneTo(clone_dir)); - } - - #[rstest] - fn decide_workspace_action_updates_when_clone_exists(temp_workspace: TempWorkspace) { - // Create a clone directory (not a whitaker workspace, just exists) - let clone_dir = temp_workspace.path.join("clone_target"); - fs::create_dir(&clone_dir).expect("failed to create clone dir"); - - let action = decide_workspace_action(&temp_workspace.path, &clone_dir, true); - - assert_eq!(action, WorkspaceAction::UpdateAt(clone_dir)); - } - - #[rstest] - fn decide_workspace_action_uses_existing_when_no_update(temp_workspace: TempWorkspace) { - let clone_dir = temp_workspace.path.join("clone_target"); - fs::create_dir(&clone_dir).expect("failed to create clone dir"); - - let action = decide_workspace_action(&temp_workspace.path, &clone_dir, false); - - assert_eq!(action, WorkspaceAction::UseExisting(clone_dir)); - } - - // ------------------------------------------------------------------------- - // Behavioural tests for workspace orchestration with mocked dependencies - // ------------------------------------------------------------------------- - - fn mock_dirs_returning(data_dir: Option) -> MockBaseDirs { - let mut mock = MockBaseDirs::new(); - mock.expect_whitaker_data_dir().return_const(data_dir); - mock - } - - #[rstest] - fn resolve_workspace_path_returns_clone_dir_when_not_in_workspace( - temp_workspace: TempWorkspace, - ) { - // Mock returns a data directory inside temp workspace - let expected_dir = temp_workspace.path.join("data").join("whitaker"); - let mock = mock_dirs_returning(Some(expected_dir.clone().into_std_path_buf())); - - let result = resolve_workspace_path(&mock); - - assert!(result.is_ok()); - assert_eq!(result.unwrap(), expected_dir); - } - - #[rstest] - fn resolve_workspace_path_errors_when_data_dir_unavailable(temp_workspace: TempWorkspace) { - let _ = temp_workspace; // Ensure fixture is used - let mock = mock_dirs_returning(None); - - let result = resolve_workspace_path(&mock); - - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!( - matches!(err, InstallerError::WorkspaceNotFound { .. }), - "expected WorkspaceNotFound error, got: {err:?}" - ); - } - - #[test] - fn clone_directory_returns_none_when_data_dir_unavailable() { - let mock = mock_dirs_returning(None); - assert!(clone_directory(&mock).is_none()); - } - - #[rstest] - fn clone_directory_returns_path_from_mock(temp_workspace: TempWorkspace) { - let expected = temp_workspace.path.join("data").join("whitaker"); - let mock = mock_dirs_returning(Some(expected.clone().into_std_path_buf())); - assert_eq!(clone_directory(&mock), Some(expected)); - } - - // Tests for find_workspace_root - - fn write_workspace_cargo_toml(dir: &Utf8Path) { - fs::write( - dir.join("Cargo.toml"), - "[workspace]\nmembers = [\"crates/*\"]\n", - ) - .expect("failed to write workspace Cargo.toml"); - } - - #[rstest] - fn find_workspace_root_finds_workspace_in_current_dir(temp_workspace: TempWorkspace) { - write_workspace_cargo_toml(&temp_workspace.path); - assert_eq!( - find_workspace_root(&temp_workspace.path).unwrap(), - temp_workspace.path - ); - } - - #[rstest] - fn find_workspace_root_finds_workspace_in_parent_dir(temp_workspace: TempWorkspace) { - write_workspace_cargo_toml(&temp_workspace.path); - let subdir = temp_workspace.path.join("crates").join("my_crate"); - fs::create_dir_all(&subdir).expect("failed to create subdirs"); - assert_eq!(find_workspace_root(&subdir).unwrap(), temp_workspace.path); - } - - #[rstest] - fn find_workspace_root_errors_when_no_workspace_found(temp_workspace: TempWorkspace) { - write_cargo_toml(&temp_workspace.path, "not_a_workspace"); - let result = find_workspace_root(&temp_workspace.path); - assert!(matches!( - result.unwrap_err(), - InstallerError::WorkspaceNotFound { .. } - )); - } -} +#[path = "workspace_tests.rs"] +mod tests; diff --git a/installer/src/workspace_tests.rs b/installer/src/workspace_tests.rs new file mode 100644 index 000000000..c08f5d888 --- /dev/null +++ b/installer/src/workspace_tests.rs @@ -0,0 +1,187 @@ +//! Tests for workspace discovery and layout. + +use super::*; +use crate::dirs::{MockBaseDirs, SystemBaseDirs}; +use rstest::{fixture, rstest}; +use std::fs; +use std::path::PathBuf; +use tempfile::TempDir; + +/// A temporary directory converted to a UTF-8 path for workspace tests. +struct TempWorkspace { + _temp: TempDir, + path: Utf8PathBuf, +} + +#[fixture] +fn temp_workspace() -> TempWorkspace { + let temp = TempDir::new().expect("failed to create temp dir"); + let path = Utf8PathBuf::try_from(temp.path().to_owned()).expect("non-UTF8 temp path"); + TempWorkspace { _temp: temp, path } +} + +fn write_cargo_toml(dir: &Utf8Path, package_name: &str) { + let cargo_toml = dir.join("Cargo.toml"); + fs::write( + cargo_toml, + format!("[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\n"), + ) + .expect("failed to write Cargo.toml"); +} + +#[rstest] +#[case::whitaker_project(Some("whitaker"), true)] +#[case::other_project(Some("other-project"), false)] +#[case::empty_dir(None, false)] +fn is_whitaker_workspace_detection( + temp_workspace: TempWorkspace, + #[case] package_name: Option<&str>, + #[case] expected: bool, +) { + if let Some(name) = package_name { + write_cargo_toml(&temp_workspace.path, name); + } + assert_eq!(is_whitaker_workspace(&temp_workspace.path), expected); +} + +#[test] +fn clone_directory_returns_some_on_supported_platforms() { + // This test may fail on unsupported platforms, but should pass on + // Linux, macOS, and Windows. + let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs"); + let dir = clone_directory(&dirs); + assert!(dir.is_some(), "expected clone_directory to return Some"); + assert!( + dir.as_ref() + .is_some_and(|p| p.as_str().contains("whitaker")), + "expected path to contain 'whitaker'" + ); +} + +#[rstest] +fn decide_workspace_action_uses_cwd_when_whitaker(temp_workspace: TempWorkspace) { + write_cargo_toml(&temp_workspace.path, "whitaker"); + let clone_dir = Utf8PathBuf::from("/nonexistent/clone/dir"); + + let action = decide_workspace_action(&temp_workspace.path, &clone_dir, true); + + assert_eq!(action, WorkspaceAction::UseCurrentDir(temp_workspace.path)); +} + +#[rstest] +fn decide_workspace_action_clones_when_empty(temp_workspace: TempWorkspace) { + // temp_workspace.path is empty (no Cargo.toml), clone_dir doesn't exist + let clone_dir = temp_workspace.path.join("clone_target"); + + let action = decide_workspace_action(&temp_workspace.path, &clone_dir, true); + + assert_eq!(action, WorkspaceAction::CloneTo(clone_dir)); +} + +#[rstest] +fn decide_workspace_action_updates_when_clone_exists(temp_workspace: TempWorkspace) { + // Create a clone directory (not a whitaker workspace, just exists) + let clone_dir = temp_workspace.path.join("clone_target"); + fs::create_dir(&clone_dir).expect("failed to create clone dir"); + + let action = decide_workspace_action(&temp_workspace.path, &clone_dir, true); + + assert_eq!(action, WorkspaceAction::UpdateAt(clone_dir)); +} + +#[rstest] +fn decide_workspace_action_uses_existing_when_no_update(temp_workspace: TempWorkspace) { + let clone_dir = temp_workspace.path.join("clone_target"); + fs::create_dir(&clone_dir).expect("failed to create clone dir"); + + let action = decide_workspace_action(&temp_workspace.path, &clone_dir, false); + + assert_eq!(action, WorkspaceAction::UseExisting(clone_dir)); +} + +// ------------------------------------------------------------------------- +// Behavioural tests for workspace orchestration with mocked dependencies +// ------------------------------------------------------------------------- + +fn mock_dirs_returning(data_dir: Option) -> MockBaseDirs { + let mut mock = MockBaseDirs::new(); + mock.expect_whitaker_data_dir().return_const(data_dir); + mock +} + +#[rstest] +fn resolve_workspace_path_returns_clone_dir_when_not_in_workspace(temp_workspace: TempWorkspace) { + // Mock returns a data directory inside temp workspace + let expected_dir = temp_workspace.path.join("data").join("whitaker"); + let mock = mock_dirs_returning(Some(expected_dir.clone().into_std_path_buf())); + + let result = resolve_workspace_path(&mock); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected_dir); +} + +#[rstest] +fn resolve_workspace_path_errors_when_data_dir_unavailable(temp_workspace: TempWorkspace) { + let _ = temp_workspace; // Ensure fixture is used + let mock = mock_dirs_returning(None); + + let result = resolve_workspace_path(&mock); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, InstallerError::WorkspaceNotFound { .. }), + "expected WorkspaceNotFound error, got: {err:?}" + ); +} + +#[test] +fn clone_directory_returns_none_when_data_dir_unavailable() { + let mock = mock_dirs_returning(None); + assert!(clone_directory(&mock).is_none()); +} + +#[rstest] +fn clone_directory_returns_path_from_mock(temp_workspace: TempWorkspace) { + let expected = temp_workspace.path.join("data").join("whitaker"); + let mock = mock_dirs_returning(Some(expected.clone().into_std_path_buf())); + assert_eq!(clone_directory(&mock), Some(expected)); +} + +// Tests for find_workspace_root + +fn write_workspace_cargo_toml(dir: &Utf8Path) { + fs::write( + dir.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/*\"]\n", + ) + .expect("failed to write workspace Cargo.toml"); +} + +#[rstest] +fn find_workspace_root_finds_workspace_in_current_dir(temp_workspace: TempWorkspace) { + write_workspace_cargo_toml(&temp_workspace.path); + assert_eq!( + find_workspace_root(&temp_workspace.path).unwrap(), + temp_workspace.path + ); +} + +#[rstest] +fn find_workspace_root_finds_workspace_in_parent_dir(temp_workspace: TempWorkspace) { + write_workspace_cargo_toml(&temp_workspace.path); + let subdir = temp_workspace.path.join("crates").join("my_crate"); + fs::create_dir_all(&subdir).expect("failed to create subdirs"); + assert_eq!(find_workspace_root(&subdir).unwrap(), temp_workspace.path); +} + +#[rstest] +fn find_workspace_root_errors_when_no_workspace_found(temp_workspace: TempWorkspace) { + write_cargo_toml(&temp_workspace.path, "not_a_workspace"); + let result = find_workspace_root(&temp_workspace.path); + assert!(matches!( + result.unwrap_err(), + InstallerError::WorkspaceNotFound { .. } + )); +} From 4f3405d7330c36d0c24295be09632a40d0da479d Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 29 Aug 2026 01:49:20 +0200 Subject: [PATCH 5/9] Fix workspace-lint violations across support crates Behaviour-preserving refactors across common/, whitaker_sarif, whitaker_clones_core, the lint crates, the suite, and the root crate, clearing the violations a stricter lint configuration later in this stack will enforce, while staying green under the current gates: - panic-prone operations give way to descriptive errors or checked alternatives (indexing and slicing via get/split_at_checked, no unwrap/expect outside tests, no panicking closures in Result contexts); - shadowed bindings are renamed meaningfully; wildcard matches name their single variants; string pushes and format chains use their cheaper forms; - the Dylint registration entry point moves behind a new declare_dylint_register_entry! macro in common, so consumer crates contain no unsafe tokens of their own; - the environment-variable test guards give way to temp_env-backed scoped helpers, and the last EnvVarGuard consumer is ported, so the guard itself retires; - missing documentation is filled in rather than waived. Installer changes are deliberately absent; they form the next PR in the stack. --- Cargo.lock | 1 + common/src/attributes/attribute.rs | 4 +- common/src/attributes/tests.rs | 4 +- common/src/brain_trait_metrics/diagnostic.rs | 14 +-- common/src/brain_trait_metrics/evaluation.rs | 24 ++-- common/src/brain_trait_metrics/item.rs | 6 +- common/src/brain_trait_metrics/metrics.rs | 63 +++------- common/src/brain_trait_metrics/tests.rs | 10 +- .../cognitive_complexity.rs | 18 +-- common/src/brain_type_metrics/diagnostic.rs | 67 +++++----- common/src/brain_type_metrics/evaluation.rs | 30 ++--- .../src/brain_type_metrics/foreign_reach.rs | 2 +- common/src/brain_type_metrics/mod.rs | 22 ++-- common/src/brain_type_metrics/tests.rs | 10 +- common/src/complexity_signal.rs | 80 +++++++++--- common/src/context.rs | 2 +- common/src/decomposition_advice/community.rs | 116 ++++++++++------- common/src/decomposition_advice/note.rs | 18 +-- common/src/decomposition_advice/profile.rs | 10 +- common/src/decomposition_advice/suggestion.rs | 47 +++---- common/src/decomposition_advice/vector.rs | 24 ++-- common/src/diagnostics.rs | 1 - common/src/dylint_entry.rs | 47 +++++++ common/src/expr.rs | 8 +- common/src/i18n/diagnostics.rs | 23 +++- common/src/i18n/helpers.rs | 6 +- common/src/i18n/loader.rs | 78 ++++++++---- common/src/i18n/locales.rs | 18 +-- common/src/i18n/mod.rs | 4 + common/src/i18n/selection.rs | 19 ++- common/src/i18n/tests.rs | 6 +- common/src/lcom4/extract.rs | 13 +- common/src/lcom4/mod.rs | 44 ++++--- common/src/lcom4/tests.rs | 8 +- common/src/lib.rs | 1 + common/src/path.rs | 8 +- common/src/rstest/argument_fingerprint.rs | 15 ++- common/src/rstest/detection.rs | 4 +- common/src/rstest/paragraph_fingerprint.rs | 28 ++++- common/src/rstest/parameter.rs | 9 +- common/src/span.rs | 13 +- common/src/test_support/mod.rs | 87 +------------ crates/bumpy_road_function/src/analysis.rs | 4 +- .../bumpy_road_function/src/driver/config.rs | 2 +- .../src/driver/diagnostic.rs | 13 +- crates/bumpy_road_function/src/driver/mod.rs | 14 ++- .../src/driver/segment_builder.rs | 4 +- crates/clippy_utils/src/lib.rs | 6 +- .../conditional_max_n_branches/src/driver.rs | 2 +- .../function_attrs_follow_docs/src/driver.rs | 24 ++-- crates/module_max_lines/src/driver.rs | 11 +- .../src/driver/mod.rs | 10 +- .../src/driver/parser.rs | 10 +- .../fail_expect_in_rstest_non_test_module.rs | 4 +- .../src/context/mod.rs | 52 ++++---- .../src/context/tests.rs | 2 +- .../src/diagnostics.rs | 35 +++--- .../no_expect_outside_tests/src/driver/mod.rs | 4 +- .../src/driver/tests.rs | 2 +- crates/no_std_fs_operations/src/behaviour.rs | 4 +- .../no_std_fs_operations/src/diagnostics.rs | 17 ++- crates/no_std_fs_operations/src/driver.rs | 2 +- crates/no_std_fs_operations/src/exclusion.rs | 2 +- .../src/exclusion_behaviour.rs | 2 +- crates/no_std_fs_operations/src/usage/mod.rs | 6 +- .../no_std_fs_operations/src/usage/tests.rs | 2 +- .../src/context/mod.rs | 37 ++---- .../src/context/tests.rs | 2 +- .../src/diagnostics.rs | 6 +- crates/no_unwrap_or_else_panic/src/driver.rs | 10 +- .../src/panic_detector.rs | 14 +-- crates/no_unwrap_or_else_panic/src/policy.rs | 2 +- .../src/tests/behaviour.rs | 2 +- .../examples/collection_zero_diagnostic.rs | 2 +- .../src/collector.rs | 13 +- .../src/driver.rs | 6 +- .../src/visitor.rs | 6 +- .../src/behaviour.rs | 2 +- .../test_must_not_have_example/src/driver.rs | 17 ++- crates/whitaker_clones_core/build_support.rs | 7 +- .../whitaker_clones_core/src/ast/features.rs | 6 +- crates/whitaker_clones_core/src/ast/hash.rs | 27 ++-- .../whitaker_clones_core/src/ast/lowering.rs | 17 ++- crates/whitaker_clones_core/src/ast/tree.rs | 16 ++- crates/whitaker_clones_core/src/hashing.rs | 19 ++- .../whitaker_clones_core/src/index/error.rs | 8 +- .../src/index/fragment_id.rs | 2 +- crates/whitaker_clones_core/src/index/kani.rs | 2 +- crates/whitaker_clones_core/src/index/lsh.rs | 4 +- .../whitaker_clones_core/src/index/minhash.rs | 38 +++--- crates/whitaker_clones_core/src/index/mod.rs | 2 +- .../whitaker_clones_core/src/index/tests.rs | 8 +- .../whitaker_clones_core/src/index/types.rs | 25 ++-- crates/whitaker_clones_core/src/lib.rs | 2 +- crates/whitaker_clones_core/src/run0/emit.rs | 23 +++- crates/whitaker_clones_core/src/run0/score.rs | 12 +- crates/whitaker_clones_core/src/run0/span.rs | 18 ++- .../src/run0/test_helpers.rs | 2 +- crates/whitaker_clones_core/src/run0/tests.rs | 34 ++--- .../src/run0/tests_emit.rs | 4 +- crates/whitaker_clones_core/src/run0/types.rs | 10 +- .../src/token/fingerprint.rs | 37 +++--- .../src/token/normalize.rs | 35 +++--- .../whitaker_clones_core/src/token/tests.rs | 21 +++- .../whitaker_clones_core/src/token/types.rs | 14 +-- .../src/builders/location_builder.rs | 16 +-- .../src/builders/result_builder.rs | 2 +- crates/whitaker_sarif/src/merge.rs | 2 +- crates/whitaker_sarif/src/test_support.rs | 8 +- .../whitaker_sarif/src/whitaker_properties.rs | 34 ++--- docs/developers-guide.md | 47 ++++--- src/testing/ui/mod.rs | 117 +++++------------- src/testing/ui/tests.rs | 43 ++++--- suite/Cargo.toml | 1 + suite/src/driver.rs | 15 +-- 115 files changed, 1092 insertions(+), 951 deletions(-) create mode 100644 common/src/dylint_entry.rs diff --git a/Cargo.lock b/Cargo.lock index 577a87cc8..cdb64b733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3761,6 +3761,7 @@ dependencies = [ "rustc_session", "rustc_span", "test_must_not_have_example", + "whitaker-common", "whitaker_test_macros", ] diff --git a/common/src/attributes/attribute.rs b/common/src/attributes/attribute.rs index 5284d7113..b1d25e4c7 100644 --- a/common/src/attributes/attribute.rs +++ b/common/src/attributes/attribute.rs @@ -22,7 +22,7 @@ impl Attribute { /// assert!(attribute.is_outer()); /// ``` #[must_use] - pub fn new(path: AttributePath, kind: AttributeKind) -> Self { + pub const fn new(path: AttributePath, kind: AttributeKind) -> Self { Self { path, kind, @@ -87,7 +87,7 @@ impl Attribute { /// assert!(attribute.path().is_doc()); /// ``` #[must_use] - pub fn path(&self) -> &AttributePath { + pub const fn path(&self) -> &AttributePath { &self.path } diff --git a/common/src/attributes/tests.rs b/common/src/attributes/tests.rs index 376e818a3..bdf17cd8b 100644 --- a/common/src/attributes/tests.rs +++ b/common/src/attributes/tests.rs @@ -5,8 +5,8 @@ use rstest::rstest; #[rstest] #[case::empty(Vec::::new())] -#[case::single(vec!["dead_code".to_string()])] -#[case::complex(vec!["cfg(feature = \"test\")".to_string(), "path(\"std::io\")".to_string()])] +#[case::single(vec!["dead_code".to_owned()])] +#[case::complex(vec!["cfg(feature = \"test\")".to_owned(), "path(\"std::io\")".to_owned()])] fn attribute_with_arguments_preserves_inputs(#[case] arguments: Vec) { let attribute = Attribute::with_arguments( AttributePath::from("allow"), diff --git a/common/src/brain_trait_metrics/diagnostic.rs b/common/src/brain_trait_metrics/diagnostic.rs index 00ca41510..5bb9a0407 100644 --- a/common/src/brain_trait_metrics/diagnostic.rs +++ b/common/src/brain_trait_metrics/diagnostic.rs @@ -72,44 +72,44 @@ impl BrainTraitDiagnostic { /// Returns the evaluation disposition. #[must_use] - pub fn disposition(&self) -> BrainTraitDisposition { + pub const fn disposition(&self) -> BrainTraitDisposition { self.disposition } /// Returns the number of required methods. #[must_use] - pub fn required_method_count(&self) -> usize { + pub const fn required_method_count(&self) -> usize { self.required_method_count } /// Returns the number of default methods. #[must_use] - pub fn default_method_count(&self) -> usize { + pub const fn default_method_count(&self) -> usize { self.default_method_count } /// Returns the total method count (required + default). #[must_use] - pub fn total_method_count(&self) -> usize { + pub const fn total_method_count(&self) -> usize { self.required_method_count + self.default_method_count } /// Returns the sum of default method cognitive complexity values. #[must_use] - pub fn default_method_cc_sum(&self) -> usize { + pub const fn default_method_cc_sum(&self) -> usize { self.default_method_cc_sum } /// Returns the total number of trait items (methods + associated /// types + associated consts). #[must_use] - pub fn total_item_count(&self) -> usize { + pub const fn total_item_count(&self) -> usize { self.total_item_count } /// Returns implementor burden (required method count). #[must_use] - pub fn implementor_burden(&self) -> usize { + pub const fn implementor_burden(&self) -> usize { self.implementor_burden } } diff --git a/common/src/brain_trait_metrics/evaluation.rs b/common/src/brain_trait_metrics/evaluation.rs index fa887ff22..992ac8490 100644 --- a/common/src/brain_trait_metrics/evaluation.rs +++ b/common/src/brain_trait_metrics/evaluation.rs @@ -84,20 +84,20 @@ impl BrainTraitThresholds { /// Total method count at or above which the warn rule's method /// condition is met. #[must_use] - pub fn methods_warn(&self) -> usize { + pub const fn methods_warn(&self) -> usize { self.methods_warn } /// Total method count at or above which the deny rule triggers. #[must_use] - pub fn methods_deny(&self) -> usize { + pub const fn methods_deny(&self) -> usize { self.methods_deny } /// Default method CC sum at or above which the warn rule's /// complexity condition is met. #[must_use] - pub fn default_cc_warn(&self) -> usize { + pub const fn default_cc_warn(&self) -> usize { self.default_cc_warn } } @@ -138,7 +138,7 @@ pub struct BrainTraitThresholdsBuilder { impl BrainTraitThresholdsBuilder { /// Creates a builder with all thresholds set to their defaults. #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self { methods_warn: DEFAULT_METHODS_WARN, methods_deny: DEFAULT_METHODS_DENY, @@ -148,28 +148,28 @@ impl BrainTraitThresholdsBuilder { /// Sets the method count warn threshold. #[must_use] - pub fn methods_warn(mut self, value: usize) -> Self { + pub const fn methods_warn(mut self, value: usize) -> Self { self.methods_warn = value; self } /// Sets the method count deny threshold. #[must_use] - pub fn methods_deny(mut self, value: usize) -> Self { + pub const fn methods_deny(mut self, value: usize) -> Self { self.methods_deny = value; self } /// Sets the default method CC sum warn threshold. #[must_use] - pub fn default_cc_warn(mut self, value: usize) -> Self { + pub const fn default_cc_warn(mut self, value: usize) -> Self { self.default_cc_warn = value; self } /// Consumes the builder and returns the completed thresholds. #[must_use] - pub fn build(self) -> BrainTraitThresholds { + pub const fn build(self) -> BrainTraitThresholds { BrainTraitThresholds { methods_warn: self.methods_warn, methods_deny: self.methods_deny, @@ -190,19 +190,19 @@ impl Default for BrainTraitThresholdsBuilder { /// Computes total method count (required + default), excluding /// associated types and consts. -fn total_method_count(metrics: &TraitMetrics) -> usize { +const fn total_method_count(metrics: &TraitMetrics) -> usize { metrics.required_method_count() + metrics.default_method_count() } /// Returns `true` when the deny condition holds (OR-based). #[must_use] -fn is_deny_triggered(metrics: &TraitMetrics, thresholds: &BrainTraitThresholds) -> bool { +const fn is_deny_triggered(metrics: &TraitMetrics, thresholds: &BrainTraitThresholds) -> bool { total_method_count(metrics) >= thresholds.methods_deny } /// Returns `true` when all warn conditions hold simultaneously (AND-based). #[must_use] -fn is_warn_triggered(metrics: &TraitMetrics, thresholds: &BrainTraitThresholds) -> bool { +const fn is_warn_triggered(metrics: &TraitMetrics, thresholds: &BrainTraitThresholds) -> bool { total_method_count(metrics) >= thresholds.methods_warn && metrics.default_method_cc_sum() >= thresholds.default_cc_warn } @@ -233,7 +233,7 @@ fn is_warn_triggered(metrics: &TraitMetrics, thresholds: &BrainTraitThresholds) /// ); /// ``` #[must_use] -pub fn evaluate_brain_trait( +pub const fn evaluate_brain_trait( metrics: &TraitMetrics, thresholds: &BrainTraitThresholds, ) -> BrainTraitDisposition { diff --git a/common/src/brain_trait_metrics/item.rs b/common/src/brain_trait_metrics/item.rs index eff9fb770..a592ed798 100644 --- a/common/src/brain_trait_metrics/item.rs +++ b/common/src/brain_trait_metrics/item.rs @@ -147,7 +147,7 @@ impl TraitItemMetrics { /// assert_eq!(item.kind(), TraitItemKind::RequiredMethod); /// ``` #[must_use] - pub fn kind(&self) -> TraitItemKind { + pub const fn kind(&self) -> TraitItemKind { self.kind } @@ -162,7 +162,7 @@ impl TraitItemMetrics { /// assert_eq!(item.default_method_cc(), Some(9)); /// ``` #[must_use] - pub fn default_method_cc(&self) -> Option { + pub const fn default_method_cc(&self) -> Option { self.default_method_cc } @@ -211,7 +211,7 @@ impl TraitItemMetrics { /// assert_eq!(trait_item_count(&items), 2); /// ``` #[must_use] -pub fn trait_item_count(items: &[TraitItemMetrics]) -> usize { +pub const fn trait_item_count(items: &[TraitItemMetrics]) -> usize { items.len() } diff --git a/common/src/brain_trait_metrics/metrics.rs b/common/src/brain_trait_metrics/metrics.rs index 62489260e..b1fd8e780 100644 --- a/common/src/brain_trait_metrics/metrics.rs +++ b/common/src/brain_trait_metrics/metrics.rs @@ -56,7 +56,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().total_item_count(), 1); /// ``` #[must_use] - pub fn total_item_count(&self) -> usize { + pub const fn total_item_count(&self) -> usize { self.total_item_count } @@ -72,7 +72,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().required_method_count(), 1); /// ``` #[must_use] - pub fn required_method_count(&self) -> usize { + pub const fn required_method_count(&self) -> usize { self.required_method_count } @@ -88,7 +88,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().default_method_count(), 1); /// ``` #[must_use] - pub fn default_method_count(&self) -> usize { + pub const fn default_method_count(&self) -> usize { self.default_method_count } @@ -105,7 +105,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().default_method_cc_sum(), 12); /// ``` #[must_use] - pub fn default_method_cc_sum(&self) -> usize { + pub const fn default_method_cc_sum(&self) -> usize { self.default_method_cc_sum } @@ -122,7 +122,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().implementor_burden(), 2); /// ``` #[must_use] - pub fn implementor_burden(&self) -> usize { + pub const fn implementor_burden(&self) -> usize { self.required_method_count } } @@ -255,7 +255,7 @@ impl TraitMetricsBuilder { /// assert!(TraitMetricsBuilder::new("Parser").is_empty()); /// ``` #[must_use] - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.items.is_empty() } @@ -277,44 +277,21 @@ impl TraitMetricsBuilder { /// ``` #[must_use] pub fn build(self) -> TraitMetrics { - let (total_item_count, required_method_count, default_method_count, default_method_cc_sum) = - self.items.iter().fold( - (0, 0, 0, 0), - |( - total_item_count, - required_method_count, - default_method_count, - default_method_cc_sum, - ), - item| { - let total_item_count = total_item_count + 1; - let (required_method_count, default_method_count, default_method_cc_sum) = - match item.kind() { - TraitItemKind::RequiredMethod => ( - required_method_count + 1, - default_method_count, - default_method_cc_sum, - ), - TraitItemKind::DefaultMethod => ( - required_method_count, - default_method_count + 1, - default_method_cc_sum + item.default_method_cc().unwrap_or(0), - ), - TraitItemKind::AssociatedType | TraitItemKind::AssociatedConst => ( - required_method_count, - default_method_count, - default_method_cc_sum, - ), - }; + let total_item_count = self.items.len(); + let mut required_method_count = 0; + let mut default_method_count = 0; + let mut default_method_cc_sum = 0; - ( - total_item_count, - required_method_count, - default_method_count, - default_method_cc_sum, - ) - }, - ); + for item in &self.items { + match item.kind() { + TraitItemKind::RequiredMethod => required_method_count += 1, + TraitItemKind::DefaultMethod => { + default_method_count += 1; + default_method_cc_sum += item.default_method_cc().unwrap_or(0); + } + TraitItemKind::AssociatedType | TraitItemKind::AssociatedConst => {} + } + } TraitMetrics { trait_name: self.trait_name, diff --git a/common/src/brain_trait_metrics/tests.rs b/common/src/brain_trait_metrics/tests.rs index e0e5965b2..f1890d7de 100644 --- a/common/src/brain_trait_metrics/tests.rs +++ b/common/src/brain_trait_metrics/tests.rs @@ -28,7 +28,7 @@ struct ExpectedTraitMetrics { burden: usize, } -fn assert_trait_metrics(metrics: &TraitMetrics, expected: ExpectedTraitMetrics) { +fn assert_trait_metrics(metrics: &TraitMetrics, expected: &ExpectedTraitMetrics) { assert_eq!(metrics.trait_name(), expected.name); assert_eq!(metrics.total_item_count(), expected.total); assert_eq!(metrics.required_method_count(), expected.required); @@ -132,7 +132,7 @@ fn builder_builds_mixed_trait_metrics() { assert_trait_metrics( &metrics, - ExpectedTraitMetrics { + &ExpectedTraitMetrics { name: "Parser", total: 4, required: 1, @@ -153,7 +153,7 @@ fn builder_add_item_supports_prebuilt_entries() { assert_trait_metrics( &metrics, - ExpectedTraitMetrics { + &ExpectedTraitMetrics { name: "Renderer", total: 2, required: 1, @@ -175,7 +175,7 @@ fn builder_filters_macro_expanded_default_methods() { assert_trait_metrics( &metrics, - ExpectedTraitMetrics { + &ExpectedTraitMetrics { name: "Parser", total: 2, required: 1, @@ -206,7 +206,7 @@ fn empty_trait_has_zeroed_metrics() { assert_trait_metrics( &metrics, - ExpectedTraitMetrics { + &ExpectedTraitMetrics { name: "EmptyTrait", total: 0, required: 0, diff --git a/common/src/brain_type_metrics/cognitive_complexity.rs b/common/src/brain_type_metrics/cognitive_complexity.rs index 117de893c..bb6c842c4 100644 --- a/common/src/brain_type_metrics/cognitive_complexity.rs +++ b/common/src/brain_type_metrics/cognitive_complexity.rs @@ -18,7 +18,7 @@ #[path = "cognitive_complexity_tests.rs"] mod tests; -/// Incrementally computes cognitive complexity following SonarSource +/// Incrementally computes cognitive complexity following `SonarSource` /// rules, with macro-expansion filtering. /// /// The HIR walker calls builder methods for each relevant node, @@ -30,7 +30,7 @@ mod tests; /// /// - **Structural** (+1): `if`, `else if`, `else`, `match`, `for`, /// `while`, `loop`, `?` operator, catch-equivalent constructs. -/// - **Nesting** (+effective_depth): applied alongside structural for +/// - **Nesting** (+`effective_depth)`: applied alongside structural for /// constructs that also incur a nesting penalty. /// - **Fundamental** (+1): boolean operator sequence breaks (`&&`, /// `||`). @@ -77,7 +77,7 @@ impl CognitiveComplexityBuilder { /// assert_eq!(cc.effective_depth(), 0); /// ``` #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self { score: 0, nesting_stack: Vec::new(), @@ -104,13 +104,13 @@ impl CognitiveComplexityBuilder { /// cc.record_structural_increment(true); // macro — filtered /// assert_eq!(cc.score(), 1); /// ``` - pub fn record_structural_increment(&mut self, is_from_expansion: bool) { + pub const fn record_structural_increment(&mut self, is_from_expansion: bool) { if !is_from_expansion { self.score += 1; } } - /// Records a nesting increment (+effective_depth). + /// Records a nesting increment (+`effective_depth`). /// /// Called alongside [`record_structural_increment`](Self::record_structural_increment) /// for constructs that also incur a nesting penalty (e.g. `if`, @@ -130,7 +130,7 @@ impl CognitiveComplexityBuilder { /// cc.pop_nesting(); /// assert_eq!(cc.build(), 1); /// ``` - pub fn record_nesting_increment(&mut self, is_from_expansion: bool) { + pub const fn record_nesting_increment(&mut self, is_from_expansion: bool) { if !is_from_expansion { self.score += self.effective_depth; } @@ -155,7 +155,7 @@ impl CognitiveComplexityBuilder { /// cc.record_fundamental_increment(true); // macro — filtered /// assert_eq!(cc.score(), 1); /// ``` - pub fn record_fundamental_increment(&mut self, is_from_expansion: bool) { + pub const fn record_fundamental_increment(&mut self, is_from_expansion: bool) { if !is_from_expansion { self.score += 1; } @@ -224,7 +224,7 @@ impl CognitiveComplexityBuilder { /// cc.pop_nesting(); /// ``` #[must_use] - pub fn effective_depth(&self) -> usize { + pub const fn effective_depth(&self) -> usize { self.effective_depth } @@ -240,7 +240,7 @@ impl CognitiveComplexityBuilder { /// assert_eq!(cc.score(), 1); /// ``` #[must_use] - pub fn score(&self) -> usize { + pub const fn score(&self) -> usize { self.score } diff --git a/common/src/brain_type_metrics/diagnostic.rs b/common/src/brain_type_metrics/diagnostic.rs index 95f21bcb3..0008fccf4 100644 --- a/common/src/brain_type_metrics/diagnostic.rs +++ b/common/src/brain_type_metrics/diagnostic.rs @@ -7,8 +7,6 @@ //! See `docs/brain-trust-lints-design.md` §Diagnostic output for the //! full format specification. -use std::fmt::Write; - use super::evaluation::BrainTypeDisposition; use super::{MethodMetrics, TypeMetrics}; use crate::decomposition_advice::{ @@ -72,25 +70,25 @@ impl BrainTypeDiagnostic { /// Returns the evaluation disposition. #[must_use] - pub fn disposition(&self) -> BrainTypeDisposition { + pub const fn disposition(&self) -> BrainTypeDisposition { self.disposition } /// Returns the Weighted Methods Count. #[must_use] - pub fn wmc(&self) -> usize { + pub const fn wmc(&self) -> usize { self.wmc } /// Returns the LCOM4 connected component count. #[must_use] - pub fn lcom4(&self) -> usize { + pub const fn lcom4(&self) -> usize { self.lcom4 } /// Returns the foreign reach count. #[must_use] - pub fn foreign_reach(&self) -> usize { + pub const fn foreign_reach(&self) -> usize { self.foreign_reach } @@ -177,25 +175,22 @@ fn format_primary_with_many_brain_methods( let lcom4 = diagnostic.lcom4(); let fr_suffix = foreign_reach_suffix(diagnostic); let n = methods.len(); - let mut msg = format!( + let method_list = methods + .iter() + .map(|bm| { + format!( + "`{}` (CC={}, LOC={})", + bm.name(), + bm.cognitive_complexity(), + bm.lines_of_code(), + ) + }) + .collect::>() + .join(", "); + format!( "`{name}` has WMC={wmc}, LCOM4={lcom4}{fr_suffix}, \ - and {n} brain methods: ", - ); - for (i, bm) in methods.iter().enumerate() { - if i > 0 { - msg.push_str(", "); - } - // Write cannot fail on String. - let _ = write!( - msg, - "`{}` (CC={}, LOC={})", - bm.name(), - bm.cognitive_complexity(), - bm.lines_of_code(), - ); - } - msg.push('.'); - msg + and {n} brain methods: {method_list}.", + ) } /// Returns the foreign reach suffix for the primary message, or an @@ -229,24 +224,26 @@ fn foreign_reach_suffix(diagnostic: &BrainTypeDiagnostic) -> String { /// ``` #[must_use] pub fn format_note(diagnostic: &BrainTypeDiagnostic) -> String { - let mut note = String::from("WMC measures total cognitive complexity across all methods."); + let mut sentences = vec![String::from( + "WMC measures total cognitive complexity across all methods.", + )]; if !diagnostic.brain_methods().is_empty() { - note.push_str(" Brain methods are methods with high complexity and size."); + sentences.push(String::from( + "Brain methods are methods with high complexity and size.", + )); } if diagnostic.lcom4() >= 2 { - note.push_str( - " LCOM4 >= 2 indicates the type has multiple unrelated \ - responsibilities.", - ); + sentences.push(String::from( + "LCOM4 >= 2 indicates the type has multiple unrelated responsibilities.", + )); } if diagnostic.foreign_reach() > 0 { - let _ = write!( - note, - " Foreign reach of {} indicates coupling to external modules.", + sentences.push(format!( + "Foreign reach of {} indicates coupling to external modules.", diagnostic.foreign_reach(), - ); + )); } - note + sentences.join(" ") } /// Formats a decomposition note from precomputed community suggestions. diff --git a/common/src/brain_type_metrics/evaluation.rs b/common/src/brain_type_metrics/evaluation.rs index 79912dec9..bc708c00e 100644 --- a/common/src/brain_type_metrics/evaluation.rs +++ b/common/src/brain_type_metrics/evaluation.rs @@ -80,31 +80,31 @@ pub struct BrainTypeThresholds { impl BrainTypeThresholds { /// WMC at or above which the warn rule's WMC condition is met. #[must_use] - pub fn wmc_warn(&self) -> usize { + pub const fn wmc_warn(&self) -> usize { self.wmc_warn } /// WMC at or above which the deny rule triggers (OR-based). #[must_use] - pub fn wmc_deny(&self) -> usize { + pub const fn wmc_deny(&self) -> usize { self.wmc_deny } /// LCOM4 at or above which the warn rule's cohesion condition is met. #[must_use] - pub fn lcom4_warn(&self) -> usize { + pub const fn lcom4_warn(&self) -> usize { self.lcom4_warn } /// LCOM4 at or above which the deny rule triggers (OR-based). #[must_use] - pub fn lcom4_deny(&self) -> usize { + pub const fn lcom4_deny(&self) -> usize { self.lcom4_deny } /// Brain method count at or above which the deny rule triggers. #[must_use] - pub fn brain_method_deny_count(&self) -> usize { + pub const fn brain_method_deny_count(&self) -> usize { self.brain_method_deny_count } } @@ -149,7 +149,7 @@ pub struct BrainTypeThresholdsBuilder { impl BrainTypeThresholdsBuilder { /// Creates a builder with all thresholds set to their defaults. #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self { wmc_warn: DEFAULT_WMC_WARN, wmc_deny: DEFAULT_WMC_DENY, @@ -161,42 +161,42 @@ impl BrainTypeThresholdsBuilder { /// Sets the WMC warn threshold. #[must_use] - pub fn wmc_warn(mut self, value: usize) -> Self { + pub const fn wmc_warn(mut self, value: usize) -> Self { self.wmc_warn = value; self } /// Sets the WMC deny threshold. #[must_use] - pub fn wmc_deny(mut self, value: usize) -> Self { + pub const fn wmc_deny(mut self, value: usize) -> Self { self.wmc_deny = value; self } /// Sets the LCOM4 warn threshold. #[must_use] - pub fn lcom4_warn(mut self, value: usize) -> Self { + pub const fn lcom4_warn(mut self, value: usize) -> Self { self.lcom4_warn = value; self } /// Sets the LCOM4 deny threshold. #[must_use] - pub fn lcom4_deny(mut self, value: usize) -> Self { + pub const fn lcom4_deny(mut self, value: usize) -> Self { self.lcom4_deny = value; self } /// Sets the brain method count deny threshold. #[must_use] - pub fn brain_method_deny_count(mut self, value: usize) -> Self { + pub const fn brain_method_deny_count(mut self, value: usize) -> Self { self.brain_method_deny_count = value; self } /// Consumes the builder and returns the completed thresholds. #[must_use] - pub fn build(self) -> BrainTypeThresholds { + pub const fn build(self) -> BrainTypeThresholds { BrainTypeThresholds { wmc_warn: self.wmc_warn, wmc_deny: self.wmc_deny, @@ -219,7 +219,7 @@ impl Default for BrainTypeThresholdsBuilder { /// Returns `true` when any single deny condition holds (OR-based). #[must_use] -fn is_deny_triggered(metrics: &TypeMetrics, thresholds: &BrainTypeThresholds) -> bool { +const fn is_deny_triggered(metrics: &TypeMetrics, thresholds: &BrainTypeThresholds) -> bool { metrics.wmc() >= thresholds.wmc_deny || metrics.brain_method_count() >= thresholds.brain_method_deny_count || metrics.lcom4() >= thresholds.lcom4_deny @@ -227,7 +227,7 @@ fn is_deny_triggered(metrics: &TypeMetrics, thresholds: &BrainTypeThresholds) -> /// Returns `true` when all warn conditions hold simultaneously (AND-based). #[must_use] -fn is_warn_triggered(metrics: &TypeMetrics, thresholds: &BrainTypeThresholds) -> bool { +const fn is_warn_triggered(metrics: &TypeMetrics, thresholds: &BrainTypeThresholds) -> bool { metrics.wmc() >= thresholds.wmc_warn && metrics.brain_method_count() >= 1 && metrics.lcom4() >= thresholds.lcom4_warn @@ -257,7 +257,7 @@ fn is_warn_triggered(metrics: &TypeMetrics, thresholds: &BrainTypeThresholds) -> /// ); /// ``` #[must_use] -pub fn evaluate_brain_type( +pub const fn evaluate_brain_type( metrics: &TypeMetrics, thresholds: &BrainTypeThresholds, ) -> BrainTypeDisposition { diff --git a/common/src/brain_type_metrics/foreign_reach.rs b/common/src/brain_type_metrics/foreign_reach.rs index 16387bc7b..efff8dfb4 100644 --- a/common/src/brain_type_metrics/foreign_reach.rs +++ b/common/src/brain_type_metrics/foreign_reach.rs @@ -121,7 +121,7 @@ impl ForeignReferenceSet { /// assert!(refs.references().contains("std::io")); /// ``` #[must_use] - pub fn references(&self) -> &BTreeSet { + pub const fn references(&self) -> &BTreeSet { &self.references } } diff --git a/common/src/brain_type_metrics/mod.rs b/common/src/brain_type_metrics/mod.rs index e255feea9..bb4fc6a2c 100644 --- a/common/src/brain_type_metrics/mod.rs +++ b/common/src/brain_type_metrics/mod.rs @@ -81,13 +81,13 @@ impl MethodMetrics { /// Returns the cognitive complexity (CC) value. #[must_use] - pub fn cognitive_complexity(&self) -> usize { + pub const fn cognitive_complexity(&self) -> usize { self.cognitive_complexity } /// Returns the lines of code (LOC) count. #[must_use] - pub fn lines_of_code(&self) -> usize { + pub const fn lines_of_code(&self) -> usize { self.lines_of_code } @@ -106,7 +106,7 @@ impl MethodMetrics { /// assert!(!m.is_brain_method(25, 200)); /// ``` #[must_use] - pub fn is_brain_method(&self, cc_threshold: usize, loc_threshold: usize) -> bool { + pub const fn is_brain_method(&self, cc_threshold: usize, loc_threshold: usize) -> bool { self.cognitive_complexity >= cc_threshold && self.lines_of_code >= loc_threshold } } @@ -215,7 +215,7 @@ impl TypeMetrics { /// Weighted Methods Count (sum of CC across all methods). #[must_use] - pub fn wmc(&self) -> usize { + pub const fn wmc(&self) -> usize { self.wmc } @@ -229,30 +229,30 @@ impl TypeMetrics { /// /// Callers that need a collected `Vec` should use `.collect()`. pub fn brain_method_names(&self) -> impl Iterator { - self.brain_methods.iter().map(|m| m.name()) + self.brain_methods.iter().map(MethodMetrics::name) } /// Number of brain methods detected. #[must_use] - pub fn brain_method_count(&self) -> usize { + pub const fn brain_method_count(&self) -> usize { self.brain_methods.len() } /// LCOM4 connected component count (1 = cohesive, >= 2 = low cohesion). #[must_use] - pub fn lcom4(&self) -> usize { + pub const fn lcom4(&self) -> usize { self.lcom4 } /// Count of distinct external modules or types referenced. #[must_use] - pub fn foreign_reach(&self) -> usize { + pub const fn foreign_reach(&self) -> usize { self.foreign_reach } /// Total number of methods in the type. #[must_use] - pub fn method_count(&self) -> usize { + pub const fn method_count(&self) -> usize { self.method_count } } @@ -326,12 +326,12 @@ impl TypeMetricsBuilder { } /// Records the LCOM4 value (connected component count). - pub fn set_lcom4(&mut self, lcom4: usize) { + pub const fn set_lcom4(&mut self, lcom4: usize) { self.lcom4 = Some(lcom4); } /// Records the foreign reach count. - pub fn set_foreign_reach(&mut self, count: usize) { + pub const fn set_foreign_reach(&mut self, count: usize) { self.foreign_reach = Some(count); } diff --git a/common/src/brain_type_metrics/tests.rs b/common/src/brain_type_metrics/tests.rs index b994a33cd..75e3c066b 100644 --- a/common/src/brain_type_metrics/tests.rs +++ b/common/src/brain_type_metrics/tests.rs @@ -99,7 +99,10 @@ fn brain_methods_one_qualifying_method() { ]; let result = brain_methods(&methods, 25, 80); assert_eq!(result.len(), 1); - assert_eq!(result[0].name(), "parse"); + let first = result + .first() + .expect("expected exactly one qualifying brain method"); + assert_eq!(first.name(), "parse"); } #[rstest] @@ -110,9 +113,8 @@ fn brain_methods_multiple_qualifying_in_order() { MethodMetrics::new("gamma", 40, 200), ]; let result = brain_methods(&methods, 25, 80); - assert_eq!(result.len(), 2); - assert_eq!(result[0].name(), "alpha"); - assert_eq!(result[1].name(), "gamma"); + let names: Vec<&str> = result.iter().map(|method| method.name()).collect(); + assert_eq!(names, ["alpha", "gamma"]); } #[rstest] diff --git a/common/src/complexity_signal.rs b/common/src/complexity_signal.rs index 29d6caffd..835d36798 100644 --- a/common/src/complexity_signal.rs +++ b/common/src/complexity_signal.rs @@ -24,6 +24,11 @@ impl LineSegment { /// /// Line numbers are one-based and ranges are inclusive. /// + /// # Errors + /// + /// - Returns [`SegmentError::LineNumberMustBeOneBased`] when either line is `0`. + /// - Returns [`SegmentError::StartAfterEnd`] when `start_line` exceeds `end_line`. + /// /// # Examples /// /// ``` @@ -34,7 +39,7 @@ impl LineSegment { /// assert_eq!(segment.end_line(), 5); /// ``` #[must_use = "Inspect the segment creation result to handle invalid ranges"] - pub fn new(start_line: usize, end_line: usize, value: f64) -> Result { + pub const fn new(start_line: usize, end_line: usize, value: f64) -> Result { if start_line == 0 || end_line == 0 { return Err(SegmentError::LineNumberMustBeOneBased { start_line, @@ -80,13 +85,23 @@ impl LineSegment { pub enum SegmentError { /// Line numbers must be one-based (line 0 is invalid). #[error("line numbers must be one-based (got start_line={start_line}, end_line={end_line})")] - LineNumberMustBeOneBased { start_line: usize, end_line: usize }, + LineNumberMustBeOneBased { + /// One-based first line requested for the segment. + start_line: usize, + /// One-based last line requested for the segment. + end_line: usize, + }, /// The segment start must not occur after its end. #[error( "segment start must not occur after end (start_line={start_line}, end_line={end_line})" )] - StartAfterEnd { start_line: usize, end_line: usize }, + StartAfterEnd { + /// One-based first line requested for the segment. + start_line: usize, + /// One-based last line requested for the segment. + end_line: usize, + }, } /// Errors emitted when building a per-line signal. @@ -96,27 +111,41 @@ pub enum SignalBuildError { #[error( "function line range must be one-based (got start_line={start_line}, end_line={end_line})" )] - FunctionLineRangeMustBeOneBased { start_line: usize, end_line: usize }, + FunctionLineRangeMustBeOneBased { + /// One-based first line of the supplied function range. + start_line: usize, + /// One-based last line of the supplied function range. + end_line: usize, + }, /// The function start must not occur after its end. #[error( "function start must not occur after end (start_line={start_line}, end_line={end_line})" )] - FunctionStartAfterEnd { start_line: usize, end_line: usize }, + FunctionStartAfterEnd { + /// One-based first line of the supplied function range. + start_line: usize, + /// One-based last line of the supplied function range. + end_line: usize, + }, /// A segment does not intersect the function's line range. #[error( "segment lies outside function range (segment={segment_start}..={segment_end}, function={function_start}..={function_end})" )] SegmentOutsideFunctionRange { + /// One-based first line covered by the offending segment. segment_start: usize, + /// One-based last line covered by the offending segment. segment_end: usize, + /// One-based first line of the function range. function_start: usize, + /// One-based last line of the function range. function_end: usize, }, } -fn validate_function_range( +const fn validate_function_range( function_start: usize, function_end: usize, ) -> Result<(), SignalBuildError> { @@ -137,7 +166,7 @@ fn validate_function_range( Ok(()) } -fn validate_segment_in_range( +const fn validate_segment_in_range( segment: &LineSegment, function_start: usize, function_end: usize, @@ -154,6 +183,10 @@ fn validate_segment_in_range( Ok(()) } +#[expect( + clippy::float_arithmetic, + reason = "bumpy-road signal processing operates on user-configured floating-point weights" +)] fn apply_segment_to_diff(segment: &LineSegment, diff: &mut [f64], function_start: usize) { let segment_start = segment.start_line().saturating_sub(function_start); let segment_end = segment.end_line().saturating_sub(function_start); @@ -167,6 +200,10 @@ fn apply_segment_to_diff(segment: &LineSegment, diff: &mut [f64], function_start } } +#[expect( + clippy::float_arithmetic, + reason = "bumpy-road signal processing operates on user-configured floating-point weights" +)] fn accumulate_signal_from_diff(diff: &[f64], len: usize) -> Vec { let mut signal = Vec::with_capacity(len); let mut running = 0.0_f64; @@ -230,11 +267,17 @@ pub fn rasterize_signal( pub enum SmoothingError { /// The moving average window must be positive. #[error("smoothing window must be positive (got {window})")] - WindowMustBePositive { window: usize }, + WindowMustBePositive { + /// The rejected window size, in lines. + window: usize, + }, /// The moving average window must be odd so the average is centred. #[error("smoothing window must be odd (got {window})")] - WindowMustBeOdd { window: usize }, + WindowMustBeOdd { + /// The rejected window size, in lines. + window: usize, + }, } /// Applies a centred moving-average smoothing window. @@ -257,16 +300,16 @@ pub enum SmoothingError { /// assert_eq!(smoothed, vec![0.0, 1.0, 1.0, 1.0, 0.0]); /// ``` #[must_use = "Inspect the smoothing result to handle invalid window sizes"] +#[expect( + clippy::float_arithmetic, + reason = "bumpy-road signal processing operates on user-configured floating-point weights" +)] pub fn smooth_moving_average(signal: &[f64], window: usize) -> Result, SmoothingError> { if window == 0 { return Err(SmoothingError::WindowMustBePositive { window }); } - fn is_even(value: usize) -> bool { - (value & 1) == 0 - } - - if is_even(window) { + if (window & 1) == 0 { return Err(SmoothingError::WindowMustBeOdd { window }); } @@ -274,11 +317,11 @@ pub fn smooth_moving_average(signal: &[f64], window: usize) -> Result, return Ok(Vec::new()); } - let half_window = window / 2; + let half_window = window.div_euclid(2); let mut prefix = Vec::with_capacity(signal.len() + 1); prefix.push(0.0_f64); for &value in signal { - let next = prefix[prefix.len() - 1] + value; + let next = prefix.last().copied().unwrap_or(0.0) + value; prefix.push(next); } @@ -287,8 +330,9 @@ pub fn smooth_moving_average(signal: &[f64], window: usize) -> Result, for idx in 0..signal.len() { let start = idx.saturating_sub(half_window); let end = (idx + half_window).min(last_index); - let sum = prefix[end + 1] - prefix[start]; - let count = (end - start + 1) as f64; + let sum = + prefix.get(end + 1).copied().unwrap_or(0.0) - prefix.get(start).copied().unwrap_or(0.0); + let count = u32::try_from(end - start + 1).map_or_else(|_| f64::from(u32::MAX), f64::from); smoothed.push(sum / count); } diff --git a/common/src/context.rs b/common/src/context.rs index 2d7b7a845..43b33db64 100644 --- a/common/src/context.rs +++ b/common/src/context.rs @@ -89,7 +89,7 @@ impl ContextEntry { /// Returns a mutable reference to the attributes for in-place updates. #[must_use] - pub fn attributes_mut(&mut self) -> &mut Vec { + pub const fn attributes_mut(&mut self) -> &mut Vec { &mut self.attributes } diff --git a/common/src/decomposition_advice/community.rs b/common/src/decomposition_advice/community.rs index 92a890604..f2b15df9a 100644 --- a/common/src/decomposition_advice/community.rs +++ b/common/src/decomposition_advice/community.rs @@ -16,7 +16,7 @@ pub(crate) struct SimilarityEdge { impl SimilarityEdge { // Used by test_support::decomposition::adjacency_report and unit tests. - pub(crate) fn new(left: usize, right: usize, weight: u64) -> Self { + pub(crate) const fn new(left: usize, right: usize, weight: u64) -> Self { Self { left, right, @@ -25,17 +25,17 @@ impl SimilarityEdge { } #[cfg(test)] - pub(crate) fn left(&self) -> usize { + pub(crate) const fn left(&self) -> usize { self.left } #[cfg(test)] - pub(crate) fn right(&self) -> usize { + pub(crate) const fn right(&self) -> usize { self.right } #[cfg(test)] - pub(crate) fn weight(&self) -> u64 { + pub(crate) const fn weight(&self) -> u64 { self.weight } } @@ -43,18 +43,18 @@ impl SimilarityEdge { pub(crate) fn build_similarity_edges(vectors: &[MethodFeatureVector]) -> Vec { let mut edges = Vec::new(); - for left in 0..vectors.len() { - for right in (left + 1)..vectors.len() { + for (left, left_vector) in vectors.iter().enumerate() { + for (right, right_vector) in vectors.iter().enumerate().skip(left + 1) { if !cosine_threshold_met( - &vectors[left], - &vectors[right], + left_vector, + right_vector, MIN_COSINE_THRESHOLD_NUMERATOR_SQUARED, MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED, ) { continue; } - let weight = dot_product(vectors[left].weights(), vectors[right].weights()); + let weight = dot_product(left_vector.weights(), right_vector.weights()); if weight == 0 { continue; } @@ -95,20 +95,20 @@ pub(crate) fn detect_communities(vectors: &[MethodFeatureVector]) -> Vec> = groups.into_values().collect(); for community in &mut communities { - community.sort_by(|left, right| { - vectors[*left] - .method_name() - .cmp(vectors[*right].method_name()) - }); + community.sort_by(|left, right| method_name(*left).cmp(&method_name(*right))); } communities.sort_by(|left, right| { right.len().cmp(&left.len()).then_with(|| { - vectors[left[0]] - .method_name() - .cmp(vectors[right[0]].method_name()) + let left_name = left.first().and_then(|&node| method_name(node)); + let right_name = right.first().and_then(|&node| method_name(node)); + left_name.cmp(&right_name) }) }); communities @@ -137,9 +137,15 @@ pub(crate) fn build_adjacency( ) -> Vec> { let mut adjacency = vec![Vec::new(); node_count]; + // Edges referencing nodes outside `0..node_count` are ignored rather than + // panicking; validated callers never produce them. for edge in edges { - adjacency[edge.left].push((edge.right, edge.weight)); - adjacency[edge.right].push((edge.left, edge.weight)); + if let Some(neighbours) = adjacency.get_mut(edge.left) { + neighbours.push((edge.right, edge.weight)); + } + if let Some(neighbours) = adjacency.get_mut(edge.right) { + neighbours.push((edge.left, edge.weight)); + } } for neighbours in &mut adjacency { @@ -195,18 +201,7 @@ pub(crate) fn propagate_labels_report( for _ in 0..max_iterations { iteration_count += 1; - let mut changed = false; - - for &node in &active_nodes { - let Some(best_label) = best_neighbour_label(node, &labels, adjacency, vectors) else { - continue; - }; - - if best_label != labels[node] { - labels[node] = best_label; - changed = true; - } - } + let changed = run_propagation_pass(vectors, adjacency, &active_nodes, &mut labels); if !changed { log::debug!( @@ -234,13 +229,42 @@ pub(crate) fn propagate_labels_report( } } +/// Runs one label-propagation pass over the active nodes. +/// +/// Returns `true` when at least one node adopted a new label, which tells the +/// caller whether propagation has converged. +fn run_propagation_pass( + vectors: &[MethodFeatureVector], + adjacency: &[Vec<(usize, u64)>], + active_nodes: &[usize], + labels: &mut [usize], +) -> bool { + let mut changed = false; + + for &node in active_nodes { + let Some(best_label) = best_neighbour_label(node, labels, adjacency, vectors) else { + continue; + }; + + // Active nodes are adjacency indices, so the label slot always exists. + if let Some(label_slot) = labels.get_mut(node) + && *label_slot != best_label + { + *label_slot = best_label; + changed = true; + } + } + + changed +} + fn best_neighbour_label( node: usize, labels: &[usize], adjacency: &[Vec<(usize, u64)>], vectors: &[MethodFeatureVector], ) -> Option { - let neighbours = &adjacency[node]; + let neighbours = adjacency.get(node)?; if neighbours.is_empty() { return None; } @@ -249,7 +273,11 @@ fn best_neighbour_label( let mut best: Option<(usize, u64)> = None; for &(neighbour, weight) in neighbours { - let label = labels[neighbour]; + // Neighbour indices come from validated adjacency rows, so the label + // lookup never misses; skipping keeps the scan panic free regardless. + let Some(&label) = labels.get(neighbour) else { + continue; + }; let score = score_label(&mut scores, label, weight); if should_replace_best(best, label, score, vectors) { @@ -270,10 +298,8 @@ fn labels_are_stable( return true; } - match best_neighbour_label(node, labels, adjacency, vectors) { - Some(best_label) => labels[node] == best_label, - None => true, - } + best_neighbour_label(node, labels, adjacency, vectors) + .is_none_or(|best_label| labels.get(node) == Some(&best_label)) }) } @@ -294,14 +320,20 @@ fn should_replace_best( Some((best_label, best_score)) => { // Prefer higher score; on tie, pick the lexically earlier method // name and then the smaller label index to keep runs deterministic. - if candidate_score != best_score { - candidate_score > best_score - } else { - let candidate_name = vectors[candidate_label].method_name(); - let best_name = vectors[best_label].method_name(); + if candidate_score == best_score { + // Labels are node indices, so both lookups always succeed; the + // `Option` ordering (`None` first) is only a type-level guard. + let candidate_name = vectors + .get(candidate_label) + .map(MethodFeatureVector::method_name); + let best_name = vectors + .get(best_label) + .map(MethodFeatureVector::method_name); candidate_name < best_name || (candidate_name == best_name && candidate_label < best_label) + } else { + candidate_score > best_score } } } diff --git a/common/src/decomposition_advice/note.rs b/common/src/decomposition_advice/note.rs index 3f24bfa73..ad2664696 100644 --- a/common/src/decomposition_advice/note.rs +++ b/common/src/decomposition_advice/note.rs @@ -63,13 +63,17 @@ pub fn format_diagnostic_note( return None; } - let visible_suggestions = &suggestions[..suggestions.len().min(MAX_SUGGESTIONS)]; let mut lines = vec![format!( "Potential decomposition for `{}`:", context.subject_name() )]; - lines.extend(visible_suggestions.iter().map(render_suggestion_line)); + lines.extend( + suggestions + .iter() + .take(MAX_SUGGESTIONS) + .map(render_suggestion_line), + ); let omitted_suggestions = suggestions.len().saturating_sub(MAX_SUGGESTIONS); if omitted_suggestions > 0 { @@ -99,17 +103,17 @@ fn render_suggestion_line(suggestion: &DecompositionSuggestion) -> String { } fn render_method_list(methods: &[String]) -> String { - let visible_methods = &methods[..methods.len().min(MAX_METHODS_PER_SUGGESTION)]; - let mut rendered = visible_methods + let rendered = methods .iter() + .take(MAX_METHODS_PER_SUGGESTION) .map(|method| format!("`{method}`")) .collect::>() .join(", "); let omitted_methods = methods.len().saturating_sub(MAX_METHODS_PER_SUGGESTION); if omitted_methods > 0 { - rendered.push_str(&format!(", +{omitted_methods} more methods")); + format!("{rendered}, +{omitted_methods} more methods") + } else { + rendered } - - rendered } diff --git a/common/src/decomposition_advice/profile.rs b/common/src/decomposition_advice/profile.rs index 65d6bc89e..1a304c04a 100644 --- a/common/src/decomposition_advice/profile.rs +++ b/common/src/decomposition_advice/profile.rs @@ -67,7 +67,7 @@ impl DecompositionContext { /// Returns the analysed subject kind. #[must_use] - pub fn subject_kind(&self) -> SubjectKind { + pub const fn subject_kind(&self) -> SubjectKind { self.subject_kind } } @@ -112,25 +112,25 @@ impl MethodProfile { /// Returns accessed fields. #[must_use] - pub fn accessed_fields(&self) -> &BTreeSet { + pub const fn accessed_fields(&self) -> &BTreeSet { &self.accessed_fields } /// Returns types used in the method signature. #[must_use] - pub fn signature_types(&self) -> &BTreeSet { + pub const fn signature_types(&self) -> &BTreeSet { &self.signature_types } /// Returns types used in local variables. #[must_use] - pub fn local_types(&self) -> &BTreeSet { + pub const fn local_types(&self) -> &BTreeSet { &self.local_types } /// Returns external domains used by the method. #[must_use] - pub fn external_domains(&self) -> &BTreeSet { + pub const fn external_domains(&self) -> &BTreeSet { &self.external_domains } } diff --git a/common/src/decomposition_advice/suggestion.rs b/common/src/decomposition_advice/suggestion.rs index 32f75910f..255380979 100644 --- a/common/src/decomposition_advice/suggestion.rs +++ b/common/src/decomposition_advice/suggestion.rs @@ -100,7 +100,7 @@ impl DecompositionSuggestion { /// Returns the suggested extraction kind. #[must_use] - pub fn extraction_kind(&self) -> SuggestedExtractionKind { + pub const fn extraction_kind(&self) -> SuggestedExtractionKind { self.extraction_kind } @@ -197,9 +197,12 @@ fn build_suggestion( Some(DecompositionSuggestion { label: label_feature.display.clone(), extraction_kind: infer_extraction_kind(context.subject_kind(), label_feature.category), + // Community indices always come from `0..vectors.len()`, so the + // filter never drops a method; it only keeps the lookup panic free. methods: community .iter() - .map(|index| vectors[*index].method_name().to_owned()) + .filter_map(|&index| vectors.get(index)) + .map(|vector| vector.method_name().to_owned()) .collect(), rationale, }) @@ -211,9 +214,16 @@ fn aggregate_features( ) -> Vec { let mut aggregated: BTreeMap = BTreeMap::new(); - for method_index in community { - for (feature_key, weight) in vectors[*method_index].weights() { - let metadata = &vectors[*method_index].metadata()[feature_key]; + for &method_index in community { + let Some(vector) = vectors.get(method_index) else { + continue; + }; + for (feature_key, weight) in vector.weights() { + // Weights and metadata are built together, so the metadata entry + // always exists for a weighted feature key. + let Some(metadata) = vector.metadata().get(feature_key) else { + continue; + }; let entry = aggregated .entry(feature_key.clone()) @@ -238,26 +248,17 @@ fn choose_label_feature(features: &[AggregatedFeature]) -> Option<&AggregatedFea FeatureCategory::LocalType, ]; - for category in LABEL_PRIORITIES { - let mut matches: Vec<_> = features + LABEL_PRIORITIES.iter().find_map(|category| { + features .iter() .filter(|feature| feature.category == *category) - .collect(); - - if matches.is_empty() { - continue; - } - - matches.sort_by(|left, right| { - right - .score - .cmp(&left.score) - .then_with(|| left.display.cmp(&right.display)) - }); - return Some(matches[0]); - } - - None + .min_by(|left, right| { + right + .score + .cmp(&left.score) + .then_with(|| left.display.cmp(&right.display)) + }) + }) } fn choose_rationale(features: &[AggregatedFeature]) -> Vec { diff --git a/common/src/decomposition_advice/vector.rs b/common/src/decomposition_advice/vector.rs index 4db3c7f03..3122b326d 100644 --- a/common/src/decomposition_advice/vector.rs +++ b/common/src/decomposition_advice/vector.rs @@ -27,7 +27,7 @@ pub(crate) enum FeatureCategory { } impl FeatureCategory { - fn prefix(self) -> &'static str { + const fn prefix(self) -> &'static str { match self { Self::Domain => "domain", Self::Field => "field", @@ -37,7 +37,7 @@ impl FeatureCategory { } } - fn weight(self) -> u64 { + const fn weight(self) -> u64 { match self { Self::Domain => DOMAIN_WEIGHT, Self::Field => FIELD_WEIGHT, @@ -47,7 +47,7 @@ impl FeatureCategory { } } - pub(crate) fn label_priority(self) -> usize { + pub(crate) const fn label_priority(self) -> usize { match self { Self::Domain => 0, Self::Field => 1, @@ -70,7 +70,7 @@ struct FeatureIdentity { } impl FeatureMetadata { - pub(crate) fn category(&self) -> FeatureCategory { + pub(crate) const fn category(&self) -> FeatureCategory { self.category } @@ -91,11 +91,11 @@ impl MethodFeatureVector { &self.method_name } - pub(crate) fn weights(&self) -> &BTreeMap { + pub(crate) const fn weights(&self) -> &BTreeMap { &self.weights } - pub(crate) fn metadata(&self) -> &BTreeMap { + pub(crate) const fn metadata(&self) -> &BTreeMap { &self.metadata } @@ -342,16 +342,20 @@ fn type_identity(type_name: &str) -> FeatureIdentity { } fn should_split_before(chars: &[char], index: usize) -> bool { - if index == 0 { + let Some(previous_index) = index.checked_sub(1) else { return false; - } + }; - let current = chars[index]; + let Some(¤t) = chars.get(index) else { + return false; + }; if !current.is_uppercase() { return false; } - let previous = chars[index - 1]; + let Some(&previous) = chars.get(previous_index) else { + return false; + }; previous.is_lowercase() || chars .get(index + 1) diff --git a/common/src/diagnostics.rs b/common/src/diagnostics.rs index 530cdd967..aeadc3aa7 100644 --- a/common/src/diagnostics.rs +++ b/common/src/diagnostics.rs @@ -1,5 +1,4 @@ //! Ergonomic builders for lint diagnostics and suggestions. -#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))] use crate::span::SourceSpan; diff --git a/common/src/dylint_entry.rs b/common/src/dylint_entry.rs new file mode 100644 index 000000000..c3e18f02a --- /dev/null +++ b/common/src/dylint_entry.rs @@ -0,0 +1,47 @@ +//! Macro support for declaring Dylint library entry points. +//! +//! Dylint locates a library's lints by calling an exported `register_lints` +//! symbol, which requires `#[unsafe(no_mangle)]`. Libraries registering more +//! than one lint cannot use `dylint_linting`'s single-lint macros, and the +//! workspace forbids in-crate `unsafe` code, so this macro provides the same +//! externally-expanded escape hatch those macros rely on: the unsafe +//! attribute originates in this crate's macro definition, keeping consumer +//! crates free of unsafe tokens. +//! +//! Scope and reuse policy: intended solely for Whitaker Dylint driver crates +//! (currently `whitaker_suite`) that must export a combined `register_lints` +//! entry point. It must not be used outside Dylint entry-point wiring. + +/// Declares the `register_lints` entry point Dylint resolves from a lint +/// library. +/// +/// The single argument is a path to a function with the signature +/// `fn(&rustc_session::Session, &mut rustc_lint::LintStore)`; it is invoked +/// with the compiler session and lint store whenever Dylint loads the +/// library. The expansion resolves `rustc_session` and `rustc_lint` at the +/// call site, so the invoking crate must have those crates as dependencies. +/// +/// # Examples +/// +/// ```ignore +/// fn register_entry(sess: &rustc_session::Session, store: &mut rustc_lint::LintStore) { +/// dylint_linting::init_config(sess); +/// store.register_lints(MY_LINT_DECLS); +/// } +/// +/// whitaker_common::declare_dylint_register_entry!(register_entry); +/// ``` +#[macro_export] +macro_rules! declare_dylint_register_entry { + ($register:path) => { + /// Dylint entry point that forwards to the library's registration + /// function. + #[unsafe(no_mangle)] + pub fn register_lints( + sess: &rustc_session::Session, + lint_store: &mut rustc_lint::LintStore, + ) { + $register(sess, lint_store); + } + }; +} diff --git a/common/src/expr.rs b/common/src/expr.rs index 88c6aa115..23a3dd531 100644 --- a/common/src/expr.rs +++ b/common/src/expr.rs @@ -1,5 +1,4 @@ //! Lightweight expression helpers for lint analysis. -#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))] use crate::path::SimplePath; @@ -7,7 +6,10 @@ use crate::path::SimplePath; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Expr { /// A call expression with a resolved callee path. - Call { callee: SimplePath }, + Call { + /// The resolved path of the function being called. + callee: SimplePath, + }, /// A path expression. Path(SimplePath), /// Any other literal expression (placeholder for expansion). @@ -29,7 +31,7 @@ pub enum Expr { /// ); /// ``` #[must_use] -pub fn def_id_of_expr_callee(expr: &Expr) -> Option<&SimplePath> { +pub const fn def_id_of_expr_callee(expr: &Expr) -> Option<&SimplePath> { match expr { Expr::Call { callee } => Some(callee), _ => None, diff --git a/common/src/i18n/diagnostics.rs b/common/src/i18n/diagnostics.rs index c7eed48dd..219ecbc9f 100644 --- a/common/src/i18n/diagnostics.rs +++ b/common/src/i18n/diagnostics.rs @@ -33,7 +33,7 @@ impl<'a> MessageKey<'a> { } } -impl<'a> AsRef for MessageKey<'a> { +impl AsRef for MessageKey<'_> { fn as_ref(&self) -> &str { self.0 } @@ -57,7 +57,7 @@ impl<'a> AttrKey<'a> { } } -impl<'a> AsRef for AttrKey<'a> { +impl AsRef for AttrKey<'_> { fn as_ref(&self) -> &str { self.0 } @@ -72,9 +72,19 @@ impl fmt::Display for AttrKey<'_> { /// Lookup trait used by lint crates to resolve translated diagnostic strings. pub trait BundleLookup { /// Resolve the primary message for `key` using `args`. + /// + /// # Errors + /// + /// Returns [`I18nError::MissingMessage`] when `key` is not defined for the + /// resolved locale. fn message(&self, key: MessageKey<'_>, args: &Arguments<'_>) -> Result; /// Resolve an attribute message for `key.attribute` using `args`. + /// + /// # Errors + /// + /// Returns [`I18nError::MissingMessage`] when `key.attribute` is not + /// defined for the resolved locale. fn attribute( &self, key: MessageKey<'_>, @@ -121,7 +131,7 @@ impl DiagnosticMessageSet { /// ); /// assert_eq!(messages.primary(), "primary"); /// ``` - pub fn new(primary: String, note: String, help: String) -> Self { + pub const fn new(primary: String, note: String, help: String) -> Self { Self { primary, note, @@ -199,10 +209,15 @@ impl DiagnosticMessageSet { } } -/// Resolve the primary, note, and help messages for a lint diagnostic. const NOTE_ATTR: AttrKey<'static> = AttrKey::new("note"); const HELP_ATTR: AttrKey<'static> = AttrKey::new("help"); +/// Resolve the primary, note, and help messages for a lint diagnostic. +/// +/// # Errors +/// +/// Returns [`I18nError::MissingMessage`] when the primary message, its `note` +/// attribute, or its `help` attribute is not defined for the resolved locale. #[must_use = "Use the resolved localization messages when emitting diagnostics"] pub fn resolve_message_set( lookup: &impl BundleLookup, diff --git a/common/src/i18n/helpers.rs b/common/src/i18n/helpers.rs index 86f5401d2..88be8edd9 100644 --- a/common/src/i18n/helpers.rs +++ b/common/src/i18n/helpers.rs @@ -52,11 +52,7 @@ pub fn get_localizer_for_lint(lint_name: &str, configuration_locale: Option<&str /// ``` #[must_use] pub fn branch_phrase(locale: &str, branches: usize) -> String { - match locale - .split_once('-') - .map(|(lang, _)| lang) - .unwrap_or(locale) - { + match locale.split_once('-').map_or(locale, |(lang, _)| lang) { "cy" => welsh_branch_phrase(branches), "gd" => gaelic_branch_phrase(branches), _ => english_branch_phrase(branches), diff --git a/common/src/i18n/loader.rs b/common/src/i18n/loader.rs index 3b5cae030..af85a178e 100644 --- a/common/src/i18n/loader.rs +++ b/common/src/i18n/loader.rs @@ -14,7 +14,7 @@ use thiserror::Error; use super::locales::supports_locale; use super::{FALLBACK_LANGUAGE, FALLBACK_LITERAL, LOADER, LanguageIdentifier}; -/// HashMap wrapper used when passing Fluent arguments to lookups. +/// `HashMap` wrapper used when passing Fluent arguments to lookups. pub type Arguments<'a> = HashMap, FluentValue<'a>>; /// Error raised when localization data cannot satisfy a caller request. @@ -22,7 +22,12 @@ pub type Arguments<'a> = HashMap, FluentValue<'a>>; pub enum I18nError { /// Raised when the requested message slug is missing for the resolved locale. #[error("message `{key}` missing for locale `{locale}`")] - MissingMessage { key: String, locale: String }, + MissingMessage { + /// The Fluent message key (including any attribute suffix) that was requested. + key: String, + /// The locale tag the lookup was resolved against. + locale: String, + }, } /// Resolve localization messages for a specific locale. @@ -55,25 +60,26 @@ impl Localizer { #[must_use] pub fn new(locale: Option<&str>) -> Self { match locale { - Some(value) if supports_locale(value) => match LanguageIdentifier::from_str(value) { - Ok(identifier) => { - let language_tag = identifier.to_string(); - - Self { - language: identifier, - language_tag, - fallback_used: false, - } - } - Err(_) => Self::fallback(), - }, + Some(value) if supports_locale(value) => LanguageIdentifier::from_str(value) + .map_or_else( + |_| Self::fallback(), + |identifier| { + let language_tag = identifier.to_string(); + + Self { + language: identifier, + language_tag, + fallback_used: false, + } + }, + ), _ => Self::fallback(), } } /// Return the resolved locale identifier. #[must_use] - pub fn language(&self) -> &LanguageIdentifier { + pub const fn language(&self) -> &LanguageIdentifier { &self.language } @@ -85,26 +91,46 @@ impl Localizer { /// Whether the fallback locale was used. #[must_use] - pub fn used_fallback(&self) -> bool { + pub const fn used_fallback(&self) -> bool { self.fallback_used } /// Fetch the translated message for `key`. + /// + /// # Errors + /// + /// Returns [`I18nError::MissingMessage`] when `key` is not defined for the + /// resolved locale. pub fn message(&self, key: &str) -> Result { self.lookup(key, None, None) } /// Fetch the translated message with Fluent arguments. + /// + /// # Errors + /// + /// Returns [`I18nError::MissingMessage`] when `key` is not defined for the + /// resolved locale. pub fn message_with_args(&self, key: &str, args: &Arguments<'_>) -> Result { self.lookup(key, None, Some(args)) } /// Fetch a translated attribute, e.g. `function.primary`. + /// + /// # Errors + /// + /// Returns [`I18nError::MissingMessage`] when `key.attribute` is not + /// defined for the resolved locale. pub fn attribute(&self, key: &str, attribute: &str) -> Result { self.lookup(key, Some(attribute), None) } /// Fetch a translated attribute with Fluent arguments. + /// + /// # Errors + /// + /// Returns [`I18nError::MissingMessage`] when `key.attribute` is not + /// defined for the resolved locale. pub fn attribute_with_args( &self, key: &str, @@ -120,17 +146,15 @@ impl Localizer { attribute: Option<&str>, args: Option<&Arguments<'_>>, ) -> Result { - let lookup_key = attribute - .map(|attr| format!("{key}.{attr}")) - .unwrap_or_else(|| key.to_string()); + let lookup_key = attribute.map_or_else(|| key.to_owned(), |attr| format!("{key}.{attr}")); - let maybe_value = match args { - Some(arguments) => { + let maybe_value = args.map_or_else( + || LOADER.try_lookup(&self.language, lookup_key.as_str()), + |arguments| { let owned_arguments = promote_arguments(arguments); LOADER.try_lookup_with_args(&self.language, lookup_key.as_str(), &owned_arguments) - } - None => LOADER.try_lookup(&self.language, lookup_key.as_str()), - }; + }, + ); maybe_value.ok_or_else(|| I18nError::MissingMessage { key: lookup_key, @@ -141,7 +165,7 @@ impl Localizer { fn fallback() -> Self { Self { language: FALLBACK_LANGUAGE.clone(), - language_tag: FALLBACK_LITERAL.to_string(), + language_tag: FALLBACK_LITERAL.to_owned(), fallback_used: true, } } @@ -152,13 +176,13 @@ fn promote_arguments( ) -> HashMap, FluentValue<'static>> { arguments .iter() - .map(|(key, value)| (Cow::Owned(key.as_ref().to_string()), promote_value(value))) + .map(|(key, value)| (Cow::Owned(key.as_ref().to_owned()), promote_value(value))) .collect() } fn promote_value(value: &FluentValue<'_>) -> FluentValue<'static> { match value { - FluentValue::String(text) => FluentValue::String(Cow::Owned(text.as_ref().to_string())), + FluentValue::String(text) => FluentValue::String(Cow::Owned(text.as_ref().to_owned())), FluentValue::Number(number) => FluentValue::Number(number.clone()), FluentValue::Custom(custom) => FluentValue::Custom(custom.duplicate()), FluentValue::None => FluentValue::None, diff --git a/common/src/i18n/locales.rs b/common/src/i18n/locales.rs index f58b0feab..6a534888a 100644 --- a/common/src/i18n/locales.rs +++ b/common/src/i18n/locales.rs @@ -4,14 +4,15 @@ //! bundles and provides utilities for checking whether a given locale tag is //! supported. -use once_cell::sync::Lazy; - use fluent_templates::{Loader, loader::LanguageIdentifier}; use super::LOADER; -static ALL_LOCALES: Lazy> = Lazy::new(|| { - let mut locales: Vec = LOADER.locales().map(|id| id.to_string()).collect(); +static ALL_LOCALES: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + let mut locales: Vec = LOADER + .locales() + .map(std::string::ToString::to_string) + .collect(); locales.sort_unstable(); locales }); @@ -25,13 +26,12 @@ pub fn available_locales() -> &'static [String] { /// Check whether a locale tag is supported by the embedded bundles. #[must_use] pub fn supports_locale(locale: &str) -> bool { - match locale.parse::() { - Ok(identifier) => { + locale + .parse::() + .is_ok_and(|identifier| { let canonical = identifier.to_string(); ALL_LOCALES .binary_search_by(|candidate| candidate.as_str().cmp(canonical.as_str())) .is_ok() - } - Err(_) => false, - } + }) } diff --git a/common/src/i18n/mod.rs b/common/src/i18n/mod.rs index 300db0588..d001f24dc 100644 --- a/common/src/i18n/mod.rs +++ b/common/src/i18n/mod.rs @@ -35,15 +35,18 @@ static_loader! { }; } +/// Locale tag used when no supported locale is requested (`en-GB`). pub const FALLBACK_LOCALE: &str = FALLBACK_LITERAL; pub(crate) const FALLBACK_LANGUAGE: LanguageIdentifier = langid!("en-GB"); /// Return the crate-local Fluent resource root used by packaging and tests. +#[must_use] pub fn locales_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(LOCALES_DIR_NAME) } /// Return the relative path inside the package tarball for a locale bundle. +#[must_use] pub fn packaged_locale_path(locale: &str, file: &str) -> PathBuf { PathBuf::from(format!( "{}-{}", @@ -56,6 +59,7 @@ pub fn packaged_locale_path(locale: &str, file: &str) -> PathBuf { } /// Return the default packaged locale path for Whitaker diagnostics. +#[must_use] pub fn packaged_fallback_locale_path() -> PathBuf { packaged_locale_path(FALLBACK_LOCALE, LOCALES_FTL_FILE) } diff --git a/common/src/i18n/selection.rs b/common/src/i18n/selection.rs index dbea901b2..ddb6277a3 100644 --- a/common/src/i18n/selection.rs +++ b/common/src/i18n/selection.rs @@ -1,6 +1,7 @@ //! Locale resolver wiring explicit overrides, environment variables, and //! configuration before falling back to the bundled localizer. +use std::borrow::Cow; use std::fmt; use log::{debug, warn}; @@ -68,13 +69,13 @@ impl LocaleSelection { /// Whether the fallback locale was used. #[must_use] - pub fn used_fallback(&self) -> bool { + pub const fn used_fallback(&self) -> bool { self.localizer.used_fallback() } /// Returns the resolved [`Localizer`]. #[must_use] - pub fn localizer(&self) -> &Localizer { + pub const fn localizer(&self) -> &Localizer { &self.localizer } @@ -130,14 +131,20 @@ pub fn resolve_localizer( configuration: Option<&str>, ) -> LocaleSelection { let candidates = [ - (LocaleSource::ExplicitArgument, explicit), - (LocaleSource::EnvironmentVariable, environment.as_deref()), - (LocaleSource::Configuration, configuration), + (LocaleSource::ExplicitArgument, explicit.map(Cow::Borrowed)), + ( + LocaleSource::EnvironmentVariable, + environment.map(Cow::Owned), + ), + ( + LocaleSource::Configuration, + configuration.map(Cow::Borrowed), + ), ]; candidates .into_iter() - .find_map(|(source, raw)| try_resolve_candidate(source, raw)) + .find_map(|(source, raw)| try_resolve_candidate(source, raw.as_deref())) .unwrap_or_else(|| LocaleSelection::new(Localizer::new(None), LocaleSource::Fallback, None)) } diff --git a/common/src/i18n/tests.rs b/common/src/i18n/tests.rs index 27d98a4ea..c21611037 100644 --- a/common/src/i18n/tests.rs +++ b/common/src/i18n/tests.rs @@ -20,9 +20,9 @@ fn resolves_locales(#[case] input: Option<&str>, #[case] expected: &str, #[case] #[test] fn enumerates_available_locales() { let locales = available_locales(); - assert!(locales.contains(&"en-GB".to_string())); - assert!(locales.contains(&"cy".to_string())); - assert!(locales.contains(&"gd".to_string())); + assert!(locales.contains(&"en-GB".to_owned())); + assert!(locales.contains(&"cy".to_owned())); + assert!(locales.contains(&"gd".to_owned())); } #[test] diff --git a/common/src/lcom4/extract.rs b/common/src/lcom4/extract.rs index 5b58afbbd..8c13aa877 100644 --- a/common/src/lcom4/extract.rs +++ b/common/src/lcom4/extract.rs @@ -353,10 +353,13 @@ mod tests { let infos = collect_method_infos(vec![b1, b2, b3]); assert_eq!(infos.len(), 3); - assert_eq!(infos[0].name(), "alpha"); - assert_eq!(infos[1].name(), "beta"); - assert_eq!(infos[2].name(), "gamma"); - assert!(infos[0].accessed_fields().contains("x")); - assert!(infos[1].called_methods().contains("alpha")); + let alpha = infos.first().expect("collected infos should include alpha"); + let beta = infos.get(1).expect("collected infos should include beta"); + let gamma = infos.get(2).expect("collected infos should include gamma"); + assert_eq!(alpha.name(), "alpha"); + assert_eq!(beta.name(), "beta"); + assert_eq!(gamma.name(), "gamma"); + assert!(alpha.accessed_fields().contains("x")); + assert!(beta.called_methods().contains("alpha")); } } diff --git a/common/src/lcom4/mod.rs b/common/src/lcom4/mod.rs index 8e8d3adf0..ec3604cd2 100644 --- a/common/src/lcom4/mod.rs +++ b/common/src/lcom4/mod.rs @@ -109,7 +109,7 @@ impl MethodInfo { /// assert!(m.accessed_fields().contains("buf")); /// ``` #[must_use] - pub fn accessed_fields(&self) -> &BTreeSet { + pub const fn accessed_fields(&self) -> &BTreeSet { &self.accessed_fields } @@ -129,7 +129,7 @@ impl MethodInfo { /// assert!(m.called_methods().contains("validate")); /// ``` #[must_use] - pub fn called_methods(&self) -> &BTreeSet { + pub const fn called_methods(&self) -> &BTreeSet { &self.called_methods } } @@ -153,15 +153,29 @@ impl UnionFind { } fn find(&mut self, x: usize) -> usize { - if self.parent[x] != x { - self.parent[x] = self.find(self.parent[x]); + // Out-of-range nodes are treated as their own root; callers only pass + // indices from `0..n`, so the fallback never fires in practice. + let parent = self.parent.get(x).copied().unwrap_or(x); + if parent == x { + return x; } - self.parent[x] + let root = self.find(parent); + self.set_parent(x, root); + root } /// Returns `true` when the first root has strictly lower rank. fn lower_rank(&self, root_a: usize, root_b: usize) -> bool { - self.rank[root_a] < self.rank[root_b] + let rank_a = self.rank.get(root_a).copied().unwrap_or(0); + let rank_b = self.rank.get(root_b).copied().unwrap_or(0); + rank_a < rank_b + } + + /// Repoints `node` at `root`, ignoring out-of-range nodes. + fn set_parent(&mut self, node: usize, root: usize) { + if let Some(parent_slot) = self.parent.get_mut(node) { + *parent_slot = root; + } } fn union(&mut self, x: usize, y: usize) { @@ -171,22 +185,24 @@ impl UnionFind { return; } if self.lower_rank(root_x, root_y) { - self.parent[root_x] = root_y; + self.set_parent(root_x, root_y); } else if self.lower_rank(root_y, root_x) { - self.parent[root_y] = root_x; + self.set_parent(root_y, root_x); } else { - self.parent[root_y] = root_x; - self.rank[root_x] += 1; + self.set_parent(root_y, root_x); + if let Some(rank_slot) = self.rank.get_mut(root_x) { + *rank_slot += 1; + } } } fn component_count(&mut self) -> usize { - let n = self.parent.len(); + let node_count = self.parent.len(); // Flatten the forest so every node points directly to its root. - for i in 0..n { - self.find(i); + for node in 0..node_count { + self.find(node); } - let mut roots: Vec = self.parent[..n].to_vec(); + let mut roots = self.parent.clone(); roots.sort_unstable(); roots.dedup(); roots.len() diff --git a/common/src/lcom4/tests.rs b/common/src/lcom4/tests.rs index 83b57cd0f..311296807 100644 --- a/common/src/lcom4/tests.rs +++ b/common/src/lcom4/tests.rs @@ -11,7 +11,7 @@ fn method_with_fields() -> fn(&str, &[&str]) -> MethodInfo { |name: &str, fields: &[&str]| -> MethodInfo { MethodInfo::new( name, - fields.iter().map(|s| (*s).to_string()).collect(), + fields.iter().map(|s| (*s).to_owned()).collect(), BTreeSet::new(), ) } @@ -23,7 +23,7 @@ fn method_with_calls() -> fn(&str, &[&str]) -> MethodInfo { MethodInfo::new( name, BTreeSet::new(), - calls.iter().map(|s| (*s).to_string()).collect(), + calls.iter().map(|s| (*s).to_owned()).collect(), ) } } @@ -33,8 +33,8 @@ fn method_with_fields_and_calls() -> fn(&str, &[&str], &[&str]) -> MethodInfo { |name: &str, fields: &[&str], calls: &[&str]| -> MethodInfo { MethodInfo::new( name, - fields.iter().map(|s| (*s).to_string()).collect(), - calls.iter().map(|s| (*s).to_string()).collect(), + fields.iter().map(|s| (*s).to_owned()).collect(), + calls.iter().map(|s| (*s).to_owned()).collect(), ) } } diff --git a/common/src/lib.rs b/common/src/lib.rs index bf7007b63..1dc994ac6 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -9,6 +9,7 @@ pub mod complexity_signal; pub mod context; pub mod decomposition_advice; pub mod diagnostics; +pub mod dylint_entry; pub mod expr; pub mod i18n; pub mod lcom4; diff --git a/common/src/path.rs b/common/src/path.rs index 7bca6e56c..3d268abf3 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -70,9 +70,11 @@ impl SimplePath { let mut candidate_iter = candidate.into_iter(); for expected in &self.segments { - match candidate_iter.next() { - Some(candidate_segment) if expected == candidate_segment.as_ref() => continue, - _ => return false, + let Some(candidate_segment) = candidate_iter.next() else { + return false; + }; + if expected != candidate_segment.as_ref() { + return false; } } diff --git a/common/src/rstest/argument_fingerprint.rs b/common/src/rstest/argument_fingerprint.rs index b18cdc3d8..8742b2bc0 100644 --- a/common/src/rstest/argument_fingerprint.rs +++ b/common/src/rstest/argument_fingerprint.rs @@ -4,11 +4,20 @@ #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum ArgAtom { /// An argument supplied by an `rstest` fixture-local parameter. - FixtureLocal { name: String }, + FixtureLocal { + /// The fixture-local parameter name as written in the test signature. + name: String, + }, /// A stable literal argument, stored as canonical source text. - ConstLit { text: String }, + ConstLit { + /// Canonical source text of the literal. + text: String, + }, /// A stable constant path, stored as a canonical definition path. - ConstPath { def_path: String }, + ConstPath { + /// Canonical definition path of the referenced constant. + def_path: String, + }, /// A present argument shape that later lowering does not support. Unsupported, } diff --git a/common/src/rstest/detection.rs b/common/src/rstest/detection.rs index b382307f5..c5c9ba059 100644 --- a/common/src/rstest/detection.rs +++ b/common/src/rstest/detection.rs @@ -76,7 +76,7 @@ impl RstestDetectionOptions { /// assert!(options.use_expansion_trace_fallback()); /// ``` #[must_use] - pub fn new( + pub const fn new( provider_param_attributes: Vec, use_expansion_trace_fallback: bool, ) -> Self { @@ -199,7 +199,7 @@ fn matches_direct_or_trace( ) -> bool { has_matching_attribute(attrs, candidates) || (options.use_expansion_trace_fallback() - && trace.is_some_and(|trace| has_matching_trace(trace, candidates))) + && trace.is_some_and(|expansion| has_matching_trace(expansion, candidates))) } fn has_matching_attribute(attrs: &[Attribute], candidates: &[&[&str]]) -> bool { diff --git a/common/src/rstest/paragraph_fingerprint.rs b/common/src/rstest/paragraph_fingerprint.rs index 787295f2e..8aa9a62d2 100644 --- a/common/src/rstest/paragraph_fingerprint.rs +++ b/common/src/rstest/paragraph_fingerprint.rs @@ -64,14 +64,14 @@ impl ParagraphNormalizer { /// reuse the original slot. #[must_use] pub fn local_slot(&mut self, local_name: impl Into) -> LocalSlot { - let local_name = local_name.into(); - if let Some(slot) = self.slots.get(&local_name) { + let owned_name = local_name.into(); + if let Some(slot) = self.slots.get(&owned_name) { return *slot; } let slot = LocalSlot::new(self.next_slot); self.next_slot += 1; - self.slots.insert(local_name, slot); + self.slots.insert(owned_name, slot); slot } } @@ -120,9 +120,19 @@ impl CalleeShape { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum ExprShape { /// A function call with known or unknown callee identity and arity. - Call { callee: CalleeShape, argc: usize }, + Call { + /// The normalized callee identity. + callee: CalleeShape, + /// The number of call arguments. + argc: usize, + }, /// A method call with method name and arity. - MethodCall { method: String, argc: usize }, + MethodCall { + /// The method name as written at the call site. + method: String, + /// The number of call arguments, excluding the receiver. + argc: usize, + }, /// A stable path expression. Path, /// A stable literal expression. @@ -185,10 +195,16 @@ impl ExprShape { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum StmtShape { /// A `let` statement represented by its initializer shape. - Let { init: ExprShape }, + Let { + /// The normalized shape of the initializer expression. + init: ExprShape, + }, /// A mutating call, optionally tied to a normalized local receiver slot. MutCall { + /// The normalized slot of the local receiver, when the receiver is a + /// known local binding. receiver: Option, + /// The normalized callee identity. callee: CalleeShape, }, } diff --git a/common/src/rstest/parameter.rs b/common/src/rstest/parameter.rs index fe4e0337b..a576948f8 100644 --- a/common/src/rstest/parameter.rs +++ b/common/src/rstest/parameter.rs @@ -37,7 +37,7 @@ impl RstestParameter { /// assert_eq!(parameter.attributes().len(), 1); /// ``` #[must_use] - pub fn new(binding: ParameterBinding, attributes: Vec) -> Self { + pub const fn new(binding: ParameterBinding, attributes: Vec) -> Self { Self { binding, attributes, @@ -70,7 +70,7 @@ impl RstestParameter { /// assert_eq!(parameter.binding_name(), None); /// ``` #[must_use] - pub fn unsupported() -> Self { + pub const fn unsupported() -> Self { Self::new(ParameterBinding::Unsupported, Vec::new()) } @@ -100,7 +100,10 @@ impl RstestParameter { #[derive(Clone, Debug, PartialEq, Eq)] pub enum RstestParameterKind { /// A fixture-local identifier binding. - FixtureLocal { name: String }, + FixtureLocal { + /// The identifier bound by the fixture parameter. + name: String, + }, /// A provider-driven input such as `#[case]` or `#[values]`. Provider, /// A binding shape that version one does not support. diff --git a/common/src/span.rs b/common/src/span.rs index 4b320976e..f53cb7e92 100644 --- a/common/src/span.rs +++ b/common/src/span.rs @@ -1,5 +1,4 @@ //! Utilities for working with source locations and spans. -#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))] use std::ops::RangeInclusive; @@ -47,7 +46,7 @@ impl SourceLocation { /// Returns true when this location is positioned after other. #[must_use] - pub const fn is_after(self, other: SourceLocation) -> bool { + pub const fn is_after(self, other: Self) -> bool { self.line > other.line || (self.line == other.line && self.column > other.column) } } @@ -62,6 +61,10 @@ pub struct SourceSpan { impl SourceSpan { /// Constructs a new span from two locations. /// + /// # Errors + /// + /// Returns [`SpanError::StartAfterEnd`] when `start` is positioned after `end`. + /// /// # Examples /// /// ``` @@ -71,7 +74,7 @@ impl SourceSpan { /// assert_eq!(span.start().line(), 1); /// ``` #[must_use = "Inspect the span creation result to handle invalid ranges"] - pub fn new(start: SourceLocation, end: SourceLocation) -> Result { + pub const fn new(start: SourceLocation, end: SourceLocation) -> Result { if start.is_after(end) { Err(SpanError::StartAfterEnd) } else { @@ -103,7 +106,7 @@ impl SourceSpan { /// assert_eq!(span_to_lines(span), 4..=6); /// ``` #[must_use] -pub fn span_to_lines(span: SourceSpan) -> RangeInclusive { +pub const fn span_to_lines(span: SourceSpan) -> RangeInclusive { span.start.line()..=span.end.line() } @@ -118,7 +121,7 @@ pub fn span_to_lines(span: SourceSpan) -> RangeInclusive { /// assert_eq!(span_line_count(span), 4); /// ``` #[must_use] -pub fn span_line_count(span: SourceSpan) -> usize { +pub const fn span_line_count(span: SourceSpan) -> usize { span.end.line() - span.start.line() + 1 } diff --git a/common/src/test_support/mod.rs b/common/src/test_support/mod.rs index 1cf75e5dd..66c0739ed 100644 --- a/common/src/test_support/mod.rs +++ b/common/src/test_support/mod.rs @@ -13,8 +13,6 @@ //! variables. //! - [`ui`]: Discovers fixtures, prepares isolated workspaces, and runs dylint UI tests with //! consistent panic handling. -//! - [`EnvVarGuard`]: Sets or removes one environment variable and restores -//! its prior state on drop. //! - [`with_locale`], [`with_env_var`], and [`with_env_var_removed`]: Scope temporary environment //! mutations (such as `DYLINT_LOCALE` overrides) to a callback so tests cannot leak global state //! between cases. @@ -24,7 +22,7 @@ pub mod fixtures; pub mod ui; use std::{ - ffi::{OsStr, OsString}, + ffi::OsStr, sync::{Mutex, MutexGuard, OnceLock, PoisonError}, }; @@ -51,89 +49,6 @@ pub fn env_test_guard() -> MutexGuard<'static, ()> { .unwrap_or_else(PoisonError::into_inner) } -/// Guard that sets one environment variable and restores its prior state. -/// -/// The guard acquires [`env_test_guard`] only while mutating the process -/// environment during construction and drop. It deliberately does not hold the -/// mutex for the full guard lifetime, so callers can execute callbacks that -/// perform their own guarded environment setup without deadlocking. Use this -/// as the shared environment-mutation helper for tests that need temporary -/// global environment changes with `env_test_guard`-serialized setup and -/// teardown semantics. -pub struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - /// Sets `key` to `value`, returning a guard that restores the previous - /// value or removes the variable when dropped. - /// - /// # Examples - /// - /// ```rust - /// use whitaker_common::test_support::EnvVarGuard; - /// - /// let _guard = EnvVarGuard::set("WHITAKER_TEST_ENV_VAR", "enabled"); - /// assert_eq!( - /// std::env::var("WHITAKER_TEST_ENV_VAR").expect("test env var should be set"), - /// "enabled", - /// ); - /// ``` - #[must_use] - pub fn set(key: &'static str, value: impl AsRef) -> Self { - let _env_guard = env_test_guard(); - let previous = std::env::var_os(key); - // SAFETY: `env_test_guard` serializes this environment mutation. - unsafe { - std::env::set_var(key, value); - } - Self { key, previous } - } - - /// Removes `key`, returning a guard that restores the previous value when - /// dropped. - /// - /// # Examples - /// - /// ```rust - /// use whitaker_common::test_support::EnvVarGuard; - /// - /// let _guard = EnvVarGuard::remove("WHITAKER_REMOVED_TEST_ENV_VAR"); - /// assert!(std::env::var_os("WHITAKER_REMOVED_TEST_ENV_VAR").is_none()); - /// ``` - #[must_use] - pub fn remove(key: &'static str) -> Self { - let _env_guard = env_test_guard(); - let previous = std::env::var_os(key); - // SAFETY: `env_test_guard` serializes this environment mutation. - unsafe { - std::env::remove_var(key); - } - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - let _env_guard = env_test_guard(); - match &self.previous { - Some(previous) => { - // SAFETY: `env_test_guard` serializes this environment mutation. - unsafe { - std::env::set_var(self.key, previous); - } - } - None => { - // SAFETY: `env_test_guard` serializes this environment mutation. - unsafe { - std::env::remove_var(self.key); - } - } - } - } -} - /// Runs `callback` with one environment variable temporarily set. /// /// The mutation is scoped to the callback and the prior value is restored diff --git a/crates/bumpy_road_function/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index 15e9401b9..8abd27c6e 100644 --- a/crates/bumpy_road_function/src/analysis.rs +++ b/crates/bumpy_road_function/src/analysis.rs @@ -97,7 +97,7 @@ pub fn normalize_settings(settings: Settings) -> Settings { } } - fn is_valid_window(window: usize) -> bool { + const fn is_valid_window(window: usize) -> bool { window != 0 && (window & 1) == 1 } @@ -259,7 +259,7 @@ fn process_sample_value(value: f64, index: usize, context: &mut BumpDetectionCon context.area = 0.0; } -fn finalize_bump( +const fn finalize_bump( start: usize, end: usize, area: f64, diff --git a/crates/bumpy_road_function/src/driver/config.rs b/crates/bumpy_road_function/src/driver/config.rs index 4388791bb..b69785f93 100644 --- a/crates/bumpy_road_function/src/driver/config.rs +++ b/crates/bumpy_road_function/src/driver/config.rs @@ -52,7 +52,7 @@ impl Default for Config { } impl Config { - pub(super) fn into_settings(self) -> Settings { + pub(super) const fn into_settings(self) -> Settings { Settings { threshold: self.threshold, window: self.window, diff --git a/crates/bumpy_road_function/src/driver/diagnostic.rs b/crates/bumpy_road_function/src/driver/diagnostic.rs index 265065111..61defdf65 100644 --- a/crates/bumpy_road_function/src/driver/diagnostic.rs +++ b/crates/bumpy_road_function/src/driver/diagnostic.rs @@ -59,8 +59,8 @@ pub(super) fn emit_diagnostic( BUMPY_ROAD_FUNCTION, input.primary_span, rustc_lint::errors::DiagDecorator(|lint| { - lint.primary_message(messages.primary().to_string()); - lint.span_note(input.primary_span, messages.note().to_string()); + lint.primary_message(messages.primary().to_owned()); + lint.span_note(input.primary_span, messages.note().to_owned()); for (ordinal, interval) in highlighted.iter().enumerate() { let Some(span) = bump_spans.get(ordinal).copied().flatten() else { @@ -74,7 +74,7 @@ pub(super) fn emit_diagnostic( lint.span_label(span, label); } - lint.help(messages.help().to_string()); + lint.help(messages.help().to_owned()); }), ); } @@ -128,7 +128,12 @@ struct LineSpanMapper { } impl LineSpanMapper { - fn new(base_span: Span, snippet_len: usize, base_line: usize, line_starts: Vec) -> Self { + const fn new( + base_span: Span, + snippet_len: usize, + base_line: usize, + line_starts: Vec, + ) -> Self { Self { base_span, snippet_len, diff --git a/crates/bumpy_road_function/src/driver/mod.rs b/crates/bumpy_road_function/src/driver/mod.rs index e3afd6857..eea678180 100644 --- a/crates/bumpy_road_function/src/driver/mod.rs +++ b/crates/bumpy_road_function/src/driver/mod.rs @@ -104,7 +104,7 @@ impl BumpyRoadFunction { } } -fn extract_item_target(item: &hir::Item<'_>) -> Option { +const fn extract_item_target(item: &hir::Item<'_>) -> Option { let hir::ItemKind::Fn { ident, body, .. } = item.kind else { return None; }; @@ -112,7 +112,7 @@ fn extract_item_target(item: &hir::Item<'_>) -> Option { Some(make_ident_target(ident, body)) } -fn extract_impl_item_target(item: &hir::ImplItem<'_>) -> Option { +const fn extract_impl_item_target(item: &hir::ImplItem<'_>) -> Option { if let hir::ImplItemKind::Fn(_, body_id) = item.kind { return Some(make_analysis_target( item.ident.name, @@ -124,7 +124,7 @@ fn extract_impl_item_target(item: &hir::ImplItem<'_>) -> Option None } -fn extract_trait_item_target(item: &hir::TraitItem<'_>) -> Option { +const fn extract_trait_item_target(item: &hir::TraitItem<'_>) -> Option { let hir::TraitItemKind::Fn(_, trait_fn) = item.kind else { return None; }; @@ -152,7 +152,11 @@ fn extract_expr_target(expr: &hir::Expr<'_>) -> Option { )) } -fn make_analysis_target(name: Symbol, primary_span: Span, body_id: hir::BodyId) -> AnalysisTarget { +const fn make_analysis_target( + name: Symbol, + primary_span: Span, + body_id: hir::BodyId, +) -> AnalysisTarget { AnalysisTarget { name, primary_span, @@ -160,7 +164,7 @@ fn make_analysis_target(name: Symbol, primary_span: Span, body_id: hir::BodyId) } } -fn make_ident_target(ident: Ident, body_id: hir::BodyId) -> AnalysisTarget { +const fn make_ident_target(ident: Ident, body_id: hir::BodyId) -> AnalysisTarget { make_analysis_target(ident.name, ident.span, body_id) } diff --git a/crates/bumpy_road_function/src/driver/segment_builder.rs b/crates/bumpy_road_function/src/driver/segment_builder.rs index 2b7f7ee44..c44d73279 100644 --- a/crates/bumpy_road_function/src/driver/segment_builder.rs +++ b/crates/bumpy_road_function/src/driver/segment_builder.rs @@ -21,7 +21,7 @@ pub(super) struct SegmentBuilder<'a, 'tcx> { } impl<'a, 'tcx> SegmentBuilder<'a, 'tcx> { - pub(super) fn new( + pub(super) const fn new( cx: &'a LateContext<'tcx>, settings: &'a Settings, function_lines: RangeInclusive, @@ -185,7 +185,7 @@ impl<'a, 'tcx> SegmentBuilder<'a, 'tcx> { } } -impl<'a, 'tcx> rustc_hir::intravisit::Visitor<'tcx> for SegmentBuilder<'a, 'tcx> { +impl<'tcx> rustc_hir::intravisit::Visitor<'tcx> for SegmentBuilder<'_, 'tcx> { fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) { Self::visit_expr(self, expr); } diff --git a/crates/clippy_utils/src/lib.rs b/crates/clippy_utils/src/lib.rs index 34d4c9290..200948252 100644 --- a/crates/clippy_utils/src/lib.rs +++ b/crates/clippy_utils/src/lib.rs @@ -7,6 +7,8 @@ #![feature(rustc_private)] +/// Macro-related helpers mirroring the subset of Clippy's `macros` module +/// used by Whitaker lints. pub mod macros { use rustc_hir as hir; use rustc_lint::LateContext; @@ -37,7 +39,7 @@ pub mod macros { return false; }; - let def_id = cx + let resolved = cx .typeck_results() .type_dependent_def_id(callee.hir_id) .or_else(|| match callee.kind { @@ -45,7 +47,7 @@ pub mod macros { _ => None, }); - let Some(def_id) = def_id else { + let Some(def_id) = resolved else { return false; }; diff --git a/crates/conditional_max_n_branches/src/driver.rs b/crates/conditional_max_n_branches/src/driver.rs index e85419cfd..2dd337f45 100644 --- a/crates/conditional_max_n_branches/src/driver.rs +++ b/crates/conditional_max_n_branches/src/driver.rs @@ -273,7 +273,7 @@ fn normalize_isolation_marks(text: &str) -> String { }) .collect() } else { - text.to_string() + text.to_owned() } } diff --git a/crates/function_attrs_follow_docs/src/driver.rs b/crates/function_attrs_follow_docs/src/driver.rs index 9f5ef801a..05e68ce26 100644 --- a/crates/function_attrs_follow_docs/src/driver.rs +++ b/crates/function_attrs_follow_docs/src/driver.rs @@ -82,7 +82,7 @@ struct ItemInfo { } impl ItemInfo { - fn new(hir_id: hir::HirId, span: Span, kind: FunctionKind) -> Self { + const fn new(hir_id: hir::HirId, span: Span, kind: FunctionKind) -> Self { Self { hir_id, span, kind } } } @@ -195,7 +195,7 @@ impl AttrInfo { (span.lo(), span.hi()) } - fn user_editable_span(&self) -> Option { + const fn user_editable_span(&self) -> Option { self.user_editable_span } } @@ -238,7 +238,7 @@ fn check_function_attributes(check: FunctionAttributeCheck<'_, '_>) { ) }); // Attribute macros can reorder attributes in HIR; rely on source order instead. - infos.sort_by_key(|info| info.source_order_key()); + infos.sort_by_key(AttrInfo::source_order_key); let Some((doc_index, offending_index)) = detect_misordered_doc(infos.as_slice()) else { return; @@ -312,9 +312,9 @@ fn emit_diagnostic(cx: &LateContext<'_>, context: DiagnosticContext, localizer: let kind = context.kind; move || fallback_messages(kind, attribute.as_str()) }); - let primary = messages.primary().to_string(); - let note = messages.note().to_string(); - let help = messages.help().to_string(); + let primary = messages.primary().to_owned(); + let note = messages.note().to_owned(); + let help = messages.help().to_owned(); cx.emit_span_lint( FUNCTION_ATTRS_FOLLOW_DOCS, @@ -341,7 +341,7 @@ fn localized_messages( args.insert(Cow::Borrowed("subject"), FluentValue::from(kind.subject())); args.insert( Cow::Borrowed("attribute"), - FluentValue::from(attribute.to_string()), + FluentValue::from(attribute.to_owned()), ); resolve_message_set(lookup, MESSAGE_KEY, &args) @@ -352,15 +352,15 @@ fn fallback_messages(kind: FunctionKind, attribute: &str) -> FunctionAttrsMessag "Doc comments on {} must precede other outer attributes.", kind.subject() ); - let note = format!("The outer attribute {attribute} appears before the doc comment.",); - let help = format!("Move the doc comment so it appears before {attribute} on the item.",); + let note = format!("The outer attribute {attribute} appears before the doc comment."); + let help = format!("Move the doc comment so it appears before {attribute} on the item."); FunctionAttrsMessages::new(primary, note, help) } fn attribute_label(cx: &LateContext<'_>, span: Span, localizer: &Localizer) -> String { match cx.sess().source_map().span_to_snippet(span) { - Ok(snippet) => snippet.trim().to_string(), + Ok(snippet) => snippet.trim().to_owned(), Err(_) => attribute_fallback(localizer), } } @@ -370,7 +370,7 @@ fn attribute_fallback(lookup: &impl BundleLookup) -> String { lookup .message(MessageKey::new("common-attribute-fallback"), &args) - .unwrap_or_else(|_| "the preceding attribute".to_string()) + .unwrap_or_else(|_| "the preceding attribute".to_owned()) } /// Recover the source span of a parsed attribute kind. @@ -385,7 +385,7 @@ fn attribute_fallback(lookup: &impl BundleLookup) -> String { /// deliberately not recovered until the ordering check needs them. Only /// variants whose shape is identical on the currently supported nightlies /// are matched; further kinds can be added as the pin advances. -fn parsed_attribute_span(kind: &AttributeKind) -> Option { +const fn parsed_attribute_span(kind: &AttributeKind) -> Option { match kind { AttributeKind::DocComment { span, .. } | AttributeKind::Ignore { span, .. } diff --git a/crates/module_max_lines/src/driver.rs b/crates/module_max_lines/src/driver.rs index 3eda99591..7ff134f37 100644 --- a/crates/module_max_lines/src/driver.rs +++ b/crates/module_max_lines/src/driver.rs @@ -93,7 +93,7 @@ impl<'tcx> LateLintPass<'tcx> for ModuleMaxLines { } } -fn evaluate_module(lines: usize, limit: usize, from_macro: bool) -> ModuleDisposition { +const fn evaluate_module(lines: usize, limit: usize, from_macro: bool) -> ModuleDisposition { if from_macro { ModuleDisposition::Ignore } else if lines > limit { @@ -110,8 +110,7 @@ fn load_configuration() -> usize { Err(error) => { debug!( target: LINT_NAME, - "failed to parse `{}` configuration: {error}; using defaults", - LINT_NAME + "failed to parse `{LINT_NAME}` configuration: {error}; using defaults" ); ModuleMaxLinesConfig::default().max_lines } @@ -169,12 +168,12 @@ fn emit_diagnostic(cx: &LateContext<'_>, info: &ModuleDiagnosticInfo, localizer: MODULE_MAX_LINES, info.ident.span, rustc_lint::errors::DiagDecorator(|lint| { - lint.primary_message(messages.primary().to_string()); + lint.primary_message(messages.primary().to_owned()); lint.span_note( module_header_span(info.item_span, info.ident.span), - messages.note().to_string(), + messages.note().to_owned(), ); - lint.help(messages.help().to_string()); + lint.help(messages.help().to_owned()); }), ); } diff --git a/crates/module_must_have_inner_docs/src/driver/mod.rs b/crates/module_must_have_inner_docs/src/driver/mod.rs index ef19d43ba..6ca554f1c 100644 --- a/crates/module_must_have_inner_docs/src/driver/mod.rs +++ b/crates/module_must_have_inner_docs/src/driver/mod.rs @@ -43,6 +43,7 @@ impl<'a> ParseInput<'a> { /// let input = ParseInput::from("example"); /// assert_eq!(input.as_str(), "example"); /// ``` + #[must_use] pub fn as_str(&self) -> &'a str { **self } @@ -194,8 +195,7 @@ fn has_inner_doc(rest: ParseInput<'_>) -> bool { while line_start < snippet.len() { let line_end = snippet[line_start..] .find('\n') - .map(|idx| line_start + idx) - .unwrap_or(snippet.len()); + .map_or(snippet.len(), |idx| line_start + idx); let line = &snippet[line_start..line_end]; if check_line_for_inner_doc(snippet, line, line_start) { return true; @@ -305,9 +305,9 @@ fn emit_diagnostic(cx: &LateContext<'_>, context: &ModuleDiagnosticContext, loca MODULE_MUST_HAVE_INNER_DOCS, context.primary_span, rustc_lint::errors::DiagDecorator(|lint| { - lint.primary_message(messages.primary().to_string()); - lint.span_note(context.header_span, messages.note().to_string()); - lint.help(messages.help().to_string()); + lint.primary_message(messages.primary().to_owned()); + lint.span_note(context.header_span, messages.note().to_owned()); + lint.help(messages.help().to_owned()); }), ); } diff --git a/crates/module_must_have_inner_docs/src/driver/parser.rs b/crates/module_must_have_inner_docs/src/driver/parser.rs index 1f36914c2..e5292ad2b 100644 --- a/crates/module_must_have_inner_docs/src/driver/parser.rs +++ b/crates/module_must_have_inner_docs/src/driver/parser.rs @@ -44,7 +44,7 @@ use crate::{AttributeBody, MetaList, ParseInput}; /// assert_eq!(offset, 5); // 2 bytes + 3 bytes. /// assert_eq!(rest.as_str(), "hello"); /// ``` -pub(super) fn skip_leading_whitespace<'a>(snippet: ParseInput<'a>) -> (usize, ParseInput<'a>) { +pub(super) fn skip_leading_whitespace(snippet: ParseInput<'_>) -> (usize, ParseInput<'_>) { let snippet_str = snippet.as_str(); let trimmed = snippet_str.trim_start_matches(char::is_whitespace); let byte_offset = snippet_str.len().saturating_sub(trimmed.len()); @@ -110,7 +110,7 @@ fn is_doc_attr(attr_body: AttributeBody<'_>) -> bool { /// /// assert!(take_ident(ParseInput::from(" 123")).is_none()); /// ``` -pub(super) fn take_ident<'a>(input: ParseInput<'a>) -> Option<(ParseInput<'a>, ParseInput<'a>)> { +pub(super) fn take_ident(input: ParseInput<'_>) -> Option<(ParseInput<'_>, ParseInput<'_>)> { let (_, trimmed) = skip_leading_whitespace(input); let trimmed_str = trimmed.as_str(); let mut iter = trimmed_str.char_indices(); @@ -149,13 +149,13 @@ fn is_doc_ident(input: ParseInput<'_>) -> bool { false } -fn is_ident_start(ch: char) -> bool { +const fn is_ident_start(ch: char) -> bool { // Ident parsing is intentionally ASCII-only; we only need to recognize // built-in attribute names such as `doc` and `cfg_attr`. ch == '_' || ch.is_ascii_alphabetic() } -fn is_ident_continue(ch: char) -> bool { +const fn is_ident_continue(ch: char) -> bool { ch == '_' || ch.is_ascii_alphanumeric() } @@ -179,7 +179,7 @@ struct ParserStateAfterFirst { } impl ParserStateAfterFirst { - fn new() -> Self { + const fn new() -> Self { Self { depth: 0, start: 0, diff --git a/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.rs b/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.rs index 57280542c..1ef4e8166 100644 --- a/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.rs +++ b/crates/no_expect_outside_tests/examples/fail_expect_in_rstest_non_test_module.rs @@ -8,13 +8,13 @@ use rstest::rstest; -#[allow(dead_code)] +#[expect(dead_code)] fn parse() { let parsed = std::iter::once("value").next(); let _ = parsed.expect("ordinary code must not inherit rstest harness status"); } -#[allow(dead_code)] +#[expect(dead_code)] mod parse { pub const VERSION: &str = "1"; } diff --git a/crates/no_expect_outside_tests/src/context/mod.rs b/crates/no_expect_outside_tests/src/context/mod.rs index d9a3f5181..780ce278e 100644 --- a/crates/no_expect_outside_tests/src/context/mod.rs +++ b/crates/no_expect_outside_tests/src/context/mod.rs @@ -47,8 +47,8 @@ pub(crate) struct ContextSummary { /// collect_context(cx, expr.hir_id, additional_test_attributes); /// assert!(!entries.is_empty() || !has_test_context_ancestry); /// ``` -pub(crate) fn collect_context<'tcx>( - cx: &LateContext<'tcx>, +pub(crate) fn collect_context( + cx: &LateContext<'_>, hir_id: hir::HirId, additional_test_attributes: &[AttributePath], ) -> (Vec, bool) { @@ -129,7 +129,7 @@ pub(crate) fn summarize_context( entry .kind() .matches_function() - .then(|| entry.name().to_string()) + .then(|| entry.name().to_owned()) }); ContextSummary { @@ -148,7 +148,7 @@ fn context_entry_for(node: Node<'_>, attrs: &[hir::Attribute]) -> Option Some(ContextEntry::new( - "impl".to_string(), + "impl".to_owned(), ContextKind::Impl, convert_attributes(attrs), )), @@ -169,7 +169,7 @@ fn context_entry_for(node: Node<'_>, attrs: &[hir::Attribute]) -> Option None, }, Node::Block(_) => Some(ContextEntry::new( - "block".to_string(), + "block".to_owned(), ContextKind::Block, convert_attributes(attrs), )), @@ -204,7 +204,7 @@ fn convert_attribute(attr: &hir::Attribute) -> Attribute { Attribute::new(path, kind) } -/// Check if a cfg_attr has a test condition and contains nested cfg(test). +/// Check if a `cfg_attr` has a test condition and contains nested cfg(test). fn check_cfg_attr_for_test(items: I) -> bool where I: IntoIterator, @@ -260,17 +260,14 @@ pub(crate) fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { if path[0] == sym::cfg { return attr .meta_item_list() - .map(|items| items.iter().cloned().any(meta_item_inner_contains_test)) - .unwrap_or(false); + .is_some_and(|items| items.iter().cloned().any(meta_item_inner_contains_test)); } if path[0] != sym::cfg_attr { return false; } - attr.meta_item_list() - .map(check_cfg_attr_for_test) - .unwrap_or(false) + attr.meta_item_list().is_some_and(check_cfg_attr_for_test) } fn meta_item_inner_contains_test(item: MetaItemInner) -> bool { @@ -290,33 +287,27 @@ fn meta_contains_test_with_polarity(meta: &MetaItem, is_positive: bool) -> bool } if path_is_ident(&meta.path, sym::not) { - return meta - .meta_item_list() - .map(|items| { - items - .iter() - .cloned() - .any(|item| meta_item_inner_contains_test_with_polarity(item, !is_positive)) - }) - .unwrap_or(false); - } - - meta.meta_item_list() - .map(|items| { + return meta.meta_item_list().is_some_and(|items| { items .iter() .cloned() - .any(|item| meta_item_inner_contains_test_with_polarity(item, is_positive)) - }) - .unwrap_or(false) + .any(|item| meta_item_inner_contains_test_with_polarity(item, !is_positive)) + }); + } + + meta.meta_item_list().is_some_and(|items| { + items + .iter() + .cloned() + .any(|item| meta_item_inner_contains_test_with_polarity(item, is_positive)) + }) } fn meta_contains_test_cfg(meta: &MetaItem) -> bool { if path_is_ident(&meta.path, sym::cfg) { return meta .meta_item_list() - .map(|items| items.iter().cloned().any(meta_item_inner_contains_test)) - .unwrap_or(false); + .is_some_and(|items| items.iter().cloned().any(meta_item_inner_contains_test)); } if !path_is_ident(&meta.path, sym::cfg_attr) { @@ -324,8 +315,7 @@ fn meta_contains_test_cfg(meta: &MetaItem) -> bool { } meta.meta_item_list() - .map(|items| check_cfg_attr_for_test(items.iter().cloned())) - .unwrap_or(false) + .is_some_and(|items| check_cfg_attr_for_test(items.iter().cloned())) } fn item_name(item: &hir::Item<'_>) -> Option { diff --git a/crates/no_expect_outside_tests/src/context/tests.rs b/crates/no_expect_outside_tests/src/context/tests.rs index 0c2519058..3a527df66 100644 --- a/crates/no_expect_outside_tests/src/context/tests.rs +++ b/crates/no_expect_outside_tests/src/context/tests.rs @@ -457,7 +457,7 @@ fn convert_attribute_handles_parsed_must_use() { // Should return a placeholder "parsed" path instead of panicking. assert_eq!( attribute.path().segments(), - &[PARSED_ATTRIBUTE_PLACEHOLDER.to_string()] + &[PARSED_ATTRIBUTE_PLACEHOLDER.to_owned()] ); assert_eq!(attribute.kind(), AttributeKind::Outer); } diff --git a/crates/no_expect_outside_tests/src/diagnostics.rs b/crates/no_expect_outside_tests/src/diagnostics.rs index 8f4535251..a8c7ce56d 100644 --- a/crates/no_expect_outside_tests/src/diagnostics.rs +++ b/crates/no_expect_outside_tests/src/diagnostics.rs @@ -81,7 +81,7 @@ impl ReceiverCategory { } } - fn as_key(self) -> &'static str { + const fn as_key(self) -> &'static str { match self { Self::Option => "option", Self::Result => "result", @@ -132,7 +132,7 @@ pub(crate) struct DiagnosticContext<'a> { } impl<'a> DiagnosticContext<'a> { - pub(crate) fn new(summary: &'a ContextSummary, localizer: &'a Localizer) -> Self { + pub(crate) const fn new(summary: &'a ContextSummary, localizer: &'a Localizer) -> Self { Self { summary, localizer } } } @@ -144,7 +144,7 @@ pub(crate) fn emit_diagnostic( context: &DiagnosticContext<'_>, ) { let receiver_ty = cx.typeck_results().expr_ty(receiver).peel_refs(); - let receiver_label = ReceiverLabel::new(format!("`{}`", receiver_ty)); + let receiver_label = ReceiverLabel::new(format!("`{receiver_ty}`")); let call_context = context_label(context.summary); let category = ReceiverCategory::classify_ty(cx, receiver_ty); @@ -152,15 +152,15 @@ pub(crate) fn emit_diagnostic( let mut args: Arguments<'static> = Arguments::default(); args.insert( Cow::Borrowed("receiver"), - FluentValue::from(receiver_label.as_ref().to_string()), + FluentValue::from(receiver_label.as_ref().to_owned()), ); args.insert( Cow::Borrowed("context"), - FluentValue::from(call_context.as_ref().to_string()), + FluentValue::from(call_context.as_ref().to_owned()), ); args.insert( Cow::Borrowed("handling"), - FluentValue::from(category.as_key().to_string()), + FluentValue::from(category.as_key().to_owned()), ); let fallback_receiver = receiver_label.clone(); @@ -175,9 +175,9 @@ pub(crate) fn emit_diagnostic( fallback_messages(&fallback_receiver, &fallback_context, category) }); - let primary = messages.primary().to_string(); - let note = messages.note().to_string(); - let help = messages.help().to_string(); + let primary = messages.primary().to_owned(); + let note = messages.note().to_owned(); + let help = messages.help().to_owned(); cx.emit_span_lint( NO_EXPECT_OUTSIDE_TESTS, @@ -204,15 +204,15 @@ fn localized_messages( let mut args: Arguments<'static> = Arguments::default(); args.insert( Cow::Borrowed("receiver"), - FluentValue::from(receiver.as_ref().to_string()), + FluentValue::from(receiver.as_ref().to_owned()), ); args.insert( Cow::Borrowed("context"), - FluentValue::from(context.as_ref().to_string()), + FluentValue::from(context.as_ref().to_owned()), ); args.insert( Cow::Borrowed("handling"), - FluentValue::from(category.as_key().to_string()), + FluentValue::from(category.as_key().to_owned()), ); resolve_message_set(lookup, MESSAGE_KEY, &args) @@ -224,18 +224,17 @@ fn fallback_messages( category: ReceiverCategory, ) -> NoExpectMessages { let primary = format!("Avoid calling expect on {receiver} outside test-only code."); - let note = format!("The call originates within {context} which is not recognized as a test.",); + let note = format!("The call originates within {context} which is not recognized as a test."); let help = category.fallback_help(receiver); NoExpectMessages::new(primary, note, help) } fn context_label(summary: &ContextSummary) -> ContextLabel { - let label = summary - .function_name - .as_ref() - .map(|name| format!("function `{name}`")) - .unwrap_or_else(|| "the surrounding scope".to_string()); + let label = summary.function_name.as_ref().map_or_else( + || "the surrounding scope".to_owned(), + |name| format!("function `{name}`"), + ); ContextLabel::new(label) } diff --git a/crates/no_expect_outside_tests/src/driver/mod.rs b/crates/no_expect_outside_tests/src/driver/mod.rs index 5ecdbfb9a..3b57ddb48 100644 --- a/crates/no_expect_outside_tests/src/driver/mod.rs +++ b/crates/no_expect_outside_tests/src/driver/mod.rs @@ -193,7 +193,7 @@ fn ancestor_function_is_test<'tcx>( }) } -fn is_in_tests_directory<'tcx>(cx: &LateContext<'tcx>) -> bool { +fn is_in_tests_directory(cx: &LateContext<'_>) -> bool { cx.tcx.sess.local_crate_source_file().is_some_and(|source| { is_integration_test_crate_root(source.path(RemapPathScopeComponents::DIAGNOSTICS)) }) @@ -233,7 +233,7 @@ fn is_likely_test_function<'tcx>( || is_in_tests_directory(cx) } -fn is_in_cfg_test_module<'tcx>(cx: &LateContext<'tcx>, hir_id: hir::HirId) -> bool { +fn is_in_cfg_test_module(cx: &LateContext<'_>, hir_id: hir::HirId) -> bool { cx.tcx.hir_parent_iter(hir_id).any(|(ancestor_id, node)| { let hir::Node::Item(item) = node else { return false; diff --git a/crates/no_expect_outside_tests/src/driver/tests.rs b/crates/no_expect_outside_tests/src/driver/tests.rs index 0659e1a3f..121d8f1bc 100644 --- a/crates/no_expect_outside_tests/src/driver/tests.rs +++ b/crates/no_expect_outside_tests/src/driver/tests.rs @@ -33,7 +33,7 @@ fn hir_attribute_from_segments(segments: &[&str]) -> hir::Attribute { hir::Attribute::Unparsed(Box::new(attr_item)) } -fn parsed_must_use_attribute() -> hir::Attribute { +const fn parsed_must_use_attribute() -> hir::Attribute { hir::Attribute::Parsed(HirAttributeKind::MustUse { span: DUMMY_SP, reason: None, diff --git a/crates/no_std_fs_operations/src/behaviour.rs b/crates/no_std_fs_operations/src/behaviour.rs index d3b848271..957d3bfdb 100644 --- a/crates/no_std_fs_operations/src/behaviour.rs +++ b/crates/no_std_fs_operations/src/behaviour.rs @@ -24,7 +24,7 @@ impl LocalizationWorld { self.operation = operation.to_owned(); } - fn mark_failure(&mut self) { + const fn mark_failure(&mut self) { self.failing = true; } @@ -114,7 +114,7 @@ fn then_failure(world: &WorldCell, key: String) { let borrow = world.borrow(); match borrow.error() { I18nError::MissingMessage { key: missing, .. } => { - assert_eq!(missing, &key.trim_matches('"')) + assert_eq!(missing, &key.trim_matches('"')); } } } diff --git a/crates/no_std_fs_operations/src/diagnostics.rs b/crates/no_std_fs_operations/src/diagnostics.rs index 77c77cb31..6b302231c 100644 --- a/crates/no_std_fs_operations/src/diagnostics.rs +++ b/crates/no_std_fs_operations/src/diagnostics.rs @@ -22,10 +22,10 @@ pub(crate) fn emit_diagnostic( let mut args: Arguments<'static> = Arguments::default(); args.insert( Cow::Borrowed("operation"), - FluentValue::from(usage.operation().to_string()), + FluentValue::from(usage.operation().to_owned()), ); - let fallback_operation = usage.operation().to_string(); + let fallback_operation = usage.operation().to_owned(); let resolution = MessageResolution { lint_name: "no_std_fs_operations", key: MESSAGE_KEY, @@ -40,9 +40,9 @@ pub(crate) fn emit_diagnostic( NO_STD_FS_OPERATIONS, span, rustc_lint::errors::DiagDecorator(move |lint| { - lint.primary_message(sanitize_message(messages.primary().to_string())); - lint.note(sanitize_message(messages.note().to_string())); - lint.help(sanitize_message(messages.help().to_string())); + lint.primary_message(sanitize_message(messages.primary().to_owned())); + lint.note(sanitize_message(messages.note().to_owned())); + lint.help(sanitize_message(messages.help().to_owned())); }), ); } @@ -59,12 +59,11 @@ fn fallback_messages(operation: &str) -> StdFsMessages { "std::fs reads the ambient working directory, ", "so it bypasses the capability model enforced by cap-std and camino." ) - .to_string(); + .to_owned(); let help = concat!( "Pass `cap_std::fs::Dir` handles and camino::Utf8Path/Utf8PathBuf arguments down to the call ", "so only explicit capabilities touch the filesystem." - ) - .to_string(); + ).to_owned(); DiagnosticMessageSet::new(primary, note, help) } @@ -82,7 +81,7 @@ pub(crate) fn localized_messages( let mut args: Arguments<'static> = Arguments::default(); args.insert( Cow::Borrowed("operation"), - FluentValue::from(operation.to_string()), + FluentValue::from(operation.to_owned()), ); resolve_message_set(lookup, MESSAGE_KEY, &args) } diff --git a/crates/no_std_fs_operations/src/driver.rs b/crates/no_std_fs_operations/src/driver.rs index 98515f4de..ea096af0d 100644 --- a/crates/no_std_fs_operations/src/driver.rs +++ b/crates/no_std_fs_operations/src/driver.rs @@ -147,7 +147,7 @@ struct LintSite { impl NoStdFsOperations { /// Centralizes exclusion logic for all lint pass methods. #[inline] - fn should_skip(&self) -> bool { + const fn should_skip(&self) -> bool { self.excluded } diff --git a/crates/no_std_fs_operations/src/exclusion.rs b/crates/no_std_fs_operations/src/exclusion.rs index e8861b85e..85f18a156 100644 --- a/crates/no_std_fs_operations/src/exclusion.rs +++ b/crates/no_std_fs_operations/src/exclusion.rs @@ -67,7 +67,7 @@ impl PathExclusions { /// /// The driver consults this before resolving an item's path so the common /// case pays no lookup cost. - pub(crate) fn is_empty(&self) -> bool { + pub(crate) const fn is_empty(&self) -> bool { self.prefixes.is_empty() } diff --git a/crates/no_std_fs_operations/src/exclusion_behaviour.rs b/crates/no_std_fs_operations/src/exclusion_behaviour.rs index db1e41d1f..5089acfde 100644 --- a/crates/no_std_fs_operations/src/exclusion_behaviour.rs +++ b/crates/no_std_fs_operations/src/exclusion_behaviour.rs @@ -26,7 +26,7 @@ impl ExclusionWorld { self.suppressed = Some(exclusions.excludes(&SimplePath::parse(item_path))); } - fn suppressed(&self) -> bool { + const fn suppressed(&self) -> bool { self.suppressed .expect("a usage should have been evaluated before asserting") } diff --git a/crates/no_std_fs_operations/src/usage/mod.rs b/crates/no_std_fs_operations/src/usage/mod.rs index f27902607..356b976d1 100644 --- a/crates/no_std_fs_operations/src/usage/mod.rs +++ b/crates/no_std_fs_operations/src/usage/mod.rs @@ -49,7 +49,7 @@ impl StdFsUsage { /// let usage = StdFsUsage::new(String::from("std::fs::read"), UsageCategory::Call); /// assert_eq!(usage.operation(), "std::fs::read"); /// ``` - pub fn new(operation: String, category: UsageCategory) -> Self { + pub const fn new(operation: String, category: UsageCategory) -> Self { Self { operation, category, @@ -151,8 +151,8 @@ fn is_std_fs_path(path: &SimplePath) -> bool { segments.len() >= 2 && segments[0] == "std" && segments[1] == "fs" } -/// Returns true if the character should be rejected in a valid std::fs label. -fn is_invalid_label_char(ch: char) -> bool { +/// Returns true if the character should be rejected in a valid `std::fs` label. +const fn is_invalid_label_char(ch: char) -> bool { ch.is_whitespace() || matches!(ch, '(' | ')') } diff --git a/crates/no_std_fs_operations/src/usage/tests.rs b/crates/no_std_fs_operations/src/usage/tests.rs index 25114112b..bffc21005 100644 --- a/crates/no_std_fs_operations/src/usage/tests.rs +++ b/crates/no_std_fs_operations/src/usage/tests.rs @@ -1,4 +1,4 @@ -//! Tests for classifying std::fs usage and its reporting metadata. +//! Tests for classifying `std::fs` usage and its reporting metadata. use super::{StdFsUsage, UsageCategory, label_is_std_fs}; use rstest::rstest; diff --git a/crates/no_unwrap_or_else_panic/src/context/mod.rs b/crates/no_unwrap_or_else_panic/src/context/mod.rs index 58426ee47..67649621f 100644 --- a/crates/no_unwrap_or_else_panic/src/context/mod.rs +++ b/crates/no_unwrap_or_else_panic/src/context/mod.rs @@ -31,10 +31,7 @@ use rustc_span::sym; /// Summarize the context for a given HIR node. #[cfg(feature = "dylint-driver")] -pub(crate) fn summarize_context<'tcx>( - cx: &LateContext<'tcx>, - hir_id: hir::HirId, -) -> ContextSummary { +pub(crate) fn summarize_context(cx: &LateContext<'_>, hir_id: hir::HirId) -> ContextSummary { let mut entries = Vec::new(); let mut has_cfg_test = false; @@ -69,7 +66,7 @@ fn context_entry_for(node: Node<'_>, attrs: &[hir::Attribute]) -> Option Some(ContextEntry::new( - "impl".to_string(), + "impl".to_owned(), ContextKind::Impl, convert_attributes(attrs), )), @@ -90,7 +87,7 @@ fn context_entry_for(node: Node<'_>, attrs: &[hir::Attribute]) -> Option None, }, Node::Block(_) => Some(ContextEntry::new( - "block".to_string(), + "block".to_owned(), ContextKind::Block, convert_attributes(attrs), )), @@ -159,17 +156,14 @@ fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { if path[0] == sym::cfg { return attr .meta_item_list() - .map(|items| items.iter().cloned().any(meta_item_inner_contains_test)) - .unwrap_or(false); + .is_some_and(|items| items.iter().cloned().any(meta_item_inner_contains_test)); } if path[0] != sym::cfg_attr { return false; } - attr.meta_item_list() - .map(check_cfg_attr_for_test) - .unwrap_or(false) + attr.meta_item_list().is_some_and(check_cfg_attr_for_test) } #[cfg(feature = "dylint-driver")] @@ -212,27 +206,22 @@ fn meta_contains_test_with_polarity(meta: &MetaItem, is_positive: bool) -> bool } if path_is_ident(&meta.path, sym::not) { - return meta - .meta_item_list() - .map(|items| { - items - .iter() - .cloned() - .any(|item| meta_item_inner_contains_test_with_polarity(item, !is_positive)) - }) - .unwrap_or(false); + return meta.meta_item_list().is_some_and(|items| { + items + .iter() + .cloned() + .any(|item| meta_item_inner_contains_test_with_polarity(item, !is_positive)) + }); } meta.meta_item_list() - .map(|items| items.iter().cloned().any(meta_item_inner_contains_test)) - .unwrap_or(false) + .is_some_and(|items| items.iter().cloned().any(meta_item_inner_contains_test)) } #[cfg(feature = "dylint-driver")] fn meta_contains_test_cfg(meta: &MetaItem) -> bool { meta.meta_item_list() - .map(|items| items.iter().cloned().any(meta_item_inner_contains_test)) - .unwrap_or(false) + .is_some_and(|items| items.iter().cloned().any(meta_item_inner_contains_test)) } #[cfg(feature = "dylint-driver")] diff --git a/crates/no_unwrap_or_else_panic/src/context/tests.rs b/crates/no_unwrap_or_else_panic/src/context/tests.rs index 20a410c89..d22d97079 100644 --- a/crates/no_unwrap_or_else_panic/src/context/tests.rs +++ b/crates/no_unwrap_or_else_panic/src/context/tests.rs @@ -31,7 +31,7 @@ fn convert_attribute_handles_parsed_must_use() { // Should return a placeholder "parsed" path instead of panicking. assert_eq!( attribute.path().segments(), - &[PARSED_ATTRIBUTE_PLACEHOLDER.to_string()] + &[PARSED_ATTRIBUTE_PLACEHOLDER.to_owned()] ); assert_eq!(attribute.kind(), AttributeKind::Outer); } diff --git a/crates/no_unwrap_or_else_panic/src/diagnostics.rs b/crates/no_unwrap_or_else_panic/src/diagnostics.rs index 97ea2d72b..d8a7628d3 100644 --- a/crates/no_unwrap_or_else_panic/src/diagnostics.rs +++ b/crates/no_unwrap_or_else_panic/src/diagnostics.rs @@ -48,9 +48,9 @@ pub(crate) fn emit_diagnostic( NO_UNWRAP_OR_ELSE_PANIC, expr.span, rustc_lint::errors::DiagDecorator(|lint| { - lint.primary_message(messages.primary().to_string()); - lint.span_note(receiver.span, messages.note().to_string()); - lint.help(messages.help().to_string()); + lint.primary_message(messages.primary().to_owned()); + lint.span_note(receiver.span, messages.note().to_owned()); + lint.help(messages.help().to_owned()); }), ); } diff --git a/crates/no_unwrap_or_else_panic/src/driver.rs b/crates/no_unwrap_or_else_panic/src/driver.rs index 39b4e1ed0..e33d27e27 100644 --- a/crates/no_unwrap_or_else_panic/src/driver.rs +++ b/crates/no_unwrap_or_else_panic/src/driver.rs @@ -114,8 +114,8 @@ impl<'tcx> LateLintPass<'tcx> for NoUnwrapOrElsePanic { } } -fn is_inside_harness_test_function<'tcx>( - cx: &LateContext<'tcx>, +fn is_inside_harness_test_function( + cx: &LateContext<'_>, hir_id: hir::HirId, harness_test_functions: &HashSet, ) -> bool { @@ -134,8 +134,8 @@ fn is_inside_harness_test_function<'tcx>( /// Summarizes the lint context for an expression, merging attribute-based and /// harness-based test detection into a single immutable result. -fn summarize_context_with_harness<'tcx>( - cx: &LateContext<'tcx>, +fn summarize_context_with_harness( + cx: &LateContext<'_>, hir_id: hir::HirId, is_test_harness: bool, harness_test_functions: &HashSet, @@ -147,7 +147,7 @@ fn summarize_context_with_harness<'tcx>( summary } -fn closure_body(expr: &hir::Expr<'_>) -> Option { +const fn closure_body(expr: &hir::Expr<'_>) -> Option { match expr.kind { ExprKind::Closure(hir::Closure { body, .. }) => Some(*body), _ => None, diff --git a/crates/no_unwrap_or_else_panic/src/panic_detector.rs b/crates/no_unwrap_or_else_panic/src/panic_detector.rs index d114a7627..433213d7c 100644 --- a/crates/no_unwrap_or_else_panic/src/panic_detector.rs +++ b/crates/no_unwrap_or_else_panic/src/panic_detector.rs @@ -50,7 +50,7 @@ impl PanicInfo { /// Returns `true` when the closure has at least one interpolated panic /// and no plain (non-interpolating) panic. #[must_use] - pub(crate) fn is_interpolated_only(&self) -> bool { + pub(crate) const fn is_interpolated_only(&self) -> bool { self.has_interpolated_panic && !self.has_plain_panic } } @@ -58,7 +58,7 @@ impl PanicInfo { /// Analyses the closure referenced by `body_id` and returns a [`PanicInfo`] /// describing whether it panics and distinguishing plain vs interpolated panics. #[must_use] -pub(crate) fn closure_panics<'tcx>(cx: &LateContext<'tcx>, body_id: hir::BodyId) -> PanicInfo { +pub(crate) fn closure_panics(cx: &LateContext<'_>, body_id: hir::BodyId) -> PanicInfo { let mut detector = PanicDetector { cx, panics: false, @@ -105,7 +105,7 @@ struct PanicDetector<'a, 'tcx> { has_interpolated_panic: bool, } -impl<'a, 'tcx> rustc_hir::intravisit::Visitor<'tcx> for PanicDetector<'a, 'tcx> { +impl<'tcx> rustc_hir::intravisit::Visitor<'tcx> for PanicDetector<'_, 'tcx> { fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { if is_panic_call(self.cx, expr) { self.panics = true; @@ -157,11 +157,11 @@ fn is_panic_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { /// entry point) for `Arguments::new_v1` or `Arguments::new_v1_formatted`, /// which are only used when format arguments are present. /// -/// The check examines the message argument's call tree for fmt::Arguments +/// The check examines the message argument's call tree for `fmt::Arguments` /// constructors, but only considers calls that are part of the compiler- -/// generated format_args expansion. This avoids false positives from unrelated +/// generated `format_args` expansion. This avoids false positives from unrelated /// user code like `panic_any(MyType::new_v1())` where `MyType::new_v1()` is -/// the payload, not a format_args constructor. +/// the payload, not a `format_args` constructor. fn panic_args_use_interpolation<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool { // Extract the panic message argument (first argument to the panic call). let ExprKind::Call(_, args) = expr.kind else { @@ -187,7 +187,7 @@ struct RuntimeArgsFinder<'a, 'tcx> { found: bool, } -impl<'a, 'tcx> rustc_hir::intravisit::Visitor<'tcx> for RuntimeArgsFinder<'a, 'tcx> { +impl<'tcx> rustc_hir::intravisit::Visitor<'tcx> for RuntimeArgsFinder<'_, 'tcx> { fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { if self.found { return; diff --git a/crates/no_unwrap_or_else_panic/src/policy.rs b/crates/no_unwrap_or_else_panic/src/policy.rs index df7c4a6e9..26dc69887 100644 --- a/crates/no_unwrap_or_else_panic/src/policy.rs +++ b/crates/no_unwrap_or_else_panic/src/policy.rs @@ -42,7 +42,7 @@ impl LintPolicy { /// assert!(should_flag(&policy, &summary, &info, false)); /// ``` #[must_use] -pub(crate) fn should_flag( +pub(crate) const fn should_flag( policy: &LintPolicy, summary: &ContextSummary, panic_info: &PanicInfo, diff --git a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs index 5f0d6a065..4ab597a67 100644 --- a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs +++ b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs @@ -21,7 +21,7 @@ struct DecisionWorld { } impl DecisionWorld { - fn evaluate(&self) -> bool { + const fn evaluate(&self) -> bool { let policy = LintPolicy::new(self.allow_in_main.get()); should_flag( &policy, diff --git a/crates/rstest_helper_should_be_fixture/examples/collection_zero_diagnostic.rs b/crates/rstest_helper_should_be_fixture/examples/collection_zero_diagnostic.rs index 5f015040c..01e152378 100644 --- a/crates/rstest_helper_should_be_fixture/examples/collection_zero_diagnostic.rs +++ b/crates/rstest_helper_should_be_fixture/examples/collection_zero_diagnostic.rs @@ -67,7 +67,7 @@ fn rstest_helper_call_collection_stays_silent(fixture: &str) { assert_eq!(built, "fixture-suffix"); } -fn nested_helper(fixture: &str) -> &str { +const fn nested_helper(fixture: &str) -> &str { fixture } diff --git a/crates/rstest_helper_should_be_fixture/src/collector.rs b/crates/rstest_helper_should_be_fixture/src/collector.rs index b7c458255..70f54632f 100644 --- a/crates/rstest_helper_should_be_fixture/src/collector.rs +++ b/crates/rstest_helper_should_be_fixture/src/collector.rs @@ -38,7 +38,7 @@ impl CallSiteRecord { /// assert_eq!(record.callee_def_id, callee); /// # } /// ``` - pub(crate) fn new( + pub(crate) const fn new( callee_def_id: DefId, fingerprint: ArgFingerprint, test_source_def_id: DefId, @@ -137,10 +137,7 @@ impl CallSiteCollector { if !self.seen.insert(location) { debug!( target: "rstest_helper_should_be_fixture", - "dropping duplicate rstest helper call-site evidence: callee={}, lo={:?}, hi={:?}", - callee_key, - lo, - hi, + "dropping duplicate rstest helper call-site evidence: callee={callee_key}, lo={lo:?}, hi={hi:?}", ); return false; } @@ -291,8 +288,7 @@ fn local_fixture_atom( } else { debug!( target: "rstest_helper_should_be_fixture", - "lowering unsupported local argument: `{}` is not an rstest fixture local", - name, + "lowering unsupported local argument: `{name}` is not an rstest fixture local", ); ArgAtom::unsupported() } @@ -336,8 +332,7 @@ pub(crate) fn resolve_local_callee<'tcx>( } else { debug!( target: "rstest_helper_should_be_fixture", - "callee resolution skipped non-local or non-function callee: {:?}", - def_id, + "callee resolution skipped non-local or non-function callee: {def_id:?}", ); None } diff --git a/crates/rstest_helper_should_be_fixture/src/driver.rs b/crates/rstest_helper_should_be_fixture/src/driver.rs index fe40f9997..e6c67636e 100644 --- a/crates/rstest_helper_should_be_fixture/src/driver.rs +++ b/crates/rstest_helper_should_be_fixture/src/driver.rs @@ -185,8 +185,7 @@ impl RstestHelperShouldBeFixture { { debug!( target: LINT_NAME, - "skipping helper call-site collection for non-rstest function: def_id={:?}", - def_id, + "skipping helper call-site collection for non-rstest function: def_id={def_id:?}", ); return; } @@ -241,8 +240,7 @@ impl<'tcx> LateLintPass<'tcx> for RstestHelperShouldBeFixture { if let Err(error) = self.write_collection_summary() { debug!( target: LINT_NAME, - "failed to write rstest helper call-site collection summary: {}", - error, + "failed to write rstest helper call-site collection summary: {error}", ); } debug!( diff --git a/crates/rstest_helper_should_be_fixture/src/visitor.rs b/crates/rstest_helper_should_be_fixture/src/visitor.rs index 28ae8c412..7b81020fb 100644 --- a/crates/rstest_helper_should_be_fixture/src/visitor.rs +++ b/crates/rstest_helper_should_be_fixture/src/visitor.rs @@ -31,7 +31,7 @@ pub(crate) struct CallSiteVisitor<'a, 'tcx> { } impl<'a, 'tcx> CallSiteVisitor<'a, 'tcx> { - pub(crate) fn new( + pub(crate) const fn new( cx: &'a LateContext<'tcx>, collector: &'a mut CallSiteCollector, test_source_def_id: DefId, @@ -94,7 +94,7 @@ impl<'tcx> Visitor<'tcx> for CallSiteVisitor<'_, 'tcx> { match expr.kind { hir::ExprKind::Call(_, args) => self.collect_call(expr, args), hir::ExprKind::MethodCall(_, receiver, args, _) => { - self.collect_call(expr, std::iter::once(receiver).chain(args)) + self.collect_call(expr, std::iter::once(receiver).chain(args)); } hir::ExprKind::Closure(hir::Closure { .. }) => { self.closure_span_fallbacks.push(expr.span); @@ -208,7 +208,7 @@ pub(crate) fn redacted_fingerprint_shape(fingerprint: &ArgFingerprint) -> String .join(",") } -fn redacted_atom_shape(atom: &ArgAtom) -> &'static str { +const fn redacted_atom_shape(atom: &ArgAtom) -> &'static str { match atom { ArgAtom::FixtureLocal { .. } => "fixture-local", ArgAtom::ConstLit { .. } => "const-lit", diff --git a/crates/test_must_not_have_example/src/behaviour.rs b/crates/test_must_not_have_example/src/behaviour.rs index 52535f429..5fd53dac7 100644 --- a/crates/test_must_not_have_example/src/behaviour.rs +++ b/crates/test_must_not_have_example/src/behaviour.rs @@ -13,7 +13,7 @@ struct DocumentationWorld { impl DocumentationWorld { fn push_line(&self, line: &str) { - self.lines.borrow_mut().push(line.to_string()); + self.lines.borrow_mut().push(line.to_owned()); } fn evaluate(&self) { diff --git a/crates/test_must_not_have_example/src/driver.rs b/crates/test_must_not_have_example/src/driver.rs index bc4667a11..da409e52d 100644 --- a/crates/test_must_not_have_example/src/driver.rs +++ b/crates/test_must_not_have_example/src/driver.rs @@ -61,14 +61,14 @@ enum ItemKindInfo<'a> { } impl<'a> ItemKindInfo<'a> { - fn ident(&self) -> &Ident { + const fn ident(&self) -> &Ident { match self { ItemKindInfo::Item { ident, .. } => ident, ItemKindInfo::ImplItem { ident, .. } | ItemKindInfo::TraitItem { ident, .. } => ident, } } - fn attrs(&self) -> &'a [hir::Attribute] { + const fn attrs(&self) -> &'a [hir::Attribute] { match self { ItemKindInfo::Item { attrs, .. } | ItemKindInfo::ImplItem { attrs, .. } @@ -113,8 +113,7 @@ impl<'tcx> LateLintPass<'tcx> for TestMustNotHaveExample { Err(error) => { debug!( target: LINT_NAME, - "failed to parse `{}` configuration: {error}; using defaults", - LINT_NAME + "failed to parse `{LINT_NAME}` configuration: {error}; using defaults" ); Config::default() } @@ -179,9 +178,9 @@ impl TestMustNotHaveExample { violation: DocExampleViolation, ) { let messages = localized_messages(&self.localizer, function.name, violation); - let primary = messages.primary().to_string(); - let note = messages.note().to_string(); - let help = messages.help().to_string(); + let primary = messages.primary().to_owned(); + let note = messages.note().to_owned(); + let help = messages.help().to_owned(); cx.emit_span_lint( TEST_MUST_NOT_HAVE_EXAMPLE, @@ -319,13 +318,13 @@ fn localized_messages( let mut args: Arguments<'static> = Arguments::default(); args.insert( Cow::Borrowed("test"), - FluentValue::from(function_name.to_string()), + FluentValue::from(function_name.to_owned()), ); let reason = violation.note_detail(); args.insert( Cow::Borrowed("reason"), - FluentValue::from(reason.to_string()), + FluentValue::from(reason.to_owned()), ); let resolution = MessageResolution { lint_name: LINT_NAME, diff --git a/crates/whitaker_clones_core/build_support.rs b/crates/whitaker_clones_core/build_support.rs index 51860980e..ee2a35543 100644 --- a/crates/whitaker_clones_core/build_support.rs +++ b/crates/whitaker_clones_core/build_support.rs @@ -76,8 +76,8 @@ fn read_manifest_text(candidate: &Utf8Path) -> Result, Box Ok(Some(manifest)), Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), Err(error) => Err(error.into()), @@ -118,8 +118,7 @@ fn workspace_manifest_not_found(manifest_dir: &Utf8Path) -> io::Error { io::Error::new( io::ErrorKind::NotFound, format!( - "could not find a parent Cargo.toml with a [workspace] table from `{}`", - manifest_dir + "could not find a parent Cargo.toml with a [workspace] table from `{manifest_dir}`" ), ) } diff --git a/crates/whitaker_clones_core/src/ast/features.rs b/crates/whitaker_clones_core/src/ast/features.rs index 1d12d9731..a3a654246 100644 --- a/crates/whitaker_clones_core/src/ast/features.rs +++ b/crates/whitaker_clones_core/src/ast/features.rs @@ -216,7 +216,7 @@ fn count_node_kinds(node: &NormalizedNode, depth: Depth, counts: &mut KindCounts } } -fn next_depth(depth: Depth) -> Depth { +const fn next_depth(depth: Depth) -> Depth { Depth::new(depth.get().saturating_add(1)) } @@ -272,10 +272,10 @@ mod tests { proptest! { #[test] fn each_representable_count_increases_its_weight( - depth in 0_u16..64, + raw_depth in 0_u16..64, count in 0_u32..u32::MAX ) { - let depth = Depth::new(depth); + let depth = Depth::new(raw_depth); let current_weight = weighted_contribution(depth, count); let increased_weight = weighted_contribution(depth, count + 1); diff --git a/crates/whitaker_clones_core/src/ast/hash.rs b/crates/whitaker_clones_core/src/ast/hash.rs index a56240e66..5e7ac86e3 100644 --- a/crates/whitaker_clones_core/src/ast/hash.rs +++ b/crates/whitaker_clones_core/src/ast/hash.rs @@ -49,26 +49,25 @@ fn seed_hash() -> u64 { } fn hash_node(seed: u64, node: &NormalizedNode) -> u64 { - let mut pending = vec![(node, 0, hash_node_header(seed, node))]; - loop { - let Some((current, child_index, _)) = pending.last_mut() else { - unreachable!("hash_node seeds pending with one node and returns before it empties") - }; - if let Some(child) = current.children().get(*child_index) { - *child_index += 1; + // Pop-based traversal keeps every stack access provably in bounds: each + // iteration pops the top entry, either revisits it with the next child + // pushed on top, or folds its completed hash into the parent. The final + // completed hash is the root's, so the loop needs no unreachable arms. + let mut pending = vec![(node, 0_usize, hash_node_header(seed, node))]; + let mut completed_hash = 0_u64; + while let Some((current, child_index, current_hash)) = pending.pop() { + if let Some(child) = current.children().get(child_index) { + pending.push((current, child_index.saturating_add(1), current_hash)); pending.push((child, 0, hash_node_header(seed, child))); continue; } - let Some((_, _, completed_hash)) = pending.pop() else { - unreachable!("hash_node pops the node it just observed through last_mut") - }; + completed_hash = current_hash; if let Some((_, _, parent_hash)) = pending.last_mut() { - *parent_hash = mix_u64(*parent_hash, completed_hash); - } else { - return completed_hash; + *parent_hash = mix_u64(*parent_hash, current_hash); } } + completed_hash } fn hash_node_header(mut hash: u64, node: &NormalizedNode) -> u64 { @@ -83,7 +82,7 @@ fn child_count(node: &NormalizedNode) -> u64 { u64::try_from(node.children().len()).unwrap_or(u64::MAX) } -fn leaf_tag(leaf: Option) -> u8 { +const fn leaf_tag(leaf: Option) -> u8 { match leaf { Some(LeafClass::Ident) => b'i', Some(LeafClass::Literal) => b'l', diff --git a/crates/whitaker_clones_core/src/ast/lowering.rs b/crates/whitaker_clones_core/src/ast/lowering.rs index 96f0b38f0..b144ea5ed 100644 --- a/crates/whitaker_clones_core/src/ast/lowering.rs +++ b/crates/whitaker_clones_core/src/ast/lowering.rs @@ -47,6 +47,13 @@ fn trace_ast_error( /// Latency metrics and feature-vector emission metrics are deferred to 7.3.2, /// where scoring and SARIF emission consume those observations. /// +/// # Errors +/// +/// Returns an [`AstError`] when the span fails re-validation against +/// `file_text`, when no syntax node covers the requested span, when the +/// selected subtree contains parser error elements, or when lowering exceeds +/// the depth or node budgets. +/// /// # Examples /// /// ``` @@ -60,7 +67,9 @@ fn trace_ast_error( /// ``` #[tracing::instrument(skip(file_text), fields(start = span.start(), end = span.end()))] pub fn lower_span(file_text: &str, span: ByteSpan) -> AstResult { - let span = ByteSpan::new(file_text, span.start(), span.end()).map_err(|error| { + // Re-validation returns a span with identical offsets, so the original + // binding can keep serving later uses without a shadowing rebind. + ByteSpan::new(file_text, span.start(), span.end()).map_err(|error| { trace_ast_error( error, "AST span lies outside the supplied source text", @@ -234,11 +243,11 @@ struct LoweringLimits { } impl LoweringLimits { - fn new(span: ByteSpan) -> Self { + const fn new(span: ByteSpan) -> Self { Self::with_depth_limit(MAX_AST_DEPTH, span) } - fn with_depth_limit(maximum_depth: usize, span: ByteSpan) -> Self { + const fn with_depth_limit(maximum_depth: usize, span: ByteSpan) -> Self { Self { maximum_depth, maximum_nodes: MAX_AST_NODES, @@ -301,7 +310,7 @@ impl LoweringLimits { Ok(NormalizedNode::new(kind_id(node.kind()), None, children)) } - fn unparsable_span(&self) -> AstError { + const fn unparsable_span(&self) -> AstError { AstError::UnparsableSpan { start: self.span.start(), end: self.span.end(), diff --git a/crates/whitaker_clones_core/src/ast/tree.rs b/crates/whitaker_clones_core/src/ast/tree.rs index 38ad82c26..0ae7ea304 100644 --- a/crates/whitaker_clones_core/src/ast/tree.rs +++ b/crates/whitaker_clones_core/src/ast/tree.rs @@ -97,7 +97,7 @@ pub enum LeafClass { pub struct NormalizedNode { kind: KindId, leaf: Option, - children: Vec, + children: Vec, } impl NormalizedNode { @@ -111,7 +111,7 @@ impl NormalizedNode { /// leaf-erasure feature extraction rely on that invariant to stay /// unambiguous. #[must_use] - pub fn new(kind: KindId, leaf: Option, children: Vec) -> Self { + pub fn new(kind: KindId, leaf: Option, children: Vec) -> Self { debug_assert!( leaf.is_none() || children.is_empty(), "a leaf-tagged NormalizedNode must have no children" @@ -137,7 +137,7 @@ impl NormalizedNode { /// Returns the ordered child nodes. #[must_use] - pub fn children(&self) -> &[NormalizedNode] { + pub fn children(&self) -> &[Self] { &self.children } } @@ -199,7 +199,15 @@ pub struct ByteSpan { impl ByteSpan { /// Validates and creates a half-open byte span. - pub fn new(source_text: &str, start: u32, end: u32) -> AstResult { + /// + /// # Errors + /// + /// Returns [`AstError::InvalidSpan`] when `end` precedes `start`, + /// [`AstError::EmptySpan`] when the span is zero width, + /// [`AstError::SpanOutOfBounds`] when the span exceeds `source_text`, and + /// [`AstError::NonCharBoundary`] when either offset splits a UTF-8 + /// character. + pub const fn new(source_text: &str, start: u32, end: u32) -> AstResult { if end < start { return Err(AstError::InvalidSpan { start, end }); } diff --git a/crates/whitaker_clones_core/src/hashing.rs b/crates/whitaker_clones_core/src/hashing.rs index 9162970aa..699c176f8 100644 --- a/crates/whitaker_clones_core/src/hashing.rs +++ b/crates/whitaker_clones_core/src/hashing.rs @@ -80,7 +80,12 @@ pub(crate) fn mix_bytes(mut current: u64, bytes: &[u8]) -> u64 { /// assert_eq!(mix_u16(FNV_OFFSET_BASIS, 0x1234), 0x0832_9407_b4eb_8443); /// ``` pub(crate) fn mix_u16(current: u64, value: u16) -> u64 { - mix_bytes(current, &value.to_le_bytes()) + // Decompose into little-endian bytes with explicit masks and shifts so the + // serialization order is spelled out rather than delegated to an + // endianness-specific method. + let low = (value & 0x00ff) as u8; + let high = (value >> 8) as u8; + mix_bytes(current, &[low, high]) } /// Mixes a `u64` using little-endian bytes. @@ -97,8 +102,16 @@ pub(crate) fn mix_u16(current: u64, value: u16) -> u64 { /// 0x999a_7071_7b39_65dd /// ); /// ``` -pub(crate) fn mix_u64(current: u64, value: u64) -> u64 { - mix_bytes(current, &value.to_le_bytes()) +pub(crate) fn mix_u64(mut current: u64, value: u64) -> u64 { + // Mix the eight little-endian bytes lowest first, using explicit masks and + // shifts so the serialization order is spelled out rather than delegated to + // an endianness-specific method. + let mut remaining = value; + for _ in 0..8 { + current = mix_byte(current, (remaining & 0xff) as u8); + remaining >>= 8; + } + current } #[cfg(test)] diff --git a/crates/whitaker_clones_core/src/index/error.rs b/crates/whitaker_clones_core/src/index/error.rs index 58de6ee7a..63bb67b74 100644 --- a/crates/whitaker_clones_core/src/index/error.rs +++ b/crates/whitaker_clones_core/src/index/error.rs @@ -1,4 +1,4 @@ -//! Error types for MinHash sketching and LSH configuration. +//! Error types for `MinHash` sketching and LSH configuration. use thiserror::Error; @@ -16,17 +16,17 @@ pub enum IndexError { /// The number of rows per band must be greater than zero. #[error("LSH rows must be greater than zero")] ZeroRows, - /// The band and row product must equal the fixed MinHash sketch size. + /// The band and row product must equal the fixed `MinHash` sketch size. #[error("LSH bands ({bands}) multiplied by rows ({rows}) must equal {expected}")] InvalidBandRowProduct { /// Requested number of bands. bands: usize, /// Requested number of rows per band. rows: usize, - /// Required fixed MinHash sketch size. + /// Required fixed `MinHash` sketch size. expected: usize, }, - /// MinHash requires at least one retained fingerprint hash. + /// `MinHash` requires at least one retained fingerprint hash. #[error("retained fingerprints must not be empty")] EmptyFingerprintSet, } diff --git a/crates/whitaker_clones_core/src/index/fragment_id.rs b/crates/whitaker_clones_core/src/index/fragment_id.rs index 09cd683c7..b927ac75c 100644 --- a/crates/whitaker_clones_core/src/index/fragment_id.rs +++ b/crates/whitaker_clones_core/src/index/fragment_id.rs @@ -24,7 +24,7 @@ impl FragmentId { /// Returns the fragment identifier as a string slice. #[must_use] - pub fn as_str(&self) -> &str { + pub const fn as_str(&self) -> &str { self.0.as_str() } diff --git a/crates/whitaker_clones_core/src/index/kani.rs b/crates/whitaker_clones_core/src/index/kani.rs index 958d2c37e..4cbba70a2 100644 --- a/crates/whitaker_clones_core/src/index/kani.rs +++ b/crates/whitaker_clones_core/src/index/kani.rs @@ -67,7 +67,7 @@ fn fragment(id: &str) -> FragmentId { } fn repeated_signature(value: u64) -> MinHashSignature { - MinHashSignature::new([value; MINHASH_SIZE]) + MinHashSignature::new(&[value; MINHASH_SIZE]) } fn two_band_config() -> LshConfig { diff --git a/crates/whitaker_clones_core/src/index/lsh.rs b/crates/whitaker_clones_core/src/index/lsh.rs index f88aaa3af..ec0cc0075 100644 --- a/crates/whitaker_clones_core/src/index/lsh.rs +++ b/crates/whitaker_clones_core/src/index/lsh.rs @@ -1,4 +1,4 @@ -//! Locality-sensitive hashing over fixed-width MinHash signatures. +//! Locality-sensitive hashing over fixed-width `MinHash` signatures. //! //! This module contains [`LshIndex`], the token-pass index that groups //! [`MinHashSignature`] band slices into locality-sensitive hashing (LSH) @@ -66,7 +66,7 @@ impl LshIndex { /// # Ok::<(), whitaker_clones_core::IndexError>(()) /// ``` #[must_use] - pub fn new(config: LshConfig) -> Self { + pub const fn new(config: LshConfig) -> Self { Self { config, #[cfg(not(kani))] diff --git a/crates/whitaker_clones_core/src/index/minhash.rs b/crates/whitaker_clones_core/src/index/minhash.rs index 9da0e5a3f..70292ef57 100644 --- a/crates/whitaker_clones_core/src/index/minhash.rs +++ b/crates/whitaker_clones_core/src/index/minhash.rs @@ -1,4 +1,4 @@ -//! Deterministic MinHash sketch generation for token-pass clone candidates. +//! Deterministic `MinHash` sketch generation for token-pass clone candidates. //! //! This module turns retained token [`Fingerprint`] values into fixed-width //! [`MinHashSignature`] values for the clone-detector index API. [`MinHasher`] @@ -7,7 +7,7 @@ //! deduplicated hash values so sketching has set semantics rather than multiset //! semantics. //! -//! Internally, [`Seed`] keeps MinHash seed values distinct from raw fingerprint +//! Internally, [`Seed`] keeps `MinHash` seed values distinct from raw fingerprint //! hashes at the type level. The hashing core still accepts raw `u64` hash //! values because fingerprints and signature lanes are represented as hash //! words throughout the index API. @@ -15,7 +15,7 @@ //! The `#[cfg(kani)]` constructors and the unrolled `sketch_values` //! implementation are proof seams for bounded model checking. They keep Kani //! harnesses focused on [`MinHasher::sketch`] invariants without changing the -//! production API or the production [`array::from_fn`] implementation. +//! production API or the production array `map` implementation. //! //! [`LshIndex`](super::LshIndex) in `lsh.rs` consumes the signatures produced //! here by partitioning them into configured bands and emitting candidate @@ -33,14 +33,14 @@ const SEED_STREAM_START: u64 = 0x243F_6A88_85A3_08D3; const SEED_STREAM_STEP: u64 = 0x9E37_79B9_7F4A_7C15; const HASH_MIX: u64 = 0x94D0_49BB_1331_11EB; -/// A typed MinHash seed value. +/// A typed `MinHash` seed value. /// /// Keeps seed values distinct from raw hash values at the type level, /// preventing accidental argument transposition inside the hashing core. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Seed(u64); -/// Deterministic MinHash sketcher for retained token fingerprints. +/// Deterministic `MinHash` sketcher for retained token fingerprints. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MinHasher { seeds: [Seed; MINHASH_SIZE], @@ -53,7 +53,7 @@ impl Default for MinHasher { } impl MinHasher { - /// Creates the fixed 128-seed MinHash family used by roadmap item 7.2.2. + /// Creates the fixed 128-seed `MinHash` family used by roadmap item 7.2.2. /// /// # Examples /// @@ -72,10 +72,10 @@ impl MinHasher { Self { seeds } } - /// Builds a MinHash sketch from retained fingerprint hashes. + /// Builds a `MinHash` sketch from retained fingerprint hashes. /// /// Duplicate fingerprint hash values are collapsed first so the sketch uses - /// MinHash set semantics rather than multiset semantics. + /// `MinHash` set semantics rather than multiset semantics. /// /// # Errors /// @@ -84,7 +84,7 @@ impl MinHasher { pub fn sketch(&self, fingerprints: &[Fingerprint]) -> IndexResult { let unique_hashes = unique_hashes(fingerprints)?; let values = sketch_values(&self.seeds, &unique_hashes); - Ok(MinHashSignature::new(values)) + Ok(MinHashSignature::new(&values)) } /// Creates a deterministic proof-only fixture for Kani harnesses. @@ -127,17 +127,17 @@ impl MinHasher { } } -/// Computes the 128-lane MinHash signature in production builds. +/// Computes the 128-lane `MinHash` signature in production builds. /// /// Two `cfg`-specific implementations exist to balance production idiom /// against proof tractability. This production variant uses idiomatic -/// [`array::from_fn`] iteration. It is mechanically equivalent to the +/// array `map` iteration. It is mechanically equivalent to the /// `#[cfg(kani)]` proof-seam variant: both call [`minimum_mixed_hash`] once per -/// lane to produce the same 128-lane MinHash signature from the seeds and +/// lane to produce the same 128-lane `MinHash` signature from the seeds and /// unique hashes. #[cfg(not(kani))] fn sketch_values(seeds: &[Seed; MINHASH_SIZE], unique_hashes: &[u64]) -> [u64; MINHASH_SIZE] { - array::from_fn(|index| minimum_mixed_hash(seeds[index], unique_hashes)) + seeds.map(|seed| minimum_mixed_hash(seed, unique_hashes)) } /// Computes the 128-lane MinHash signature with explicit unrolling for Kani. @@ -146,7 +146,7 @@ fn sketch_values(seeds: &[Seed; MINHASH_SIZE], unique_hashes: &[u64]) -> [u64; M /// against proof tractability. This `#[cfg(kani)]` proof seam manually unrolls /// every [`minimum_mixed_hash`] call so Kani's bounded model checker does not /// spend proof budget on iterator or loop state expansion. The explicit array -/// literal is mechanically equivalent to the production [`array::from_fn`] +/// literal is mechanically equivalent to the production array `map` /// implementation: both compute the same 128-lane MinHash signature from the /// seeds and unique hashes. // `@codescene/suppress` Large Method proof-seam: keep manual unrolling for Kani tractability @@ -284,7 +284,7 @@ fn sketch_values(seeds: &[Seed; MINHASH_SIZE], unique_hashes: &[u64]) -> [u64; M ] } -/// Extracts sorted, deduplicated fingerprint hashes for MinHash set semantics. +/// Extracts sorted, deduplicated fingerprint hashes for `MinHash` set semantics. /// /// This `pub(super)` helper is restricted to the parent module and converts /// retained [`Fingerprint`] values into the Vec-backed set representation used @@ -312,7 +312,7 @@ fn minimum_mixed_hash(seed: Seed, hashes: &[u64]) -> u64 { }) } -fn mix_hash(seed: u64, hash: u64) -> u64 { +const fn mix_hash(seed: u64, hash: u64) -> u64 { splitmix64(seed ^ hash.wrapping_mul(HASH_MIX)) } @@ -321,16 +321,16 @@ fn mix_hash(seed: u64, hash: u64) -> u64 { /// Both `next_seed` and `splitmix64` intentionally add `SEED_STREAM_STEP` to /// create a non-overlapping, deterministic seed sequence compatible with the /// seed-streaming approach. This double-increment is deliberate, not a bug. -fn next_seed(state: &mut u64) -> Seed { +const fn next_seed(state: &mut u64) -> Seed { *state = state.wrapping_add(SEED_STREAM_STEP); Seed(splitmix64(*state)) } -/// SplitMix64 generator with deliberate `SEED_STREAM_STEP` addition. +/// `SplitMix64` generator with deliberate `SEED_STREAM_STEP` addition. /// /// This function applies `SEED_STREAM_STEP` in addition to the increment in /// `next_seed` to ensure deterministic, non-overlapping seed values. -fn splitmix64(value: u64) -> u64 { +const fn splitmix64(value: u64) -> u64 { let mut mixed = value.wrapping_add(SEED_STREAM_STEP); mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); diff --git a/crates/whitaker_clones_core/src/index/mod.rs b/crates/whitaker_clones_core/src/index/mod.rs index daec3fc48..fcfe230d6 100644 --- a/crates/whitaker_clones_core/src/index/mod.rs +++ b/crates/whitaker_clones_core/src/index/mod.rs @@ -1,4 +1,4 @@ -//! MinHash and LSH indexing for token-pass candidate generation. +//! `MinHash` and LSH indexing for token-pass candidate generation. mod error; mod fragment_id; diff --git a/crates/whitaker_clones_core/src/index/tests.rs b/crates/whitaker_clones_core/src/index/tests.rs index fd99e7044..36501cad2 100644 --- a/crates/whitaker_clones_core/src/index/tests.rs +++ b/crates/whitaker_clones_core/src/index/tests.rs @@ -1,4 +1,4 @@ -//! Unit tests for MinHash and LSH candidate generation. +//! Unit tests for `MinHash` and LSH candidate generation. use rstest::{fixture, rstest}; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -108,8 +108,8 @@ fn config_rejects_invalid_inputs(#[case] case: ((usize, usize), IndexError)) { #[rstest] #[case((1, MINHASH_SIZE))] -#[case((2, MINHASH_SIZE / 2))] -#[case((4, MINHASH_SIZE / 4))] +#[case((2, MINHASH_SIZE.div_euclid(2)))] +#[case((4, MINHASH_SIZE.div_euclid(4)))] #[case((32, 4))] fn config_accepts_valid_inputs(#[case] case: (usize, usize)) { let (bands, rows) = case; @@ -238,7 +238,7 @@ fn canonical_ordering_across_multiple_pairs_and_bands( shared_signature: MinHashSignature, distinct_signature: MinHashSignature, ) { - let config = LshConfig::new(4, MINHASH_SIZE / 4).expect("LSH config should validate"); + let config = LshConfig::new(4, MINHASH_SIZE.div_euclid(4)).expect("LSH config should validate"); let mut forward = LshIndex::new(config); forward.insert(&fragment_ids.alpha, &shared_signature); diff --git a/crates/whitaker_clones_core/src/index/types.rs b/crates/whitaker_clones_core/src/index/types.rs index faeef0f91..107ee5a61 100644 --- a/crates/whitaker_clones_core/src/index/types.rs +++ b/crates/whitaker_clones_core/src/index/types.rs @@ -1,10 +1,10 @@ -//! Shared MinHash and LSH index types. +//! Shared `MinHash` and LSH index types. use std::{num::NonZeroUsize, slice::ChunksExact}; use super::{FragmentId, IndexError, IndexResult}; -/// The fixed MinHash sketch width for roadmap item 7.2.2. +/// The fixed `MinHash` sketch width for roadmap item 7.2.2. pub const MINHASH_SIZE: usize = 128; /// A canonical fragment pair emitted by the LSH candidate filter. @@ -58,7 +58,7 @@ impl CandidatePair { } } -/// Validated LSH settings for the fixed-width MinHash sketch. +/// Validated LSH settings for the fixed-width `MinHash` sketch. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct LshConfig { bands: NonZeroUsize, @@ -88,14 +88,17 @@ impl LshConfig { /// # Ok::<(), whitaker_clones_core::IndexError>(()) /// ``` pub fn new(bands: usize, rows: usize) -> IndexResult { - let Some(bands) = NonZeroUsize::new(bands) else { + let Some(band_count) = NonZeroUsize::new(bands) else { return Err(IndexError::ZeroBands); }; - let Some(rows) = NonZeroUsize::new(rows) else { + let Some(row_count) = NonZeroUsize::new(rows) else { return Err(IndexError::ZeroRows); }; - validate_product(bands, rows)?; - Ok(Self { bands, rows }) + validate_product(band_count, row_count)?; + Ok(Self { + bands: band_count, + rows: row_count, + }) } /// Returns the number of LSH bands. @@ -111,7 +114,7 @@ impl LshConfig { } } -fn validate_product(bands: NonZeroUsize, rows: NonZeroUsize) -> IndexResult<()> { +const fn validate_product(bands: NonZeroUsize, rows: NonZeroUsize) -> IndexResult<()> { match bands.get().checked_mul(rows.get()) { Some(MINHASH_SIZE) => Ok(()), Some(_) | None => Err(IndexError::invalid_band_row_product( @@ -121,14 +124,14 @@ fn validate_product(bands: NonZeroUsize, rows: NonZeroUsize) -> IndexResult<()> } } -/// A fixed-width MinHash sketch over retained fingerprint hashes. +/// A fixed-width `MinHash` sketch over retained fingerprint hashes. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MinHashSignature([u64; MINHASH_SIZE]); impl MinHashSignature { #[must_use] - pub(crate) const fn new(values: [u64; MINHASH_SIZE]) -> Self { - Self(values) + pub(crate) const fn new(values: &[u64; MINHASH_SIZE]) -> Self { + Self(*values) } /// Returns the sketch values in order. diff --git a/crates/whitaker_clones_core/src/lib.rs b/crates/whitaker_clones_core/src/lib.rs index 1d3339a6a..1b76b2e20 100644 --- a/crates/whitaker_clones_core/src/lib.rs +++ b/crates/whitaker_clones_core/src/lib.rs @@ -7,7 +7,7 @@ //! - `k`-shingling over normalized token streams. //! - 64-bit Rabin-Karp rolling hashes for shingles. //! - Winnowing to retain stable representative fingerprints. -//! - Deterministic MinHash sketches over retained fingerprints. +//! - Deterministic `MinHash` sketches over retained fingerprints. //! - Locality-sensitive hashing (LSH) candidate generation. //! - Token-pass acceptance and SARIF Run 0 emission for accepted pairs. diff --git a/crates/whitaker_clones_core/src/run0/emit.rs b/crates/whitaker_clones_core/src/run0/emit.rs index b1d747e9f..681b94afb 100644 --- a/crates/whitaker_clones_core/src/run0/emit.rs +++ b/crates/whitaker_clones_core/src/run0/emit.rs @@ -197,25 +197,25 @@ fn compact_span(region: &whitaker_sarif::Region) -> String { region.end_line.unwrap_or(region.start_line), region .end_column - .unwrap_or(region.start_column.unwrap_or(1)) + .unwrap_or_else(|| region.start_column.unwrap_or(1)) ) } -fn profile_number(profile: NormProfile) -> &'static str { +const fn profile_number(profile: NormProfile) -> &'static str { match profile { NormProfile::T1 => "1", NormProfile::T2 => "2", } } -fn profile_name(profile: NormProfile) -> &'static str { +const fn profile_name(profile: NormProfile) -> &'static str { match profile { NormProfile::T1 => "T1", NormProfile::T2 => "T2", } } -fn profile_sort_key(profile: NormProfile) -> u8 { +const fn profile_sort_key(profile: NormProfile) -> u8 { match profile { NormProfile::T1 => 1, NormProfile::T2 => 2, @@ -295,11 +295,24 @@ fn token_hash(left: &TokenFragment, right: &TokenFragment) -> String { let mut hasher = Sha256::new(); for value in values { - hasher.update(value.to_be_bytes()); + hasher.update(u64_big_endian_bytes(value)); } digest_hex(hasher.finalize()) } +/// Serializes a `u64` as big-endian bytes with explicit masks and shifts. +/// +/// The digest contract fixes the byte order, so the decomposition is spelled +/// out here rather than delegated to an endianness-specific method. +fn u64_big_endian_bytes(value: u64) -> [u8; 8] { + let mut bytes = [0_u8; 8]; + let mut remaining = value; + for slot in bytes.iter_mut().rev() { + *slot = (remaining & 0xff) as u8; + remaining >>= 8; + } + bytes +} fn digest_hex(bytes: impl AsRef<[u8]>) -> String { let mut output = String::new(); for byte in bytes.as_ref() { diff --git a/crates/whitaker_clones_core/src/run0/score.rs b/crates/whitaker_clones_core/src/run0/score.rs index 9a17afb6c..1bcf94584 100644 --- a/crates/whitaker_clones_core/src/run0/score.rs +++ b/crates/whitaker_clones_core/src/run0/score.rs @@ -128,7 +128,7 @@ impl SimilarityThreshold { } /// Returns `true` if the threshold represents a ratio within `(0, 1]`. - fn is_valid(self) -> bool { + const fn is_valid(self) -> bool { self.numerator != 0 && self.denominator != 0 && self.numerator <= self.denominator } @@ -142,7 +142,7 @@ impl SimilarityThreshold { } } -pub(crate) fn select_rule_profile( +pub(crate) const fn select_rule_profile( profile: NormProfile, score: SimilarityRatio, type1_threshold: SimilarityThreshold, @@ -184,7 +184,7 @@ fn unique_hashes(fingerprints: &[Fingerprint]) -> BTreeSet { /// to avoid panics on pathological inputs; clamped results only occur in /// extreme/unrealistic cases. Callers should validate counts if they may /// approach `usize::MAX`. -fn meets_threshold(score: SimilarityRatio, threshold: SimilarityThreshold) -> bool { +const fn meets_threshold(score: SimilarityRatio, threshold: SimilarityThreshold) -> bool { score.intersection.saturating_mul(threshold.denominator) >= score.union.saturating_mul(threshold.numerator) } @@ -195,8 +195,10 @@ fn repeated_division(numerator: usize, denominator: usize) -> Option<(usize, usi return None; } - let quotient = numerator / denominator; - let remainder = numerator % denominator; + // For unsigned integers Euclidean division matches truncating division + // exactly, so the quotient and remainder are unchanged. + let quotient = numerator.div_euclid(denominator); + let remainder = numerator.rem_euclid(denominator); Some((quotient, remainder)) } diff --git a/crates/whitaker_clones_core/src/run0/span.rs b/crates/whitaker_clones_core/src/run0/span.rs index 0023a23b3..bd22fa62d 100644 --- a/crates/whitaker_clones_core/src/run0/span.rs +++ b/crates/whitaker_clones_core/src/run0/span.rs @@ -12,11 +12,17 @@ pub(crate) fn region_for_range( validate_range(fragment_id, source_text, &range)?; let starts = line_starts(source_text); let (start_line, start_column) = line_and_column(source_text, &starts, range.start); - let end_position = source_text[..range.end] + // `validate_range` has already proved `range.end` is an in-bounds char + // boundary, so the fallible slice only fails defensively. + let Some(prefix) = source_text.get(..range.end) else { + return Err(Run0Error::InvalidUtf8Boundary { + fragment_id: fragment_id.to_owned(), + }); + }; + let end_position = prefix .char_indices() .next_back() - .map(|(i, _)| i) - .unwrap_or(range.start); + .map_or(range.start, |(i, _)| i); let (end_line, end_column) = line_and_column(source_text, &starts, end_position); RegionBuilder::new(start_line) @@ -64,9 +70,11 @@ fn line_and_column(source_text: &str, starts: &[usize], offset: usize) -> (usize let line_index = starts .partition_point(|start| *start <= offset) .saturating_sub(1); - let line_start = starts[line_index]; + // `starts` always begins with 0, so both fallbacks preserve the + // start-of-text behaviour if the partition point ever degenerated. + let line_start = starts.get(line_index).copied().unwrap_or(0); let clamped_offset = offset.min(source_text.len()); - let line_slice = &source_text[line_start..clamped_offset]; + let line_slice = source_text.get(line_start..clamped_offset).unwrap_or(""); let utf16_count = line_slice.encode_utf16().count(); (line_index.saturating_add(1), utf16_count.saturating_add(1)) } diff --git a/crates/whitaker_clones_core/src/run0/test_helpers.rs b/crates/whitaker_clones_core/src/run0/test_helpers.rs index 43b90ed39..671fba632 100644 --- a/crates/whitaker_clones_core/src/run0/test_helpers.rs +++ b/crates/whitaker_clones_core/src/run0/test_helpers.rs @@ -16,7 +16,7 @@ pub(super) fn fingerprint(hash: u64, range: std::ops::Range) -> Fingerpri Fingerprint::new(hash, range) } -pub(super) fn fragment(input: FragmentInput<'_>) -> TokenFragment { +pub(super) fn fragment(input: &FragmentInput<'_>) -> TokenFragment { TokenFragment::new( FragmentId::from(input.id), input.profile, diff --git a/crates/whitaker_clones_core/src/run0/tests.rs b/crates/whitaker_clones_core/src/run0/tests.rs index 58e5f7f03..ebe506c12 100644 --- a/crates/whitaker_clones_core/src/run0/tests.rs +++ b/crates/whitaker_clones_core/src/run0/tests.rs @@ -17,7 +17,7 @@ fn build_pair_and_accept( right: FragmentInput<'_>, cfg: &TokenPassConfig, ) -> Result, Run0Error> { - let fragments = vec![fragment(left), fragment(right)]; + let fragments = vec![fragment(&left), fragment(&right)]; accept_candidate_pairs(&fragments, &[pair("alpha", "beta")], cfg) } @@ -27,8 +27,11 @@ fn assert_single_accepted( expected_score: SimilarityRatio, ) { assert_eq!(accepted.len(), 1); - assert_eq!(accepted[0].profile(), expected_profile); - assert_eq!(accepted[0].score(), expected_score); + let only = accepted + .first() + .expect("exactly one accepted pair should be present"); + assert_eq!(only.profile(), expected_profile); + assert_eq!(only.score(), expected_score); } /// Resolves the region for `range`, panicking with context on failure. @@ -150,14 +153,14 @@ fn multi_line_region_tracks_trailing_newline() { #[test] fn emit_run0_uses_primary_and_related_locations() { let fragments = vec![ - fragment(FragmentInput { + fragment(&FragmentInput { id: "alpha", profile: NormProfile::T1, file_uri: "src/a.rs", source_text: "fn a() {}\n", hashes: &[(11, 0..8)], }), - fragment(FragmentInput { + fragment(&FragmentInput { id: "beta", profile: NormProfile::T1, file_uri: "src/b.rs", @@ -196,28 +199,28 @@ fn emit_run0_uses_primary_and_related_locations() { #[test] fn emit_run0_sorts_and_deduplicates_results() { let fragments = vec![ - fragment(FragmentInput { + fragment(&FragmentInput { id: "alpha", profile: NormProfile::T1, file_uri: "src/a.rs", source_text: "fn a() {}\n", hashes: &[(11, 0..8)], }), - fragment(FragmentInput { + fragment(&FragmentInput { id: "beta", profile: NormProfile::T1, file_uri: "src/b.rs", source_text: "fn b() {}\n", hashes: &[(11, 0..8)], }), - fragment(FragmentInput { + fragment(&FragmentInput { id: "gamma", profile: NormProfile::T2, file_uri: "src/c.rs", source_text: "fn c(x: i32) {}\n", hashes: &[(1, 0..15), (2, 0..15)], }), - fragment(FragmentInput { + fragment(&FragmentInput { id: "delta", profile: NormProfile::T2, file_uri: "src/d.rs", @@ -247,8 +250,11 @@ fn emit_run0_sorts_and_deduplicates_results() { .unwrap_or_else(|error| panic!("unexpected emit error: {error}")); assert_eq!(run.results.len(), 2); - assert_eq!(run.results[0].rule_id, WHK001_ID); - assert_eq!(run.results[1].rule_id, WHK002_ID); + let [first, second] = run.results.as_slice() else { + panic!("expected exactly two results"); + }; + assert_eq!(first.rule_id, WHK001_ID); + assert_eq!(second.rule_id, WHK002_ID); } #[test] @@ -292,7 +298,7 @@ fn invalid_utf8_boundary_produces_typed_error() { #[test] fn missing_fragment_produces_typed_error() { - let fragments = vec![fragment(FragmentInput { + let fragments = vec![fragment(&FragmentInput { id: "alpha", profile: NormProfile::T1, file_uri: "src/a.rs", @@ -316,14 +322,14 @@ fn missing_fragment_produces_typed_error() { #[test] fn mixed_profiles_produces_typed_error() { let fragments = vec![ - fragment(FragmentInput { + fragment(&FragmentInput { id: "alpha", profile: NormProfile::T1, file_uri: "src/a.rs", source_text: "fn a() {}\n", hashes: &[(1, 0..8), (2, 0..8)], }), - fragment(FragmentInput { + fragment(&FragmentInput { id: "beta", profile: NormProfile::T2, file_uri: "src/b.rs", diff --git a/crates/whitaker_clones_core/src/run0/tests_emit.rs b/crates/whitaker_clones_core/src/run0/tests_emit.rs index 310d07cfe..0b8063c93 100644 --- a/crates/whitaker_clones_core/src/run0/tests_emit.rs +++ b/crates/whitaker_clones_core/src/run0/tests_emit.rs @@ -36,14 +36,14 @@ fn jaccard_returns_none_for_empty_fragments() { fn make_t2_emission_run() -> whitaker_sarif::Run { let fragments = vec![ - fragment(FragmentInput { + fragment(&FragmentInput { id: "alpha", profile: NormProfile::T2, file_uri: "src/a.rs", source_text: "fn a(x: i32) {}\n", hashes: &[(1, 0..15), (2, 0..15)], }), - fragment(FragmentInput { + fragment(&FragmentInput { id: "beta", profile: NormProfile::T2, file_uri: "src/b.rs", diff --git a/crates/whitaker_clones_core/src/run0/types.rs b/crates/whitaker_clones_core/src/run0/types.rs index 8ee0a8229..07fdca3b7 100644 --- a/crates/whitaker_clones_core/src/run0/types.rs +++ b/crates/whitaker_clones_core/src/run0/types.rs @@ -73,19 +73,19 @@ impl TokenFragment { /// Returns the source artefact URI used in SARIF output. #[must_use] - pub fn file_uri(&self) -> &str { + pub const fn file_uri(&self) -> &str { self.file_uri.as_str() } /// Returns the original source text used for byte-range mapping. #[must_use] - pub fn source_text(&self) -> &str { + pub const fn source_text(&self) -> &str { self.source_text.as_str() } /// Returns the retained token fingerprints for this fragment. #[must_use] - pub fn retained_fingerprints(&self) -> &[Fingerprint] { + pub const fn retained_fingerprints(&self) -> &[Fingerprint] { self.retained_fingerprints.as_slice() } } @@ -160,13 +160,13 @@ impl TokenPassConfig { /// Returns the SARIF producer name. #[must_use] - pub fn tool_name(&self) -> &str { + pub const fn tool_name(&self) -> &str { self.tool_name.as_str() } /// Returns the SARIF producer version. #[must_use] - pub fn tool_version(&self) -> &str { + pub const fn tool_version(&self) -> &str { self.tool_version.as_str() } diff --git a/crates/whitaker_clones_core/src/token/fingerprint.rs b/crates/whitaker_clones_core/src/token/fingerprint.rs index dee874702..b20e1213b 100644 --- a/crates/whitaker_clones_core/src/token/fingerprint.rs +++ b/crates/whitaker_clones_core/src/token/fingerprint.rs @@ -182,32 +182,33 @@ fn hash_canonical_identifier_bytes(mut hash: u64, index: usize) -> u64 { hash_byte(hash, b'>') } -fn hash_usize_bytes(mut hash: u64, value: usize) -> u64 { +fn hash_usize_bytes(mut hash: u64, mut value: usize) -> u64 { + const ASCII_DIGITS: [u8; 10] = *b"0123456789"; let mut buffer = [0_u8; 20]; - let mut value = value; - let mut remaining = buffer.as_mut_slice(); if value == 0 { return hash_byte(hash, b'0'); } - while value > 0 { - let (slot, rest) = match remaining.split_last_mut() { - Some(parts) => parts, - None => unreachable!("usize decimal digits always fit within the buffer"), - }; - #[expect( - clippy::cast_possible_truncation, - reason = "a decimal digit always fits in u8" - )] - { - *slot = b'0' + (value % 10) as u8; + // Write digits least-significant first from the end of the buffer. A + // 64-bit `usize` has at most 20 decimal digits, so the reverse iterator + // never runs out before `value` reaches zero. + let mut digits = 0_usize; + for slot in buffer.iter_mut().rev() { + if value == 0 { + break; } - remaining = rest; - value /= 10; + // `rem_euclid(10)` is always below 10, so the lookup never falls + // through to the fallback digit. + *slot = ASCII_DIGITS + .get(value.rem_euclid(10)) + .copied() + .unwrap_or(b'0'); + value = value.div_euclid(10); + digits = digits.saturating_add(1); } - let start = remaining.len(); + let start = buffer.len().saturating_sub(digits); for byte in buffer.iter().skip(start) { hash = hash_byte(hash, *byte); } @@ -215,7 +216,7 @@ fn hash_usize_bytes(mut hash: u64, value: usize) -> u64 { hash } -fn token_kind_tag(token: &NormalizedToken) -> u8 { +const fn token_kind_tag(token: &NormalizedToken) -> u8 { match token.kind { super::types::NormalizedTokenKind::Atom(_) => b'a', super::types::NormalizedTokenKind::Identifier(_) => b'i', diff --git a/crates/whitaker_clones_core/src/token/normalize.rs b/crates/whitaker_clones_core/src/token/normalize.rs index c9a82b8bc..84680dc5a 100644 --- a/crates/whitaker_clones_core/src/token/normalize.rs +++ b/crates/whitaker_clones_core/src/token/normalize.rs @@ -121,10 +121,12 @@ fn process_token( start: range.start, end: range.end, }), - TokenKind::Literal { kind, .. } => { - ensure_literal_is_terminated(kind, &range)?; + TokenKind::Literal { + kind: literal_kind, .. + } => { + ensure_literal_is_terminated(literal_kind, &range)?; Ok(Some(NormalizedToken::new( - normalize_literal(text, kind, profile), + normalize_literal(text, literal_kind, profile), range, ))) } @@ -217,7 +219,7 @@ pub fn normalize(source: &str, profile: NormProfile) -> Result) -> Result<()> { +const fn ensure_literal_is_terminated(kind: LiteralKind, range: &Range) -> Result<()> { let terminated = match kind { LiteralKind::Int { .. } | LiteralKind::Float { .. } => true, LiteralKind::Char { terminated } @@ -245,15 +247,17 @@ fn normalize_ident( profile: NormProfile, state: &mut CanonicalState, ) -> NormalizedTokenKind { - match keyword_label(text) { - Some(keyword) => NormalizedTokenKind::Atom(keyword), - None => normalize_symbolic_text( - text, - profile, - || state.identifier_index(text), - NormalizedTokenKind::Identifier, - ), - } + keyword_label(text).map_or_else( + || { + normalize_symbolic_text( + text, + profile, + || state.identifier_index(text), + NormalizedTokenKind::Identifier, + ) + }, + NormalizedTokenKind::Atom, + ) } fn normalize_literal(text: &str, kind: LiteralKind, profile: NormProfile) -> NormalizedTokenKind { @@ -276,7 +280,7 @@ struct LiteralLabels { kind: &'static str, } -fn literal_labels(kind: LiteralKind) -> LiteralLabels { +const fn literal_labels(kind: LiteralKind) -> LiteralLabels { match kind { LiteralKind::Int { .. } => LiteralLabels { canonical: "", @@ -361,8 +365,7 @@ fn atom_label(kind: TokenKind) -> &'static str { | TokenKind::Unknown => { debug_assert!( false, - "Token kind {:?} should be handled before atom_label", - kind + "Token kind {kind:?} should be handled before atom_label" ); "" } diff --git a/crates/whitaker_clones_core/src/token/tests.rs b/crates/whitaker_clones_core/src/token/tests.rs index 007ffe138..3cfc71125 100644 --- a/crates/whitaker_clones_core/src/token/tests.rs +++ b/crates/whitaker_clones_core/src/token/tests.rs @@ -67,7 +67,9 @@ fn t2_canonicalizes_identifiers_literals_and_lifetimes() { fn byte_ranges_point_to_original_source() { let source = "fn demo() { value + 1 }"; let tokens = normalize(source, NormProfile::T1).expect("normalization should succeed"); - let value = &tokens[5]; + let value = tokens + .get(5) + .expect("token stream should contain the `value` identifier"); assert_eq!(value.range, 12..17); assert_eq!(source.get(value.range.clone()), Some("value")); @@ -102,7 +104,8 @@ fn exact_k_tokens_yields_one_hash() { ); assert_eq!(hashes.len(), 1); - assert_eq!(hashes[0].range, 0..12); + let only = hashes.first().expect("exactly one hash should be present"); + assert_eq!(only.range, 0..12); } #[test] @@ -232,7 +235,11 @@ fn shebang_is_stripped_equivalently_to_shebang_free_source() { "shebang should not affect the normalized token kinds" ); assert_eq!( - with_shebang[0].range.start, + with_shebang + .first() + .expect("normalized stream should not be empty") + .range + .start, source_with_shebang.find("fn").expect("fn present") ); } @@ -300,7 +307,13 @@ fn literal_variants_are_terminated_and_canonicalized( literal_syms.len() >= 2, "expected at least two literal tokens for the repeated literal pair" ); - assert_eq!(literal_syms[0], literal_syms[1], "{assertion_message}"); + let first = literal_syms + .first() + .expect("first literal symbol should be present"); + let second = literal_syms + .get(1) + .expect("second literal symbol should be present"); + assert_eq!(first, second, "{assertion_message}"); } #[test] diff --git a/crates/whitaker_clones_core/src/token/types.rs b/crates/whitaker_clones_core/src/token/types.rs index d1c705340..08a446d01 100644 --- a/crates/whitaker_clones_core/src/token/types.rs +++ b/crates/whitaker_clones_core/src/token/types.rs @@ -108,10 +108,9 @@ impl TryFrom for ShingleSize { type Error = TokenPassError; fn try_from(value: usize) -> Result { - match NonZeroUsize::new(value) { - Some(value) => Ok(Self(value)), - None => Err(TokenPassError::ZeroShingleSize), - } + NonZeroUsize::new(value) + .ok_or(TokenPassError::ZeroShingleSize) + .map(Self) } } @@ -131,10 +130,9 @@ impl TryFrom for WinnowWindow { type Error = TokenPassError; fn try_from(value: usize) -> Result { - match NonZeroUsize::new(value) { - Some(value) => Ok(Self(value)), - None => Err(TokenPassError::ZeroWinnowWindow), - } + NonZeroUsize::new(value) + .ok_or(TokenPassError::ZeroWinnowWindow) + .map(Self) } } diff --git a/crates/whitaker_sarif/src/builders/location_builder.rs b/crates/whitaker_sarif/src/builders/location_builder.rs index ff5b161a5..0da961a2f 100644 --- a/crates/whitaker_sarif/src/builders/location_builder.rs +++ b/crates/whitaker_sarif/src/builders/location_builder.rs @@ -32,7 +32,7 @@ pub struct RegionBuilder { impl RegionBuilder { /// Creates a builder with the given 1-based start line. #[must_use] - pub fn new(start_line: usize) -> Self { + pub const fn new(start_line: usize) -> Self { Self { start_line, start_column: None, @@ -45,35 +45,35 @@ impl RegionBuilder { /// Sets the 1-based start column. #[must_use] - pub fn with_start_column(mut self, col: usize) -> Self { + pub const fn with_start_column(mut self, col: usize) -> Self { self.start_column = Some(col); self } /// Sets the 1-based end line. #[must_use] - pub fn with_end_line(mut self, line: usize) -> Self { + pub const fn with_end_line(mut self, line: usize) -> Self { self.end_line = Some(line); self } /// Sets the 1-based end column. #[must_use] - pub fn with_end_column(mut self, col: usize) -> Self { + pub const fn with_end_column(mut self, col: usize) -> Self { self.end_column = Some(col); self } /// Sets the byte offset from the start of the artefact. #[must_use] - pub fn with_byte_offset(mut self, offset: usize) -> Self { + pub const fn with_byte_offset(mut self, offset: usize) -> Self { self.byte_offset = Some(offset); self } /// Sets the byte length. #[must_use] - pub fn with_byte_length(mut self, length: usize) -> Self { + pub const fn with_byte_length(mut self, length: usize) -> Self { self.byte_length = Some(length); self } @@ -192,7 +192,7 @@ impl LocationBuilder { /// Sets the region within the artefact. #[must_use] - pub fn with_region(mut self, region: Region) -> Self { + pub const fn with_region(mut self, region: Region) -> Self { self.region = Some(region); self } @@ -269,7 +269,7 @@ mod tests { .with_region(region) .build(); match loc.physical_location.region.as_ref() { - Some(region) => assert_eq!(region.start_line, 42), + Some(attached) => assert_eq!(attached.start_line, 42), None => panic!("expected region to be present"), } } diff --git a/crates/whitaker_sarif/src/builders/result_builder.rs b/crates/whitaker_sarif/src/builders/result_builder.rs index 574e413be..f791d4a15 100644 --- a/crates/whitaker_sarif/src/builders/result_builder.rs +++ b/crates/whitaker_sarif/src/builders/result_builder.rs @@ -50,7 +50,7 @@ impl ResultBuilder { /// Sets the severity level. #[must_use] - pub fn with_level(mut self, level: Level) -> Self { + pub const fn with_level(mut self, level: Level) -> Self { self.level = level; self } diff --git a/crates/whitaker_sarif/src/merge.rs b/crates/whitaker_sarif/src/merge.rs index e16bf4331..083b73666 100644 --- a/crates/whitaker_sarif/src/merge.rs +++ b/crates/whitaker_sarif/src/merge.rs @@ -43,7 +43,7 @@ struct RegionKey { } impl RegionKey { - fn from_region(region: &Region) -> Self { + const fn from_region(region: &Region) -> Self { Self { start_line: region.start_line, start_column: region.start_column, diff --git a/crates/whitaker_sarif/src/test_support.rs b/crates/whitaker_sarif/src/test_support.rs index f941d6a44..7510b0bf2 100644 --- a/crates/whitaker_sarif/src/test_support.rs +++ b/crates/whitaker_sarif/src/test_support.rs @@ -49,7 +49,13 @@ pub fn assert_serialized_json(value: &impl Serialize, check: impl FnOnce(&str)) /// Builds a [`SarifResult`] with a fingerprint, location, and region. /// -/// Panics on builder failure; intended only for test code. +/// Intended only for test code. +/// +/// # Panics +/// +/// Panics if the region or result builder rejects its inputs, which indicates +/// a defect in the test fixture rather than a recoverable condition. +#[must_use] pub fn make_keyed_result(rule: &str, file: &str, line: usize, fp: &str) -> SarifResult { let region = match RegionBuilder::new(line).with_end_line(line + 5).build() { Ok(r) => r, diff --git a/crates/whitaker_sarif/src/whitaker_properties.rs b/crates/whitaker_sarif/src/whitaker_properties.rs index 48176c7fd..f4f1136f2 100644 --- a/crates/whitaker_sarif/src/whitaker_properties.rs +++ b/crates/whitaker_sarif/src/whitaker_properties.rs @@ -152,42 +152,42 @@ impl WhitakerPropertiesBuilder { /// Sets the k-shingle size. #[must_use] - pub fn with_k(mut self, k: usize) -> Self { + pub const fn with_k(mut self, k: usize) -> Self { self.k = k; self } /// Sets the winnowing window size. #[must_use] - pub fn with_window(mut self, window: usize) -> Self { + pub const fn with_window(mut self, window: usize) -> Self { self.window = window; self } /// Sets the Jaccard similarity score. #[must_use] - pub fn with_jaccard(mut self, jaccard: f64) -> Self { + pub const fn with_jaccard(mut self, jaccard: f64) -> Self { self.jaccard = jaccard; self } /// Sets the cosine similarity score. #[must_use] - pub fn with_cosine(mut self, cosine: f64) -> Self { + pub const fn with_cosine(mut self, cosine: f64) -> Self { self.cosine = cosine; self } /// Sets the clone group identifier. #[must_use] - pub fn with_group_id(mut self, group_id: usize) -> Self { + pub const fn with_group_id(mut self, group_id: usize) -> Self { self.group_id = group_id; self } /// Sets the clone class size. #[must_use] - pub fn with_class_size(mut self, class_size: usize) -> Self { + pub const fn with_class_size(mut self, class_size: usize) -> Self { self.class_size = class_size; self } @@ -232,12 +232,12 @@ mod tests { .with_class_size(4) .build(); match props { - Ok(props) => { - assert_eq!(props.profile, "T1"); - assert_eq!(props.k, 25); - assert_eq!(props.window, 16); - assert_eq!(props.group_id, 174); - assert_eq!(props.class_size, 4); + Ok(built) => { + assert_eq!(built.profile, "T1"); + assert_eq!(built.k, 25); + assert_eq!(built.window, 16); + assert_eq!(built.group_id, 174); + assert_eq!(built.class_size, 4); } Err(e) => panic!("unexpected error: {e}"), } @@ -247,10 +247,10 @@ mod tests { fn into_value_wraps_under_whitaker_key() { let props = WhitakerPropertiesBuilder::new("T2").build(); match props { - Ok(props) => { - let value = props.try_to_value(); + Ok(built) => { + let value = built.try_to_value(); match value { - Ok(value) => assert!(value.get("whitaker").is_some()), + Ok(wrapped) => assert!(wrapped.get("whitaker").is_some()), Err(e) => panic!("unexpected serialization error: {e}"), } } @@ -262,9 +262,9 @@ mod tests { fn try_from_value_extracts_properties() { let props = WhitakerPropertiesBuilder::new("T1").with_k(10).build(); match props { - Ok(props) => match props.try_to_value() { + Ok(built) => match built.try_to_value() { Ok(value) => match WhitakerProperties::try_from(&value) { - Ok(extracted) => assert_eq!(extracted, props), + Ok(extracted) => assert_eq!(extracted, built), Err(e) => panic!("unexpected extraction error: {e}"), }, Err(e) => panic!("unexpected serialization error: {e}"), diff --git a/docs/developers-guide.md b/docs/developers-guide.md index a15b1fd17..53d792a50 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -1747,23 +1747,24 @@ the regression suite: `temp-env` provides scoped environment overrides, `tempfile` provides isolated target directories, and `rstest` powers the fixture-based test setup used by the staged-suite coverage. -#### Shared UI harness environment guards - -Workspace-level UI harness tests that mutate process-wide environment variables -must use `whitaker_common::test_support::EnvVarGuard`. Use `EnvVarGuard::set` -to install a temporary value and `EnvVarGuard::remove` to make a variable -absent for the duration of a test. The guard acquires `env_test_guard()` only -while it captures, mutates, or restores the variable; it must not hold that -mutex while a runner callback executes, because the callback may need its own -guarded environment setup. - -`whitaker::testing::ui::run_with_runner` applies a specialized guard before -invoking the Dylint UI runner. On every platform it clears `RUSTC_WRAPPER` only -while the runner needs bare `rustc` invocations for -`dylint_testing::Test::example`. On Windows it also sets `VCPKG_ROOT` to -`C:\vcpkg` when that directory exists and the variable is otherwise absent. -Restoration uses the same shared environment mutex, but the runner callback -itself executes without holding that mutex to avoid nested-lock deadlocks. + +#### Shared UI harness environment overrides + +Workspace-level UI harness tests that mutate process-wide environment +variables must use the scoped helpers in `whitaker_common::test_support`: +`with_env_var` installs a temporary value, `with_env_var_removed` makes a +variable absent, and `with_locale` overrides (or clears) `DYLINT_LOCALE` for +the duration of a callback. The helpers delegate to `temp_env`, whose +re-entrant global lock serializes scoped mutations while permitting nesting +from the same thread, and every prior value is restored when the callback +returns or panics. The workspace forbids `unsafe` code, so tests must never +call `std::env::set_var`/`remove_var` directly. + +`whitaker::testing::ui::run_with_runner` wraps the Dylint UI runner in the +same scoped helpers. On every platform it clears `RUSTC_WRAPPER` while the +runner needs bare `rustc` invocations for `dylint_testing::Test::example`. On +Windows it also sets `VCPKG_ROOT` to `C:\vcpkg` when that directory exists +and the variable is otherwise absent. Example-based UI tests in `rstest_helper_should_be_fixture` also use a cross-process directory lock under the system temporary directory. `nextest` @@ -2088,6 +2089,18 @@ It can be promoted to standard by: `suite/Cargo.toml` 3. Updating documentation to reflect the change + +### Suite entry point + +Dylint resolves a library's lints through an exported `register_lints` +symbol, which requires `#[unsafe(no_mangle)]`. The workspace forbids in-crate +`unsafe` code, so the suite declares its entry point through +`whitaker_common::declare_dylint_register_entry!`, which expands the unsafe +attribute from an external macro — the same escape hatch +`dylint_linting::impl_late_lint!` relies on for single-lint crates. The macro +is reserved for Dylint driver crates; it must not be used for any other +symbol export. + ## Creating a New Lint ### Generating from the template diff --git a/src/testing/ui/mod.rs b/src/testing/ui/mod.rs index b35348933..d2ee5e36f 100644 --- a/src/testing/ui/mod.rs +++ b/src/testing/ui/mod.rs @@ -6,14 +6,14 @@ //! This module centralizes input validation so lint crates can depend on a //! small helper rather than repeat the same checks. -use std::{env, fmt}; +use std::fmt; use camino::{Utf8Path, Utf8PathBuf}; mod toolchain; use self::toolchain::{CrateName, ensure_toolchain_library}; -use whitaker_common::test_support::env_test_guard; +use whitaker_common::test_support::with_env_var_removed; /// Errors produced when preparing or executing Dylint UI tests. #[derive(Clone, Debug, Eq, PartialEq)] @@ -161,20 +161,21 @@ pub fn run_with_runner( let crate_name_owned = CrateName::try_from(crate_name).map_err(|_| HarnessError::EmptyCrateName)?; let crate_name_str = crate_name_owned.as_str(); - let _runner_env_guard = runner_env_guard(); - ensure_toolchain_library(&crate_name_owned)?; + with_runner_env(|| { + ensure_toolchain_library(&crate_name_owned)?; - match runner(crate_name_str, directory.as_ref()) { - Ok(()) => Ok(()), - Err(message) => Err(HarnessError::RunnerFailure { - crate_name: crate_name_owned.into_inner(), - directory, - message, - }), - } + match runner(crate_name_str, directory.as_ref()) { + Ok(()) => Ok(()), + Err(message) => Err(HarnessError::RunnerFailure { + crate_name: crate_name_owned.clone().into_inner(), + directory: directory.clone(), + message, + }), + } + }) } -/// Serializes environment mutations required by `run_with_runner`. +/// Runs `callback` with the environment `run_with_runner` requires. /// /// `RUSTC_WRAPPER` must be cleared on every platform when set (for example to /// `sccache`), because `dylint_testing::Test::example` scans @@ -187,85 +188,29 @@ pub fn run_with_runner( /// - `VCPKG_ROOT`: must be set to `C:\vcpkg` when that directory exists and the /// variable is otherwise absent, so downstream `cargo` invocations resolve vcpkg. /// -/// Each mutation and restoration step acquires `env_test_guard()` only for the -/// environment write itself. The guard deliberately does not hold that mutex -/// across the UI runner callback, because runner closures can perform their -/// own environment-guarded setup. -struct RunnerEnvGuard { - #[cfg(windows)] - vcpkg_root_was_absent: bool, - rustc_wrapper_previous: Option, +/// The scoped mutations are serialized by `temp_env`'s re-entrant global lock, +/// so runner closures that perform their own scoped environment setup nest +/// without deadlocking, and every prior value is restored when the callback +/// returns or panics. +fn with_runner_env(callback: impl FnOnce() -> R) -> R { + with_vcpkg_root(|| with_env_var_removed("RUSTC_WRAPPER", callback)) } -impl Drop for RunnerEnvGuard { - fn drop(&mut self) { - let _env_guard = env_test_guard(); - - // SAFETY: `env_test_guard` serializes the restoration writes below. - #[cfg(windows)] - { - if self.vcpkg_root_was_absent { - unsafe { - env::remove_var("VCPKG_ROOT"); - } - } - } - if let Some(prev) = &self.rustc_wrapper_previous { - unsafe { - env::set_var("RUSTC_WRAPPER", prev); - } - } - } -} - -fn runner_env_guard() -> Option { - #[cfg(windows)] - let vcpkg_candidate = Utf8Path::new(r"C:\vcpkg"); - #[cfg(windows)] - let vcpkg_applicable = vcpkg_candidate.is_dir(); - - let _env_guard = env_test_guard(); - let has_rustc_wrapper = env::var_os("RUSTC_WRAPPER").is_some(); - - #[cfg(windows)] - if !vcpkg_applicable && !has_rustc_wrapper { - return None; - } - #[cfg(not(windows))] - if !has_rustc_wrapper { - return None; - } +#[cfg(windows)] +fn with_vcpkg_root(callback: impl FnOnce() -> R) -> R { + use whitaker_common::test_support::with_env_var; - // All environment reads and writes below are serialized by `_env_guard`. - #[cfg(windows)] - let vcpkg_root_was_absent = if vcpkg_applicable && env::var_os("VCPKG_ROOT").is_none() { - // SAFETY: `_env_guard` serializes concurrent environment mutations. - unsafe { - env::set_var("VCPKG_ROOT", vcpkg_candidate.as_std_path()); - } - true + let candidate = Utf8Path::new(r"C:\vcpkg"); + if candidate.is_dir() && std::env::var_os("VCPKG_ROOT").is_none() { + with_env_var("VCPKG_ROOT", candidate.as_std_path(), callback) } else { - false - }; - - let rustc_wrapper_previous = env::var_os("RUSTC_WRAPPER").inspect(|_| { - // SAFETY: `_env_guard` serializes concurrent environment mutations. - unsafe { - env::remove_var("RUSTC_WRAPPER"); - } - }); - - #[cfg(windows)] - if !vcpkg_root_was_absent && rustc_wrapper_previous.is_none() { - // Nothing was mutated; release the guard early. - return None; + callback() } +} - Some(RunnerEnvGuard { - #[cfg(windows)] - vcpkg_root_was_absent, - rustc_wrapper_previous, - }) +#[cfg(not(windows))] +fn with_vcpkg_root(callback: impl FnOnce() -> R) -> R { + callback() } fn directory_is_rooted(path: &Utf8Path) -> bool { diff --git a/src/testing/ui/tests.rs b/src/testing/ui/tests.rs index bcdf9d98c..2ef12c3ac 100644 --- a/src/testing/ui/tests.rs +++ b/src/testing/ui/tests.rs @@ -5,7 +5,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use rstest::rstest; use std::env; use std::sync::{Mutex, MutexGuard, OnceLock}; -use whitaker_common::test_support::EnvVarGuard; +use whitaker_common::test_support::with_env_var; #[rstest] #[case( @@ -97,31 +97,34 @@ fn propagates_runner_failures() { #[test] fn runner_env_guard_clears_and_restores_rustc_wrapper() { let _serial_guard = runner_env_guard_test_lock(); - let _guard = EnvVarGuard::set("RUSTC_WRAPPER", "sccache"); - - run_with_runner("lint", "ui", |_, _| { - assert_eq!(env::var_os("RUSTC_WRAPPER"), None); - Ok(()) - }) - .expect("runner should execute with RUSTC_WRAPPER cleared"); - - assert_eq!(env::var_os("RUSTC_WRAPPER"), Some("sccache".into())); + with_env_var("RUSTC_WRAPPER", "sccache", || { + run_with_runner("lint", "ui", |_, _| { + assert_eq!(env::var_os("RUSTC_WRAPPER"), None); + Ok(()) + }) + .expect("runner should execute with RUSTC_WRAPPER cleared"); + + assert_eq!(env::var_os("RUSTC_WRAPPER"), Some("sccache".into())); + }); } #[cfg(windows)] #[test] fn windows_env_guard_leaves_absent_rustc_wrapper_untouched() { - let _serial_guard = runner_env_guard_test_lock(); - let _vcpkg_root = EnvVarGuard::set("VCPKG_ROOT", r"C:\vcpkg"); - let _rustc_wrapper = EnvVarGuard::remove("RUSTC_WRAPPER"); + use whitaker_common::test_support::with_env_var_removed; - run_with_runner("lint", "ui", |_, _| { - assert_eq!(env::var_os("RUSTC_WRAPPER"), None); - Ok(()) - }) - .expect("runner should execute without installing RUSTC_WRAPPER"); - - assert_eq!(env::var_os("RUSTC_WRAPPER"), None); + let _serial_guard = runner_env_guard_test_lock(); + with_env_var("VCPKG_ROOT", r"C:\vcpkg", || { + with_env_var_removed("RUSTC_WRAPPER", || { + run_with_runner("lint", "ui", |_, _| { + assert_eq!(env::var_os("RUSTC_WRAPPER"), None); + Ok(()) + }) + .expect("runner should execute without installing RUSTC_WRAPPER"); + + assert_eq!(env::var_os("RUSTC_WRAPPER"), None); + }); + }); } #[test] diff --git a/suite/Cargo.toml b/suite/Cargo.toml index 5623c642d..80ec9f96a 100644 --- a/suite/Cargo.toml +++ b/suite/Cargo.toml @@ -32,6 +32,7 @@ experimental-rstest-helper-should-be-fixture = [ ] [dependencies] +whitaker-common = { workspace = true } dylint_linting = { workspace = true, optional = true } rustc_lint = { workspace = true, optional = true } rustc_session = { workspace = true, optional = true } diff --git a/suite/src/driver.rs b/suite/src/driver.rs index 49a862276..7ee02fd48 100644 --- a/suite/src/driver.rs +++ b/suite/src/driver.rs @@ -80,19 +80,14 @@ pub fn register_suite_lints(store: &mut LintStore) { /// assert!(names.contains(&"no_unwrap_or_else_panic".to_string())); /// ``` #[must_use] -pub fn suite_lint_decls() -> &'static [&'static Lint] { +pub const fn suite_lint_decls() -> &'static [&'static Lint] { SUITE_LINT_DECLS } -/// Dylint entrypoint that initializes configuration and registers lints. -/// -/// # Safety -/// -/// Callers must pass non-null, correctly initialized `Session` and -/// `LintStore` references from the host compiler context that remain valid on -/// this thread for the duration of the call. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn register_lints(sess: &Session, store: &mut LintStore) { +/// Initializes Dylint configuration then registers the suite lints. +fn register_suite_entry(sess: &Session, store: &mut LintStore) { dylint_linting::init_config(sess); register_suite_lints(store); } + +whitaker_common::declare_dylint_register_entry!(register_suite_entry); From 5acaf5363df34910758b352a05c08ba0d12e9a13 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 30 Aug 2026 10:34:32 +0200 Subject: [PATCH 6/9] Isolate lint declarations and clear remaining support-crate findings Complete the support-crate wave with the second half of the donor's fixes, keeping them out of the installer PR above. Each lint crate's `impl_late_lint!` invocation moves into a private `declaration` module carrying one reasoned expectation: the macro expands to items with no documentable source location, so `missing_docs` cannot be satisfied at the invocation site, and an `#[expect]` on the macro call itself does not propagate into the expansion. The same treatment isolates `base_newtype!`, which emits an explicit `Clone` on a `Copy` type. Each crate then re-exports an explicit `{LINT, LintPass}` surface rather than a glob. The remainder are the usual behaviour-preserving fixes: parameters taken by reference where they were needlessly moved, checked indexing and slicing, total matches in place of `unreachable!`, meaningful names for shadowed bindings, and assertion messages where they were missing. --- crates/bumpy_road_function/src/analysis.rs | 5 ++ crates/bumpy_road_function/src/driver/mod.rs | 43 +++++++--- .../src/driver/segment_builder.rs | 4 + crates/bumpy_road_function/src/lib.rs | 7 +- .../conditional_max_n_branches/src/driver.rs | 18 ++-- crates/conditional_max_n_branches/src/lib.rs | 7 +- .../function_attrs_follow_docs/src/driver.rs | 62 +++++++++----- crates/function_attrs_follow_docs/src/lib.rs | 7 +- crates/module_max_lines/src/driver.rs | 43 +++++++--- crates/module_max_lines/src/lib.rs | 7 +- .../src/driver/inner_attr.rs | 18 ++-- .../src/driver/mod.rs | 84 ++++++++++++++----- .../src/driver/parser.rs | 27 +++--- crates/module_must_have_inner_docs/src/lib.rs | 13 ++- .../src/context/mod.rs | 20 ++--- .../src/context/tests.rs | 67 ++++++++++----- .../no_expect_outside_tests/src/driver/mod.rs | 32 +++++-- .../src/driver/tests.rs | 6 +- crates/no_expect_outside_tests/src/lib.rs | 7 +- crates/no_std_fs_operations/src/behaviour.rs | 45 ++++++---- .../no_std_fs_operations/src/diagnostics.rs | 16 ++-- crates/no_std_fs_operations/src/driver.rs | 39 ++++++--- crates/no_std_fs_operations/src/exclusion.rs | 16 ++-- .../src/exclusion_behaviour.rs | 8 +- crates/no_std_fs_operations/src/lib.rs | 7 +- crates/no_std_fs_operations/src/usage/mod.rs | 9 +- .../src/context/mod.rs | 20 ++--- crates/no_unwrap_or_else_panic/src/driver.rs | 30 +++++-- crates/no_unwrap_or_else_panic/src/lib.rs | 7 +- .../src/panic_detector.rs | 6 +- crates/no_unwrap_or_else_panic/src/policy.rs | 18 ++-- .../src/tests/behaviour.rs | 6 +- .../src/driver.rs | 53 ++++++++---- .../src/driver_tests.rs | 4 +- .../src/lib.rs | 7 +- .../src/visitor.rs | 12 +-- .../test_must_not_have_example/src/driver.rs | 62 +++++++++----- .../src/heuristics.rs | 5 +- crates/test_must_not_have_example/src/lib.rs | 7 +- tests/config_loading.rs | 5 +- tests/ui_harness.rs | 11 ++- 41 files changed, 601 insertions(+), 269 deletions(-) diff --git a/crates/bumpy_road_function/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index 8abd27c6e..4ba784ec7 100644 --- a/crates/bumpy_road_function/src/analysis.rs +++ b/crates/bumpy_road_function/src/analysis.rs @@ -238,6 +238,11 @@ pub fn detect_bumps(smoothed: &[f64], threshold: f64, min_bump_lines: usize) -> context.into_intervals() } +#[expect( + clippy::float_arithmetic, + reason = "bump areas accumulate smoothed complexity samples, which are inherently \ + floating-point" +)] fn process_sample_value(value: f64, index: usize, context: &mut BumpDetectionContext) { if value >= context.threshold { if context.current_start.is_none() { diff --git a/crates/bumpy_road_function/src/driver/mod.rs b/crates/bumpy_road_function/src/driver/mod.rs index eea678180..434f537dd 100644 --- a/crates/bumpy_road_function/src/driver/mod.rs +++ b/crates/bumpy_road_function/src/driver/mod.rs @@ -28,13 +28,32 @@ use self::config::load_configuration; use self::diagnostic::{DiagnosticInput, emit_diagnostic}; use self::segment_builder::{SegmentBuilder, span_line_range}; -dylint_linting::impl_late_lint! { - pub BUMPY_ROAD_FUNCTION, - Warn, - "functions should avoid multiple separated clusters of complex conditional logic", - BumpyRoadFunction::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::BumpyRoadFunction; + + dylint_linting::impl_late_lint! { + /// Lint flagging functions with multiple separated clusters of complex + /// conditional logic ("bumpy road" complexity profiles). + pub BUMPY_ROAD_FUNCTION, + Warn, + "functions should avoid multiple separated clusters of complex conditional logic", + BumpyRoadFunction::default() + } } +pub use declaration::BUMPY_ROAD_FUNCTION; + /// Lint pass that caches configuration and localization for a crate. pub struct BumpyRoadFunction { settings: Settings, @@ -62,7 +81,7 @@ impl<'tcx> LateLintPass<'tcx> for BumpyRoadFunction { return; }; - self.analyse_if_not_expanded(cx, item.span, target); + self.analyse_if_not_expanded(cx, item.span, &target); } fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'tcx>) { @@ -70,7 +89,7 @@ impl<'tcx> LateLintPass<'tcx> for BumpyRoadFunction { return; }; - self.analyse_if_not_expanded(cx, item.span, target); + self.analyse_if_not_expanded(cx, item.span, &target); } fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'tcx>) { @@ -78,7 +97,7 @@ impl<'tcx> LateLintPass<'tcx> for BumpyRoadFunction { return; }; - self.analyse_if_not_expanded(cx, item.span, target); + self.analyse_if_not_expanded(cx, item.span, &target); } fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) { @@ -90,12 +109,12 @@ impl<'tcx> LateLintPass<'tcx> for BumpyRoadFunction { return; }; - self.analyse_if_not_expanded(cx, expr.span, target); + self.analyse_if_not_expanded(cx, expr.span, &target); } } impl BumpyRoadFunction { - fn analyse_if_not_expanded(&self, cx: &LateContext<'_>, span: Span, target: AnalysisTarget) { + fn analyse_if_not_expanded(&self, cx: &LateContext<'_>, span: Span, target: &AnalysisTarget) { if span.from_expansion() { return; } @@ -176,7 +195,7 @@ struct AnalysisTarget { fn analyse_body( cx: &LateContext<'_>, - target: AnalysisTarget, + target: &AnalysisTarget, settings: &Settings, localizer: &Localizer, ) { @@ -207,7 +226,7 @@ fn analyse_body( }; let smoothed = match smooth_moving_average(&signal, settings.window) { - Ok(signal) => signal, + Ok(smoothed) => smoothed, Err(error) => { cx.tcx.sess.dcx().span_delayed_bug( body_span, diff --git a/crates/bumpy_road_function/src/driver/segment_builder.rs b/crates/bumpy_road_function/src/driver/segment_builder.rs index c44d73279..00839f225 100644 --- a/crates/bumpy_road_function/src/driver/segment_builder.rs +++ b/crates/bumpy_road_function/src/driver/segment_builder.rs @@ -134,6 +134,10 @@ impl<'a, 'tcx> SegmentBuilder<'a, 'tcx> { self.push_segment(span, self.settings.weights.flow); } + #[expect( + clippy::float_arithmetic, + reason = "predicate weights are floating-point tuning parameters scaled by branch counts" + )] fn push_predicate_segment(&mut self, expr: &'tcx hir::Expr<'tcx>) { if matches!(expr.kind, ExprKind::Let(..)) { return; diff --git a/crates/bumpy_road_function/src/lib.rs b/crates/bumpy_road_function/src/lib.rs index b3f3ced3b..8eac58230 100644 --- a/crates/bumpy_road_function/src/lib.rs +++ b/crates/bumpy_road_function/src/lib.rs @@ -11,8 +11,13 @@ pub mod analysis; #[cfg(feature = "dylint-driver")] mod driver; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{BUMPY_ROAD_FUNCTION, BumpyRoadFunction}; #[cfg(not(feature = "dylint-driver"))] mod stub { diff --git a/crates/conditional_max_n_branches/src/driver.rs b/crates/conditional_max_n_branches/src/driver.rs index 2dd337f45..1dd499a40 100644 --- a/crates/conditional_max_n_branches/src/driver.rs +++ b/crates/conditional_max_n_branches/src/driver.rs @@ -60,12 +60,18 @@ impl Default for ConditionalMaxNBranches { } } -/// Registration glue expanded from `dylint_linting::impl_late_lint!`. +/// Dylint lint declaration and registration glue. /// -/// The macro generates undocumented public registration items, so the -/// expansion is scoped to this private module and only the documented lint -/// static is re-exported. -mod registration { +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + use super::ConditionalMaxNBranches; dylint_linting::impl_late_lint! { @@ -78,7 +84,7 @@ mod registration { } } -pub use registration::CONDITIONAL_MAX_N_BRANCHES; +pub use declaration::CONDITIONAL_MAX_N_BRANCHES; impl<'tcx> LateLintPass<'tcx> for ConditionalMaxNBranches { fn check_crate(&mut self, _cx: &LateContext<'tcx>) { diff --git a/crates/conditional_max_n_branches/src/lib.rs b/crates/conditional_max_n_branches/src/lib.rs index 6f94e6016..6abe4970f 100644 --- a/crates/conditional_max_n_branches/src/lib.rs +++ b/crates/conditional_max_n_branches/src/lib.rs @@ -4,8 +4,13 @@ #[cfg(feature = "dylint-driver")] mod driver; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{CONDITIONAL_MAX_N_BRANCHES, ConditionalMaxNBranches}; #[cfg(not(feature = "dylint-driver"))] mod stub { diff --git a/crates/function_attrs_follow_docs/src/driver.rs b/crates/function_attrs_follow_docs/src/driver.rs index 05e68ce26..1ab6e4d7d 100644 --- a/crates/function_attrs_follow_docs/src/driver.rs +++ b/crates/function_attrs_follow_docs/src/driver.rs @@ -32,13 +32,31 @@ impl Default for FunctionAttrsFollowDocs { } } -dylint_linting::impl_late_lint! { - pub FUNCTION_ATTRS_FOLLOW_DOCS, - Warn, - "doc comments on functions must precede other outer attributes", - FunctionAttrsFollowDocs::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::FunctionAttrsFollowDocs; + + dylint_linting::impl_late_lint! { + /// Warns when outer attributes on a function precede its doc comments. + pub FUNCTION_ATTRS_FOLLOW_DOCS, + Warn, + "doc comments on functions must precede other outer attributes", + FunctionAttrsFollowDocs::default() + } } +pub use declaration::FUNCTION_ATTRS_FOLLOW_DOCS; + impl<'tcx> LateLintPass<'tcx> for FunctionAttrsFollowDocs { fn check_crate(&mut self, _cx: &LateContext<'tcx>) { let shared_config = SharedConfig::load(); @@ -50,7 +68,7 @@ impl<'tcx> LateLintPass<'tcx> for FunctionAttrsFollowDocs { if let hir::ItemKind::Fn { .. } = item.kind { self.check_item_attributes( cx, - ItemInfo::new(item.hir_id(), item.span, FunctionKind::Function), + &ItemInfo::new(item.hir_id(), item.span, FunctionKind::Function), ); } } @@ -59,7 +77,7 @@ impl<'tcx> LateLintPass<'tcx> for FunctionAttrsFollowDocs { if let hir::ImplItemKind::Fn(..) = item.kind { self.check_item_attributes( cx, - ItemInfo::new(item.hir_id(), item.span, FunctionKind::Method), + &ItemInfo::new(item.hir_id(), item.span, FunctionKind::Method), ); } } @@ -68,7 +86,7 @@ impl<'tcx> LateLintPass<'tcx> for FunctionAttrsFollowDocs { if let hir::TraitItemKind::Fn(..) = item.kind { self.check_item_attributes( cx, - ItemInfo::new(item.hir_id(), item.span, FunctionKind::TraitMethod), + &ItemInfo::new(item.hir_id(), item.span, FunctionKind::TraitMethod), ); } } @@ -88,9 +106,9 @@ impl ItemInfo { } impl<'tcx> FunctionAttrsFollowDocs { - fn check_item_attributes(&self, cx: &LateContext<'tcx>, item: ItemInfo) { + fn check_item_attributes(&self, cx: &LateContext<'tcx>, item: &ItemInfo) { let attrs = cx.tcx.hir_attrs(item.hir_id); - check_function_attributes(FunctionAttributeCheck { + check_function_attributes(&FunctionAttributeCheck { cx, attrs, item_span: item.span, @@ -223,7 +241,7 @@ struct FunctionAttributeCheck<'tcx, 'a> { localizer: &'a Localizer, } -fn check_function_attributes(check: FunctionAttributeCheck<'_, '_>) { +fn check_function_attributes(check: &FunctionAttributeCheck<'_, '_>) { let item_user_editable_span = recover_user_editable_hir_span(check.item_span); let mut infos: Vec = check .attrs @@ -244,8 +262,9 @@ fn check_function_attributes(check: FunctionAttributeCheck<'_, '_>) { return; }; - let doc = &infos[doc_index]; - let offending = &infos[offending_index]; + let (Some(doc), Some(offending)) = (infos.get(doc_index), infos.get(offending_index)) else { + return; + }; let diagnostic_context = DiagnosticContext { doc_span: doc.span(), offending_span: offending.span(), @@ -265,7 +284,7 @@ fn attribute_within_item( item_span: Option, raw_item_span: Span, ) -> bool { - let Some(attribute_span) = attribute_span else { + let Some(editable_attribute_span) = attribute_span else { return false; }; @@ -273,14 +292,15 @@ fn attribute_within_item( return true; } - let item_span = item_span.unwrap_or(raw_item_span); + let bounding_item_span = item_span.unwrap_or(raw_item_span); // Modern nightlies exclude attributes from the item span, so outer // attributes sit immediately before it. Accept spans contained in the // item (older behaviour and inner attributes) or preceding it (outer // attributes on current nightlies). - let contained = attribute_span.lo() >= item_span.lo() && attribute_span.hi() <= item_span.hi(); - let precedes = attribute_span.hi() <= item_span.lo(); + let contained = editable_attribute_span.lo() >= bounding_item_span.lo() + && editable_attribute_span.hi() <= bounding_item_span.hi(); + let precedes = editable_attribute_span.hi() <= bounding_item_span.lo(); contained || precedes } @@ -359,10 +379,10 @@ fn fallback_messages(kind: FunctionKind, attribute: &str) -> FunctionAttrsMessag } fn attribute_label(cx: &LateContext<'_>, span: Span, localizer: &Localizer) -> String { - match cx.sess().source_map().span_to_snippet(span) { - Ok(snippet) => snippet.trim().to_owned(), - Err(_) => attribute_fallback(localizer), - } + cx.sess().source_map().span_to_snippet(span).map_or_else( + |_| attribute_fallback(localizer), + |snippet| snippet.trim().to_owned(), + ) } fn attribute_fallback(lookup: &impl BundleLookup) -> String { diff --git a/crates/function_attrs_follow_docs/src/lib.rs b/crates/function_attrs_follow_docs/src/lib.rs index 23f486797..c1535c6be 100644 --- a/crates/function_attrs_follow_docs/src/lib.rs +++ b/crates/function_attrs_follow_docs/src/lib.rs @@ -15,8 +15,13 @@ #[cfg(feature = "dylint-driver")] mod driver; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{FUNCTION_ATTRS_FOLLOW_DOCS, FunctionAttrsFollowDocs}; #[cfg(not(feature = "dylint-driver"))] mod stub { diff --git a/crates/module_max_lines/src/driver.rs b/crates/module_max_lines/src/driver.rs index 7ff134f37..2dbf7afac 100644 --- a/crates/module_max_lines/src/driver.rs +++ b/crates/module_max_lines/src/driver.rs @@ -26,13 +26,31 @@ enum ModuleDisposition { ExceedsLimit, } -dylint_linting::impl_late_lint! { - pub MODULE_MAX_LINES, - Warn, - "modules should stay within the configured maximum line count", - ModuleMaxLines::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::ModuleMaxLines; + + dylint_linting::impl_late_lint! { + /// Warns when a module exceeds the configured maximum line count. + pub MODULE_MAX_LINES, + Warn, + "modules should stay within the configured maximum line count", + ModuleMaxLines::default() + } } +pub use declaration::MODULE_MAX_LINES; + /// Lint pass that tracks configuration and localization state while checking modules. pub struct ModuleMaxLines { max_lines: usize, @@ -56,9 +74,8 @@ impl<'tcx> LateLintPass<'tcx> for ModuleMaxLines { } fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) { - let (ident, module) = match item.kind { - hir::ItemKind::Mod(ident, module) => (ident, module), - _ => return, + let hir::ItemKind::Mod(ident, module) = item.kind else { + return; }; let span = module_body_span(cx, item, module); @@ -125,7 +142,7 @@ fn count_lines(source_map: &SourceMap, span: Span) -> Option { let contiguous = info .lines .windows(2) - .all(|pair| pair[1].line_index == pair[0].line_index + 1); + .all(|pair| matches!(pair, [previous, next] if next.line_index == previous.line_index + 1)); if !contiguous { debug!( target: LINT_NAME, @@ -152,8 +169,12 @@ fn emit_diagnostic(cx: &LateContext<'_>, info: &ModuleDiagnosticInfo, localizer: let mut args: Arguments<'_> = Arguments::default(); let module_name = info.ident.name.as_str(); args.insert(Cow::Borrowed("module"), FluentValue::from(module_name)); - args.insert(Cow::Borrowed("lines"), FluentValue::from(info.lines as i64)); - args.insert(Cow::Borrowed("limit"), FluentValue::from(info.limit as i64)); + // Fluent arguments are `i64`; module sizes never approach the bound, so + // saturating keeps the diagnostic honest without a fallible path. + let line_count = i64::try_from(info.lines).unwrap_or(i64::MAX); + let line_limit = i64::try_from(info.limit).unwrap_or(i64::MAX); + args.insert(Cow::Borrowed("lines"), FluentValue::from(line_count)); + args.insert(Cow::Borrowed("limit"), FluentValue::from(line_limit)); let resolution = MessageResolution { lint_name: LINT_NAME, diff --git a/crates/module_max_lines/src/lib.rs b/crates/module_max_lines/src/lib.rs index 840470ad6..c83e2af7d 100644 --- a/crates/module_max_lines/src/lib.rs +++ b/crates/module_max_lines/src/lib.rs @@ -8,8 +8,13 @@ #[cfg(feature = "dylint-driver")] mod driver; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{MODULE_MAX_LINES, ModuleMaxLines}; #[cfg(not(feature = "dylint-driver"))] mod stub { diff --git a/crates/module_must_have_inner_docs/src/driver/inner_attr.rs b/crates/module_must_have_inner_docs/src/driver/inner_attr.rs index d73b62d6f..1bdf4b3fc 100644 --- a/crates/module_must_have_inner_docs/src/driver/inner_attr.rs +++ b/crates/module_must_have_inner_docs/src/driver/inner_attr.rs @@ -38,7 +38,8 @@ fn has_case_incorrect_doc_in_meta_list(list: &str) -> bool { } } - segment_has_case_incorrect_doc(&list[state.start..]) + list.get(state.start..) + .is_some_and(segment_has_case_incorrect_doc) } // Extracted to reduce nested complexity in `has_case_incorrect_doc_in_meta_list`. @@ -47,7 +48,10 @@ fn handle_character(ch: char, state: &mut ParserState, list: &str, idx: usize) - '(' => state.depth += 1, ')' => state.depth = state.depth.saturating_sub(1), ',' if state.depth == 0 => { - if segment_has_case_incorrect_doc(&list[state.start..idx]) { + if list + .get(state.start..idx) + .is_some_and(segment_has_case_incorrect_doc) + { return true; } state.start = idx + 1; @@ -89,8 +93,12 @@ fn cfg_attr_has_case_incorrect_doc(rest: ParseInput<'_>) -> bool { return false; }; - let args = &content[..close_idx]; - let attr_section = &args[attr_section_start + 1..]; + let Some(args) = content.get(..close_idx) else { + return false; + }; + let Some(attr_section) = args.get(attr_section_start + 1..) else { + return false; + }; has_case_incorrect_doc_in_meta_list(attr_section) } @@ -103,7 +111,7 @@ fn inner_attribute_body(rest: ParseInput<'_>) -> Option> { // Missing `]` is tolerated here: downstream identifier parsing will // gracefully reject malformed content by failing to match expected patterns. let attr_end = body.find(']').unwrap_or(body.len()); - Some(AttributeBody::from(&body[..attr_end])) + body.get(..attr_end).map(AttributeBody::from) } /// Detects inner attributes like `#![DOC = ...]` or `#![cfg_attr(..., Doc = ...)]` diff --git a/crates/module_must_have_inner_docs/src/driver/mod.rs b/crates/module_must_have_inner_docs/src/driver/mod.rs index 6ca554f1c..994c395b4 100644 --- a/crates/module_must_have_inner_docs/src/driver/mod.rs +++ b/crates/module_must_have_inner_docs/src/driver/mod.rs @@ -9,7 +9,6 @@ use std::borrow::Cow; use log::debug; -use newt_hype::base_newtype; use rustc_hir as hir; use rustc_lint::{LateContext, LateLintPass, LintContext}; #[cfg(test)] @@ -26,7 +25,24 @@ use whitaker_common::i18n::{ mod inner_attr; mod parser; -base_newtype!(StrWrapper); +/// Shared string newtype backing the parser's snippet aliases. +/// +/// `newt_hype::base_newtype!` emits both `Copy` and an explicit `Clone` impl. +/// The impl is generated inside the external macro, so it has no source +/// location that could be changed to a derive; isolating the invocation keeps +/// the expectation scoped to exactly that generated impl. +mod str_wrapper { + #![expect( + clippy::expl_impl_clone_on_copy, + reason = "newt_hype::base_newtype! emits an explicit Clone impl on a Copy type" + )] + + use newt_hype::base_newtype; + + base_newtype!(StrWrapper); +} + +pub use str_wrapper::StrWrapper; pub type SourceSnippet<'a> = StrWrapper<&'a str>; pub type AttributeBody<'a> = StrWrapper<&'a str>; @@ -52,13 +68,31 @@ impl<'a> ParseInput<'a> { const LINT_NAME: &str = "module_must_have_inner_docs"; const MESSAGE_KEY: MessageKey<'static> = MessageKey::new(LINT_NAME); -dylint_linting::impl_late_lint! { - pub MODULE_MUST_HAVE_INNER_DOCS, - Warn, - "modules must begin with an inner doc comment", - ModuleMustHaveInnerDocs::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::ModuleMustHaveInnerDocs; + + dylint_linting::impl_late_lint! { + /// Warns when a module body does not open with an inner doc comment. + pub MODULE_MUST_HAVE_INNER_DOCS, + Warn, + "modules must begin with an inner doc comment", + ModuleMustHaveInnerDocs::default() + } } +pub use declaration::MODULE_MUST_HAVE_INNER_DOCS; + /// Lint pass enforcing leading inner doc comments on modules. pub struct ModuleMustHaveInnerDocs { localizer: Localizer, @@ -79,9 +113,8 @@ impl<'tcx> LateLintPass<'tcx> for ModuleMustHaveInnerDocs { } fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) { - let (ident, module) = match item.kind { - hir::ItemKind::Mod(ident, module) => (ident, module), - _ => return, + let hir::ItemKind::Mod(ident, module) = item.kind else { + return; }; if item.span.from_expansion() { @@ -192,16 +225,17 @@ fn has_inner_doc(rest: ParseInput<'_>) -> bool { let snippet = rest.as_str(); let mut line_start = 0; - while line_start < snippet.len() { - let line_end = snippet[line_start..] - .find('\n') - .map_or(snippet.len(), |idx| line_start + idx); - let line = &snippet[line_start..line_end]; + // `split_inclusive` keeps each terminator attached, so accumulating the + // yielded lengths reproduces the byte offset of every line start. + for line_with_terminator in snippet.split_inclusive('\n') { + let line = line_with_terminator + .strip_suffix('\n') + .unwrap_or(line_with_terminator); if check_line_for_inner_doc(snippet, line, line_start) { return true; } - line_start = line_end.saturating_add(1); + line_start = line_start.saturating_add(line_with_terminator.len()); } false @@ -223,10 +257,13 @@ fn check_line_for_inner_doc(snippet: &str, line: &str, line_start: usize) -> boo search_start = offset.saturating_add(2); } - while let Some(local_idx) = line[search_start..].find("#!") { + while let Some(local_idx) = line.get(search_start..).and_then(|tail| tail.find("#!")) { let absolute_idx = search_start + local_idx; - let offset = line_start + absolute_idx; - if parser::is_doc_comment(ParseInput::from(&snippet[offset..])) { + let snippet_offset = line_start + absolute_idx; + let Some(tail) = snippet.get(snippet_offset..) else { + break; + }; + if parser::is_doc_comment(ParseInput::from(tail)) { return true; } search_start = absolute_idx + 2; @@ -282,8 +319,13 @@ fn primary_span_for_disposition( fn first_token_span(module_body: Span, offset: usize, len: usize) -> Span { let base = module_body.shrink_to_lo(); - let start = base.lo() + BytePos(offset as u32); - let hi = start + BytePos(len.max(1) as u32); + // Source files never exceed `u32::MAX` bytes, so a failed conversion means + // the caller supplied a nonsensical offset; fall back to the module start. + let (Ok(byte_offset), Ok(byte_len)) = (u32::try_from(offset), u32::try_from(len.max(1))) else { + return base; + }; + let start = base.lo() + BytePos(byte_offset); + let hi = start + BytePos(byte_len); base.with_lo(start).with_hi(hi) } diff --git a/crates/module_must_have_inner_docs/src/driver/parser.rs b/crates/module_must_have_inner_docs/src/driver/parser.rs index e5292ad2b..72a7402a7 100644 --- a/crates/module_must_have_inner_docs/src/driver/parser.rs +++ b/crates/module_must_have_inner_docs/src/driver/parser.rs @@ -78,8 +78,9 @@ pub(super) fn is_doc_comment(rest: ParseInput<'_>) -> bool { let (_, tail) = skip_leading_whitespace(ParseInput::from(after_bang)); if let Some(body) = tail.strip_prefix('[') { let attr_end = body.find(']').unwrap_or(body.len()); - let attr_body = AttributeBody::from(&body[..attr_end]); - return is_doc_attr(attr_body); + return body + .get(..attr_end) + .is_some_and(|attr_body| is_doc_attr(AttributeBody::from(attr_body))); } } false @@ -114,12 +115,12 @@ pub(super) fn take_ident(input: ParseInput<'_>) -> Option<(ParseInput<'_>, Parse let (_, trimmed) = skip_leading_whitespace(input); let trimmed_str = trimmed.as_str(); let mut iter = trimmed_str.char_indices(); - let (start, ch) = iter.next()?; - if !is_ident_start(ch) { + let (start, first_ch) = iter.next()?; + if !is_ident_start(first_ch) { return None; } - let mut end = start + ch.len_utf8(); + let mut end = start + first_ch.len_utf8(); for (idx, ch) in iter { if is_ident_continue(ch) { end = idx + ch.len_utf8(); @@ -128,8 +129,8 @@ pub(super) fn take_ident(input: ParseInput<'_>) -> Option<(ParseInput<'_>, Parse } } - let ident = ParseInput::from(&trimmed_str[..end]); - Some((ident, ParseInput::from(&trimmed_str[end..]))) + let (ident, remainder) = trimmed_str.split_at_checked(end)?; + Some((ParseInput::from(ident), ParseInput::from(remainder))) } // Detects documentation by matching a `doc` ident directly or inside `cfg_attr`. @@ -160,14 +161,16 @@ const fn is_ident_continue(ch: char) -> bool { } pub(super) fn cfg_attr_has_doc(tail: ParseInput<'_>) -> bool { - let (_, tail) = skip_leading_whitespace(tail); - let Some(args) = tail.strip_prefix('(') else { + let (_, trimmed) = skip_leading_whitespace(tail); + let Some(args) = trimmed.strip_prefix('(') else { return false; }; let Some(close_idx) = args.rfind(')') else { return false; }; - let meta_list = &args[..close_idx]; + let Some(meta_list) = args.get(..close_idx) else { + return false; + }; has_doc_in_meta_list_after_first(MetaList::from(meta_list)) } @@ -198,7 +201,7 @@ fn has_doc_in_meta_list_after_first(list: MetaList<'_>) -> bool { } } - state.seen_comma && segment_is_doc(&list_str[state.start..]) + state.seen_comma && list_str.get(state.start..).is_some_and(segment_is_doc) } fn process_char_for_doc_after_first( @@ -217,7 +220,7 @@ fn process_char_for_doc_after_first( false } ',' if state.depth == 0 => { - if state.seen_comma && segment_is_doc(&list_str[state.start..idx]) { + if state.seen_comma && list_str.get(state.start..idx).is_some_and(segment_is_doc) { return true; } state.seen_comma = true; diff --git a/crates/module_must_have_inner_docs/src/lib.rs b/crates/module_must_have_inner_docs/src/lib.rs index 1f79354b2..7821c2de2 100644 --- a/crates/module_must_have_inner_docs/src/lib.rs +++ b/crates/module_must_have_inner_docs/src/lib.rs @@ -26,5 +26,16 @@ #[cfg(feature = "dylint-driver")] mod driver; +// The snippet newtypes are parser plumbing shared between the driver's +// submodules through the crate root. They are generated by `base_newtype!`, +// so the underlying struct has no documentable source location either; +// keeping the aliases crate-visible leaves them outwith the public surface. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub(crate) use driver::{AttributeBody, MetaList, ParseInput}; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. +#[cfg(feature = "dylint-driver")] +pub use driver::{MODULE_MUST_HAVE_INNER_DOCS, ModuleMustHaveInnerDocs}; diff --git a/crates/no_expect_outside_tests/src/context/mod.rs b/crates/no_expect_outside_tests/src/context/mod.rs index 780ce278e..5ae48079b 100644 --- a/crates/no_expect_outside_tests/src/context/mod.rs +++ b/crates/no_expect_outside_tests/src/context/mod.rs @@ -195,10 +195,10 @@ fn convert_attribute(attr: &hir::Attribute) -> Attribute { return Attribute::new(AttributePath::from(PARSED_ATTRIBUTE_PLACEHOLDER), kind); }; let mut names = attr.path().into_iter().map(|symbol| symbol.to_string()); - match names.next() { - Some(first) => AttributePath::new(std::iter::once(first).chain(names)), - None => AttributePath::from("unknown"), - } + names.next().map_or_else( + || AttributePath::from("unknown"), + |first| AttributePath::new(std::iter::once(first).chain(names)), + ) }; Attribute::new(path, kind) @@ -253,17 +253,17 @@ pub(crate) fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { }; let path = attr.path(); - if path.len() != 1 { + let [name] = path.as_slice() else { return false; - } + }; - if path[0] == sym::cfg { + if *name == sym::cfg { return attr .meta_item_list() .is_some_and(|items| items.iter().cloned().any(meta_item_inner_contains_test)); } - if path[0] != sym::cfg_attr { + if *name != sym::cfg_attr { return false; } @@ -326,12 +326,12 @@ fn attribute_style(attr: &hir::Attribute) -> AttrStyle { match attr { hir::Attribute::Unparsed(item) => item.style, hir::Attribute::Parsed(HirAttributeKind::DocComment { style, .. }) => *style, - _ => AttrStyle::Outer, + hir::Attribute::Parsed(_) => AttrStyle::Outer, } } fn path_is_ident(path: &rustc_ast::Path, symbol: rustc_span::Symbol) -> bool { - path.segments.len() == 1 && path.segments[0].ident.name == symbol + matches!(&*path.segments, [segment] if segment.ident.name == symbol) } #[cfg(test)] diff --git a/crates/no_expect_outside_tests/src/context/tests.rs b/crates/no_expect_outside_tests/src/context/tests.rs index 3a527df66..cd624e27c 100644 --- a/crates/no_expect_outside_tests/src/context/tests.rs +++ b/crates/no_expect_outside_tests/src/context/tests.rs @@ -54,6 +54,25 @@ enum AttributeFixture { CfgAndBuiltInTest, } +/// Outcome expected from `has_test_ancestry` for a table-driven case. +#[cfg(feature = "dylint-driver")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum AncestryOutcome { + TestContext, + NotTestContext, +} + +#[cfg(feature = "dylint-driver")] +impl AncestryOutcome { + const fn from_detection(is_test_context: bool) -> Self { + if is_test_context { + Self::TestContext + } else { + Self::NotTestContext + } + } +} + #[cfg(feature = "dylint-driver")] #[derive(Clone, Copy, Debug)] struct HasTestAncestryCase { @@ -61,7 +80,7 @@ struct HasTestAncestryCase { attr_fixture: AttributeFixture, is_function_item: bool, include_custom_attribute: bool, - expected: bool, + expected: AncestryOutcome, } // Common path constants @@ -279,8 +298,12 @@ fn build_cfg_attr_test_allow() -> MetaItem { /// Helper function to test `meta_contains_test_cfg` behaviour. /// Must be called within `create_default_session_globals_then`. #[cfg(feature = "dylint-driver")] -fn assert_meta_test_cfg(meta: MetaItem, expected: bool) { - assert_eq!(meta_contains_test_cfg(&meta), expected); +fn assert_meta_test_cfg(meta: &MetaItem, expected: bool) { + assert_eq!( + meta_contains_test_cfg(meta), + expected, + "meta item should report test-cfg detection as {expected}" + ); } /// Asserts that `convert_attribute` preserves path segments for the given @@ -340,42 +363,42 @@ fn build_additional_test_attributes(include_custom: bool) -> Vec attr_fixture: AttributeFixture::None, is_function_item: false, include_custom_attribute: false, - expected: true, + expected: AncestryOutcome::TestContext, })] #[case::cfg_test_attribute_detected(HasTestAncestryCase { has_test_context_ancestry: false, attr_fixture: AttributeFixture::CfgTest, is_function_item: false, include_custom_attribute: false, - expected: true, + expected: AncestryOutcome::TestContext, })] #[case::built_in_test_attribute_on_function_item(HasTestAncestryCase { has_test_context_ancestry: false, attr_fixture: AttributeFixture::BuiltInTest, is_function_item: true, include_custom_attribute: false, - expected: true, + expected: AncestryOutcome::TestContext, })] #[case::configured_test_attribute_on_function_item(HasTestAncestryCase { has_test_context_ancestry: false, attr_fixture: AttributeFixture::CustomTest, is_function_item: true, include_custom_attribute: true, - expected: true, + expected: AncestryOutcome::TestContext, })] #[case::all_detection_paths_together(HasTestAncestryCase { has_test_context_ancestry: true, attr_fixture: AttributeFixture::CfgAndBuiltInTest, is_function_item: true, include_custom_attribute: true, - expected: true, + expected: AncestryOutcome::TestContext, })] #[case::negative_case(HasTestAncestryCase { has_test_context_ancestry: false, attr_fixture: AttributeFixture::Allow, is_function_item: false, include_custom_attribute: false, - expected: false, + expected: AncestryOutcome::NotTestContext, })] fn has_test_ancestry_detects_test_context(#[case] case: HasTestAncestryCase) { create_default_session_globals_then(|| { @@ -383,14 +406,16 @@ fn has_test_ancestry_detects_test_context(#[case] case: HasTestAncestryCase) { let additional_test_attributes = build_additional_test_attributes(case.include_custom_attribute); + let outcome = AncestryOutcome::from_detection(has_test_ancestry( + case.has_test_context_ancestry, + &attrs, + case.is_function_item, + &additional_test_attributes, + )); + assert_eq!( - has_test_ancestry( - case.has_test_context_ancestry, - &attrs, - case.is_function_item, - &additional_test_attributes, - ), - case.expected, + outcome, case.expected, + "test-ancestry detection should match the expected outcome for {case:?}" ); }); } @@ -400,7 +425,7 @@ fn has_test_ancestry_detects_test_context(#[case] case: HasTestAncestryCase) { #[test] fn meta_contains_test_cfg_any_test_doctest() { create_default_session_globals_then(|| { - assert_meta_test_cfg(build_cfg_any_test_doctest(), true); + assert_meta_test_cfg(&build_cfg_any_test_doctest(), true); }); } @@ -409,7 +434,7 @@ fn meta_contains_test_cfg_any_test_doctest() { #[test] fn meta_contains_test_cfg_all_test_unix() { create_default_session_globals_then(|| { - assert_meta_test_cfg(build_cfg_all_test_unix(), true); + assert_meta_test_cfg(&build_cfg_all_test_unix(), true); }); } @@ -418,7 +443,7 @@ fn meta_contains_test_cfg_all_test_unix() { #[test] fn meta_contains_test_cfg_not_test() { create_default_session_globals_then(|| { - assert_meta_test_cfg(build_cfg_not_test(), false); + assert_meta_test_cfg(&build_cfg_not_test(), false); }); } @@ -427,7 +452,7 @@ fn meta_contains_test_cfg_not_test() { #[test] fn meta_contains_test_cfg_attr_test_cfg_test() { create_default_session_globals_then(|| { - assert_meta_test_cfg(build_cfg_attr_test_cfg_test(), true); + assert_meta_test_cfg(&build_cfg_attr_test_cfg_test(), true); }); } @@ -436,7 +461,7 @@ fn meta_contains_test_cfg_attr_test_cfg_test() { #[test] fn meta_contains_test_cfg_attr_test_allow() { create_default_session_globals_then(|| { - assert_meta_test_cfg(build_cfg_attr_test_allow(), false); + assert_meta_test_cfg(&build_cfg_attr_test_allow(), false); }); } diff --git a/crates/no_expect_outside_tests/src/driver/mod.rs b/crates/no_expect_outside_tests/src/driver/mod.rs index 3b57ddb48..8b6bf4c80 100644 --- a/crates/no_expect_outside_tests/src/driver/mod.rs +++ b/crates/no_expect_outside_tests/src/driver/mod.rs @@ -26,13 +26,31 @@ use whitaker_common::{AttributePath, Localizer, get_localizer_for_lint}; use crate::context::{collect_context, is_cfg_test_attribute, summarize_context}; use crate::diagnostics::{DiagnosticContext, emit_diagnostic}; -dylint_linting::impl_late_lint! { - pub NO_EXPECT_OUTSIDE_TESTS, - Deny, - "`.expect(..)` must not be used outside of test or doctest contexts", - NoExpectOutsideTests::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::NoExpectOutsideTests; + + dylint_linting::impl_late_lint! { + /// Denies `.expect(..)` calls outside test and doctest contexts. + pub NO_EXPECT_OUTSIDE_TESTS, + Deny, + "`.expect(..)` must not be used outside of test or doctest contexts", + NoExpectOutsideTests::default() + } } +pub use declaration::NO_EXPECT_OUTSIDE_TESTS; + #[derive(Default, Deserialize)] struct Config { #[serde(default)] @@ -153,12 +171,12 @@ fn receiver_is_option_or_result<'tcx>( } fn ty_is_option_or_result<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { - let ty = cx + let normalized = cx .tcx .normalize_erasing_regions(cx.typing_env(), ty::Unnormalized::new_wip(ty)) .peel_refs(); - let Some(adt) = ty.ty_adt_def() else { + let Some(adt) = normalized.ty_adt_def() else { return false; }; diff --git a/crates/no_expect_outside_tests/src/driver/tests.rs b/crates/no_expect_outside_tests/src/driver/tests.rs index 121d8f1bc..0c01dbbca 100644 --- a/crates/no_expect_outside_tests/src/driver/tests.rs +++ b/crates/no_expect_outside_tests/src/driver/tests.rs @@ -113,6 +113,7 @@ fn assert_has_test_like_attributes( assert_eq!( has_test_like_hir_attributes(&attrs, additional_test_attributes), expected, + "attribute segments {attr_segments:?} should report test-like as {expected}", ); }); } @@ -129,7 +130,10 @@ fn has_test_like_hir_attributes_detects_test_attributes(#[case] case_index: usiz &[&["tokio", "test"]], &[&["core", "prelude", "v1", "test"]], ]; - assert_has_test_like_attributes(test_cases[case_index], &[], true); + let segments = test_cases + .get(case_index) + .expect("case index should address a declared test case"); + assert_has_test_like_attributes(segments, &[], true); } #[test] diff --git a/crates/no_expect_outside_tests/src/lib.rs b/crates/no_expect_outside_tests/src/lib.rs index f399d4d23..2d3372fb9 100644 --- a/crates/no_expect_outside_tests/src/lib.rs +++ b/crates/no_expect_outside_tests/src/lib.rs @@ -25,8 +25,13 @@ mod ui { whitaker::declare_ui_tests!("ui"); } +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{NO_EXPECT_OUTSIDE_TESTS, NoExpectOutsideTests}; #[cfg(not(feature = "dylint-driver"))] mod stub { diff --git a/crates/no_std_fs_operations/src/behaviour.rs b/crates/no_std_fs_operations/src/behaviour.rs index 957d3bfdb..0a3488827 100644 --- a/crates/no_std_fs_operations/src/behaviour.rs +++ b/crates/no_std_fs_operations/src/behaviour.rs @@ -21,7 +21,7 @@ impl LocalizationWorld { } fn set_operation(&mut self, operation: &str) { - self.operation = operation.to_owned(); + operation.clone_into(&mut self.operation); } const fn mark_failure(&mut self) { @@ -33,26 +33,26 @@ impl LocalizationWorld { let result = if self.failing { localized_messages(&FailingLookup::new("no_std_fs_operations"), &op) } else { - let localizer = self.localizer.as_ref().expect("a locale must be selected"); + let Some(localizer) = self.localizer.as_ref() else { + panic!("a locale must be selected before resolving messages") + }; localized_messages(localizer, &op) }; self.result = Some(result); } fn messages(&self) -> &StdFsMessages { - self.result - .as_ref() - .expect("localization result should be recorded") - .as_ref() - .expect("localization should succeed") + let Some(Ok(messages)) = self.result.as_ref().map(Result::as_ref) else { + panic!("localization should have been resolved successfully") + }; + messages } fn error(&self) -> &I18nError { - self.result - .as_ref() - .expect("localization result should be recorded") - .as_ref() - .expect_err("localization should fail") + let Some(Err(error)) = self.result.as_ref().map(Result::as_ref) else { + panic!("localization should have been resolved to a failure") + }; + error } } @@ -92,21 +92,30 @@ fn when_localize(world: &WorldCell) { fn then_primary(world: &WorldCell, snippet: String) { let needle = snippet.trim_matches('"'); let borrow = world.borrow(); - assert!(borrow.messages().primary().contains(needle)); + assert!( + borrow.messages().primary().contains(needle), + "primary message should mention `{needle}`" + ); } #[then("the note references {snippet}")] fn then_note(world: &WorldCell, snippet: String) { let needle = snippet.trim_matches('"'); let borrow = world.borrow(); - assert!(borrow.messages().note().contains(needle)); + assert!( + borrow.messages().note().contains(needle), + "note message should mention `{needle}`" + ); } #[then("the help references {snippet}")] fn then_help(world: &WorldCell, snippet: String) { let needle = snippet.trim_matches('"'); let borrow = world.borrow(); - assert!(borrow.messages().help().contains(needle)); + assert!( + borrow.messages().help().contains(needle), + "help message should mention `{needle}`" + ); } #[then("localization fails for {key}")] @@ -114,7 +123,11 @@ fn then_failure(world: &WorldCell, key: String) { let borrow = world.borrow(); match borrow.error() { I18nError::MissingMessage { key: missing, .. } => { - assert_eq!(missing, &key.trim_matches('"')); + assert_eq!( + missing, + &key.trim_matches('"'), + "localization should fail for the requested key" + ); } } } diff --git a/crates/no_std_fs_operations/src/diagnostics.rs b/crates/no_std_fs_operations/src/diagnostics.rs index 6b302231c..ea8ebcc6a 100644 --- a/crates/no_std_fs_operations/src/diagnostics.rs +++ b/crates/no_std_fs_operations/src/diagnostics.rs @@ -16,7 +16,7 @@ use whitaker_common::i18n::{BundleLookup, I18nError, resolve_message_set}; pub(crate) fn emit_diagnostic( cx: &LateContext<'_>, span: Span, - usage: StdFsUsage, + usage: &StdFsUsage, localizer: &Localizer, ) { let mut args: Arguments<'static> = Arguments::default(); @@ -40,9 +40,9 @@ pub(crate) fn emit_diagnostic( NO_STD_FS_OPERATIONS, span, rustc_lint::errors::DiagDecorator(move |lint| { - lint.primary_message(sanitize_message(messages.primary().to_owned())); - lint.note(sanitize_message(messages.note().to_owned())); - lint.help(sanitize_message(messages.help().to_owned())); + lint.primary_message(sanitize_message(messages.primary())); + lint.note(sanitize_message(messages.note())); + lint.help(sanitize_message(messages.help())); }), ); } @@ -67,7 +67,7 @@ fn fallback_messages(operation: &str) -> StdFsMessages { DiagnosticMessageSet::new(primary, note, help) } -fn sanitize_message(text: String) -> String { +fn sanitize_message(text: &str) -> String { text.chars() .filter(|ch| !matches!(ch, '\u{2068}' | '\u{2069}')) .collect() @@ -94,13 +94,13 @@ mod tests { #[test] fn removes_isolation_marks() { - let raw = String::from("\u{2068}std::fs::read\u{2069}"); + let raw = "\u{2068}std::fs::read\u{2069}"; assert_eq!(sanitize_message(raw), "std::fs::read"); } #[test] fn preserves_clean_text() { - let raw = String::from("std::fs::File::open"); - assert_eq!(sanitize_message(raw.clone()), raw); + let raw = "std::fs::File::open"; + assert_eq!(sanitize_message(raw), raw); } } diff --git a/crates/no_std_fs_operations/src/driver.rs b/crates/no_std_fs_operations/src/driver.rs index ea096af0d..94d593401 100644 --- a/crates/no_std_fs_operations/src/driver.rs +++ b/crates/no_std_fs_operations/src/driver.rs @@ -18,6 +18,8 @@ use whitaker_common::SimplePath; use whitaker_common::i18n::Localizer; use whitaker_common::i18n::get_localizer_for_lint; +/// Lint pass that tracks localization and exclusion state while checking +/// `std::fs` usage. pub struct NoStdFsOperations { localizer: Localizer, excluded: bool, @@ -34,13 +36,31 @@ impl Default for NoStdFsOperations { } } -dylint_linting::impl_late_lint! { - pub NO_STD_FS_OPERATIONS, - Deny, - "std::fs operations bypass Whitaker's capability-based filesystem policy", - NoStdFsOperations::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::NoStdFsOperations; + + dylint_linting::impl_late_lint! { + /// Denies direct `std::fs` use that bypasses the capability policy. + pub NO_STD_FS_OPERATIONS, + Deny, + "std::fs operations bypass Whitaker's capability-based filesystem policy", + NoStdFsOperations::default() + } } +pub use declaration::NO_STD_FS_OPERATIONS; + impl<'tcx> LateLintPass<'tcx> for NoStdFsOperations { fn check_crate(&mut self, cx: &LateContext<'tcx>) { let shared_config = SharedConfig::load(); @@ -106,7 +126,7 @@ impl<'tcx> LateLintPass<'tcx> for NoStdFsOperations { .and_then(|def_id| classify_def_id(cx, def_id, UsageCategory::Call)); if usage.is_none() { - usage = self.receiver_usage_for_method(cx, receiver, segment.ident.as_str()); + usage = Self::receiver_usage_for_method(cx, receiver, segment.ident.as_str()); } self.emit_optional(cx, site, usage); @@ -155,7 +175,7 @@ impl NoStdFsOperations { if self.should_skip() { return; } - let Some(usage) = usage else { + let Some(detected) = usage else { return; }; // Resolve the enclosing item's path only for genuine `std::fs` hits @@ -164,7 +184,7 @@ impl NoStdFsOperations { if self.is_path_excluded(cx, site.hir_id) { return; } - self.emit(cx, site.span, usage); + self.emit(cx, site.span, &detected); } /// Returns `true` when the item enclosing `hir_id` falls within a @@ -177,12 +197,11 @@ impl NoStdFsOperations { .excludes(&enclosing_item_path(cx, hir_id)) } - fn emit(&self, cx: &LateContext<'_>, span: Span, usage: StdFsUsage) { + fn emit(&self, cx: &LateContext<'_>, span: Span, usage: &StdFsUsage) { emit_diagnostic(cx, span, usage, &self.localizer); } fn receiver_usage_for_method( - &self, cx: &LateContext<'_>, receiver: &hir::Expr<'_>, method: &str, diff --git a/crates/no_std_fs_operations/src/exclusion.rs b/crates/no_std_fs_operations/src/exclusion.rs index 85f18a156..eb6d481f9 100644 --- a/crates/no_std_fs_operations/src/exclusion.rs +++ b/crates/no_std_fs_operations/src/exclusion.rs @@ -77,10 +77,9 @@ impl PathExclusions { /// `std::fs` usage. pub(crate) fn excludes(&self, item_path: &SimplePath) -> bool { let item = item_path.segments(); - self.prefixes.iter().any(|prefix| { - let prefix = prefix.segments(); - prefix.len() <= item.len() && item[..prefix.len()] == *prefix - }) + self.prefixes + .iter() + .any(|prefix| item.starts_with(prefix.segments())) } } @@ -105,7 +104,8 @@ fn bounded_entry(entry: &str) -> String { while !entry.is_char_boundary(end) { end -= 1; } - format!("{:?}… ({} bytes total)", &entry[..end], entry.len()) + let truncated = entry.get(..end).unwrap_or(entry); + format!("{truncated:?}… ({} bytes total)", entry.len()) } #[cfg(test)] @@ -247,9 +247,9 @@ mod tests { // Oracle: excluded iff some configured prefix is a genuine // segment-wise prefix of the item path. - let expected = configured.iter().any(|prefix| { - prefix.len() <= item.len() && item[..prefix.len()] == prefix[..] - }); + let expected = configured + .iter() + .any(|prefix| item.starts_with(prefix.as_slice())); prop_assert_eq!(exclusions.excludes(&item_path), expected); } diff --git a/crates/no_std_fs_operations/src/exclusion_behaviour.rs b/crates/no_std_fs_operations/src/exclusion_behaviour.rs index 5089acfde..f5dcd57b4 100644 --- a/crates/no_std_fs_operations/src/exclusion_behaviour.rs +++ b/crates/no_std_fs_operations/src/exclusion_behaviour.rs @@ -26,9 +26,11 @@ impl ExclusionWorld { self.suppressed = Some(exclusions.excludes(&SimplePath::parse(item_path))); } - const fn suppressed(&self) -> bool { - self.suppressed - .expect("a usage should have been evaluated before asserting") + fn suppressed(&self) -> bool { + let Some(suppressed) = self.suppressed else { + panic!("a usage should have been evaluated before asserting") + }; + suppressed } } diff --git a/crates/no_std_fs_operations/src/lib.rs b/crates/no_std_fs_operations/src/lib.rs index 7af853cb0..1db30eb08 100644 --- a/crates/no_std_fs_operations/src/lib.rs +++ b/crates/no_std_fs_operations/src/lib.rs @@ -20,5 +20,10 @@ mod tests; mod usage; #[cfg(feature = "dylint-driver")] pub use config::NoStdFsConfig; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{NO_STD_FS_OPERATIONS, NoStdFsOperations}; diff --git a/crates/no_std_fs_operations/src/usage/mod.rs b/crates/no_std_fs_operations/src/usage/mod.rs index 356b976d1..c9ad6d70b 100644 --- a/crates/no_std_fs_operations/src/usage/mod.rs +++ b/crates/no_std_fs_operations/src/usage/mod.rs @@ -147,8 +147,7 @@ pub fn classify_def_id( } fn is_std_fs_path(path: &SimplePath) -> bool { - let segments = path.segments(); - segments.len() >= 2 && segments[0] == "std" && segments[1] == "fs" + matches!(path.segments(), [first, second, ..] if first == "std" && second == "fs") } /// Returns true if the character should be rejected in a valid `std::fs` label. @@ -165,11 +164,9 @@ pub(crate) fn label_is_std_fs(label: &str) -> bool { return false; } - if !label.starts_with("std::fs") { + let Some(remainder) = label.strip_prefix("std::fs") else { return false; - } - - let remainder = &label["std::fs".len()..]; + }; if remainder.is_empty() { return true; } diff --git a/crates/no_unwrap_or_else_panic/src/context/mod.rs b/crates/no_unwrap_or_else_panic/src/context/mod.rs index 67649621f..7c3620b59 100644 --- a/crates/no_unwrap_or_else_panic/src/context/mod.rs +++ b/crates/no_unwrap_or_else_panic/src/context/mod.rs @@ -115,10 +115,10 @@ fn convert_attribute(attr: &hir::Attribute) -> Attribute { return Attribute::new(AttributePath::from(PARSED_ATTRIBUTE_PLACEHOLDER), kind); }; let mut names = attr.path().into_iter().map(|symbol| symbol.to_string()); - match names.next() { - Some(first) => AttributePath::new(std::iter::once(first).chain(names)), - None => AttributePath::from("unknown"), - } + names.next().map_or_else( + || AttributePath::from("unknown"), + |first| AttributePath::new(std::iter::once(first).chain(names)), + ) }; Attribute::new(path, kind) @@ -129,7 +129,7 @@ fn attribute_style(attr: &hir::Attribute) -> AttrStyle { match attr { hir::Attribute::Unparsed(item) => item.style, hir::Attribute::Parsed(HirAttributeKind::DocComment { style, .. }) => *style, - _ => AttrStyle::Outer, + hir::Attribute::Parsed(_) => AttrStyle::Outer, } } @@ -149,17 +149,17 @@ fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { }; let path = attr.path(); - if path.len() != 1 { + let [name] = path.as_slice() else { return false; - } + }; - if path[0] == sym::cfg { + if *name == sym::cfg { return attr .meta_item_list() .is_some_and(|items| items.iter().cloned().any(meta_item_inner_contains_test)); } - if path[0] != sym::cfg_attr { + if *name != sym::cfg_attr { return false; } @@ -226,7 +226,7 @@ fn meta_contains_test_cfg(meta: &MetaItem) -> bool { #[cfg(feature = "dylint-driver")] fn path_is_ident(path: &AstPath, ident: rustc_span::Symbol) -> bool { - path.segments.len() == 1 && path.segments[0].ident.name == ident + matches!(&*path.segments, [segment] if segment.ident.name == ident) } #[cfg(test)] diff --git a/crates/no_unwrap_or_else_panic/src/driver.rs b/crates/no_unwrap_or_else_panic/src/driver.rs index e33d27e27..377199f15 100644 --- a/crates/no_unwrap_or_else_panic/src/driver.rs +++ b/crates/no_unwrap_or_else_panic/src/driver.rs @@ -14,13 +14,31 @@ use std::collections::HashSet; use whitaker::SharedConfig; use whitaker_common::i18n::{Localizer, get_localizer_for_lint}; -dylint_linting::impl_late_lint! { - pub NO_UNWRAP_OR_ELSE_PANIC, - Deny, - "forbid `unwrap_or_else` whose closure panics (directly or via unwrap/expect)", - NoUnwrapOrElsePanic::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::NoUnwrapOrElsePanic; + + dylint_linting::impl_late_lint! { + /// Denies `unwrap_or_else` fallbacks whose closure panics. + pub NO_UNWRAP_OR_ELSE_PANIC, + Deny, + "forbid `unwrap_or_else` whose closure panics (directly or via unwrap/expect)", + NoUnwrapOrElsePanic::default() + } } +pub use declaration::NO_UNWRAP_OR_ELSE_PANIC; + #[derive(Debug, Default, Deserialize)] #[serde(default, deny_unknown_fields)] struct Config { @@ -106,7 +124,7 @@ impl<'tcx> LateLintPass<'tcx> for NoUnwrapOrElsePanic { ); let panic_info = closure_panics(cx, body_id); - if !should_flag(&self.policy, &summary, &panic_info, self.is_doctest) { + if !should_flag(self.policy, summary, panic_info, self.is_doctest) { return; } diff --git a/crates/no_unwrap_or_else_panic/src/lib.rs b/crates/no_unwrap_or_else_panic/src/lib.rs index 7407c8d8f..c862c6fae 100644 --- a/crates/no_unwrap_or_else_panic/src/lib.rs +++ b/crates/no_unwrap_or_else_panic/src/lib.rs @@ -23,8 +23,13 @@ mod panic_detector; #[cfg(feature = "dylint-driver")] mod policy; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{NO_UNWRAP_OR_ELSE_PANIC, NoUnwrapOrElsePanic}; #[cfg(not(feature = "dylint-driver"))] mod stub { diff --git a/crates/no_unwrap_or_else_panic/src/panic_detector.rs b/crates/no_unwrap_or_else_panic/src/panic_detector.rs index 433213d7c..c958aba99 100644 --- a/crates/no_unwrap_or_else_panic/src/panic_detector.rs +++ b/crates/no_unwrap_or_else_panic/src/panic_detector.rs @@ -50,7 +50,7 @@ impl PanicInfo { /// Returns `true` when the closure has at least one interpolated panic /// and no plain (non-interpolating) panic. #[must_use] - pub(crate) const fn is_interpolated_only(&self) -> bool { + pub(crate) const fn is_interpolated_only(self) -> bool { self.has_interpolated_panic && !self.has_plain_panic } } @@ -85,12 +85,12 @@ pub(crate) fn receiver_is_option_or_result<'tcx>( } fn ty_is_option_or_result<'tcx>(cx: &LateContext<'tcx>, ty: ty::Ty<'tcx>) -> bool { - let ty = cx + let normalized = cx .tcx .normalize_erasing_regions(cx.typing_env(), ty::Unnormalized::new_wip(ty)) .peel_refs(); - let Some(adt) = ty.ty_adt_def() else { + let Some(adt) = normalized.ty_adt_def() else { return false; }; diff --git a/crates/no_unwrap_or_else_panic/src/policy.rs b/crates/no_unwrap_or_else_panic/src/policy.rs index 26dc69887..f837dad72 100644 --- a/crates/no_unwrap_or_else_panic/src/policy.rs +++ b/crates/no_unwrap_or_else_panic/src/policy.rs @@ -39,13 +39,13 @@ impl LintPolicy { /// has_plain_panic: true, /// has_interpolated_panic: false, /// }; -/// assert!(should_flag(&policy, &summary, &info, false)); +/// assert!(should_flag(policy, summary, info, false)); /// ``` #[must_use] pub(crate) const fn should_flag( - policy: &LintPolicy, - summary: &ContextSummary, - panic_info: &PanicInfo, + policy: LintPolicy, + summary: ContextSummary, + panic_info: PanicInfo, is_doctest: bool, ) -> bool { if !panic_info.panics { @@ -155,13 +155,9 @@ mod tests { })] fn policy_evaluation(#[case] case: PolicyCase) { assert_eq!( - should_flag( - &case.policy, - &case.context, - &case.panic_info, - case.is_doctest - ), - case.should_flag + should_flag(case.policy, case.context, case.panic_info, case.is_doctest), + case.should_flag, + "policy evaluation should match the expected decision" ); } } diff --git a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs index 4ab597a67..4b7666b4a 100644 --- a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs +++ b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs @@ -24,9 +24,9 @@ impl DecisionWorld { const fn evaluate(&self) -> bool { let policy = LintPolicy::new(self.allow_in_main.get()); should_flag( - &policy, - &self.summary.get(), - &self.panic_info.get(), + policy, + self.summary.get(), + self.panic_info.get(), self.is_doctest.get(), ) } diff --git a/crates/rstest_helper_should_be_fixture/src/driver.rs b/crates/rstest_helper_should_be_fixture/src/driver.rs index e6c67636e..8d2426387 100644 --- a/crates/rstest_helper_should_be_fixture/src/driver.rs +++ b/crates/rstest_helper_should_be_fixture/src/driver.rs @@ -40,13 +40,31 @@ const DEFAULT_PROVIDER_PARAM_ATTRIBUTES: &[&str] = type ConfigLoadResult = Result; -dylint_linting::impl_late_lint! { - pub RSTEST_HELPER_SHOULD_BE_FIXTURE, - Warn, - "repeated rstest helper calls should be extracted into fixtures", - RstestHelperShouldBeFixture::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::RstestHelperShouldBeFixture; + + dylint_linting::impl_late_lint! { + /// Warns when repeated rstest helper calls should become fixtures. + pub RSTEST_HELPER_SHOULD_BE_FIXTURE, + Warn, + "repeated rstest helper calls should be extracted into fixtures", + RstestHelperShouldBeFixture::default() + } } +pub use declaration::RSTEST_HELPER_SHOULD_BE_FIXTURE; + /// Configuration for the `rstest_helper_should_be_fixture` lint. /// /// Values are loaded from `dylint.toml` and normalized so threshold settings @@ -131,10 +149,10 @@ impl RstestHelperShouldBeFixture { fn apply_loaded_crate_configuration( &mut self, config: ConfigLoadResult, - shared_config: SharedConfig, + shared_config: &SharedConfig, ) { - let config = match config { - Ok(config) => config, + let resolved = match config { + Ok(loaded) => loaded, Err(error) => { debug!( target: LINT_NAME, @@ -144,10 +162,10 @@ impl RstestHelperShouldBeFixture { } }; - self.apply_crate_configuration(config, shared_config); + self.apply_crate_configuration(resolved, shared_config); } - fn apply_crate_configuration(&mut self, config: Config, shared_config: SharedConfig) { + fn apply_crate_configuration(&mut self, config: Config, shared_config: &SharedConfig) { debug!( target: LINT_NAME, "applying `{LINT_NAME}` configuration: min_calls={}, min_distinct_tests={}, \ @@ -203,7 +221,7 @@ impl RstestHelperShouldBeFixture { impl<'tcx> LateLintPass<'tcx> for RstestHelperShouldBeFixture { fn check_crate(&mut self, cx: &LateContext<'tcx>) { - self.apply_loaded_crate_configuration(load_configuration(), load_shared_config()); + self.apply_loaded_crate_configuration(load_configuration(), &load_shared_config()); self.rstest_collection_roots = whitaker::hir::collect_rstest_companion_test_functions(cx); } @@ -260,12 +278,15 @@ impl RstestHelperShouldBeFixture { self.collector.record_count(), ); for (callee, records) in self.collector.iter() { - summary.push_str(&format!("callee={callee};records={}\n", records.len())); + summary.push_str("callee="); + summary.push_str(callee); + summary.push_str(";records="); + summary.push_str(&records.len().to_string()); + summary.push('\n'); for record in records { - summary.push_str(&format!( - "fingerprint={}\n", - redacted_fingerprint_shape(&record.fingerprint) - )); + summary.push_str("fingerprint="); + summary.push_str(&redacted_fingerprint_shape(&record.fingerprint)); + summary.push('\n'); } } summary diff --git a/crates/rstest_helper_should_be_fixture/src/driver_tests.rs b/crates/rstest_helper_should_be_fixture/src/driver_tests.rs index b855167f1..843f8d26b 100644 --- a/crates/rstest_helper_should_be_fixture/src/driver_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/driver_tests.rs @@ -154,7 +154,7 @@ fn applying_crate_configuration_initializes_pass_state() { ..Config::default() }; - pass.apply_crate_configuration(config.clone(), SharedConfig::default()); + pass.apply_crate_configuration(config.clone(), &SharedConfig::default()); assert_eq!(pass.config, config.normalized()); assert!(pass.detection_options.use_expansion_trace_fallback()); @@ -174,7 +174,7 @@ fn check_crate_configuration_loads_and_normalizes_config() { pass.apply_loaded_crate_configuration( loaded_configuration::(Ok(Some(config))), - SharedConfig::default(), + &SharedConfig::default(), ); assert_eq!(pass.config.min_calls, 2); diff --git a/crates/rstest_helper_should_be_fixture/src/lib.rs b/crates/rstest_helper_should_be_fixture/src/lib.rs index fe2b6a98f..4d2af2b1f 100644 --- a/crates/rstest_helper_should_be_fixture/src/lib.rs +++ b/crates/rstest_helper_should_be_fixture/src/lib.rs @@ -13,5 +13,10 @@ mod driver; #[cfg(feature = "dylint-driver")] mod visitor; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{RSTEST_HELPER_SHOULD_BE_FIXTURE, RstestHelperShouldBeFixture}; diff --git a/crates/rstest_helper_should_be_fixture/src/visitor.rs b/crates/rstest_helper_should_be_fixture/src/visitor.rs index 7b81020fb..d890a199f 100644 --- a/crates/rstest_helper_should_be_fixture/src/visitor.rs +++ b/crates/rstest_helper_should_be_fixture/src/visitor.rs @@ -115,7 +115,7 @@ impl CallSiteVisitor<'_, '_> { self.closure_span_fallbacks .iter() .rev() - .find_map(|span| whitaker::hir::recover_user_editable_hir_span(*span)) + .find_map(|fallback| whitaker::hir::recover_user_editable_hir_span(*fallback)) }) } } @@ -190,11 +190,13 @@ fn attribute_kind(attr: &hir::Attribute) -> AttributeKind { } } +/// Parsed attributes never reach this helper because `attribute_path` filters +/// them out; treating them as outer keeps the mapping total. fn attribute_style(attr: &hir::Attribute) -> AttrStyle { - let hir::Attribute::Unparsed(item) = attr else { - unreachable!("attribute_path filters parsed attributes"); - }; - item.style + match attr { + hir::Attribute::Unparsed(item) => item.style, + hir::Attribute::Parsed(_) => AttrStyle::Outer, + } } pub(crate) fn redacted_fingerprint_shape(fingerprint: &ArgFingerprint) -> String { diff --git a/crates/test_must_not_have_example/src/driver.rs b/crates/test_must_not_have_example/src/driver.rs index da409e52d..453c17def 100644 --- a/crates/test_must_not_have_example/src/driver.rs +++ b/crates/test_must_not_have_example/src/driver.rs @@ -25,13 +25,31 @@ struct Config { additional_test_attributes: Vec, } -dylint_linting::impl_late_lint! { - pub TEST_MUST_NOT_HAVE_EXAMPLE, - Warn, - "test functions should not include examples or fenced code in documentation", - TestMustNotHaveExample::default() +/// Dylint lint declaration and registration glue. +/// +/// `impl_late_lint!` expands to the Dylint ABI entry point and the +/// `impl_lint_pass!` accessor, neither of which has a source location that +/// could carry documentation. Isolating the invocation keeps the expectation +/// scoped to exactly those generated items. +mod declaration { + #![expect( + missing_docs, + reason = "dylint_linting macro expansion emits items with no documentable source location" + )] + + use super::TestMustNotHaveExample; + + dylint_linting::impl_late_lint! { + /// Warns when a test function's documentation contains an example. + pub TEST_MUST_NOT_HAVE_EXAMPLE, + Warn, + "test functions should not include examples or fenced code in documentation", + TestMustNotHaveExample::default() + } } +pub use declaration::TEST_MUST_NOT_HAVE_EXAMPLE; + /// Lint pass that checks test documentation for example sections. pub struct TestMustNotHaveExample { /// Additional attribute paths configured as test-like markers. @@ -85,7 +103,7 @@ macro_rules! impl_check_method { let attrs = cx.tcx.hir_attrs(item.hir_id()); self.check_function_item( cx, - ItemKindInfo::$variant { + &ItemKindInfo::$variant { ident: &item.ident, attrs, }, @@ -135,7 +153,7 @@ impl<'tcx> LateLintPass<'tcx> for TestMustNotHaveExample { return; }; let attrs = cx.tcx.hir_attrs(item.hir_id()); - self.check_function_item(cx, ItemKindInfo::Item { ident, attrs }, Some(item)); + self.check_function_item(cx, &ItemKindInfo::Item { ident, attrs }, Some(item)); } } @@ -154,11 +172,7 @@ impl<'tcx> LateLintPass<'tcx> for TestMustNotHaveExample { } impl TestMustNotHaveExample { - fn detect_violation( - &self, - attrs: &[hir::Attribute], - is_test: bool, - ) -> Option { + fn detect_violation(attrs: &[hir::Attribute], is_test: bool) -> Option { if !is_test { return None; } @@ -174,7 +188,7 @@ impl TestMustNotHaveExample { fn emit_violation( &self, cx: &LateContext<'_>, - function: FunctionSite<'_>, + function: &FunctionSite<'_>, violation: DocExampleViolation, ) { let messages = localized_messages(&self.localizer, function.name, violation); @@ -196,20 +210,26 @@ impl TestMustNotHaveExample { fn check_function_item<'tcx>( &mut self, cx: &LateContext<'tcx>, - item_info: ItemKindInfo<'_>, + item_info: &ItemKindInfo<'_>, item: Option<&'tcx hir::Item<'tcx>>, ) { let attrs = item_info.attrs(); - let is_test = if let Some(item) = item { - is_test_function_item(cx, item, attrs, self.additional_test_attributes.as_slice()) - } else { - has_test_like_hir_attributes(attrs, self.additional_test_attributes.as_slice()) - }; + let is_test = item.map_or_else( + || has_test_like_hir_attributes(attrs, self.additional_test_attributes.as_slice()), + |test_item| { + is_test_function_item( + cx, + test_item, + attrs, + self.additional_test_attributes.as_slice(), + ) + }, + ); - if let Some(violation) = self.detect_violation(attrs, is_test) { + if let Some(violation) = Self::detect_violation(attrs, is_test) { self.emit_violation( cx, - FunctionSite { + &FunctionSite { name: item_info.ident().name.as_str(), span: item_info.ident().span, }, diff --git a/crates/test_must_not_have_example/src/heuristics.rs b/crates/test_must_not_have_example/src/heuristics.rs index af5de74a2..18347195f 100644 --- a/crates/test_must_not_have_example/src/heuristics.rs +++ b/crates/test_must_not_have_example/src/heuristics.rs @@ -62,7 +62,10 @@ fn is_examples_heading(line: &str) -> bool { return false; } - let remainder = trimmed[heading_level..].trim_start(); + let Some(after_hashes) = trimmed.get(heading_level..) else { + return false; + }; + let remainder = after_hashes.trim_start(); matches!( remainder .trim_end_matches(|ch: char| ch.is_ascii_whitespace()) diff --git a/crates/test_must_not_have_example/src/lib.rs b/crates/test_must_not_have_example/src/lib.rs index e2f8029b3..a5ea1715e 100644 --- a/crates/test_must_not_have_example/src/lib.rs +++ b/crates/test_must_not_have_example/src/lib.rs @@ -11,8 +11,13 @@ mod heuristics; #[path = "lib_ui_tests.rs"] mod ui; +// Re-export only the documented lint surface. `impl_late_lint!` also expands +// to the Dylint ABI entry point and lint-pass glue, which have no source +// location that could carry documentation; keeping them out of the public +// path satisfies `missing_docs` without suppressing it. The `no_mangle` +// symbol is still exported from the cdylib for standalone Dylint loading. #[cfg(feature = "dylint-driver")] -pub use driver::*; +pub use driver::{TEST_MUST_NOT_HAVE_EXAMPLE, TestMustNotHaveExample}; #[cfg(not(feature = "dylint-driver"))] mod stub { diff --git a/tests/config_loading.rs b/tests/config_loading.rs index 832df279a..76dd862bd 100644 --- a/tests/config_loading.rs +++ b/tests/config_loading.rs @@ -121,7 +121,10 @@ fn load_config( let maybe_source = config_source.borrow().clone(); let outcome = catch_unwind(AssertUnwindSafe(|| { SharedConfig::load_with("module_max_lines", |crate_name| { - assert_eq!(crate_name, "module_max_lines"); + assert_eq!( + crate_name, "module_max_lines", + "the loader should request configuration for the requested lint", + ); maybe_source .as_ref() .map_or_else(SharedConfig::default, |input| { diff --git a/tests/ui_harness.rs b/tests/ui_harness.rs index b442a3f71..408120c29 100644 --- a/tests/ui_harness.rs +++ b/tests/ui_harness.rs @@ -140,7 +140,11 @@ fn assert_absolute_error(harness_world: &HarnessWorld, path: String) { let borrow = harness_world.harness_result.borrow(); match borrow.as_ref() { Some(Err(HarnessError::AbsoluteDirectory { directory })) => { - assert_eq!(directory, &Utf8PathBuf::from(path)); + assert_eq!( + directory, + &Utf8PathBuf::from(path), + "the rejected directory should be reported verbatim", + ); } Some(Ok(())) => panic!("expected an error but harness succeeded"), Some(Err(error)) => panic!("expected an absolute directory error, found {error}"), @@ -154,7 +158,10 @@ fn assert_runner_failure(harness_world: &HarnessWorld, snippet: StepString) { let snippet_value = snippet.into_inner(); match borrow.as_ref() { Some(Err(HarnessError::RunnerFailure { message, .. })) => { - assert!(message.contains(snippet_value.as_str())); + assert!( + message.contains(snippet_value.as_str()), + "runner failure should mention {snippet_value}, got: {message}", + ); } Some(Ok(())) => panic!("expected an error but harness succeeded"), Some(Err(error)) => panic!("expected a runner failure error, found {error}"), From f9bde135df76c0d71b39e0305da0e37a15730a32 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 30 Aug 2026 11:04:03 +0200 Subject: [PATCH 7/9] Fix workspace-lint violations across the installer Clear the installer's share of the violations a stricter lint configuration later in this stack will enforce, while staying green under the current gates. Several API changes land here because the fixes require them: - `BaseDirs` methods drop their redundant suffixes (`home`, `executables`, `whitaker_data`); the external `UserDirs::home_dir` call inside the implementation keeps its own name; - `InstallArgs` regroups its flags into `lint_selection`, `execution`, and `skip` flatten structs, so no single struct carries an excess of booleans; - install metrics report integer permille rather than a float rate, so assertions compare exactly instead of within a tolerance; - test helpers that previously panicked (`LocalServer::start`, the fake-binary writers, `load_cargo_toml`, `extract_binstall_table`) return `Result` and let callers propagate; - parameters that were needlessly moved are taken by reference; - the repository-release install path moves to its own module, with its executor, tool, and dependency grouped into a `RepositoryInstallRequest` so the extraction stays within the argument budget; - the root check swaps `libc::geteuid` for `rustix`, removing the last `unsafe` block in the installer and the `libc` dependency with it. A `#[expect(clippy::cast_sign_loss)]` is removed rather than retained: the timestamp arithmetic it covered now uses `div_euclid`/`rem_euclid` and a checked conversion. --- Cargo.lock | 2 +- Cargo.toml | 1 - docs/whitaker-dylint-suite-design.md | 2 +- installer/Cargo.toml | 2 +- installer/src/artefact/extraction.rs | 2 + installer/src/artefact/git_sha.rs | 8 +- installer/src/artefact/manifest.rs | 14 +- installer/src/artefact/manifest_parser.rs | 2 + installer/src/artefact/naming.rs | 45 +++-- installer/src/artefact/packaging.rs | 17 +- installer/src/artefact/packaging_tests.rs | 12 +- installer/src/artefact/schema_version.rs | 6 +- installer/src/artefact/sha256_digest.rs | 8 +- installer/src/artefact/target.rs | 4 +- installer/src/artefact/toolchain_channel.rs | 4 +- installer/src/artefact/verification.rs | 4 +- .../src/bin/package_dependency_binary.rs | 67 ++++--- installer/src/bin/package_installer_bin.rs | 18 +- installer/src/bin/package_lints.rs | 123 +++++++------ installer/src/binstall_metadata.rs | 26 +-- installer/src/binstall_metadata_tests.rs | 24 +-- installer/src/builder.rs | 6 +- installer/src/cli.rs | 84 ++++++--- installer/src/cli_tests.rs | 50 +++--- installer/src/crate_name.rs | 2 + .../dependency_binaries/install/checksum.rs | 61 ++++--- .../dependency_binaries/install/downloader.rs | 21 ++- .../downloader_boundary_non_utf8_tests.rs | 2 +- .../install/downloader_boundary_tests.rs | 2 +- .../dependency_binaries/install/extractor.rs | 10 +- .../install/http_test_server.rs | 49 ++--- .../dependency_binaries/install/installer.rs | 4 +- .../dependency_binaries/install/metadata.rs | 2 +- .../src/dependency_binaries/install/tests.rs | 49 ++--- installer/src/dependency_binaries/manifest.rs | 10 +- installer/src/dependency_packaging.rs | 62 ++++--- installer/src/dependency_packaging_tests.rs | 2 +- installer/src/deps/install.rs | 168 ++++++------------ installer/src/deps/install_repository.rs | 135 ++++++++++++++ installer/src/deps/install_tests.rs | 7 +- installer/src/deps/mod.rs | 40 +++-- installer/src/deps/path_tests.rs | 40 +++-- installer/src/deps/tests.rs | 32 ++-- installer/src/deps/tests_repository.rs | 11 +- installer/src/dirs.rs | 33 ++-- installer/src/error.rs | 12 +- installer/src/git.rs | 138 ++++++++------ installer/src/hex.rs | 16 +- installer/src/install_flow/mod.rs | 36 ++-- installer/src/install_flow/tests.rs | 66 ++++--- installer/src/install_metrics.rs | 79 ++++---- installer/src/install_metrics_tests.rs | 22 +-- installer/src/installer_packaging.rs | 17 +- installer/src/installer_packaging_tests.rs | 8 +- installer/src/list.rs | 3 +- installer/src/list_output.rs | 54 ++++-- installer/src/list_tests.rs | 93 +++++++--- installer/src/main.rs | 46 ++--- installer/src/output.rs | 35 ++-- installer/src/prebuilt_path.rs | 14 +- installer/src/resolution.rs | 100 ++++++----- installer/src/scanner.rs | 27 +-- installer/src/staged_suite.rs | 55 +++--- installer/src/stager.rs | 8 +- .../dependency_binary_helpers_fakes.rs | 68 +++++-- installer/src/test_utils/mod.rs | 8 +- installer/src/tests/fast_path.rs | 8 +- installer/src/tests/mod.rs | 28 +-- installer/src/toolchain/mod.rs | 2 +- installer/src/version.rs | 2 + installer/src/workspace.rs | 9 +- installer/src/workspace_tests.rs | 107 +++++++---- installer/src/wrapper.rs | 80 ++++++--- .../tests/behaviour_artefact_packaging.rs | 2 +- installer/tests/behaviour_binstall.rs | 4 +- .../tests/behaviour_dependency_binaries.rs | 10 +- installer/tests/behaviour_install_metrics.rs | 32 ++-- .../tests/behaviour_installer_release.rs | 2 +- installer/tests/behaviour_prebuilt.rs | 4 +- installer/tests/behaviour_staging.rs | 4 +- installer/tests/behaviour_toolchain.rs | 4 +- .../tests/features/install_metrics.feature | 16 +- typos.local.toml | 2 +- typos.toml | 2 +- 84 files changed, 1524 insertions(+), 972 deletions(-) create mode 100644 installer/src/deps/install_repository.rs diff --git a/Cargo.lock b/Cargo.lock index cdb64b733..646530afd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3676,12 +3676,12 @@ dependencies = [ "directories-next", "flate2", "fs2", - "libc", "log", "mockall", "rstest", "rstest-bdd", "rstest-bdd-macros", + "rustix", "serde", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml index 27733f1f3..6104aac40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ clap = "4.5.38" directories-next = "2.0.0" dylint_linting = "6" dylint_testing = "6" -libc = "0.2" fluent-templates = "^0.15.0" insta = { version = "1", features = ["json"] } once_cell = "^1.21.3" diff --git a/docs/whitaker-dylint-suite-design.md b/docs/whitaker-dylint-suite-design.md index 25bd591c5..af0e75b02 100644 --- a/docs/whitaker-dylint-suite-design.md +++ b/docs/whitaker-dylint-suite-design.md @@ -1660,7 +1660,7 @@ artefacts remain optional. The decision is recorded in `docs/adr-001-prebuilt-dylint-libraries.md` (Accepted 2026-02-03). **Implementation decision (2026-02-18):** The installer now derives prebuilt -extraction paths from `BaseDirs::whitaker_data_dir()` and writes libraries to +extraction paths from `BaseDirs::whitaker_data()` and writes libraries to `/lints///lib`. Wrapper scripts and shell snippets use that exact `lib` directory for `DYLINT_LIBRARY_PATH`. Local build staging continues to honour `--target-dir` and existing release-layout behaviour. diff --git a/installer/Cargo.toml b/installer/Cargo.toml index 570e46ff5..c6e4107a1 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -79,7 +79,7 @@ zstd = { workspace = true } [dev-dependencies] whitaker_test_macros = { workspace = true } -libc = { workspace = true } +rustix = { version = "1.1.4", features = ["process"] } mockall = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } diff --git a/installer/src/artefact/extraction.rs b/installer/src/artefact/extraction.rs index 0c7e3af3a..cb7333b62 100644 --- a/installer/src/artefact/extraction.rs +++ b/installer/src/artefact/extraction.rs @@ -113,6 +113,8 @@ fn validate_entry_path(path: &Path) -> Result<(), ExtractionError> { #[cfg(test)] mod tests { + //! Tests for artefact archive extraction. + use super::*; use rstest::rstest; use std::path::PathBuf; diff --git a/installer/src/artefact/git_sha.rs b/installer/src/artefact/git_sha.rs index 4bc775b5d..db105824b 100644 --- a/installer/src/artefact/git_sha.rs +++ b/installer/src/artefact/git_sha.rs @@ -131,10 +131,10 @@ fn validate_git_sha(value: &str) -> Result<()> { .chars() .find(|c| !c.is_ascii_hexdigit() || c.is_ascii_uppercase()) { - let reason = if !bad.is_ascii_hexdigit() { - format!("non-hex character '{bad}'") - } else { + let reason = if bad.is_ascii_hexdigit() { "SHA must be lowercase".to_owned() + } else { + format!("non-hex character '{bad}'") }; return Err(ArtefactError::InvalidGitSha { value: value.to_owned(), @@ -146,6 +146,8 @@ fn validate_git_sha(value: &str) -> Result<()> { #[cfg(test)] mod tests { + //! Tests for git SHA parsing and validation. + use super::*; use rstest::rstest; diff --git a/installer/src/artefact/manifest.rs b/installer/src/artefact/manifest.rs index 2a1c2ea36..c59e1c9cc 100644 --- a/installer/src/artefact/manifest.rs +++ b/installer/src/artefact/manifest.rs @@ -187,7 +187,7 @@ impl Manifest { /// assert_eq!(manifest.git_sha().as_str(), "abc1234"); /// ``` #[must_use] - pub fn new(provenance: ManifestProvenance, content: ManifestContent) -> Self { + pub const fn new(provenance: ManifestProvenance, content: ManifestContent) -> Self { Self { provenance, content, @@ -203,7 +203,7 @@ impl Manifest { /// assert_eq!(manifest.git_sha().as_str(), "abc1234"); /// ``` #[must_use] - pub fn git_sha(&self) -> &GitSha { + pub const fn git_sha(&self) -> &GitSha { &self.provenance.git_sha } @@ -216,7 +216,7 @@ impl Manifest { /// assert_eq!(u32::from(manifest.schema_version()), 1); /// ``` #[must_use] - pub fn schema_version(&self) -> SchemaVersion { + pub const fn schema_version(&self) -> SchemaVersion { self.provenance.schema_version } @@ -229,7 +229,7 @@ impl Manifest { /// assert_eq!(manifest.toolchain().as_str(), "nightly-2026-05-28"); /// ``` #[must_use] - pub fn toolchain(&self) -> &ToolchainChannel { + pub const fn toolchain(&self) -> &ToolchainChannel { &self.provenance.toolchain } @@ -242,7 +242,7 @@ impl Manifest { /// assert_eq!(manifest.target().as_str(), "x86_64-unknown-linux-gnu"); /// ``` #[must_use] - pub fn target(&self) -> &TargetTriple { + pub const fn target(&self) -> &TargetTriple { &self.provenance.target } @@ -255,7 +255,7 @@ impl Manifest { /// assert_eq!(manifest.generated_at().as_str(), "2026-05-28T00:00:00Z"); /// ``` #[must_use] - pub fn generated_at(&self) -> &GeneratedAt { + pub const fn generated_at(&self) -> &GeneratedAt { &self.content.generated_at } @@ -281,7 +281,7 @@ impl Manifest { /// assert_eq!(manifest.sha256().as_str().len(), 64); /// ``` #[must_use] - pub fn sha256(&self) -> &Sha256Digest { + pub const fn sha256(&self) -> &Sha256Digest { &self.content.sha256 } } diff --git a/installer/src/artefact/manifest_parser.rs b/installer/src/artefact/manifest_parser.rs index 1adcc02da..3cff9f7f3 100644 --- a/installer/src/artefact/manifest_parser.rs +++ b/installer/src/artefact/manifest_parser.rs @@ -48,6 +48,8 @@ pub fn parse_manifest(json: &str) -> Result { #[cfg(test)] mod tests { + //! Tests for artefact manifest parsing. + use super::*; use rstest::rstest; diff --git a/installer/src/artefact/naming.rs b/installer/src/artefact/naming.rs index af6d94c2c..703fb047d 100644 --- a/installer/src/artefact/naming.rs +++ b/installer/src/artefact/naming.rs @@ -51,7 +51,7 @@ pub struct ArtefactName { impl ArtefactName { /// Create an artefact name from validated components. #[must_use] - pub fn new(git_sha: GitSha, toolchain: ToolchainChannel, target: TargetTriple) -> Self { + pub const fn new(git_sha: GitSha, toolchain: ToolchainChannel, target: TargetTriple) -> Self { Self { git_sha, toolchain, @@ -61,19 +61,19 @@ impl ArtefactName { /// Return the git SHA component. #[must_use] - pub fn git_sha(&self) -> &GitSha { + pub const fn git_sha(&self) -> &GitSha { &self.git_sha } /// Return the toolchain channel component. #[must_use] - pub fn toolchain(&self) -> &ToolchainChannel { + pub const fn toolchain(&self) -> &ToolchainChannel { &self.toolchain } /// Return the target triple component. #[must_use] - pub fn target(&self) -> &TargetTriple { + pub const fn target(&self) -> &TargetTriple { &self.target } @@ -96,22 +96,27 @@ impl fmt::Display for ArtefactName { #[cfg(test)] mod tests { + //! Tests for artefact release-asset naming. + use super::*; + use crate::artefact::error::ArtefactError; use rstest::{fixture, rstest}; #[fixture] - fn sample_name() -> ArtefactName { - ArtefactName::new( - GitSha::try_from("abc1234").expect("valid sha"), - ToolchainChannel::try_from("nightly-2026-05-28").expect("valid channel"), - TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"), - ) + fn sample_name() -> Result { + Ok(ArtefactName::new( + GitSha::try_from("abc1234")?, + ToolchainChannel::try_from("nightly-2026-05-28")?, + TargetTriple::try_from("x86_64-unknown-linux-gnu")?, + )) } #[rstest] - fn display_matches_adr_format(sample_name: ArtefactName) { + fn display_matches_adr_format(sample_name: Result) { + let name = sample_name.expect("sample artefact name should build"); + assert_eq!( - sample_name.to_string(), + name.to_string(), concat!( "whitaker-lints-abc1234-nightly-2026-05-28", "-x86_64-unknown-linux-gnu.tar.zst" @@ -120,15 +125,19 @@ mod tests { } #[rstest] - fn filename_matches_display(sample_name: ArtefactName) { - assert_eq!(sample_name.filename(), sample_name.to_string()); + fn filename_matches_display(sample_name: Result) { + let name = sample_name.expect("sample artefact name should build"); + + assert_eq!(name.filename(), name.to_string()); } #[rstest] - fn accessors_return_components(sample_name: ArtefactName) { - assert_eq!(sample_name.git_sha().as_str(), "abc1234"); - assert_eq!(sample_name.toolchain().as_str(), "nightly-2026-05-28"); - assert_eq!(sample_name.target().as_str(), "x86_64-unknown-linux-gnu"); + fn accessors_return_components(sample_name: Result) { + let name = sample_name.expect("sample artefact name should build"); + + assert_eq!(name.git_sha().as_str(), "abc1234"); + assert_eq!(name.toolchain().as_str(), "nightly-2026-05-28"); + assert_eq!(name.target().as_str(), "x86_64-unknown-linux-gnu"); } #[rstest] diff --git a/installer/src/artefact/packaging.rs b/installer/src/artefact/packaging.rs index b253e2531..28c09649c 100644 --- a/installer/src/artefact/packaging.rs +++ b/installer/src/artefact/packaging.rs @@ -72,8 +72,8 @@ pub struct PackageOutput { /// Compute the SHA-256 digest of a file. /// -/// Reads the file at `path` in chunks and returns the lowercase hex -/// digest as a validated [`Sha256Digest`]. +/// Streams the file at `path` through the hasher and returns the +/// lowercase hex digest as a validated [`Sha256Digest`]. /// /// # Errors /// @@ -84,11 +84,14 @@ pub fn compute_sha256(path: &Path) -> Result { let mut hasher = Sha256::new(); let mut buffer = [0u8; 8192]; loop { - let bytes_read = file.read(&mut buffer)?; - if bytes_read == 0 { + let read_count = file.read(&mut buffer)?; + if read_count == 0 { break; } - hasher.update(&buffer[..bytes_read]); + let Some(chunk) = buffer.get(..read_count) else { + break; + }; + hasher.update(chunk); } let hex = to_lower_hex(&hasher.finalize()); Ok(Sha256Digest::try_from(hex)?) @@ -160,7 +163,7 @@ pub fn generate_manifest_json(manifest: &Manifest) -> Result Result { +pub fn package_artefact(params: &PackageParams) -> Result { if params.library_files.is_empty() { return Err(PackagingError::EmptyFileList); } @@ -182,7 +185,7 @@ pub fn package_artefact(params: PackageParams) -> Result Result Self { + pub const fn current() -> Self { Self(CURRENT_MAX) } /// Return the inner version number. #[must_use] - pub fn as_u32(self) -> u32 { + pub const fn as_u32(self) -> u32 { self.0 } } @@ -81,6 +81,8 @@ impl fmt::Display for SchemaVersion { #[cfg(test)] mod tests { + //! Tests for artefact manifest schema versioning. + use super::*; #[test] diff --git a/installer/src/artefact/sha256_digest.rs b/installer/src/artefact/sha256_digest.rs index 1332219f4..3e9dda26c 100644 --- a/installer/src/artefact/sha256_digest.rs +++ b/installer/src/artefact/sha256_digest.rs @@ -114,10 +114,10 @@ fn validate_sha256(value: &str) -> Result<()> { .chars() .find(|c| !c.is_ascii_hexdigit() || c.is_ascii_uppercase()) { - let reason = if !bad.is_ascii_hexdigit() { - format!("non-hex character '{bad}'") - } else { + let reason = if bad.is_ascii_hexdigit() { "digest must be lowercase".to_owned() + } else { + format!("non-hex character '{bad}'") }; return Err(ArtefactError::InvalidSha256Digest { reason }); } @@ -126,6 +126,8 @@ fn validate_sha256(value: &str) -> Result<()> { #[cfg(test)] mod tests { + //! Tests for SHA-256 digest parsing and formatting. + use super::*; use rstest::{fixture, rstest}; diff --git a/installer/src/artefact/target.rs b/installer/src/artefact/target.rs index f05aeb41b..c02462501 100644 --- a/installer/src/artefact/target.rs +++ b/installer/src/artefact/target.rs @@ -51,7 +51,7 @@ impl TargetTriple { /// Return the full list of supported target triples. #[must_use] - pub fn supported() -> &'static [&'static str] { + pub const fn supported() -> &'static [&'static str] { SUPPORTED_TARGETS } @@ -159,6 +159,8 @@ impl fmt::Display for TargetTriple { #[cfg(test)] mod tests { + //! Tests for target-triple parsing and validation. + use super::*; use rstest::rstest; diff --git a/installer/src/artefact/toolchain_channel.rs b/installer/src/artefact/toolchain_channel.rs index 03e0b0dcb..5f4b802b7 100644 --- a/installer/src/artefact/toolchain_channel.rs +++ b/installer/src/artefact/toolchain_channel.rs @@ -28,7 +28,7 @@ pub struct ToolchainChannel(String); /// Check that every byte is ASCII alphanumeric, a hyphen, a dot, or an /// underscore. Underscores appear in host-qualified toolchain names /// (e.g. `nightly-2026-05-28-x86_64-unknown-linux-gnu`). -fn is_valid_channel_char(c: char) -> bool { +const fn is_valid_channel_char(c: char) -> bool { c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' } @@ -121,6 +121,8 @@ impl fmt::Display for ToolchainChannel { #[cfg(test)] mod tests { + //! Tests for toolchain channel parsing and validation. + use super::*; use rstest::rstest; diff --git a/installer/src/artefact/verification.rs b/installer/src/artefact/verification.rs index e558fdc49..b1cfcc375 100644 --- a/installer/src/artefact/verification.rs +++ b/installer/src/artefact/verification.rs @@ -33,7 +33,7 @@ impl VerificationPolicy { /// digest of the downloaded archive and compare it against the digest /// recorded in the manifest before extracting any files. #[must_use] - pub fn require_checksum(&self) -> bool { + pub const fn require_checksum(&self) -> bool { self.require_checksum } } @@ -81,6 +81,8 @@ impl fmt::Display for VerificationFailureAction { #[cfg(test)] mod tests { + //! Tests for artefact checksum verification. + use super::*; #[test] diff --git a/installer/src/bin/package_dependency_binary.rs b/installer/src/bin/package_dependency_binary.rs index bcec4f0c1..e39769076 100644 --- a/installer/src/bin/package_dependency_binary.rs +++ b/installer/src/bin/package_dependency_binary.rs @@ -1,7 +1,8 @@ //! Package dependency binaries and shared provenance assets for release uploads. use clap::{Parser, Subcommand}; -use std::path::PathBuf; +use std::io::Write; +use std::path::{Path, PathBuf}; use thiserror::Error; use whitaker_installer::dependency_binaries::{ find_dependency_binary, required_dependency_binaries, @@ -11,6 +12,7 @@ use whitaker_installer::dependency_packaging::{ write_provenance_markdown, }; use whitaker_installer::installer_packaging::TargetTriple; +use whitaker_installer::output::write_stderr_line; /// Package repository-hosted dependency binaries for release publication. #[derive(Parser, Debug)] @@ -65,6 +67,9 @@ enum CliError { #[error("{0}")] Target(#[from] whitaker_installer::artefact::error::ArtefactError), + + #[error("failed to write to stdout: {0}")] + Stdout(#[from] std::io::Error), } /// Parse command-line arguments, execute the requested subcommand, and report @@ -72,7 +77,7 @@ enum CliError { fn main() { let cli = Cli::parse(); if let Err(error) = run(cli) { - eprintln!("error: {error}"); + write_stderr_line(&mut std::io::stderr(), format!("error: {error}")); std::process::exit(1); } } @@ -85,32 +90,50 @@ fn run(cli: Cli) -> Result<(), CliError> { target, binary_path, output_dir, - } => { - let dependency = find_dependency_binary(&package) - .map_err(|error| CliError::Manifest(error.to_string()))? - .cloned() - .ok_or(CliError::UnknownPackage(package))?; - let target = TargetTriple::try_from(target.as_str())?; - let output = package_dependency_binary(DependencyPackageParams { - dependency, - target, - binary_path, - output_dir, - })?; - println!("Created {}", output.archive_path.display()); - } - Command::Provenance { output_dir } => { - let dependencies = required_dependency_binaries() - .map_err(|error| CliError::Manifest(error.to_string()))?; - let output = write_provenance_markdown(&output_dir, dependencies)?; - println!("Created {}", output.display()); - } + } => run_package(package, &target, binary_path, output_dir), + Command::Provenance { output_dir } => run_provenance(&output_dir), } +} + +/// Package a single dependency binary into a release archive. +fn run_package( + package: String, + target: &str, + binary_path: PathBuf, + output_dir: PathBuf, +) -> Result<(), CliError> { + let dependency = find_dependency_binary(&package) + .map_err(|error| CliError::Manifest(error.to_string()))? + .cloned() + .ok_or(CliError::UnknownPackage(package))?; + let target_triple = TargetTriple::try_from(target)?; + let output = package_dependency_binary(&DependencyPackageParams { + dependency, + target: target_triple, + binary_path, + output_dir, + })?; + writeln!( + std::io::stdout(), + "Created {}", + output.archive_path.display() + )?; + Ok(()) +} + +/// Write the dependency provenance summary into `output_dir`. +fn run_provenance(output_dir: &Path) -> Result<(), CliError> { + let dependencies = + required_dependency_binaries().map_err(|error| CliError::Manifest(error.to_string()))?; + let output = write_provenance_markdown(output_dir, dependencies)?; + writeln!(std::io::stdout(), "Created {}", output.display())?; Ok(()) } #[cfg(test)] mod tests { + //! Tests for the dependency-binary packaging command. + use super::*; use tempfile::tempdir; use whitaker_installer::dependency_binaries::provenance_filename; diff --git a/installer/src/bin/package_installer_bin.rs b/installer/src/bin/package_installer_bin.rs index 944d74e04..169ed3369 100644 --- a/installer/src/bin/package_installer_bin.rs +++ b/installer/src/bin/package_installer_bin.rs @@ -5,12 +5,14 @@ //! by the release CI workflow to create binstall-compatible archives. use clap::Parser; +use std::io::Write; use std::path::PathBuf; use thiserror::Error; use whitaker_installer::artefact::error::ArtefactError; use whitaker_installer::installer_packaging::{ InstallerPackagingError, TargetTriple, Version, package_installer, }; +use whitaker_installer::output::write_stderr_line; /// Package the `whitaker-installer` binary into a release archive. /// @@ -47,12 +49,16 @@ enum CliError { /// An invalid target triple was provided. #[error("{0}")] Artefact(#[from] ArtefactError), + + /// Failed to write the success report to standard output. + #[error("failed to write to stdout: {0}")] + Stdout(#[from] std::io::Error), } fn main() { let cli = Cli::parse(); if let Err(err) = run(cli) { - eprintln!("error: {err}"); + write_stderr_line(&mut std::io::stderr(), format!("error: {err}")); std::process::exit(1); } } @@ -66,13 +72,19 @@ fn run(cli: Cli) -> Result<(), CliError> { output_dir: cli.output_dir, }; - let output = package_installer(params)?; - println!("Created {}", output.archive_path.display()); + let output = package_installer(¶ms)?; + writeln!( + std::io::stdout(), + "Created {}", + output.archive_path.display() + )?; Ok(()) } #[cfg(test)] mod tests { + //! Tests for the installer packaging command. + use super::*; use clap::Parser; use rstest::rstest; diff --git a/installer/src/bin/package_lints.rs b/installer/src/bin/package_lints.rs index 1c6cdcd1f..b2cee0605 100644 --- a/installer/src/bin/package_lints.rs +++ b/installer/src/bin/package_lints.rs @@ -5,6 +5,7 @@ //! rolling-release CI workflow. use clap::Parser; +use std::io::Write; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; @@ -17,6 +18,7 @@ use whitaker_installer::artefact::packaging::{ use whitaker_installer::artefact::packaging_error::PackagingError; use whitaker_installer::artefact::target::TargetTriple; use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; +use whitaker_installer::output::write_stderr_line; use whitaker_installer::resolution::{LINT_CRATES, SUITE_CRATE}; /// Package prebuilt lint libraries into `.tar.zst` archives following @@ -82,12 +84,16 @@ enum PackageCliError { /// Failed to read the system clock. #[error("system time error: {0}")] SystemTime(#[from] std::time::SystemTimeError), + + /// Failed to write the success report to standard output. + #[error("failed to write to stdout: {0}")] + Stdout(#[from] std::io::Error), } fn main() { let cli = PackageCli::parse(); if let Err(err) = run(cli) { - eprintln!("error: {err}"); + write_stderr_line(&mut std::io::stderr(), format!("error: {err}")); std::process::exit(1); } } @@ -119,6 +125,7 @@ fn run(cli: PackageCli) -> Result<(), PackageCliError> { std::fs::create_dir_all(&cli.output_dir).map_err(PackagingError::from)?; + let out_dir = cli.output_dir.clone(); let params = PackageParams { git_sha, toolchain, @@ -128,14 +135,14 @@ fn run(cli: PackageCli) -> Result<(), PackageCliError> { generated_at: GeneratedAt::new(timestamp), }; - let output = package_artefact(params)?; + let output = package_artefact(¶ms)?; let manifest_json = generate_manifest_json(&output.manifest)?; - let out_dir = output.archive_path.parent().expect("archive has parent"); let manifest_filename = format!("manifest-{}.json", output.manifest.target()); let manifest_path = out_dir.join(manifest_filename); std::fs::write(&manifest_path, &manifest_json).map_err(PackagingError::from)?; - println!("Created {}", output.archive_path.display()); - println!("Manifest {}", manifest_path.display()); + let mut stdout = std::io::stdout(); + writeln!(stdout, "Created {}", output.archive_path.display())?; + writeln!(stdout, "Manifest {}", manifest_path.display())?; Ok(()) } @@ -174,34 +181,37 @@ fn validate_library_files(paths: &[PathBuf]) -> Result<(), PackageCliError> { /// Verify that `ts` matches the expected `YYYY-MM-DDThh:mm:ssZ` shape. fn validate_iso8601(ts: &str) -> Result<(), PackageCliError> { - let b = ts.as_bytes(); - if !has_valid_length(b) { - return Err(PackageCliError::InvalidTimestamp(ts.to_owned())); - } - if !has_valid_separators(b) { - return Err(PackageCliError::InvalidTimestamp(ts.to_owned())); - } - if !has_valid_digits(b) { + let &[ + y0, + y1, + y2, + y3, + b'-', + mo0, + mo1, + b'-', + d0, + d1, + b'T', + h0, + h1, + b':', + mi0, + mi1, + b':', + s0, + s1, + b'Z', + ] = ts.as_bytes() + else { return Err(PackageCliError::InvalidTimestamp(ts.to_owned())); + }; + let digits = [y0, y1, y2, y3, mo0, mo1, d0, d1, h0, h1, mi0, mi1, s0, s1]; + if digits.iter().all(u8::is_ascii_digit) { + Ok(()) + } else { + Err(PackageCliError::InvalidTimestamp(ts.to_owned())) } - Ok(()) -} - -fn has_valid_length(b: &[u8]) -> bool { - b.len() == 20 -} - -fn has_valid_separators(b: &[u8]) -> bool { - b[4] == b'-' && b[7] == b'-' && b[10] == b'T' && b[13] == b':' && b[16] == b':' && b[19] == b'Z' -} - -/// Digit positions in `YYYY-MM-DDThh:mm:ssZ` (start, end pairs). -const DIGIT_RANGES: [(usize, usize); 6] = [(0, 4), (5, 7), (8, 10), (11, 13), (14, 16), (17, 19)]; - -fn has_valid_digits(b: &[u8]) -> bool { - DIGIT_RANGES - .iter() - .all(|&(s, e)| b[s..e].iter().all(u8::is_ascii_digit)) } /// Return the current UTC time as an ISO 8601 string (`YYYY-MM-DDThh:mm:ssZ`). @@ -215,10 +225,10 @@ fn now_utc_iso8601() -> Result { /// Format a Unix epoch timestamp as `YYYY-MM-DDThh:mm:ssZ`. fn format_epoch_secs(epoch_secs: u64) -> String { let (year, month, day) = civil_from_epoch(epoch_secs); - let day_secs = (epoch_secs % 86_400) as u32; - let hour = day_secs / 3_600; - let minute = (day_secs % 3_600) / 60; - let second = day_secs % 60; + let day_secs = epoch_secs.rem_euclid(86_400); + let hour = day_secs.div_euclid(3_600); + let minute = day_secs.rem_euclid(3_600).div_euclid(60); + let second = day_secs.rem_euclid(60); format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z") } @@ -226,26 +236,25 @@ fn format_epoch_secs(epoch_secs: u64) -> String { /// /// Adapted from Howard Hinnant's `civil_from_days` algorithm, which is /// public domain and widely used in C++ `` implementations. -fn civil_from_epoch(epoch_secs: u64) -> (u32, u32, u32) { - let z = (epoch_secs / 86_400) as i64 + 719_468; +const fn civil_from_epoch(epoch_secs: u64) -> (i64, u64, u64) { + let z = epoch_secs.div_euclid(86_400).cast_signed() + 719_468; let era = z.div_euclid(146_097); - let doe = z.rem_euclid(146_097) as u64; // day of era [0, 146_096] - let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; - let y = (yoe as i64) + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // day of year - let mp = (5 * doy + 2) / 153; - let d = doy - (153 * mp + 2) / 5 + 1; + let doe = z.rem_euclid(146_097).cast_unsigned(); // day of era [0, 146_096] + let yoe = (doe - doe.div_euclid(1_460) + doe.div_euclid(36_524) - doe.div_euclid(146_096)) + .div_euclid(365); + let y = yoe.cast_signed() + era * 400; + let doy = doe - (365 * yoe + yoe.div_euclid(4) - yoe.div_euclid(100)); // day of year + let mp = (5 * doy + 2).div_euclid(153); + let d = doy - (153 * mp + 2).div_euclid(5) + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; - let y = if m <= 2 { y + 1 } else { y }; - #[expect( - clippy::cast_sign_loss, - reason = "year is always positive for post-epoch dates" - )] - (y as u32, m as u32, d as u32) + let year = if m <= 2 { y + 1 } else { y }; + (year, m, d) } #[cfg(test)] mod tests { + //! Tests for the lint-library packaging command. + use super::*; use clap::Parser; use rstest::{fixture, rstest}; @@ -274,8 +283,8 @@ mod tests { } #[fixture] - fn linux_target() -> TargetTriple { - TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid") + fn linux_target() -> Result { + TargetTriple::try_from("x86_64-unknown-linux-gnu") } #[test] @@ -331,7 +340,11 @@ mod tests { } #[rstest] - fn discover_library_files_finds_expected_files(linux_target: TargetTriple) { + fn discover_library_files_finds_expected_files( + #[from(linux_target)] linux_target_res: Result, + ) { + let linux_target = linux_target_res.expect("linux target triple should validate"); + let dir = tempfile::tempdir().expect("temp dir"); for name in LINT_CRATES.iter().chain(std::iter::once(&SUITE_CRATE)) { fs::write(dir.path().join(format!("lib{name}.so")), b"fake").expect("write"); @@ -356,7 +369,11 @@ mod tests { } #[rstest] - fn discover_library_files_rejects_missing(linux_target: TargetTriple) { + fn discover_library_files_rejects_missing( + #[from(linux_target)] linux_target_res: Result, + ) { + let linux_target = linux_target_res.expect("linux target triple should validate"); + let dir = tempfile::tempdir().expect("temp dir"); fs::write(dir.path().join("libconditional_max_n_branches.so"), b"fake").expect("write"); let result = discover_library_files(dir.path(), &linux_target); diff --git a/installer/src/binstall_metadata.rs b/installer/src/binstall_metadata.rs index 1c0a0fa69..8bc1797cc 100644 --- a/installer/src/binstall_metadata.rs +++ b/installer/src/binstall_metadata.rs @@ -93,15 +93,19 @@ pub fn expand_bin_dir(version: &str, target: &str) -> String { /// Returns the full TOML table for `installer/Cargo.toml`, located via /// `CARGO_MANIFEST_DIR`. This helper is shared by unit tests and /// behaviour-driven scenarios to avoid duplicating manifest-loading logic. +/// +/// # Errors +/// +/// Returns a description of the failure if the manifest cannot be read +/// or does not parse as TOML. #[cfg(any(test, feature = "test-support"))] -#[must_use] -pub fn load_cargo_toml() -> toml::Table { +pub fn load_cargo_toml() -> Result { let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let cargo_toml_path = manifest_dir.join("Cargo.toml"); let content = std::fs::read_to_string(&cargo_toml_path) - .unwrap_or_else(|err| panic!("failed to read {}: {err}", cargo_toml_path.display())); - content.parse::().unwrap_or_else(|err| { - panic!( + .map_err(|err| format!("failed to read {}: {err}", cargo_toml_path.display()))?; + content.parse::().map_err(|err| { + format!( "failed to parse {} as TOML: {err}", cargo_toml_path.display() ) @@ -111,17 +115,19 @@ pub fn load_cargo_toml() -> toml::Table { /// Extract the `[package.metadata.binstall]` sub-table from a parsed /// `Cargo.toml`. /// -/// Panics if the expected table path is missing. +/// # Errors +/// +/// Returns a description of the failure if the expected table path is +/// missing. #[cfg(any(test, feature = "test-support"))] -#[must_use] -pub fn extract_binstall_table(table: &toml::Table) -> toml::Table { +pub fn extract_binstall_table(table: &toml::Table) -> Result { table .get("package") .and_then(|p| p.get("metadata")) .and_then(|m| m.get("binstall")) .and_then(|b| b.as_table()) - .expect("[package.metadata.binstall] table not found") - .clone() + .cloned() + .ok_or_else(|| "[package.metadata.binstall] table not found".to_owned()) } #[cfg(test)] diff --git a/installer/src/binstall_metadata_tests.rs b/installer/src/binstall_metadata_tests.rs index d2407d922..2adf60e1d 100644 --- a/installer/src/binstall_metadata_tests.rs +++ b/installer/src/binstall_metadata_tests.rs @@ -10,8 +10,8 @@ use super::*; #[rstest] fn pkg_url_matches_design_document() { - let table = load_cargo_toml(); - let binstall = extract_binstall_table(&table); + let table = load_cargo_toml().expect("load installer Cargo.toml"); + let binstall = extract_binstall_table(&table).expect("extract binstall table"); let pkg_url = binstall .get("pkg-url") .and_then(|v| v.as_str()) @@ -21,8 +21,8 @@ fn pkg_url_matches_design_document() { #[rstest] fn bin_dir_matches_design_document() { - let table = load_cargo_toml(); - let binstall = extract_binstall_table(&table); + let table = load_cargo_toml().expect("load installer Cargo.toml"); + let binstall = extract_binstall_table(&table).expect("extract binstall table"); let bin_dir = binstall .get("bin-dir") .and_then(|v| v.as_str()) @@ -32,8 +32,8 @@ fn bin_dir_matches_design_document() { #[rstest] fn default_pkg_fmt_is_tgz() { - let table = load_cargo_toml(); - let binstall = extract_binstall_table(&table); + let table = load_cargo_toml().expect("load installer Cargo.toml"); + let binstall = extract_binstall_table(&table).expect("extract binstall table"); let pkg_fmt = binstall .get("pkg-fmt") .and_then(|v| v.as_str()) @@ -43,8 +43,8 @@ fn default_pkg_fmt_is_tgz() { #[rstest] fn windows_override_uses_zip() { - let table = load_cargo_toml(); - let binstall = extract_binstall_table(&table); + let table = load_cargo_toml().expect("load installer Cargo.toml"); + let binstall = extract_binstall_table(&table).expect("extract binstall table"); let overrides = binstall .get("overrides") .and_then(|o| o.as_table()) @@ -62,8 +62,8 @@ fn windows_override_uses_zip() { #[rstest] fn no_unexpected_overrides() { - let table = load_cargo_toml(); - let binstall = extract_binstall_table(&table); + let table = load_cargo_toml().expect("load installer Cargo.toml"); + let binstall = extract_binstall_table(&table).expect("extract binstall table"); let overrides = binstall .get("overrides") .and_then(|o| o.as_table()) @@ -82,8 +82,8 @@ fn no_unexpected_overrides() { #[rstest] fn essential_binstall_fields_present() { - let table = load_cargo_toml(); - let binstall = extract_binstall_table(&table); + let table = load_cargo_toml().expect("load installer Cargo.toml"); + let binstall = extract_binstall_table(&table).expect("extract binstall table"); let required = ["pkg-url", "bin-dir", "pkg-fmt"]; for key in &required { assert!( diff --git a/installer/src/builder.rs b/installer/src/builder.rs index 9fdbc1aa2..739358dc7 100644 --- a/installer/src/builder.rs +++ b/installer/src/builder.rs @@ -63,7 +63,7 @@ pub struct Builder { impl Builder { /// Create a new builder with the given configuration. #[must_use] - pub fn new(config: BuildConfig) -> Self { + pub const fn new(config: BuildConfig) -> Self { Self { config } } @@ -186,7 +186,7 @@ impl Builder { /// This method is primarily useful for testing to verify that the correct /// configuration was constructed. #[must_use] - pub fn config(&self) -> &BuildConfig { + pub const fn config(&self) -> &BuildConfig { &self.config } } @@ -229,6 +229,8 @@ pub const fn library_prefix() -> &'static str { #[cfg(test)] mod tests { + //! Tests for lint-library build orchestration. + use super::*; use rstest::{fixture, rstest}; diff --git a/installer/src/cli.rs b/installer/src/cli.rs index 0ef5fbc51..fcc281437 100644 --- a/installer/src/cli.rs +++ b/installer/src/cli.rs @@ -83,13 +83,9 @@ pub struct InstallArgs { #[arg(short, long, value_name = "NAME")] pub lint: Vec, - /// Build all individual lint crates instead of the aggregated suite. - #[arg(long, conflicts_with = "lint")] - pub individual_lints: bool, - - /// Include experimental lints when available. - #[arg(long)] - pub experimental: bool, + /// Flags selecting which lint crates are built. + #[command(flatten)] + pub lint_selection: LintSelectionFlags, /// Number of parallel cargo build jobs. #[arg(short, long, value_name = "N")] @@ -99,13 +95,9 @@ pub struct InstallArgs { #[arg(long, value_name = "TOOLCHAIN")] pub toolchain: Option, - /// Install rustc-codegen-cranelift via rustup. - #[arg(long, default_value_t = false)] - pub cranelift: bool, - - /// Show configuration and exit without building. - #[arg(long)] - pub dry_run: bool, + /// Flags adjusting how the installer executes the build. + #[command(flatten)] + pub execution: ExecutionFlags, /// Increase cargo output verbosity (repeatable: -v, -vv, -vvv). #[arg( @@ -121,6 +113,48 @@ pub struct InstallArgs { #[arg(short, long, conflicts_with = "verbosity")] pub quiet: bool, + /// Flags that skip individual installation steps. + #[command(flatten)] + pub skip: SkipFlags, + + /// Skip prebuilt artefact download and build from source. + #[arg(long = "build-only")] + pub is_build_only: bool, +} + +/// Flags selecting which lint crates are built. +/// +/// Flattened into [`InstallArgs`] so the command-line surface is unchanged. +#[derive(clap::Args, Debug, Clone, Default)] +pub struct LintSelectionFlags { + /// Build all individual lint crates instead of the aggregated suite. + #[arg(long, conflicts_with = "lint")] + pub individual_lints: bool, + + /// Include experimental lints when available. + #[arg(long)] + pub experimental: bool, +} + +/// Flags adjusting how the installer executes the build. +/// +/// Flattened into [`InstallArgs`] so the command-line surface is unchanged. +#[derive(clap::Args, Debug, Clone, Default)] +pub struct ExecutionFlags { + /// Install rustc-codegen-cranelift via rustup. + #[arg(long, default_value_t = false)] + pub cranelift: bool, + + /// Show configuration and exit without building. + #[arg(long)] + pub dry_run: bool, +} + +/// Flags that skip individual installation steps. +/// +/// Flattened into [`InstallArgs`] so the command-line surface is unchanged. +#[derive(clap::Args, Debug, Clone, Default)] +pub struct SkipFlags { /// Skip installation of cargo-dylint and dylint-link. #[arg(long)] pub skip_deps: bool, @@ -132,10 +166,6 @@ pub struct InstallArgs { /// Do not update existing repository clone. #[arg(long)] pub no_update: bool, - - /// Skip prebuilt artefact download and build from source. - #[arg(long = "build-only")] - pub is_build_only: bool, } /// Arguments for the list command. @@ -178,7 +208,7 @@ impl InstallArgs { /// ``` #[must_use] pub fn should_attempt_prebuilt(&self, requested_crates: &[CrateName]) -> bool { - if self.is_build_only || self.experimental { + if self.is_build_only || self.lint_selection.experimental { return false; } !requested_crates @@ -199,25 +229,21 @@ impl Default for InstallArgs { /// use whitaker_installer::cli::InstallArgs; /// /// let args = InstallArgs::default(); - /// assert!(!args.individual_lints); - /// assert!(!args.skip_deps); + /// assert!(!args.lint_selection.individual_lints); + /// assert!(!args.skip.skip_deps); /// assert!(args.lint.is_empty()); /// ``` fn default() -> Self { Self { target_dir: None, lint: Vec::new(), - individual_lints: false, - experimental: false, + lint_selection: LintSelectionFlags::default(), jobs: None, toolchain: None, - cranelift: false, - dry_run: false, + execution: ExecutionFlags::default(), verbosity: 0, quiet: false, - skip_deps: false, - skip_wrapper: false, - no_update: false, + skip: SkipFlags::default(), is_build_only: false, } } @@ -256,7 +282,7 @@ impl Cli { /// install arguments. Callers should check `self.command` before calling /// this method if the `List` case needs different handling. #[must_use] - pub fn install_args(&self) -> &InstallArgs { + pub const fn install_args(&self) -> &InstallArgs { match &self.command { Some(Command::Install(args)) => args, Some(Command::List(_)) | None => &self.install, diff --git a/installer/src/cli_tests.rs b/installer/src/cli_tests.rs index 048118e9b..39bfc8195 100644 --- a/installer/src/cli_tests.rs +++ b/installer/src/cli_tests.rs @@ -26,16 +26,19 @@ fn assert_default_lint_selection(cli: &Cli) { assert_default_conditions(&[ ("target_dir", cli.install.target_dir.is_none()), ("lint", cli.install.lint.is_empty()), - ("individual_lints", !cli.install.individual_lints), - ("experimental", !cli.install.experimental), + ( + "individual_lints", + !cli.install.lint_selection.individual_lints, + ), + ("experimental", !cli.install.lint_selection.experimental), ]); } /// Assert that execution options default to a full, non-quiet build. fn assert_default_execution_options(cli: &Cli) { assert_default_conditions(&[ - ("cranelift", !cli.install.cranelift), - ("dry_run", !cli.install.dry_run), + ("cranelift", !cli.install.execution.cranelift), + ("dry_run", !cli.install.execution.dry_run), ("verbosity", cli.install.verbosity == 0), ("quiet", !cli.install.quiet), ]); @@ -44,9 +47,9 @@ fn assert_default_execution_options(cli: &Cli) { /// Assert that no pipeline steps are skipped by default. fn assert_default_skip_flags(cli: &Cli) { assert_default_conditions(&[ - ("skip_deps", !cli.install.skip_deps), - ("skip_wrapper", !cli.install.skip_wrapper), - ("no_update", !cli.install.no_update), + ("skip_deps", !cli.install.skip.skip_deps), + ("skip_wrapper", !cli.install.skip.skip_wrapper), + ("no_update", !cli.install.skip.no_update), ("is_build_only", !cli.install.is_build_only), ]); } @@ -115,7 +118,7 @@ fn cli_parses_install_with_args() { ]); match cli.command { Some(Command::Install(args)) => { - assert!(args.experimental); + assert!(args.lint_selection.experimental); assert_eq!(args.lint, vec!["module_max_lines"]); } _ => panic!("expected Install command"), @@ -142,7 +145,10 @@ fn should_attempt_prebuilt_false_when_build_only() { #[test] fn should_attempt_prebuilt_false_when_experimental_flag_enabled() { let args = InstallArgs { - experimental: true, + lint_selection: LintSelectionFlags { + experimental: true, + ..LintSelectionFlags::default() + }, ..InstallArgs::default() }; let requested = vec![CrateName::from("whitaker_suite")]; @@ -158,15 +164,15 @@ fn should_attempt_prebuilt_true_for_stable_bumpy_road_requests() { /// Parameterized tests for boolean CLI flags (backwards compatibility). #[rstest] -#[case::individual_lints(&["whitaker-installer", "--individual-lints"], |cli: &Cli| cli.install.individual_lints)] -#[case::experimental(&["whitaker-installer", "--experimental"], |cli: &Cli| cli.install.experimental)] -#[case::cranelift(&["whitaker-installer", "--cranelift"], |cli: &Cli| cli.install.cranelift)] -#[case::dry_run(&["whitaker-installer", "--dry-run"], |cli: &Cli| cli.install.dry_run)] +#[case::individual_lints(&["whitaker-installer", "--individual-lints"], |cli: &Cli| cli.install.lint_selection.individual_lints)] +#[case::experimental(&["whitaker-installer", "--experimental"], |cli: &Cli| cli.install.lint_selection.experimental)] +#[case::cranelift(&["whitaker-installer", "--cranelift"], |cli: &Cli| cli.install.execution.cranelift)] +#[case::dry_run(&["whitaker-installer", "--dry-run"], |cli: &Cli| cli.install.execution.dry_run)] #[case::verbose(&["whitaker-installer", "-v"], |cli: &Cli| cli.install.verbosity > 0)] #[case::quiet(&["whitaker-installer", "-q"], |cli: &Cli| cli.install.quiet)] -#[case::skip_deps(&["whitaker-installer", "--skip-deps"], |cli: &Cli| cli.install.skip_deps)] -#[case::skip_wrapper(&["whitaker-installer", "--skip-wrapper"], |cli: &Cli| cli.install.skip_wrapper)] -#[case::no_update(&["whitaker-installer", "--no-update"], |cli: &Cli| cli.install.no_update)] +#[case::skip_deps(&["whitaker-installer", "--skip-deps"], |cli: &Cli| cli.install.skip.skip_deps)] +#[case::skip_wrapper(&["whitaker-installer", "--skip-wrapper"], |cli: &Cli| cli.install.skip.skip_wrapper)] +#[case::no_update(&["whitaker-installer", "--no-update"], |cli: &Cli| cli.install.skip.no_update)] #[case::build_only(&["whitaker-installer", "--build-only"], |cli: &Cli| cli.install.is_build_only)] fn cli_parses_boolean_flags(#[case] args: &[&str], #[case] check: fn(&Cli) -> bool) { let cli = Cli::parse_from(args); @@ -196,10 +202,10 @@ fn cli_rejects_conflicting_flags(#[case] args: &[&str]) { fn install_args_default_is_valid() { let args = InstallArgs::default(); assert_default_conditions(&[ - ("individual_lints", !args.individual_lints), - ("experimental", !args.experimental), - ("cranelift", !args.cranelift), - ("skip_deps", !args.skip_deps), + ("individual_lints", !args.lint_selection.individual_lints), + ("experimental", !args.lint_selection.experimental), + ("cranelift", !args.execution.cranelift), + ("skip_deps", !args.skip.skip_deps), ]); } @@ -214,12 +220,12 @@ fn list_args_default_is_valid() { fn install_args_returns_flattened_when_no_subcommand() { let cli = Cli::parse_from(["whitaker-installer", "--experimental"]); let args = cli.install_args(); - assert!(args.experimental); + assert!(args.lint_selection.experimental); } #[test] fn install_args_returns_subcommand_args_when_present() { let cli = Cli::parse_from(["whitaker-installer", "install", "--dry-run"]); let args = cli.install_args(); - assert!(args.dry_run); + assert!(args.execution.dry_run); } diff --git a/installer/src/crate_name.rs b/installer/src/crate_name.rs index 5a384cc7e..c468fe13a 100644 --- a/installer/src/crate_name.rs +++ b/installer/src/crate_name.rs @@ -60,6 +60,8 @@ impl fmt::Display for CrateName { #[cfg(test)] mod tests { + //! Tests for crate-name normalization. + use super::*; use std::collections::HashSet; diff --git a/installer/src/dependency_binaries/install/checksum.rs b/installer/src/dependency_binaries/install/checksum.rs index ed7ab3347..fb069bd69 100644 --- a/installer/src/dependency_binaries/install/checksum.rs +++ b/installer/src/dependency_binaries/install/checksum.rs @@ -95,7 +95,7 @@ pub(super) fn fetch_expected_checksum( ); DependencyBinaryInstallError::Download { url: checksum_url.to_owned(), - reason: "empty or invalid checksum file".to_string(), + reason: "empty or invalid checksum file".to_owned(), } })?; let expected = token.to_ascii_lowercase(); @@ -133,7 +133,10 @@ fn compute_sha256(mut reader: impl Read) -> io::Result { Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, Err(error) => return Err(error), }; - hasher.update(&buffer[..bytes_read]); + let chunk = buffer + .get(..bytes_read) + .ok_or_else(|| io::Error::other("reader returned an out-of-range byte count"))?; + hasher.update(chunk); } Ok(to_lower_hex(&hasher.finalize())) } @@ -159,7 +162,7 @@ pub(super) fn verify_archive_checksum( if actual_checksum != expected { return Err(DependencyBinaryInstallError::Checksum { archive: archive.to_path_buf(), - expected: expected.to_string(), + expected: expected.to_owned(), actual: actual_checksum, }); } @@ -176,16 +179,16 @@ mod tests { use tempfile::NamedTempFile; /// Write `contents` to a fresh temp file and return the handle. - fn temp_file_with(contents: &[u8]) -> NamedTempFile { - let mut file = NamedTempFile::new().expect("create temp file"); - file.write_all(contents).expect("write temp file"); - file.flush().expect("flush temp file"); - file + fn temp_file_with(contents: &[u8]) -> io::Result { + let mut file = NamedTempFile::new()?; + file.write_all(contents)?; + file.flush()?; + Ok(file) } /// Reopen `file` as a fresh read handle for streaming into the hasher. - fn read_handle(file: &NamedTempFile) -> impl Read { - file.reopen().expect("reopen temp file") + fn read_handle(file: &NamedTempFile) -> io::Result { + file.reopen() } #[rstest] @@ -232,9 +235,10 @@ mod tests { #[test] fn compute_sha256_matches_known_vector() { - let file = temp_file_with(b"abc"); + let file = temp_file_with(b"abc").expect("temp file should be written"); assert_eq!( - compute_sha256(read_handle(&file)).expect("hash archive stream"), + compute_sha256(read_handle(&file).expect("temp file should reopen")) + .expect("hash archive stream"), concat!( "ba7816bf8f01cfea414140de5dae2223", "b00361a396177a9cb410ff61f20015ad", @@ -261,9 +265,12 @@ mod tests { } let take = self.data.len().min(self.chunk).min(buf.len()); let (to_copy, rest) = self.data.split_at(take); - buf.get_mut(..take) - .expect("take is bounded by buf.len()") - .copy_from_slice(to_copy); + let Some(target) = buf.get_mut(..take) else { + return Err(io::Error::other( + "read length must be bounded by the buffer", + )); + }; + target.copy_from_slice(to_copy); self.data = rest; Ok(take) } @@ -311,17 +318,29 @@ mod tests { #[test] fn verify_archive_checksum_accepts_a_matching_digest() { - let file = temp_file_with(b"hello world"); - let expected = compute_sha256(read_handle(&file)).expect("hash archive stream"); - assert!(verify_archive_checksum(read_handle(&file), file.path(), &expected).is_ok()); + let file = temp_file_with(b"hello world").expect("temp file should be written"); + let expected = compute_sha256(read_handle(&file).expect("temp file should reopen")) + .expect("hash archive stream"); + assert!( + verify_archive_checksum( + read_handle(&file).expect("temp file should reopen"), + file.path(), + &expected + ) + .is_ok() + ); } #[test] fn verify_archive_checksum_rejects_a_mismatched_digest() { - let file = temp_file_with(b"hello world"); + let file = temp_file_with(b"hello world").expect("temp file should be written"); let wrong = "0".repeat(64); - let error = verify_archive_checksum(read_handle(&file), file.path(), &wrong) - .expect_err("mismatched checksum must fail"); + let error = verify_archive_checksum( + read_handle(&file).expect("temp file should reopen"), + file.path(), + &wrong, + ) + .expect_err("mismatched checksum must fail"); match error { DependencyBinaryInstallError::Checksum { archive, diff --git a/installer/src/dependency_binaries/install/downloader.rs b/installer/src/dependency_binaries/install/downloader.rs index dbd3c15a8..f6ea8f206 100644 --- a/installer/src/dependency_binaries/install/downloader.rs +++ b/installer/src/dependency_binaries/install/downloader.rs @@ -100,25 +100,30 @@ pub(super) fn download_from_urls( // Acquire the parent-directory capability up front so every archive read and // write flows through it (never ambient `std::fs`); validation happens here, // before any HTTP request. - let destination = open_download_destination(destination)?; - destination.download_archive(agent, archive_url)?; + let destination_handle = open_download_destination(destination)?; + destination_handle.download_archive(agent, archive_url)?; // Any failure after the archive is written removes it, so a retry never sees // a partial or unverified file. let expected_checksum = fetch_expected_checksum(agent, checksum_url) - .inspect_err(|_| destination.remove_partial_archive())?; + .inspect_err(|_| destination_handle.remove_partial_archive())?; // Re-open the written archive and verify it; `verify_archive_checksum` // consumes the reader, closing the handle before any cleanup below. - let archive = destination.open_archive().inspect_err(|error| { + let archive = destination_handle.open_archive().inspect_err(|error| { warn!( category = CATEGORY_CAPABILITY, - archive_name = %destination.archive_name, + archive_name = %destination_handle.archive_name, error = %error, "failed to reopen archive for verification", ); - destination.remove_partial_archive(); + destination_handle.remove_partial_archive(); })?; - match verify_archive_checksum(archive, destination.path.as_std_path(), &expected_checksum) { + let verification = verify_archive_checksum( + archive, + destination_handle.path.as_std_path(), + &expected_checksum, + ); + match verification { Ok(()) => { debug!( category = CATEGORY_CHECKSUM, @@ -136,7 +141,7 @@ pub(super) fn download_from_urls( "archive checksum verification failed", ); // Remove the unverified archive so a retry never observes stale data. - destination.remove_partial_archive(); + destination_handle.remove_partial_archive(); Err(error) } } diff --git a/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs index 288ced087..7b091cf5c 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs @@ -11,7 +11,7 @@ fn download_from_urls_rejects_a_non_utf8_destination_before_any_request() { // No route is registered; the server exists only to prove it is never // contacted for an invalid destination. - let server = LocalServer::start(HashMap::new()); + let server = LocalServer::start(HashMap::new()).expect("local server should start"); // 0x80 is a lone UTF-8 continuation byte, so this path is never valid UTF-8 // and must be rejected during validation, before any HTTP request. diff --git a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs index 5b9c43ff4..493c38722 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs @@ -73,7 +73,7 @@ impl DownloadHarness { fn download_harness( #[default(HashMap::new())] routes: HashMap, ) -> std::io::Result { - let server = LocalServer::start(routes); + let server = LocalServer::start(routes)?; let temp = TempDir::new()?; let destination = temp.path().join("archive.tgz"); Ok(DownloadHarness { diff --git a/installer/src/dependency_binaries/install/extractor.rs b/installer/src/dependency_binaries/install/extractor.rs index 1fcde049c..62036bf22 100644 --- a/installer/src/dependency_binaries/install/extractor.rs +++ b/installer/src/dependency_binaries/install/extractor.rs @@ -57,8 +57,8 @@ pub(crate) fn extract_from_tgz( archive: archive_path.to_path_buf(), reason: error.to_string(), }; - for entry in archive.entries().map_err(map_archive_err)? { - let mut entry = entry.map_err(map_archive_err)?; + for entry_result in archive.entries().map_err(map_archive_err)? { + let mut entry = entry_result.map_err(map_archive_err)?; let path = entry.path().map_err(map_archive_err)?.into_owned(); if path == Path::new(expected_member_path) { return extract_entry_to_destination(&mut entry, expected_member_path, destination_dir); @@ -84,17 +84,17 @@ pub(crate) fn extract_from_zip( })?; for index in 0..archive.len() { - let mut file = + let mut member = archive .by_index(index) .map_err(|error| DependencyBinaryInstallError::Extraction { archive: archive_path.to_path_buf(), reason: error.to_string(), })?; - if file.name() != expected_member_path { + if member.name() != expected_member_path { continue; } - return extract_entry_to_destination(&mut file, expected_member_path, destination_dir); + return extract_entry_to_destination(&mut member, expected_member_path, destination_dir); } Err(DependencyBinaryInstallError::MissingBinaryInArchive { diff --git a/installer/src/dependency_binaries/install/http_test_server.rs b/installer/src/dependency_binaries/install/http_test_server.rs index 46f486c23..fe6d665fd 100644 --- a/installer/src/dependency_binaries/install/http_test_server.rs +++ b/installer/src/dependency_binaries/install/http_test_server.rs @@ -10,7 +10,7 @@ use std::io; use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, PoisonError}; use std::thread::{self, JoinHandle}; use std::time::Duration; @@ -52,12 +52,10 @@ pub(super) struct LocalServer { } impl LocalServer { - pub(super) fn start(routes: HashMap) -> Self { - let listener = TcpListener::bind("127.0.0.1:0").expect("bind loopback listener"); - let port = listener.local_addr().expect("resolve local addr").port(); - listener - .set_nonblocking(true) - .expect("set listener non-blocking"); + pub(super) fn start(routes: HashMap) -> io::Result { + let listener = TcpListener::bind("127.0.0.1:0")?; + let port = listener.local_addr()?.port(); + listener.set_nonblocking(true)?; let requested = Arc::new(Mutex::new(Vec::new())); let stop = Arc::new(AtomicBool::new(false)); let handle = { @@ -67,12 +65,12 @@ impl LocalServer { run_server(&listener, &routes, &requested_for_thread, &stop_for_thread); }) }; - Self { + Ok(Self { base_url: format!("http://127.0.0.1:{port}"), requested, stop, handle: Some(handle), - } + }) } pub(super) fn url(&self, path: &str) -> String { @@ -80,7 +78,12 @@ impl LocalServer { } pub(super) fn requested_paths(&self) -> Vec { - self.requested.lock().expect("lock requested paths").clone() + // A poisoned lock only means a test thread panicked while logging a + // request; the recorded paths remain a valid `Vec`. + self.requested + .lock() + .unwrap_or_else(PoisonError::into_inner) + .clone() } } @@ -117,6 +120,14 @@ fn run_server( /// Read one request, record its path, and write the matching canned response /// (or a 404). `Connection: close` lets the client frame the response end. +/// Restores blocking mode and bounds reads and writes on an accepted socket. +fn configure_connection(stream: &TcpStream) -> io::Result<()> { + stream.set_nonblocking(false)?; + stream.set_read_timeout(Some(Duration::from_secs(5)))?; + stream.set_write_timeout(Some(Duration::from_secs(5)))?; + Ok(()) +} + fn serve_connection( mut stream: TcpStream, routes: &HashMap, @@ -126,14 +137,7 @@ fn serve_connection( // (the listener is non-blocking only so the accept loop can poll for // shutdown). `try_clone` shares the socket, so `peer` inherits these. // Drop the connection if the socket cannot be configured. - if stream.set_nonblocking(false).is_err() - || stream - .set_read_timeout(Some(Duration::from_secs(5))) - .is_err() - || stream - .set_write_timeout(Some(Duration::from_secs(5))) - .is_err() - { + if configure_connection(&stream).is_err() { return; } let Ok(peer) = stream.try_clone() else { @@ -153,7 +157,8 @@ fn serve_connection( loop { let mut line = String::new(); let bytes_read = reader.read_line(&mut line).unwrap_or(0); - if bytes_read == 0 || line == "\r\n" || line == "\n" { + let is_blank_line = matches!(line.as_str(), "\r\n" | "\n"); + if bytes_read == 0 || is_blank_line { break; } } @@ -170,7 +175,11 @@ fn serve_connection( ) }, ); - requested.lock().expect("lock requested paths").push(path); + // See `LocalServer::requested_paths` for why poisoning is recovered here. + requested + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(path); let header = format!( concat!( "HTTP/1.1 {}\r\n", diff --git a/installer/src/dependency_binaries/install/installer.rs b/installer/src/dependency_binaries/install/installer.rs index 1130bdc28..a2648365c 100644 --- a/installer/src/dependency_binaries/install/installer.rs +++ b/installer/src/dependency_binaries/install/installer.rs @@ -80,7 +80,7 @@ pub enum DependencyBinaryInstallError { impl DependencyBinaryInstallError { /// Returns `true` when the failure is caused by a missing repository asset. #[must_use] - pub(crate) fn is_not_found(&self) -> bool { + pub(crate) const fn is_not_found(&self) -> bool { matches!(self, Self::NotFound { .. }) } } @@ -143,7 +143,7 @@ pub(crate) fn install_with( ) -> Result { let bin_dir = support .dirs - .bin_dir() + .executables() .ok_or(DependencyBinaryInstallError::MissingBinDir)?; fs::create_dir_all(bin_dir.as_path())?; diff --git a/installer/src/dependency_binaries/install/metadata.rs b/installer/src/dependency_binaries/install/metadata.rs index 4ebf48100..5f512b197 100644 --- a/installer/src/dependency_binaries/install/metadata.rs +++ b/installer/src/dependency_binaries/install/metadata.rs @@ -37,7 +37,7 @@ pub fn host_target() -> Option { /// Return the release-side provenance asset filename. #[must_use] -pub fn provenance_filename() -> &'static str { +pub const fn provenance_filename() -> &'static str { PROVENANCE_FILENAME } diff --git a/installer/src/dependency_binaries/install/tests.rs b/installer/src/dependency_binaries/install/tests.rs index 6b82a999f..1ea9e99d9 100644 --- a/installer/src/dependency_binaries/install/tests.rs +++ b/installer/src/dependency_binaries/install/tests.rs @@ -17,21 +17,26 @@ use std::path::{Path, PathBuf}; fn run_install_scenario( dependency_name: &str, writes_binary: bool, -) -> ( +) -> std::io::Result<( tempfile::TempDir, PathBuf, Result, -) { - let temp_dir = tempfile::tempdir().expect("temp dir"); +)> { + let temp_dir = tempfile::tempdir()?; let bin_dir = temp_dir.path().join("bin"); let mut dirs = MockBaseDirs::new(); - dirs.expect_bin_dir() + dirs.expect_executables() .once() .return_const(Some(bin_dir.clone())); let dependency = crate::dependency_binaries::find_dependency_binary(dependency_name) - .expect("dependency manifest should load") - .expect("dependency should exist"); - let target = TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"); + .map_err(std::io::Error::other)? + .ok_or_else(|| { + std::io::Error::other(format!( + "dependency `{dependency_name}` is not in the manifest" + )) + })?; + let target = + TargetTriple::try_from("x86_64-unknown-linux-gnu").map_err(std::io::Error::other)?; let mut downloader = MockDependencyArchiveDownloader::new(); let expected_archive = archive_filename(dependency, &target); downloader @@ -54,9 +59,11 @@ fn run_install_scenario( binary: expected_member_path.to_owned(), }); } - let binary_name = Path::new(expected_member_path) - .file_name() - .expect("member path should include a filename"); + let Some(binary_name) = Path::new(expected_member_path).file_name() else { + return Err(DependencyBinaryInstallError::MissingBinaryInArchive { + binary: expected_member_path.to_owned(), + }); + }; let path = destination_dir.join(binary_name); fs::write(&path, b"fake binary")?; Ok(path) @@ -71,24 +78,24 @@ fn run_install_scenario( }, ); - (temp_dir, bin_dir, result) + Ok((temp_dir, bin_dir, result)) } #[fixture] -fn cargo_dylint_install_result() -> ( +fn cargo_dylint_install_result() -> std::io::Result<( tempfile::TempDir, PathBuf, Result, -) { +)> { run_install_scenario("cargo-dylint", true) } #[fixture] -fn missing_binary_install_result() -> ( +fn missing_binary_install_result() -> std::io::Result<( tempfile::TempDir, PathBuf, Result, -) { +)> { run_install_scenario("dylint-link", false) } @@ -115,12 +122,13 @@ fn binary_filename_adds_windows_suffix() { #[rstest] fn install_with_creates_missing_bin_directory( - cargo_dylint_install_result: ( + #[from(cargo_dylint_install_result)] scenario: std::io::Result<( tempfile::TempDir, PathBuf, Result, - ), + )>, ) { + let cargo_dylint_install_result = scenario.expect("install scenario should be staged"); let (_temp_dir, bin_dir, result) = cargo_dylint_install_result; let installed_path = result.expect("install"); assert!(bin_dir.is_dir()); @@ -137,12 +145,13 @@ fn install_with_creates_missing_bin_directory( #[rstest] fn install_with_returns_error_when_binary_missing_after_extract( - missing_binary_install_result: ( + #[from(missing_binary_install_result)] scenario: std::io::Result<( tempfile::TempDir, PathBuf, Result, - ), + )>, ) { + let missing_binary_install_result = scenario.expect("install scenario should be staged"); let (_temp_dir, _bin_dir, result) = missing_binary_install_result; let dependency = crate::dependency_binaries::find_dependency_binary("dylint-link") .expect("dependency manifest should load") @@ -154,7 +163,7 @@ fn install_with_returns_error_when_binary_missing_after_extract( DependencyBinaryInstallError::MissingBinaryInArchive { binary } => { assert_eq!(binary, expected_path); } - other => panic!("expected MissingBinaryInArchive, got {:?}", other), + other => panic!("expected MissingBinaryInArchive, got {other:?}"), } } diff --git a/installer/src/dependency_binaries/manifest.rs b/installer/src/dependency_binaries/manifest.rs index 51bfdc95f..4d422b3b2 100644 --- a/installer/src/dependency_binaries/manifest.rs +++ b/installer/src/dependency_binaries/manifest.rs @@ -108,7 +108,7 @@ pub enum ManifestError { impl From for ManifestError { fn from(error: toml::de::Error) -> Self { - ManifestError::ParseError(error.to_string()) + Self::ParseError(error.to_string()) } } @@ -123,7 +123,7 @@ impl From for ManifestError { /// assert!(contents.contains("dependency_binaries")); /// ``` #[must_use] -pub fn manifest_contents() -> &'static str { +pub const fn manifest_contents() -> &'static str { include_str!("../../dependency-binaries.toml") } @@ -201,8 +201,8 @@ pub fn parse_manifest(contents: &str) -> Result, ManifestE let mut seen_packages = std::collections::HashSet::new(); for dependency in &manifest.dependency_binaries { let package = dependency.package(); - if !seen_packages.insert(package.to_string()) { - return Err(ManifestError::DuplicatePackage(package.to_string())); + if !seen_packages.insert(package.to_owned()) { + return Err(ManifestError::DuplicatePackage(package.to_owned())); } } @@ -293,6 +293,8 @@ pub fn find_dependency_binary( #[cfg(test)] mod tests { + //! Tests for the embedded dependency-binary manifest. + use super::*; use rstest::{fixture, rstest}; diff --git a/installer/src/dependency_packaging.rs b/installer/src/dependency_packaging.rs index a8f137c61..6944c59cc 100644 --- a/installer/src/dependency_packaging.rs +++ b/installer/src/dependency_packaging.rs @@ -7,7 +7,7 @@ use crate::dependency_binaries::{DependencyBinary, binary_filename, provenance_filename}; pub use crate::installer_packaging::{ArchiveFormat, TargetTriple}; use std::fs; -use std::io::{Read, Write}; +use std::io; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -71,8 +71,14 @@ pub fn archive_format(target: &TargetTriple) -> ArchiveFormat { } /// Package a dependency executable into the deterministic repository archive. +/// +/// # Errors +/// +/// Returns [`DependencyPackagingError::BinaryNotFound`] when +/// `params.binary_path` is not a file, and propagates I/O or archive +/// failures encountered while writing the archive. pub fn package_dependency_binary( - params: DependencyPackageParams, + params: &DependencyPackageParams, ) -> Result { if !params.binary_path.is_file() { return Err(DependencyPackagingError::BinaryNotFound( @@ -96,10 +102,10 @@ pub fn package_dependency_binary( } } - let archive_path = archive_path.canonicalize()?; + let canonical_archive_path = archive_path.canonicalize()?; Ok(DependencyPackageOutput { - archive_path, + archive_path: canonical_archive_path, archive_name, }) } @@ -107,21 +113,32 @@ pub fn package_dependency_binary( /// Render the shared dependency-binary provenance and licence document. #[must_use] pub fn render_provenance_markdown(dependencies: &[DependencyBinary]) -> String { - let mut output = String::from("# Dependency binary licences and provenance\n\n"); - output.push_str( - "Whitaker publishes the following third-party dependency binaries from repository releases.\n\n", - ); - for dependency in dependencies { - output.push_str(&format!("## {}\n\n", dependency.package())); - output.push_str(&format!("- Binary: `{}`\n", dependency.binary())); - output.push_str(&format!("- Version: `{}`\n", dependency.version())); - output.push_str(&format!("- Licence: `{}`\n", dependency.license())); - output.push_str(&format!("- Repository: {}\n\n", dependency.repository())); - } - output + let sections: String = dependencies.iter().map(render_dependency_section).collect(); + format!( + "# Dependency binary licences and provenance\n\n\ + Whitaker publishes the following third-party dependency binaries \ + from repository releases.\n\n{sections}" + ) +} + +/// Render the provenance section for a single dependency binary. +fn render_dependency_section(dependency: &DependencyBinary) -> String { + format!( + "## {}\n\n- Binary: `{}`\n- Version: `{}`\n- Licence: `{}`\n- Repository: {}\n\n", + dependency.package(), + dependency.binary(), + dependency.version(), + dependency.license(), + dependency.repository() + ) } /// Write the shared provenance document to `output_dir`. +/// +/// # Errors +/// +/// Returns [`DependencyPackagingError::Io`] when the output directory +/// cannot be created or the document cannot be written. pub fn write_provenance_markdown( output_dir: &Path, dependencies: &[DependencyBinary], @@ -144,8 +161,8 @@ fn create_tgz_archive( let mut archive = tar::Builder::new(gz_encoder); archive.mode(tar::HeaderMode::Deterministic); archive.append_path_with_name(binary_path, format!("{inner_dir}/{binary_name}"))?; - let gz_encoder = archive.into_inner()?; - gz_encoder.finish()?; + let finished_encoder = archive.into_inner()?; + finished_encoder.finish()?; Ok(()) } @@ -163,14 +180,7 @@ fn create_zip_archive( .compression_method(zip::CompressionMethod::Deflated); zip_writer.start_file(format!("{inner_dir}/{binary_name}"), options)?; let mut binary_file = fs::File::open(binary_path)?; - let mut buffer = [0u8; 8_192]; - loop { - let bytes_read = binary_file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - zip_writer.write_all(&buffer[..bytes_read])?; - } + io::copy(&mut binary_file, &mut zip_writer)?; zip_writer.finish()?; Ok(()) } diff --git a/installer/src/dependency_packaging_tests.rs b/installer/src/dependency_packaging_tests.rs index e4aa09fbe..7965d2a2e 100644 --- a/installer/src/dependency_packaging_tests.rs +++ b/installer/src/dependency_packaging_tests.rs @@ -85,7 +85,7 @@ fn package_dependency_binary_handles_binary_presence( fs::write(&binary_path, b"binary").expect("write fake binary"); } - let result = package_dependency_binary(DependencyPackageParams { + let result = package_dependency_binary(&DependencyPackageParams { dependency: dependency.clone(), target: linux_target.clone(), binary_path: binary_path.clone(), diff --git a/installer/src/deps/install.rs b/installer/src/deps/install.rs index acd5a801d..5aa5fcf3a 100644 --- a/installer/src/deps/install.rs +++ b/installer/src/deps/install.rs @@ -11,6 +11,14 @@ use super::{ DylintToolStatus, is_binstall_available, is_tool_installed, }; +#[path = "install_repository.rs"] +mod repository; + +use repository::{ + RepositoryInstall, RepositoryInstallRequest, attempt_repository_install, + resolve_dependency_binary, +}; + pub(super) struct RepositoryInstallContext<'a> { pub(super) dirs: &'a dyn crate::dirs::BaseDirs, pub(super) installer: &'a dyn DependencyBinaryInstaller, @@ -25,14 +33,14 @@ pub(super) struct InstallContext<'a> { pub(super) fn install_missing_tools( executor: &dyn CommandExecutor, - status: &DylintToolStatus, + status: DylintToolStatus, stderr: &mut dyn Write, context: &InstallContext<'_>, ) -> Result<()> { - let mut remaining_status = *status; + let mut remaining_status = status; - for tool in DEPENDENCY_TOOLS.iter() { - if !should_install_tool(&remaining_status, tool) { + for tool in &DEPENDENCY_TOOLS { + if !should_install_tool(remaining_status, tool) { continue; } @@ -49,11 +57,13 @@ pub(super) fn repository_install_context<'a>( target: Option<&'a TargetTriple>, ) -> Option> { match (dirs, installer, target) { - (Some(dirs), Some(installer), Some(target)) => Some(RepositoryInstallContext { - dirs, - installer, - target, - }), + (Some(base_dirs), Some(binary_installer), Some(target_triple)) => { + Some(RepositoryInstallContext { + dirs: base_dirs, + installer: binary_installer, + target: target_triple, + }) + } _ => None, } } @@ -66,7 +76,7 @@ pub(super) fn cargo_fallback_mode(executor: &dyn CommandExecutor) -> InstallMode } } -pub(super) fn should_install_tool(status: &DylintToolStatus, tool: &DependencyTool) -> bool { +pub(super) fn should_install_tool(status: DylintToolStatus, tool: &DependencyTool) -> bool { if tool == &CARGO_DYLINT_TOOL { !status.cargo_dylint } else if tool == &DYLINT_LINK_TOOL { @@ -85,62 +95,19 @@ pub(super) fn install_tool( let mut cargo_install_plan = CargoInstallPlan::new(tool); if let Some(repo) = &context.repo { - let Some(dependency) = find_dependency_binary(tool.package).map_err(|error| { - InstallerError::DependencyInstall { - tool: tool.package, - message: error.to_string(), - } - })? - else { - return Err(InstallerError::DependencyInstall { - tool: tool.package, - message: format!( - "dependency manifest is missing an entry for {}", - tool.package - ), - }); - }; + let dependency = resolve_dependency_binary(tool)?; cargo_install_plan = cargo_install_plan.with_version(dependency.version()); - match repo.installer.install(dependency, repo.target, repo.dirs) { - Ok(_) if repository_install_verified(executor, tool) => { - write_message( - stderr, - context.quiet, - format!("Installed {} from repository release.", tool.package), - ); - return Ok(InstallOutcome::RepositoryRelease); - } - Ok(_) => { - write_message( - stderr, - context.quiet, - format!( - "Repository install for {} failed verification; falling back to Cargo.", - tool.package - ), - ); - } - Err(error) if error.is_not_found() => { + let request = RepositoryInstallRequest { + executor, + tool, + dependency, + }; + match attempt_repository_install(&request, stderr, context, repo) { + RepositoryInstall::Installed => return Ok(InstallOutcome::RepositoryRelease), + RepositoryInstall::FallBackToCargo => {} + RepositoryInstall::FallBackToCargoInstall => { cargo_install_plan = cargo_install_plan.skip_binstall(); - write_message( - stderr, - context.quiet, - format!( - "Repository install for {} unavailable: {error}. Falling back to Cargo.", - tool.package - ), - ); - } - Err(error) => { - write_message( - stderr, - context.quiet, - format!( - "Repository install for {} unavailable: {error}. Falling back to Cargo.", - tool.package - ), - ); } } } @@ -169,7 +136,7 @@ struct CargoInstallPlan<'a> { } impl<'a> CargoInstallPlan<'a> { - fn new(tool: &'a DependencyTool) -> Self { + const fn new(tool: &'a DependencyTool) -> Self { Self { tool, version: None, @@ -177,14 +144,14 @@ impl<'a> CargoInstallPlan<'a> { } } - fn with_version(self, version: &'a str) -> Self { + const fn with_version(self, version: &'a str) -> Self { Self { version: Some(version), ..self } } - fn skip_binstall(self) -> Self { + const fn skip_binstall(self) -> Self { Self { skip_binstall: true, ..self @@ -206,7 +173,7 @@ fn install_tool_with_cargo( write_message( stderr, context.quiet, - format!( + &format!( "cargo binstall failed for {}; falling back to cargo install.", cargo_install_plan.tool.package ), @@ -240,7 +207,7 @@ fn try_binstall( write_message( stderr, quiet, - format!( + &format!( "Installed {} with cargo binstall.", cargo_install_plan.tool.package ), @@ -255,18 +222,21 @@ fn run_cargo_install( quiet: bool, ) -> Result { let mut args = vec!["install"]; - let success_message = if let Some(version) = cargo_install_plan.version { - args.extend(["--locked", "--version", version]); - format!( - "Installed {} from source with cargo install.", - cargo_install_plan.tool.package - ) - } else { - format!( - "Installed {} with cargo install.", - cargo_install_plan.tool.package - ) - }; + let success_message = cargo_install_plan.version.map_or_else( + || { + format!( + "Installed {} with cargo install.", + cargo_install_plan.tool.package + ) + }, + |version| { + args.extend(["--locked", "--version", version]); + format!( + "Installed {} from source with cargo install.", + cargo_install_plan.tool.package + ) + }, + ); args.push(cargo_install_plan.tool.package); let output = executor.run("cargo", &args)?; @@ -288,36 +258,10 @@ fn run_cargo_install( }); } - write_message(stderr, quiet, success_message); + write_message(stderr, quiet, &success_message); Ok(InstallOutcome::CargoInstall) } -/// Verify a repository-release install of `tool`. -/// -/// The trust boundary for a repository install is established entirely by the -/// installer pipeline: the release asset name pins the package and version, -/// the `.sha256` sidecar establishes integrity, extraction confirms the -/// expected archive member, and the permission step establishes launch -/// eligibility. A successful install is therefore sufficient evidence on its -/// own. -/// -/// `dylint-link` is additionally never executed as a health check. It is a -/// linker wrapper that forwards its entire argument list to the underlying -/// linker, so it has no reliable self-reporting subcommand: `--version` exits -/// early and `--help` depends on a usable linker and toolchain in the ambient -/// environment. Probing it rejects valid, verified artefacts and forces a -/// source build that cannot succeed on toolchains older than the crate's -/// rustc floor. -/// -/// `cargo-dylint` keeps the generic check because it reports its own version -/// and must additionally be discoverable by Cargo as a subcommand. -fn repository_install_verified(executor: &dyn CommandExecutor, tool: &DependencyTool) -> bool { - if tool == &DYLINT_LINK_TOOL { - return true; - } - is_tool_installed(executor, tool) -} - pub(super) fn command_error_message(output: &Output) -> String { let stderr = String::from_utf8_lossy(&output.stderr); let trimmed = stderr.trim(); @@ -328,11 +272,13 @@ pub(super) fn command_error_message(output: &Output) -> String { } } -pub(super) fn write_message(stderr: &mut dyn Write, quiet: bool, message: String) { +pub(super) fn write_message(stderr: &mut dyn Write, quiet: bool, message: &str) { if quiet { return; } - let _ = writeln!(stderr, "{message}"); + // Progress output is best effort; a failed stderr write must not + // abort the installation. + drop(writeln!(stderr, "{message}")); } pub(super) fn command_succeeds(executor: &dyn CommandExecutor, cmd: &str, args: &[&str]) -> bool { @@ -341,7 +287,7 @@ pub(super) fn command_succeeds(executor: &dyn CommandExecutor, cmd: &str, args: .is_ok_and(|output| output.status.success()) } -fn should_refresh_companions(outcome: InstallOutcome, status: &DylintToolStatus) -> bool { +fn should_refresh_companions(outcome: InstallOutcome, status: DylintToolStatus) -> bool { outcome != InstallOutcome::RepositoryRelease && !status.dylint_link } @@ -354,7 +300,7 @@ fn update_status_after_install( if tool == &CARGO_DYLINT_TOOL { status.cargo_dylint = true; - if should_refresh_companions(outcome, status) { + if should_refresh_companions(outcome, *status) { // Installing cargo-dylint locally can also provide dylint-link. status.dylint_link = is_tool_installed(executor, &DYLINT_LINK_TOOL); } diff --git a/installer/src/deps/install_repository.rs b/installer/src/deps/install_repository.rs new file mode 100644 index 000000000..c64586ca8 --- /dev/null +++ b/installer/src/deps/install_repository.rs @@ -0,0 +1,135 @@ +//! Repository-release installation path for dependency tools. +//! +//! Whitaker publishes prebuilt `cargo-dylint` and `dylint-link` binaries as +//! release assets. This module resolves the manifest entry for a tool, drives +//! the release install, and reports which Cargo fallback (if any) remains +//! viable when the release cannot be used. + +use std::io::Write; + +use super::{ + CommandExecutor, DYLINT_LINK_TOOL, DependencyTool, InstallContext, InstallerError, + RepositoryInstallContext, Result, find_dependency_binary, is_tool_installed, write_message, +}; +use crate::dependency_binaries::DependencyBinary; + +/// Outcome of attempting a repository-release install for one dependency tool. +pub(super) enum RepositoryInstall { + /// The release was installed and verified; no Cargo fallback is needed. + Installed, + /// The release was unusable; fall back to the configured Cargo mode. + FallBackToCargo, + /// The release is absent upstream, so `cargo binstall` cannot help either. + FallBackToCargoInstall, +} + +/// Looks up the manifest entry describing the release asset for `tool`. +pub(super) fn resolve_dependency_binary( + tool: &DependencyTool, +) -> Result<&'static DependencyBinary> { + let entry = find_dependency_binary(tool.package).map_err(|error| { + InstallerError::DependencyInstall { + tool: tool.package, + message: error.to_string(), + } + })?; + + entry.ok_or_else(|| InstallerError::DependencyInstall { + tool: tool.package, + message: format!( + "dependency manifest is missing an entry for {}", + tool.package + ), + }) +} + +/// The tool to install from a repository release, together with the +/// collaborators needed to perform and verify that install. +/// +/// Grouped so [`attempt_repository_install`] stays within the workspace +/// argument budget; the three fields are only ever supplied together. +pub(super) struct RepositoryInstallRequest<'a> { + /// Executor used to probe whether the installed binary runs. + pub(super) executor: &'a dyn CommandExecutor, + /// The dependency tool being installed. + pub(super) tool: &'a DependencyTool, + /// Release metadata identifying the artefact to fetch. + pub(super) dependency: &'static DependencyBinary, +} + +/// Installs the requested tool from a repository release, reporting whether +/// Cargo must still run and, if so, which fallback mode remains viable. +pub(super) fn attempt_repository_install( + request: &RepositoryInstallRequest<'_>, + stderr: &mut dyn Write, + context: &InstallContext<'_>, + repo: &RepositoryInstallContext<'_>, +) -> RepositoryInstall { + let tool = request.tool; + match repo + .installer + .install(request.dependency, repo.target, repo.dirs) + { + Ok(_) if repository_install_verified(request.executor, tool) => { + write_message( + stderr, + context.quiet, + &format!("Installed {} from repository release.", tool.package), + ); + RepositoryInstall::Installed + } + Ok(_) => { + write_message( + stderr, + context.quiet, + &format!( + "Repository install for {} failed verification; falling back to Cargo.", + tool.package + ), + ); + RepositoryInstall::FallBackToCargo + } + Err(error) => { + let not_found = error.is_not_found(); + write_message( + stderr, + context.quiet, + &format!( + "Repository install for {} unavailable: {error}. Falling back to Cargo.", + tool.package + ), + ); + if not_found { + RepositoryInstall::FallBackToCargoInstall + } else { + RepositoryInstall::FallBackToCargo + } + } + } +} + +/// Verify a repository-release install of `tool`. +/// +/// The trust boundary for a repository install is established entirely by the +/// installer pipeline: the release asset name pins the package and version, +/// the `.sha256` sidecar establishes integrity, extraction confirms the +/// expected archive member, and the permission step establishes launch +/// eligibility. A successful install is therefore sufficient evidence on its +/// own. +/// +/// `dylint-link` is additionally never executed as a health check. It is a +/// linker wrapper that forwards its entire argument list to the underlying +/// linker, so it has no reliable self-reporting subcommand: `--version` exits +/// early and `--help` depends on a usable linker and toolchain in the ambient +/// environment. Probing it rejects valid, verified artefacts and forces a +/// source build that cannot succeed on toolchains older than the crate's +/// rustc floor. +/// +/// `cargo-dylint` keeps the generic check because it reports its own version +/// and must additionally be discoverable by Cargo as a subcommand. +fn repository_install_verified(executor: &dyn CommandExecutor, tool: &DependencyTool) -> bool { + if tool == &DYLINT_LINK_TOOL { + return true; + } + is_tool_installed(executor, tool) +} diff --git a/installer/src/deps/install_tests.rs b/installer/src/deps/install_tests.rs index a4c62899a..d6f9c7f44 100644 --- a/installer/src/deps/install_tests.rs +++ b/installer/src/deps/install_tests.rs @@ -27,7 +27,8 @@ fn update_status_after_install_refreshes_link_for_local_cargo_dylint_installs( assert!(status.cargo_dylint); assert!(status.dylint_link); executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[test] @@ -66,7 +67,7 @@ fn should_install_tool_returns_expected( dylint_link, }; - assert_eq!(should_install_tool(&status, tool), expected); + assert_eq!(should_install_tool(status, tool), expected); } #[rstest] @@ -90,5 +91,5 @@ fn should_refresh_companions_returns_expected( #[case] status: DylintToolStatus, #[case] expected: bool, ) { - assert_eq!(should_refresh_companions(outcome, &status), expected); + assert_eq!(should_refresh_companions(outcome, status), expected); } diff --git a/installer/src/deps/mod.rs b/installer/src/deps/mod.rs index 242c05c2b..005e11923 100644 --- a/installer/src/deps/mod.rs +++ b/installer/src/deps/mod.rs @@ -5,8 +5,8 @@ //! release archives before falling back to `cargo binstall` or `cargo install`. use crate::dependency_binaries::{ - DependencyBinaryInstaller, RepositoryDependencyBinaryInstaller, find_dependency_binary, - host_target, + DependencyBinary, DependencyBinaryInstaller, RepositoryDependencyBinaryInstaller, + find_dependency_binary, host_target, }; use crate::dirs::{BaseDirs, SystemBaseDirs}; use crate::error::{InstallerError, Result}; @@ -16,7 +16,10 @@ use std::path::Path; use std::process::{Command, Output}; mod install; -use install::*; +use install::{ + InstallContext, cargo_fallback_mode, command_succeeds, install_missing_tools, + repository_install_context, +}; /// Abstraction for running external commands. pub trait CommandExecutor { @@ -74,7 +77,7 @@ pub struct DylintToolStatus { impl DylintToolStatus { /// Returns `true` when both tools are installed. #[must_use] - pub fn all_installed(&self) -> bool { + pub const fn all_installed(&self) -> bool { self.cargo_dylint && self.dylint_link } } @@ -102,6 +105,11 @@ pub fn check_dylint_tools(executor: &dyn CommandExecutor) -> DylintToolStatus { } /// Install missing tools without emitting progress output. +/// +/// # Errors +/// +/// Returns an error when any missing tool cannot be installed by the +/// repository, `cargo binstall`, or `cargo install` strategies. pub fn install_dylint_tools( executor: &dyn CommandExecutor, status: &DylintToolStatus, @@ -111,6 +119,11 @@ pub fn install_dylint_tools( } /// Install missing tools while writing progress output to `stderr`. +/// +/// # Errors +/// +/// Returns an error when any missing tool cannot be installed by the +/// repository, `cargo binstall`, or `cargo install` strategies. pub fn install_dylint_tools_with_output( executor: &dyn CommandExecutor, status: &DylintToolStatus, @@ -124,7 +137,7 @@ pub fn install_dylint_tools_with_output( let cargo_fallback_mode = cargo_fallback_mode(executor); install_missing_tools( executor, - status, + *status, stderr, &InstallContext { repo: repository_install_context( @@ -145,12 +158,12 @@ pub fn install_dylint_tools_with_options( executor: &dyn CommandExecutor, status: &DylintToolStatus, stderr: &mut dyn Write, - options: DependencyInstallOptions<'_>, + options: &DependencyInstallOptions<'_>, ) -> Result<()> { let cargo_fallback_mode = cargo_fallback_mode(executor); install_missing_tools( executor, - status, + *status, stderr, &InstallContext { repo: repository_install_context( @@ -174,7 +187,7 @@ fn is_tool_installed(executor: &dyn CommandExecutor, tool: &DependencyTool) -> b let expected_version = find_dependency_binary(tool.package) .ok() .flatten() - .map(|dependency| dependency.version()); + .map(DependencyBinary::version); if tool == &DYLINT_LINK_TOOL { return is_dylint_link_installed(executor, expected_version); @@ -192,7 +205,7 @@ fn is_dylint_link_installed( if find_binary_on_path(DYLINT_LINK_TOOL.command).is_none() { return false; } - let Some(expected_version) = expected_version else { + let Some(version_to_match) = expected_version else { return true; }; // `dylint-link` is a pure linker wrapper: it forwards its entire argument @@ -201,7 +214,7 @@ fn is_dylint_link_installed( // registry of installed binaries instead, which records the version each // binary was installed at. cargo_installed_version(executor, DYLINT_LINK_TOOL.package) - .is_some_and(|version| version == expected_version) + .is_some_and(|version| version == version_to_match) } fn is_versioned_tool_installed( @@ -209,13 +222,13 @@ fn is_versioned_tool_installed( tool: &DependencyTool, expected_version: Option<&str>, ) -> bool { - let Some(expected_version) = expected_version else { + let Some(version_to_match) = expected_version else { return command_succeeds(executor, tool.command, tool.args); }; executor.run(tool.command, tool.args).is_ok_and(|output| { output.status.success() && first_semver_token(&String::from_utf8_lossy(&output.stdout)) - .is_some_and(|version| version == expected_version) + .is_some_and(|version| version == version_to_match) }) } @@ -327,8 +340,7 @@ fn is_executable_file(path: &Path) -> bool { use std::os::unix::fs::PermissionsExt; std::fs::metadata(path) - .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) - .unwrap_or(false) + .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) } #[cfg(not(unix))] diff --git a/installer/src/deps/path_tests.rs b/installer/src/deps/path_tests.rs index c9385b36e..92ec99cb1 100644 --- a/installer/src/deps/path_tests.rs +++ b/installer/src/deps/path_tests.rs @@ -32,7 +32,8 @@ fn check_dylint_tools_reports_installed_tools() { } ); executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[rstest::rstest] @@ -41,7 +42,7 @@ fn check_dylint_tools_reports_installed_tools() { fn check_dylint_tools_rejects_unusable_cargo_dylint_output(#[case] version_stdout: &str) { // The fake PATH keeps dylint-link absent so only cargo-dylint is probed. with_fake_path( - |_| (), + |_| Ok(()), || { let executor = StubExecutor::new(vec![cargo_dylint_check_with_result(Ok( stdout_output(version_stdout), @@ -58,7 +59,8 @@ fn check_dylint_tools_rejects_unusable_cargo_dylint_output(#[case] version_stdou ); executor.assert_finished(); }, - ); + ) + .expect("prepare fake PATH"); } #[rstest::rstest] @@ -82,14 +84,17 @@ fn check_dylint_tools_rejects_unpinned_dylint_link(#[case] install_list_check: E } ); executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[test] fn check_dylint_tools_rejects_non_invocable_dylint_link_on_path() { with_fake_path( |directories| { - let first_dir = &directories[0]; + let first_dir = directories.first().ok_or_else(|| { + std::io::Error::other("fake PATH should contain at least one directory") + })?; #[cfg(windows)] let binary_path = first_dir.join("dylint-link.cmd"); #[cfg(not(windows))] @@ -111,7 +116,8 @@ fn check_dylint_tools_rejects_non_invocable_dylint_link_on_path() { ); executor.assert_finished(); }, - ); + ) + .expect("prepare fake PATH"); } #[test] @@ -133,18 +139,21 @@ fn is_binary_on_path_returns_false_when_path_is_empty() { #[test] fn is_binary_on_path_returns_false_when_binary_is_missing_from_all_directories() { with_fake_path( - |_| (), + |_| Ok(()), || { assert!(!is_binary_on_path("dylint-link")); }, - ); + ) + .expect("prepare fake PATH"); } #[test] fn is_binary_on_path_checks_multiple_directories() { with_fake_path( |directories| { - let second_dir = &directories[1]; + let second_dir = directories.get(1).ok_or_else(|| { + std::io::Error::other("fake PATH should contain at least two directories") + })?; #[cfg(windows)] let binary_path = second_dir.join("dylint-link.exe"); #[cfg(not(windows))] @@ -155,7 +164,8 @@ fn is_binary_on_path_checks_multiple_directories() { || { assert!(is_binary_on_path("dylint-link")); }, - ); + ) + .expect("prepare fake PATH"); } #[cfg(unix)] @@ -163,7 +173,7 @@ fn is_binary_on_path_checks_multiple_directories() { fn is_executable_file_rejects_non_executable_files() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let binary_path = temp_dir.path().join("dylint-link"); - write_fake_binary(&binary_path, false); + write_fake_binary(&binary_path, false).expect("write fake binary"); assert!(!is_executable_file(&binary_path)); } @@ -173,7 +183,7 @@ fn is_executable_file_rejects_non_executable_files() { fn is_executable_file_accepts_executable_files() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let binary_path = temp_dir.path().join("dylint-link"); - write_fake_binary(&binary_path, true); + write_fake_binary(&binary_path, true).expect("write fake binary"); assert!(is_executable_file(&binary_path)); } @@ -192,7 +202,8 @@ fn is_binary_on_path_handles_windows_executable_suffixes( || { assert_eq!(is_binary_on_path(binary_name), expected); }, - ); + ) + .expect("prepare fake PATH"); } #[cfg(windows)] @@ -219,5 +230,6 @@ fn check_dylint_tools_detects_dylint_link_via_pathext_suffix() { executor.assert_finished(); }); }, - ); + ) + .expect("prepare fake PATH"); } diff --git a/installer/src/deps/tests.rs b/installer/src/deps/tests.rs index d0fe9faff..6a265d3ca 100644 --- a/installer/src/deps/tests.rs +++ b/installer/src/deps/tests.rs @@ -1,6 +1,7 @@ //! Tests for Dylint tool dependency installation and fallback behaviour. use super::*; +use crate::artefact::error::ArtefactError; use crate::dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}; use crate::installer_packaging::TargetTriple; use crate::test_utils::dependency_binary_helpers::{ @@ -10,21 +11,23 @@ use crate::test_utils::dependency_binary_helpers::{ use crate::test_utils::{ExpectedCall, StubDirs, StubExecutor, failure_output, success_output}; use std::path::PathBuf; -fn install_options<'a>( - repository_installer: &'a dyn DependencyBinaryInstaller, +const OPTIONS_MSG: &str = "dependency install options should build"; + +fn install_options( + repository_installer: &dyn DependencyBinaryInstaller, quiet: bool, -) -> DependencyInstallOptions<'a> { +) -> std::result::Result, ArtefactError> { let dirs = StubDirs { bin_dir: Some(PathBuf::from("/tmp/bin")), }; - let target = TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"); - DependencyInstallOptions { + let target = TargetTriple::try_from("x86_64-unknown-linux-gnu")?; + Ok(DependencyInstallOptions { // Intentional leak in tests to extend lifetime for trait object; acceptable here. dirs: Box::leak(Box::new(dirs)), repository_installer, target: Some(target), quiet, - } + }) } #[test] @@ -57,7 +60,7 @@ fn install_dylint_tools_uses_repository_release_first() { dylint_link: true, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("repository install should succeed"); @@ -89,7 +92,7 @@ fn install_dylint_tools_falls_back_to_binstall_when_repository_unavailable() { dylint_link: true, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("cargo binstall fallback should succeed"); @@ -123,7 +126,7 @@ fn install_dylint_tools_falls_back_to_cargo_install_when_binstall_missing() { dylint_link: true, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("cargo install fallback should succeed"); @@ -153,7 +156,7 @@ fn install_dylint_tools_falls_back_when_repository_verification_fails() { dylint_link: true, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("fallback after verification failure should succeed"); @@ -186,7 +189,7 @@ fn install_dylint_tools_reports_total_failure_after_all_fallbacks() { dylint_link: true, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect_err("install should fail after all fallbacks"); @@ -226,7 +229,7 @@ fn install_dylint_tools_builds_from_source_when_repository_asset_is_missing() { dylint_link: true, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("source build should succeed"); @@ -270,10 +273,11 @@ fn install_dylint_tools_skips_dylint_link_when_cargo_dylint_source_build_install dylint_link: false, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("cargo-dylint source build should satisfy both tools"); - }); + }) + .expect("prepare fake PATH"); let output = String::from_utf8(stderr).expect("stderr should be UTF-8"); assert!(output.contains("Installed cargo-dylint from source with cargo install.")); diff --git a/installer/src/deps/tests_repository.rs b/installer/src/deps/tests_repository.rs index 9ef30750c..6d3657217 100644 --- a/installer/src/deps/tests_repository.rs +++ b/installer/src/deps/tests_repository.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use super::{ - DependencyBinaryInstaller, DependencyTool, DylintToolStatus, + DependencyBinaryInstaller, DependencyTool, DylintToolStatus, OPTIONS_MSG, install::{InstallContext, InstallMode, install_tool, repository_install_context}, install_dylint_tools_with_options, install_options, }; @@ -31,7 +31,7 @@ fn staged_unrunnable_dylint_link() -> std::io::Result<(tempfile::TempDir, PathBu dir.path(), "dylint-link", ); - crate::test_utils::dependency_binary_helpers::write_fake_binary_with_status(&path, true, 1); + crate::test_utils::dependency_binary_helpers::write_fake_binary_with_status(&path, true, 1)?; Ok((dir, path)) } @@ -61,7 +61,7 @@ fn install_dylint_tools_accepts_repository_dylint_link_without_executing_it() { dylint_link: false, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("repository install should satisfy dylint-link"); @@ -103,10 +103,11 @@ fn install_dylint_tools_falls_back_when_repository_dylint_link_install_fails() { dylint_link: false, }, &mut stderr, - install_options(&repository_installer, false), + &install_options(&repository_installer, false).expect(OPTIONS_MSG), ) .expect("binstall fallback should succeed"); - }); + }) + .expect("prepare fake PATH"); let output = String::from_utf8(stderr).expect("stderr should be UTF-8"); assert!(output.contains("Repository install for dylint-link unavailable")); diff --git a/installer/src/dirs.rs b/installer/src/dirs.rs index 7139ea0b7..b63ae9194 100644 --- a/installer/src/dirs.rs +++ b/installer/src/dirs.rs @@ -33,7 +33,7 @@ fn xdg_bin_home() -> Option { /// use whitaker_installer::dirs::{BaseDirs, SystemBaseDirs}; /// /// let dirs = SystemBaseDirs::new().expect("failed to initialize directories"); -/// if let Some(bin_dir) = dirs.bin_dir() { +/// if let Some(bin_dir) = dirs.executables() { /// println!("Executables go in: {}", bin_dir.display()); /// } /// ``` @@ -43,21 +43,21 @@ pub trait BaseDirs { /// /// - Unix: `$HOME` or `/home/` /// - Windows: `%USERPROFILE%` or `C:\Users\` - fn home_dir(&self) -> Option; + fn home(&self) -> Option; /// Returns the directory for user executables. /// /// On Unix, respects `XDG_BIN_HOME` if set, otherwise falls back to /// `~/.local/bin`. On Windows, returns `~/.local/bin`. This follows the /// XDG Base Directory Specification convention. - fn bin_dir(&self) -> Option; + fn executables(&self) -> Option; /// Returns the directory for cloning the Whitaker repository. /// /// - Linux: `~/.local/share/whitaker` /// - macOS: `~/Library/Application Support/whitaker` /// - Windows: `%LOCALAPPDATA%\whitaker` - fn whitaker_data_dir(&self) -> Option; + fn whitaker_data(&self) -> Option; } /// Real implementation of [`BaseDirs`] using the `directories-next` crate. @@ -73,7 +73,7 @@ pub trait BaseDirs { /// use whitaker_installer::dirs::{BaseDirs, SystemBaseDirs}; /// /// let dirs = SystemBaseDirs::new().expect("failed to initialize directories"); -/// let data_dir = dirs.whitaker_data_dir() +/// let data_dir = dirs.whitaker_data() /// .expect("could not determine data directory"); /// println!("Whitaker data at: {}", data_dir.display()); /// ``` @@ -101,25 +101,27 @@ impl SystemBaseDirs { } impl BaseDirs for SystemBaseDirs { - fn home_dir(&self) -> Option { + fn home(&self) -> Option { Some(self.user_dirs.home_dir().to_owned()) } - fn bin_dir(&self) -> Option { + fn executables(&self) -> Option { #[cfg(unix)] if let Some(path) = xdg_bin_home() { return Some(path); } - self.home_dir().map(|h| h.join(".local").join("bin")) + self.home().map(|h| h.join(".local").join("bin")) } - fn whitaker_data_dir(&self) -> Option { + fn whitaker_data(&self) -> Option { Some(self.project_dirs.data_dir().to_owned()) } } #[cfg(test)] mod tests { + //! Tests for base-directory resolution. + use super::*; use crate::test_support::env_test_guard; @@ -127,16 +129,13 @@ mod tests { fn system_base_dirs_returns_some_on_supported_platforms() { let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs"); - assert!( - dirs.home_dir().is_some(), - "expected home_dir to return Some" - ); + assert!(dirs.home().is_some(), "expected home_dir to return Some"); } #[test] fn whitaker_data_dir_contains_whitaker() { let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs"); - let data_dir = dirs.whitaker_data_dir(); + let data_dir = dirs.whitaker_data(); assert!( data_dir.is_some(), @@ -156,7 +155,7 @@ mod tests { // Temporarily unset XDG_BIN_HOME to test the fallback temp_env::with_var_unset("XDG_BIN_HOME", || { let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs"); - let bin_dir = dirs.bin_dir(); + let bin_dir = dirs.executables(); assert!(bin_dir.is_some(), "expected bin_dir to return Some"); assert!( @@ -174,7 +173,7 @@ mod tests { let _guard = env_test_guard(); temp_env::with_var("XDG_BIN_HOME", Some("/custom/bin"), || { let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs"); - let bin_dir = dirs.bin_dir().expect("expected bin_dir to return Some"); + let bin_dir = dirs.executables().expect("expected bin_dir to return Some"); assert_eq!(bin_dir, PathBuf::from("/custom/bin")); }); @@ -186,7 +185,7 @@ mod tests { let _guard = env_test_guard(); temp_env::with_var("XDG_BIN_HOME", Some("relative/path"), || { let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs"); - let bin_dir = dirs.bin_dir().expect("expected bin_dir to return Some"); + let bin_dir = dirs.executables().expect("expected bin_dir to return Some"); // Should fall back to ~/.local/bin since XDG_BIN_HOME is relative assert!( diff --git a/installer/src/error.rs b/installer/src/error.rs index 93e45cdc4..122dda29c 100644 --- a/installer/src/error.rs +++ b/installer/src/error.rs @@ -251,14 +251,18 @@ impl Clone for InstallerError { Self::WriteFailed { source } => Self::WriteFailed { source: clone_io_error(source), }, + // These variants are cloned by `clone_toolchain_variant` before + // this match runs. Should that helper ever regress, degrade to a + // detection error carrying the formatted message rather than + // panicking inside `clone`. Self::ToolchainDetection { .. } | Self::ToolchainFileNotFound { .. } | Self::InvalidToolchainFile { .. } | Self::ToolchainNotInstalled { .. } | Self::ToolchainInstallFailed { .. } - | Self::ToolchainComponentInstallFailed { .. } => { - unreachable!("handled by clone_toolchain_variant") - } + | Self::ToolchainComponentInstallFailed { .. } => Self::ToolchainDetection { + reason: self.to_string(), + }, #[cfg(any(test, feature = "test-support"))] Self::StubMismatch { message } => Self::StubMismatch { message: message.clone(), @@ -272,6 +276,8 @@ pub type Result = std::result::Result; #[cfg(test)] mod tests { + //! Tests for installer error construction and display. + use super::*; #[test] diff --git a/installer/src/git.rs b/installer/src/git.rs index f59a00e1c..595810032 100644 --- a/installer/src/git.rs +++ b/installer/src/git.rs @@ -7,12 +7,14 @@ use crate::error::{InstallerError, Result}; use crate::workspace::WHITAKER_REPO_URL; use camino::Utf8Path; -use std::process::{Command, Output, Stdio}; +use std::io::Read; +use std::process::{Child, Command, Output, Stdio}; +use std::thread::JoinHandle; use std::time::Duration; use wait_timeout::ChildExt; /// Default timeout for git operations (5 minutes). -const GIT_TIMEOUT: Duration = Duration::from_secs(300); +const GIT_TIMEOUT: Duration = Duration::from_mins(5); /// Clones the Whitaker repository to the specified target directory. /// @@ -79,6 +81,31 @@ fn run_git_with_timeout( working_dir: Option<&Utf8Path>, operation: &'static str, ) -> Result { + let mut child = spawn_git_child(args, working_dir)?; + + // Take ownership of pipes before spawning threads to avoid blocking. + // If either pipe is missing, use empty readers. + let stdout_reader = spawn_pipe_reader(child.stdout.take()); + let stderr_reader = spawn_pipe_reader(child.stderr.take()); + + let Some(status) = child.wait_timeout(GIT_TIMEOUT)? else { + return Err(abandon_timed_out_git( + child, + stdout_reader, + stderr_reader, + operation, + )); + }; + + Ok(Output { + status, + stdout: join_pipe_reader(stdout_reader, operation, "stdout")?.into_bytes(), + stderr: join_pipe_reader(stderr_reader, operation, "stderr")?.into_bytes(), + }) +} + +/// Spawns the git child process with both standard streams piped. +fn spawn_git_child(args: &[&str], working_dir: Option<&Utf8Path>) -> Result { let mut cmd = Command::new("git"); cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped()); @@ -86,70 +113,63 @@ fn run_git_with_timeout( cmd.current_dir(dir.as_std_path()); } - let mut child = cmd.spawn()?; - - // Take ownership of pipes before spawning threads to avoid blocking. - // If either pipe is missing, use empty readers. - let stdout_pipe = child.stdout.take(); - let stderr_pipe = child.stderr.take(); + Ok(cmd.spawn()?) +} - // Spawn threads to read pipes concurrently whilst the process runs. - let stdout_thread = std::thread::spawn(move || -> std::io::Result { - stdout_pipe - .map(std::io::read_to_string) +/// Handle for a thread draining one of the child's output pipes. +type PipeReader = JoinHandle>; + +/// Drains an optional child pipe on its own thread, yielding an empty string +/// when the pipe is absent. +fn spawn_pipe_reader(pipe: Option) -> PipeReader +where + R: Read + Send + 'static, +{ + std::thread::spawn(move || { + pipe.map(std::io::read_to_string) .transpose() - .map(|opt| opt.unwrap_or_default()) - }); - let stderr_thread = std::thread::spawn(move || -> std::io::Result { - stderr_pipe - .map(std::io::read_to_string) - .transpose() - .map(|opt| opt.unwrap_or_default()) - }); - - match child.wait_timeout(GIT_TIMEOUT)? { - Some(status) => { - // Command completed within timeout - collect output from threads - let stdout = stdout_thread - .join() - .map_err(|_| InstallerError::Git { - operation, - message: "failed to read stdout".to_owned(), - })? - .unwrap_or_default(); - let stderr = stderr_thread - .join() - .map_err(|_| InstallerError::Git { - operation, - message: "failed to read stderr".to_owned(), - })? - .unwrap_or_default(); - - Ok(Output { - status, - stdout: stdout.into_bytes(), - stderr: stderr.into_bytes(), - }) - } - None => { - // Timeout - kill the process and wait for threads to finish - let _ = child.kill(); - let _ = child.wait(); - let _ = stdout_thread.join(); - let _ = stderr_thread.join(); - Err(InstallerError::Git { - operation, - message: format!( - "operation timed out after {} seconds", - GIT_TIMEOUT.as_secs() - ), - }) - } + .map(Option::unwrap_or_default) + }) +} + +/// Joins a pipe reader, reporting a git error when the reader thread panicked. +fn join_pipe_reader(reader: PipeReader, operation: &'static str, stream: &str) -> Result { + Ok(reader + .join() + .map_err(|_| InstallerError::Git { + operation, + message: format!("failed to read {stream}"), + })? + .unwrap_or_default()) +} + +/// Kills a timed-out git child and reaps its reader threads before reporting. +/// +/// Each cleanup result is discarded deliberately: the operation has already +/// failed, so cleanup is best effort. +fn abandon_timed_out_git( + mut child: Child, + stdout_reader: PipeReader, + stderr_reader: PipeReader, + operation: &'static str, +) -> InstallerError { + drop(child.kill()); + drop(child.wait()); + drop(stdout_reader.join()); + drop(stderr_reader.join()); + InstallerError::Git { + operation, + message: format!( + "operation timed out after {} seconds", + GIT_TIMEOUT.as_secs() + ), } } #[cfg(test)] mod tests { + //! Tests for git clone and update helpers. + use super::*; #[test] diff --git a/installer/src/hex.rs b/installer/src/hex.rs index a21c63045..50a005e61 100644 --- a/installer/src/hex.rs +++ b/installer/src/hex.rs @@ -14,15 +14,25 @@ /// allocation. #[must_use] pub(crate) fn to_lower_hex(bytes: &[u8]) -> String { - const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef"; let mut hex = String::with_capacity(bytes.len() * 2); for &byte in bytes { - hex.push(char::from(HEX_DIGITS[usize::from(byte >> 4)])); - hex.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)])); + hex.push(nibble_to_hex(byte >> 4)); + hex.push(nibble_to_hex(byte & 0x0f)); } hex } +/// Map the low four bits of `nibble` to a lowercase hexadecimal digit. +fn nibble_to_hex(nibble: u8) -> char { + let masked = nibble & 0x0f; + let ascii = if masked < 10 { + b'0' + masked + } else { + b'a' + (masked - 10) + }; + char::from(ascii) +} + #[cfg(test)] mod tests { //! Tests for the lowercase hexadecimal digest formatter. diff --git a/installer/src/install_flow/mod.rs b/installer/src/install_flow/mod.rs index 54bf69a13..1404a1594 100644 --- a/installer/src/install_flow/mod.rs +++ b/installer/src/install_flow/mod.rs @@ -56,8 +56,8 @@ pub(crate) fn ensure_dylint_tools_with_executor( stderr: &mut dyn Write, ) -> Result<()> { let status = check_dylint_tools(executor); - ensure_dylint_tools_core(quiet, stderr, status.all_installed(), |stderr| { - install_dylint_tools_with_output(executor, &status, quiet, stderr) + ensure_dylint_tools_core(quiet, stderr, status.all_installed(), |sink| { + install_dylint_tools_with_output(executor, &status, quiet, sink) }) } @@ -65,11 +65,11 @@ pub(crate) fn ensure_dylint_tools_with_executor( pub(crate) fn ensure_dylint_tools_with_options( executor: &dyn CommandExecutor, stderr: &mut dyn Write, - options: DependencyInstallOptions<'_>, + options: &DependencyInstallOptions<'_>, ) -> Result<()> { let status = check_dylint_tools(executor); - ensure_dylint_tools_core(options.quiet, stderr, status.all_installed(), |stderr| { - install_dylint_tools_with_options(executor, &status, stderr, options) + ensure_dylint_tools_core(options.quiet, stderr, status.all_installed(), |sink| { + install_dylint_tools_with_options(executor, &status, sink, options) }) } @@ -112,14 +112,17 @@ pub(crate) fn write_prebuilt_fallback_message( } /// Attempt prebuilt installation and return staged path when successful. +/// +/// Returns `None` when prebuilt installation is skipped or fails; every failure +/// mode is reported on `stderr` and falls back to local compilation. pub(crate) fn try_prebuilt_installation( context: &PrebuiltInstallationContext<'_>, stderr: &mut dyn Write, -) -> Result> { +) -> Option { try_prebuilt_installation_with( context, stderr, - PrebuiltInstallationHooks { + &PrebuiltInstallationHooks { detect_host_target, resolve_destination_dir: prebuilt_library_dir, attempt_prebuilt, @@ -133,6 +136,7 @@ type ResolveDestinationDirFn = fn(&dyn BaseDirs, &str, &str) -> Result, &mut dyn Write) -> PrebuiltResult; type PruneLibrariesFn = fn(&Utf8Path, &str, &[CrateName]) -> Result<()>; +#[derive(Clone, Copy)] struct PrebuiltInstallationHooks { detect_host_target: DetectHostTargetFn, resolve_destination_dir: ResolveDestinationDirFn, @@ -143,27 +147,27 @@ struct PrebuiltInstallationHooks { fn try_prebuilt_installation_with( context: &PrebuiltInstallationContext<'_>, stderr: &mut dyn Write, - hooks: PrebuiltInstallationHooks, -) -> Result> { + hooks: &PrebuiltInstallationHooks, +) -> Option { let PrebuiltInstallationHooks { detect_host_target, resolve_destination_dir, attempt_prebuilt, prune_prebuilt_libraries, - } = hooks; + } = *hooks; if !context .args .should_attempt_prebuilt(context.requested_crates) { - return Ok(None); + return None; } let host_target = match detect_host_target() { Ok(target) => target, Err(error) => { write_prebuilt_fallback_message(context.args.quiet, &error, stderr); - return Ok(None); + return None; } }; @@ -172,7 +176,7 @@ fn try_prebuilt_installation_with( Ok(destination) => destination, Err(error) => { write_prebuilt_fallback_message(context.args.quiet, &error, stderr); - return Ok(None); + return None; } }; @@ -185,7 +189,7 @@ fn try_prebuilt_installation_with( let PrebuiltResult::Success { staging_path } = attempt_prebuilt(&prebuilt_config, stderr) else { - return Ok(None); + return None; }; if let Err(error) = prune_prebuilt_libraries( &staging_path, @@ -193,9 +197,9 @@ fn try_prebuilt_installation_with( context.requested_crates, ) { write_prebuilt_fallback_message(context.args.quiet, &error, stderr); - return Ok(None); + return None; } - Ok(Some(staging_path)) + Some(staging_path) } fn requested_crate_names(requested_crates: &[CrateName]) -> HashSet<&str> { diff --git a/installer/src/install_flow/tests.rs b/installer/src/install_flow/tests.rs index c520829aa..64eebea23 100644 --- a/installer/src/install_flow/tests.rs +++ b/installer/src/install_flow/tests.rs @@ -17,30 +17,25 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home_dir(&self) -> Option { + fn home(&self) -> Option { None } - fn bin_dir(&self) -> Option { + fn executables(&self) -> Option { None } - fn whitaker_data_dir(&self) -> Option { + fn whitaker_data(&self) -> Option { self.data_dir.clone() } } static PRUNE_HOOK_CALLED: AtomicBool = AtomicBool::new(false); -fn stub_detect_host_target() -> Result { - Ok("x86_64-unknown-linux-gnu".to_owned()) -} +/// Host-target detection stub that always resolves to a fixed Linux target. +const STUB_DETECT_HOST_TARGET: DetectHostTargetFn = || Ok("x86_64-unknown-linux-gnu".to_owned()); -fn stub_resolve_destination_dir( - _dirs: &dyn BaseDirs, - _toolchain_channel: &str, - _host_target: &str, -) -> Result { - Ok(Utf8PathBuf::from("/tmp/whitaker-test-data/lints")) -} +/// Destination resolution stub that always yields a fixed library directory. +const STUB_RESOLVE_DESTINATION_DIR: ResolveDestinationDirFn = + |_dirs, _channel, _host_target| Ok(Utf8PathBuf::from("/tmp/whitaker-test-data/lints")); fn stub_attempt_prebuilt(_config: &PrebuiltConfig<'_>, _stderr: &mut dyn Write) -> PrebuiltResult { PrebuiltResult::Success { @@ -60,27 +55,26 @@ fn stub_prune_prebuilt_libraries( } #[fixture] -fn staging_fixture() -> StagingFixture { - let temp_dir = tempfile::tempdir().expect("tempdir should be available"); +fn staging_fixture() -> std::io::Result { + let temp_dir = tempfile::tempdir()?; let staging_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) - .expect("tempdir path should be utf-8"); - fs::create_dir_all(staging_path.as_std_path()).expect("staging path should be creatable"); - StagingFixture { + .map_err(|_| std::io::Error::other("temporary directory path must be UTF-8"))?; + fs::create_dir_all(staging_path.as_std_path())?; + Ok(StagingFixture { _temp_dir: temp_dir, staging_path, toolchain: "nightly-2026-05-28", - } + }) } fn create_staged_library( staging_path: &Utf8Path, crate_name: &str, toolchain: &str, -) -> Utf8PathBuf { +) -> std::io::Result { let library_path = staging_path.join(staged_library_filename(crate_name, toolchain)); - fs::write(library_path.as_std_path(), b"fake prebuilt library") - .expect("test setup should write staged library"); - library_path + fs::write(library_path.as_std_path(), b"fake prebuilt library")?; + Ok(library_path) } #[rstest] @@ -100,15 +94,16 @@ fn create_staged_library( &[SUITE_CRATE, "no_expect_outside_tests"] )] fn prune_prebuilt_libraries_keeps_only_requested_crates( - staging_fixture: StagingFixture, + #[from(staging_fixture)] staging_res: std::io::Result, #[case] requested: &[&str], #[case] retained: &[&str], #[case] removed: &[&str], ) { + let staging_fixture = staging_res.expect("staging fixture should be created"); let StagingFixture { - _temp_dir: _, staging_path, toolchain, + .. } = staging_fixture; let foreign_path = staging_path.join("libforeign_lint@nightly-2026-05-28.so"); @@ -117,7 +112,8 @@ fn prune_prebuilt_libraries_keeps_only_requested_crates( let mut staged = Vec::new(); for crate_name in retained.iter().chain(removed.iter()) { - let path = create_staged_library(&staging_path, crate_name, toolchain); + let path = create_staged_library(&staging_path, crate_name, toolchain) + .expect("test setup should write staged library"); staged.push(((*crate_name).to_owned(), path)); } @@ -180,29 +176,29 @@ fn try_prebuilt_installation_prune_error_falls_back_to_local_build() { let result = try_prebuilt_installation_with( &context, &mut stderr, - PrebuiltInstallationHooks { - detect_host_target: stub_detect_host_target, - resolve_destination_dir: stub_resolve_destination_dir, + &PrebuiltInstallationHooks { + detect_host_target: STUB_DETECT_HOST_TARGET, + resolve_destination_dir: STUB_RESOLVE_DESTINATION_DIR, attempt_prebuilt: stub_attempt_prebuilt, prune_prebuilt_libraries: stub_prune_prebuilt_libraries, }, ); assert!( - matches!(result, Ok(None)), + result.is_none(), "prune failure should trigger fallback to local compilation" ); assert!( PRUNE_HOOK_CALLED.load(Ordering::SeqCst), "prune hook should be invoked" ); - let stderr = String::from_utf8(stderr).expect("stderr should be utf-8"); + let stderr_text = String::from_utf8(stderr).expect("stderr should be utf-8"); assert!( - stderr.contains("Prebuilt download unavailable: staging failed: forced prune failure"), - "fallback reason should include prune error, stderr: {stderr}" + stderr_text.contains("Prebuilt download unavailable: staging failed: forced prune failure"), + "fallback reason should include prune error, stderr: {stderr_text}" ); assert!( - stderr.contains("Falling back to local compilation."), - "fallback message should be emitted, stderr: {stderr}" + stderr_text.contains("Falling back to local compilation."), + "fallback message should be emitted, stderr: {stderr_text}" ); } diff --git a/installer/src/install_metrics.rs b/installer/src/install_metrics.rs index c05fcb811..a10f0a8d3 100644 --- a/installer/src/install_metrics.rs +++ b/installer/src/install_metrics.rs @@ -49,7 +49,7 @@ impl InstallMetrics { /// assert_eq!(metrics.total_installs(), 1); /// ``` #[must_use] - pub fn total_installs(&self) -> u64 { + pub const fn total_installs(&self) -> u64 { self.total_installs } @@ -66,7 +66,7 @@ impl InstallMetrics { /// assert_eq!(metrics.download_installs(), 1); /// ``` #[must_use] - pub fn download_installs(&self) -> u64 { + pub const fn download_installs(&self) -> u64 { self.download_installs } @@ -83,7 +83,7 @@ impl InstallMetrics { /// assert_eq!(metrics.build_installs(), 1); /// ``` #[must_use] - pub fn build_installs(&self) -> u64 { + pub const fn build_installs(&self) -> u64 { self.build_installs } @@ -101,36 +101,36 @@ impl InstallMetrics { /// ); /// ``` #[must_use] - pub fn total_install_duration(&self) -> Duration { + pub const fn total_install_duration(&self) -> Duration { Duration::from_millis(self.total_install_millis) } - /// Returns `download_installs / total_installs`. + /// Returns `download_installs / total_installs` in permille (0–1000). /// /// # Examples /// /// ``` /// use whitaker_installer::install_metrics::InstallMetrics; /// - /// assert_eq!(InstallMetrics::default().download_rate(), 0.0); + /// assert_eq!(InstallMetrics::default().download_rate_permille(), 0); /// ``` #[must_use] - pub fn download_rate(&self) -> f64 { - rate(self.download_installs, self.total_installs) + pub const fn download_rate_permille(&self) -> u64 { + rate_permille(self.download_installs, self.total_installs) } - /// Returns `build_installs / total_installs`. + /// Returns `build_installs / total_installs` in permille (0–1000). /// /// # Examples /// /// ``` /// use whitaker_installer::install_metrics::InstallMetrics; /// - /// assert_eq!(InstallMetrics::default().build_rate(), 0.0); + /// assert_eq!(InstallMetrics::default().build_rate_permille(), 0); /// ``` #[must_use] - pub fn build_rate(&self) -> f64 { - rate(self.build_installs, self.total_installs) + pub const fn build_rate_permille(&self) -> u64 { + rate_permille(self.build_installs, self.total_installs) } /// Records one successful install event. @@ -179,17 +179,19 @@ impl InstallMetrics { /// ``` #[must_use] pub fn summary_line(&self) -> String { + let download_percent = format_permille_as_percent(self.download_rate_permille()); + let build_percent = format_permille_as_percent(self.build_rate_permille()); format!( concat!( - "Install metrics: download {}/{} ({:.1}%), build {}/{} ({:.1}%), ", + "Install metrics: download {}/{} ({}%), build {}/{} ({}%), ", "total installation time {}" ), self.download_installs, self.total_installs, - self.download_rate() * 100.0, + download_percent, self.build_installs, self.total_installs, - self.build_rate() * 100.0, + build_percent, format_duration(self.total_install_duration()), ) } @@ -204,18 +206,23 @@ pub struct RecordOutcome { impl RecordOutcome { /// Returns the updated aggregate metrics. #[must_use] - pub fn metrics(&self) -> &InstallMetrics { + pub const fn metrics(&self) -> &InstallMetrics { &self.metrics } /// Returns true when a malformed metrics file was reset to defaults. #[must_use] - pub fn recovered_from_corrupt_file(&self) -> bool { + pub const fn recovered_from_corrupt_file(&self) -> bool { self.recovered_from_corrupt_file } } /// Records one successful install in Whitaker's metrics store. +/// +/// # Errors +/// +/// Returns an [`InstallMetricsError`] when the data directory is missing or +/// the metrics file cannot be created, locked, read, or written. pub fn record_install( dirs: &dyn BaseDirs, mode: InstallMode, @@ -226,6 +233,11 @@ pub fn record_install( } /// Records one successful install at an explicit metrics file path. +/// +/// # Errors +/// +/// Returns an [`InstallMetricsError`] when the metrics directory or file +/// cannot be created, or the file cannot be locked, read, or written. pub fn record_install_at_path( metrics_path: &Path, mode: InstallMode, @@ -254,7 +266,7 @@ pub fn record_install_at_path( fn metrics_path(dirs: &dyn BaseDirs) -> Result { let data_dir = dirs - .whitaker_data_dir() + .whitaker_data() .ok_or(InstallMetricsError::MissingDataDirectory)?; Ok(data_dir.join(METRICS_DIRNAME).join(METRICS_FILENAME)) } @@ -309,10 +321,10 @@ fn load_metrics( return Ok((InstallMetrics::default(), false)); } - match serde_json::from_str::(&content) { - Ok(metrics) => Ok((metrics, false)), - Err(_) => Ok((InstallMetrics::default(), true)), - } + serde_json::from_str::(&content).map_or_else( + |_| Ok((InstallMetrics::default(), true)), + |metrics| Ok((metrics, false)), + ) } fn persist_metrics( @@ -333,14 +345,21 @@ fn persist_metrics( }) } -fn rate(part: u64, whole: u64) -> f64 { +/// Returns `part / whole` in permille (parts per thousand), or zero when +/// `whole` is zero. The product saturates rather than overflowing. +const fn rate_permille(part: u64, whole: u64) -> u64 { if whole == 0 { - 0.0 + 0 } else { - part as f64 / whole as f64 + part.saturating_mul(1000).div_euclid(whole) } } +/// Renders a permille value as a percentage with one decimal place. +fn format_permille_as_percent(permille: u64) -> String { + format!("{}.{}", permille.div_euclid(10), permille.rem_euclid(10)) +} + fn duration_to_millis(duration: Duration) -> u64 { u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) } @@ -348,9 +367,9 @@ fn duration_to_millis(duration: Duration) -> u64 { fn format_duration(duration: Duration) -> String { let total_seconds = duration.as_secs(); let millis = duration.subsec_millis(); - let hours = total_seconds / 3600; - let minutes = (total_seconds % 3600) / 60; - let seconds = total_seconds % 60; + let hours = total_seconds.div_euclid(3600); + let minutes = total_seconds.rem_euclid(3600).div_euclid(60); + let seconds = total_seconds.rem_euclid(60); if should_format_with_hours(hours) { return format!("{hours}h {minutes}m {seconds}.{millis:03}s"); @@ -361,10 +380,10 @@ fn format_duration(duration: Duration) -> String { format!("{seconds}.{millis:03}s") } -fn should_format_with_hours(hours: u64) -> bool { +const fn should_format_with_hours(hours: u64) -> bool { hours > 0 } -fn should_format_with_minutes(minutes: u64) -> bool { +const fn should_format_with_minutes(minutes: u64) -> bool { minutes > 0 } diff --git a/installer/src/install_metrics_tests.rs b/installer/src/install_metrics_tests.rs index dfb9787d1..3dbce5d5e 100644 --- a/installer/src/install_metrics_tests.rs +++ b/installer/src/install_metrics_tests.rs @@ -26,14 +26,12 @@ fn metrics_path_fixture() -> std::io::Result { #[test] fn zero_state_rates_are_zero() { let metrics = InstallMetrics::default(); - assert!( - metrics.download_rate().abs() < f64::EPSILON, + assert_eq!( + metrics.download_rate_permille(), + 0, "zero-state download rate" ); - assert!( - metrics.build_rate().abs() < f64::EPSILON, - "zero-state build rate" - ); + assert_eq!(metrics.build_rate_permille(), 0, "zero-state build rate"); } #[test] @@ -46,14 +44,12 @@ fn record_install_updates_counts_and_duration() { assert_eq!(metrics.download_installs(), 1); assert_eq!(metrics.build_installs(), 1); assert_eq!(metrics.total_install_duration(), Duration::from_secs(2)); - assert!( - (metrics.download_rate() - 0.5).abs() < f64::EPSILON, - "download rate" - ); - assert!( - (metrics.build_rate() - 0.5).abs() < f64::EPSILON, - "build rate" + assert_eq!( + metrics.download_rate_permille(), + 500, + "download rate permille" ); + assert_eq!(metrics.build_rate_permille(), 500, "build rate permille"); } #[rstest] diff --git a/installer/src/installer_packaging.rs b/installer/src/installer_packaging.rs index 4ad2b6060..ed2a3dfcf 100644 --- a/installer/src/installer_packaging.rs +++ b/installer/src/installer_packaging.rs @@ -21,7 +21,7 @@ pub use crate::version::Version; use crate::binstall_metadata::{DEFAULT_PKG_FMT, WINDOWS_PKG_FMT}; use std::fs; -use std::io::{Read, Write}; +use std::io; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -191,7 +191,7 @@ pub fn archive_format(target: &TargetTriple) -> ArchiveFormat { /// not exist, or [`InstallerPackagingError::Io`] / /// [`InstallerPackagingError::Zip`] on archive creation failures. pub fn package_installer( - params: InstallerPackageParams, + params: &InstallerPackageParams, ) -> Result { if !params.binary_path.is_file() { return Err(InstallerPackagingError::BinaryNotFound( @@ -234,8 +234,8 @@ fn create_tgz_archive( let archive_entry_path = format!("{inner_dir}/{bin_name}"); archive.append_path_with_name(binary_path, &archive_entry_path)?; - let gz_encoder = archive.into_inner()?; - gz_encoder.finish()?; + let finished_encoder = archive.into_inner()?; + finished_encoder.finish()?; Ok(()) } @@ -256,14 +256,7 @@ fn create_zip_archive( zip_writer.start_file(&archive_entry_path, options)?; let mut binary_file = fs::File::open(binary_path)?; - let mut buffer = [0u8; 8192]; - loop { - let bytes_read = binary_file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - zip_writer.write_all(&buffer[..bytes_read])?; - } + io::copy(&mut binary_file, &mut zip_writer)?; zip_writer.finish()?; Ok(()) diff --git a/installer/src/installer_packaging_tests.rs b/installer/src/installer_packaging_tests.rs index c575b4752..8bd9350fd 100644 --- a/installer/src/installer_packaging_tests.rs +++ b/installer/src/installer_packaging_tests.rs @@ -201,7 +201,7 @@ fn package_installer_creates_archive( let fixture = packaging_fixture(target, content).expect("packaging fixture should be staged"); let params = params_from_fixture(&fixture, "0.2.1", target).expect("packaging params should build"); - let output = package_installer(params).expect("packaging should succeed"); + let output = package_installer(¶ms).expect("packaging should succeed"); assert!(output.archive_path.exists(), "archive should exist"); assert_eq!(output.archive_name, expected_name); @@ -227,7 +227,7 @@ fn package_installer_rejects_missing_binary() { output_dir: temp.path().to_path_buf(), }; - let err = package_installer(params).expect_err("should fail"); + let err = package_installer(¶ms).expect_err("should fail"); assert!( matches!(err, InstallerPackagingError::BinaryNotFound(ref p) if *p == missing), "expected BinaryNotFound, got {err:?}" @@ -252,7 +252,7 @@ fn package_installer_returns_io_error_for_unwritable_output() { output_dir: unwritable, }; - let err = package_installer(params).expect_err("should fail on unwritable output dir"); + let err = package_installer(¶ms).expect_err("should fail on unwritable output dir"); assert!( matches!(err, InstallerPackagingError::Io(_)), "expected Io error, got {err:?}" @@ -290,7 +290,7 @@ fn tgz_archive_preserves_binary_content() { let params = params_from_fixture(&fixture, "0.2.1", "aarch64-unknown-linux-gnu") .expect("packaging params should build"); - let output = package_installer(params).expect("packaging"); + let output = package_installer(¶ms).expect("packaging"); let file = fs::File::open(&output.archive_path).expect("open"); let gz = flate2::read::GzDecoder::new(file); let mut tar_archive = tar::Archive::new(gz); diff --git a/installer/src/list.rs b/installer/src/list.rs index cfb5bb0c9..d4b14b046 100644 --- a/installer/src/list.rs +++ b/installer/src/list.rs @@ -74,7 +74,7 @@ fn sort_installed_libraries(installed: &mut InstalledLints) { fn default_prebuilt_target_dir() -> Option { SystemBaseDirs::new() - .and_then(|dirs| dirs.whitaker_data_dir()) + .and_then(|dirs| dirs.whitaker_data()) .and_then(|path| Utf8PathBuf::from_path_buf(path).ok()) .map(|path| path.join("lints")) } @@ -109,6 +109,7 @@ fn determine_scan_roots(cli_target: Option<&Utf8Path>) -> Result Option { let cwd = match std::env::current_dir() { Ok(path) => path, diff --git a/installer/src/list_output.rs b/installer/src/list_output.rs index 9f9c3a151..752220524 100644 --- a/installer/src/list_output.rs +++ b/installer/src/list_output.rs @@ -5,7 +5,7 @@ use serde::Serialize; -use crate::scanner::{InstalledLints, lints_for_library}; +use crate::scanner::{InstalledLibrary, InstalledLints, lints_for_library}; /// Format installed lints for human-readable output. /// @@ -31,27 +31,45 @@ pub fn format_human(lints: &InstalledLints, active_toolchain: Option<&str>) -> S for (toolchain, libraries) in &lints.by_toolchain { output.push('\n'); - - let active_marker = active_toolchain - .filter(|active| *active == toolchain) - .map_or(String::new(), |_| " (active)".to_owned()); - - output.push_str(&format!("Toolchain: {toolchain}{active_marker}\n")); - output.push_str(" Libraries:\n"); - - for library in libraries { - output.push_str(&format!(" {}\n", library.crate_name)); - - let lint_names = lints_for_library(&library.crate_name); - for lint in lint_names { - output.push_str(&format!(" - {lint}\n")); - } - } + output.push_str(&format_toolchain_section( + toolchain, + libraries, + active_toolchain, + )); } output } +/// Render one toolchain section of the human-readable listing. +fn format_toolchain_section( + toolchain: &str, + libraries: &[InstalledLibrary], + active_toolchain: Option<&str>, +) -> String { + let active_marker = if active_toolchain == Some(toolchain) { + " (active)" + } else { + "" + }; + let library_lines: String = libraries.iter().map(format_library_lines).collect(); + format!("Toolchain: {toolchain}{active_marker}\n Libraries:\n{library_lines}") +} + +/// Render the listing lines for one installed library and its lints. +fn format_library_lines(library: &InstalledLibrary) -> String { + let lint_lines: String = + lints_for_library(&library.crate_name) + .iter() + .fold(String::new(), |mut output, lint| { + output.push_str(" - "); + output.push_str(lint); + output.push('\n'); + output + }); + format!(" {}\n{lint_lines}", library.crate_name) +} + /// Format installed lints as JSON. /// /// # Examples @@ -133,6 +151,8 @@ pub struct LibraryEntry { #[cfg(test)] mod tests { + //! Tests for human-readable and JSON list formatting. + use super::*; use crate::builder::CrateName; use crate::scanner::InstalledLibrary; diff --git a/installer/src/list_tests.rs b/installer/src/list_tests.rs index 88d2a871b..de6a1bc41 100644 --- a/installer/src/list_tests.rs +++ b/installer/src/list_tests.rs @@ -16,10 +16,11 @@ struct TempTarget { } #[fixture] -fn temp_target() -> TempTarget { - let temp = TempDir::new().expect("failed to create temp dir"); - let path = Utf8PathBuf::try_from(temp.path().to_owned()).expect("non-UTF8 temp path"); - TempTarget { _temp: temp, path } +fn temp_target() -> std::io::Result { + let temp = TempDir::new()?; + let path = Utf8PathBuf::try_from(temp.path().to_owned()) + .map_err(|_| std::io::Error::other("temporary directory path must be UTF-8"))?; + Ok(TempTarget { _temp: temp, path }) } /// A Write implementation that always fails, for testing error paths. @@ -61,11 +62,15 @@ impl MockLibraryKind { } } -fn create_mock_library_internal(target_dir: &Utf8Path, toolchain: &str, kind: MockLibraryKind) { +fn create_mock_library_internal( + target_dir: &Utf8Path, + toolchain: &str, + kind: MockLibraryKind, +) -> std::io::Result<()> { use crate::builder::{library_extension, library_prefix}; let lib_dir = kind.library_dir(target_dir, toolchain); - fs::create_dir_all(&lib_dir).expect("failed to create target library directory"); + fs::create_dir_all(&lib_dir)?; let filename = format!( "{}whitaker_suite@{toolchain}{}", @@ -73,20 +78,20 @@ fn create_mock_library_internal(target_dir: &Utf8Path, toolchain: &str, kind: Mo library_extension() ); - let error_msg = match kind { - MockLibraryKind::Local => "failed to create mock library", - MockLibraryKind::Prebuilt { .. } => "failed to create prebuilt mock library", - }; - fs::write(lib_dir.join(filename), kind.content()).expect(error_msg); + fs::write(lib_dir.join(filename), kind.content()) } /// Helper to create a mock installed library in the target directory for tests. -fn create_mock_library(target_dir: &Utf8Path, toolchain: &str) { - create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Local); +fn create_mock_library(target_dir: &Utf8Path, toolchain: &str) -> std::io::Result<()> { + create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Local) } -fn create_mock_prebuilt_library(target_dir: &Utf8Path, toolchain: &str, target: &'static str) { - create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Prebuilt { target }); +fn create_mock_prebuilt_library( + target_dir: &Utf8Path, + toolchain: &str, + target: &'static str, +) -> std::io::Result<()> { + create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Prebuilt { target }) } // ------------------------------------------------------------------------- @@ -94,7 +99,11 @@ fn create_mock_prebuilt_library(target_dir: &Utf8Path, toolchain: &str, target: // ------------------------------------------------------------------------- #[rstest] -fn run_list_outputs_human_readable_format(temp_target: TempTarget) { +fn run_list_outputs_human_readable_format( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + let args = ListArgs { json: false, target_dir: Some(temp_target.path.clone()), @@ -112,11 +121,14 @@ fn run_list_outputs_human_readable_format(temp_target: TempTarget) { #[case::json_format(true, &["toolchains", "\"active\""])] #[case::human_format(false, &["nightly-2026-05-28", "whitaker_suite"])] fn run_list_with_installed_library_includes_expected_output( - temp_target: TempTarget, + #[from(temp_target)] temp_target_res: std::io::Result, #[case] json: bool, #[case] expected: &[&str], ) { - create_mock_library(&temp_target.path, "nightly-2026-05-28"); + let temp_target = temp_target_res.expect("temporary target directory should be created"); + + create_mock_library(&temp_target.path, "nightly-2026-05-28") + .expect("mock library should be staged"); let args = ListArgs { json, target_dir: Some(temp_target.path.clone()), @@ -136,12 +148,17 @@ fn run_list_with_installed_library_includes_expected_output( } #[rstest] -fn run_list_finds_prebuilt_layout_libraries(temp_target: TempTarget) { +fn run_list_finds_prebuilt_layout_libraries( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + create_mock_prebuilt_library( &temp_target.path, "nightly-2026-05-28", "x86_64-unknown-linux-gnu", - ); + ) + .expect("mock prebuilt library should be staged"); let args = ListArgs { json: false, target_dir: Some(temp_target.path.clone()), @@ -157,7 +174,11 @@ fn run_list_finds_prebuilt_layout_libraries(temp_target: TempTarget) { } #[rstest] -fn run_list_returns_write_failed_on_stdout_error(temp_target: TempTarget) { +fn run_list_returns_write_failed_on_stdout_error( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + let args = ListArgs { json: false, target_dir: Some(temp_target.path.clone()), @@ -178,7 +199,11 @@ fn run_list_returns_write_failed_on_stdout_error(temp_target: TempTarget) { // ------------------------------------------------------------------------- #[rstest] -fn detect_active_toolchain_in_returns_none_when_no_toolchain_file(temp_target: TempTarget) { +fn detect_active_toolchain_in_returns_none_when_no_toolchain_file( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + let result = detect_active_toolchain_in(&temp_target.path); assert!( result.is_none(), @@ -187,7 +212,11 @@ fn detect_active_toolchain_in_returns_none_when_no_toolchain_file(temp_target: T } #[rstest] -fn detect_active_toolchain_in_returns_channel_when_toolchain_file_exists(temp_target: TempTarget) { +fn detect_active_toolchain_in_returns_channel_when_toolchain_file_exists( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + // Create a rust-toolchain.toml file let toolchain_content = r#"[toolchain] channel = "nightly-2026-05-28" @@ -208,7 +237,11 @@ channel = "nightly-2026-05-28" // ------------------------------------------------------------------------- #[rstest] -fn determine_target_dir_returns_cli_value_when_provided(temp_target: TempTarget) { +fn determine_target_dir_returns_cli_value_when_provided( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + let result = determine_target_dir_with(Some(&temp_target.path), || None); assert!(result.is_ok(), "expected success, got: {result:?}"); @@ -216,7 +249,11 @@ fn determine_target_dir_returns_cli_value_when_provided(temp_target: TempTarget) } #[rstest] -fn determine_target_dir_falls_back_to_default_when_cli_is_none(temp_target: TempTarget) { +fn determine_target_dir_falls_back_to_default_when_cli_is_none( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + let default_path = temp_target.path.clone(); let result = determine_target_dir_with(None, || Some(default_path.clone())); @@ -237,7 +274,11 @@ fn determine_target_dir_returns_error_when_no_default_available() { } #[rstest] -fn determine_target_dir_prefers_cli_over_default(temp_target: TempTarget) { +fn determine_target_dir_prefers_cli_over_default( + #[from(temp_target)] temp_target_res: std::io::Result, +) { + let temp_target = temp_target_res.expect("temporary target directory should be created"); + let cli_path = temp_target.path.clone(); let default_path = temp_target.path.join("should_not_be_used"); diff --git a/installer/src/main.rs b/installer/src/main.rs index 1e9c2665a..a95778398 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -24,7 +24,7 @@ use whitaker_installer::dirs::{BaseDirs, SystemBaseDirs}; use whitaker_installer::error::{InstallerError, Result}; use whitaker_installer::install_metrics::InstallMode; use whitaker_installer::list::{determine_target_dir, run_list}; -use whitaker_installer::output::{DryRunInfo, ShellSnippet, write_stderr_line}; +use whitaker_installer::output::{DryRunInfo, DryRunSkips, ShellSnippet, write_stderr_line}; use whitaker_installer::pipeline::{PipelineContext, perform_build, stage_libraries}; use whitaker_installer::prebuilt_path::prebuilt_library_dir; use whitaker_installer::resolution::{ @@ -54,8 +54,8 @@ fn run(cli: &Cli, stdout: &mut dyn Write, stderr: &mut dyn Write) -> Result<()> } /// Returns the set of additional rustup components requested by the CLI flags. -fn resolve_additional_components(args: &InstallArgs) -> &'static [&'static str] { - if args.cranelift { +const fn resolve_additional_components(args: &InstallArgs) -> &'static [&'static str] { + if args.execution.cranelift { &["rustc-codegen-cranelift"] } else { &[] @@ -76,7 +76,7 @@ fn try_fast_path_installation( requested_crates: context.requested_crates, toolchain_channel: context.toolchain.channel(), }; - if let Some(staging_path) = try_prebuilt_installation(&prebuilt_context, stderr)? { + if let Some(staging_path) = try_prebuilt_installation(&prebuilt_context, stderr) { return Ok(Some((staging_path, InstallMode::Download))); } if let Some(staging_path) = staged_suite::try_test_staged_suite_installation( @@ -102,12 +102,12 @@ fn run_install(args: &InstallArgs, stderr: &mut dyn Write) -> Result<()> { let dirs = SystemBaseDirs::new().ok_or_else(|| InstallerError::WorkspaceNotFound { reason: "could not determine platform directories".to_owned(), })?; - if args.dry_run { + if args.execution.dry_run { return run_dry(args, &dirs, stderr); } let install_started = Instant::now(); // Step 1: Check and install Dylint dependencies if needed - if !args.skip_deps { + if !args.skip.skip_deps { ensure_dylint_tools(args.quiet, stderr)?; } // Step 2: Ensure workspace is available (clone if needed) @@ -148,7 +148,7 @@ fn run_install(args: &InstallArgs, stderr: &mut dyn Write) -> Result<()> { target_dir: &target_dir, jobs: args.jobs, verbosity: args.verbosity, - experimental: args.experimental, + experimental: args.lint_selection.experimental, quiet: args.quiet, }; // Step 4: Build and stage @@ -180,9 +180,11 @@ fn run_dry(args: &InstallArgs, dirs: &dyn BaseDirs, stderr: &mut dyn Write) -> R target_dir: &target_dir, verbosity: args.verbosity, quiet: args.quiet, - skip_deps: args.skip_deps, - skip_wrapper: args.skip_wrapper, - no_update: args.no_update, + skips: DryRunSkips { + deps: args.skip.skip_deps, + wrapper: args.skip.skip_wrapper, + update: args.skip.no_update, + }, jobs: args.jobs, crates: &requested_crates, }; @@ -225,15 +227,15 @@ fn ensure_whitaker_workspace( if !args.quiet && let Some(clone_dir) = clone_directory(dirs) { - let cwd = std::env::current_dir() + let utf8_cwd = std::env::current_dir() .ok() .and_then(|p| Utf8PathBuf::try_from(p).ok()); - let Some(cwd) = cwd else { - return ensure_workspace(dirs, !args.no_update); + let Some(cwd) = utf8_cwd else { + return ensure_workspace(dirs, !args.skip.no_update); }; - match decide_workspace_action(&cwd, &clone_dir, !args.no_update) { + match decide_workspace_action(&cwd, &clone_dir, !args.skip.no_update) { WorkspaceAction::CloneTo(dir) => { write_stderr_line(stderr, format!("Cloning Whitaker repository to {dir}...")); } @@ -244,7 +246,7 @@ fn ensure_whitaker_workspace( } } - ensure_workspace(dirs, !args.no_update) + ensure_workspace(dirs, !args.skip.no_update) } /// Detects or overrides the toolchain, then verifies it is installed. @@ -252,10 +254,10 @@ fn resolve_toolchain( workspace_root: &Utf8Path, override_channel: Option<&str>, ) -> Result { - match override_channel { - Some(channel) => Ok(Toolchain::with_override(workspace_root, channel)), - None => Toolchain::detect(workspace_root), - } + override_channel.map_or_else( + || Toolchain::detect(workspace_root), + |channel| Ok(Toolchain::with_override(workspace_root, channel)), + ) } fn ensure_toolchain_installed( @@ -282,7 +284,7 @@ fn finish_install( staging_path: &Utf8Path, stderr: &mut dyn Write, ) -> Result<()> { - if args.skip_wrapper { + if args.skip.skip_wrapper { write_stderr_line(stderr, ""); write_stderr_line(stderr, ShellSnippet::new(staging_path).display_text()); } else { @@ -334,8 +336,8 @@ fn resolve_requested_crates(args: &InstallArgs) -> Result> { .collect(); let options = CrateResolutionOptions { - individual_lints: args.individual_lints, - experimental: args.experimental, + individual_lints: args.lint_selection.individual_lints, + experimental: args.lint_selection.experimental, }; if !lint_crates.is_empty() { validate_crate_names(&lint_crates, &options)?; diff --git a/installer/src/output.rs b/installer/src/output.rs index e8ac48bd8..3dac5eb43 100644 --- a/installer/src/output.rs +++ b/installer/src/output.rs @@ -99,9 +99,11 @@ pub fn success_message(count: usize, target_dir: &Utf8Path) -> String { /// target_dir: &target, /// verbosity: 0, /// quiet: false, -/// skip_deps: false, -/// skip_wrapper: false, -/// no_update: false, +/// skips: whitaker_installer::output::DryRunSkips { +/// deps: false, +/// wrapper: false, +/// update: false, +/// }, /// jobs: None, /// crates: &crates, /// }; @@ -122,18 +124,25 @@ pub struct DryRunInfo<'a> { pub verbosity: u8, /// Whether quiet mode is enabled. pub quiet: bool, - /// Whether dependency installation is skipped. - pub skip_deps: bool, - /// Whether wrapper script generation is skipped. - pub skip_wrapper: bool, - /// Whether repository updates are disabled. - pub no_update: bool, + /// Which installation steps are skipped. + pub skips: DryRunSkips, /// Optional parallel job count. pub jobs: Option, /// Crates to be built. pub crates: &'a [CrateName], } +/// Step-skipping flags reported in dry-run output. +#[derive(Debug, Clone, Copy, Default)] +pub struct DryRunSkips { + /// Whether dependency installation is skipped. + pub deps: bool, + /// Whether wrapper script generation is skipped. + pub wrapper: bool, + /// Whether repository updates are disabled. + pub update: bool, +} + impl DryRunInfo<'_> { /// Format the dry-run information for display. #[must_use] @@ -146,9 +155,9 @@ impl DryRunInfo<'_> { format!("Target directory: {}", self.target_dir), format!("Verbosity level: {}", self.verbosity), format!("Quiet: {}", self.quiet), - format!("Skip deps: {}", self.skip_deps), - format!("Skip wrapper: {}", self.skip_wrapper), - format!("No update: {}", self.no_update), + format!("Skip deps: {}", self.skips.deps), + format!("Skip wrapper: {}", self.skips.wrapper), + format!("No update: {}", self.skips.update), ]; if let Some(jobs) = self.jobs { @@ -167,6 +176,8 @@ impl DryRunInfo<'_> { #[cfg(test)] mod tests { + //! Tests for installer output rendering. + use super::*; use camino::Utf8PathBuf; use rstest::{fixture, rstest}; diff --git a/installer/src/prebuilt_path.rs b/installer/src/prebuilt_path.rs index 31a88e6b2..fbe7990d6 100644 --- a/installer/src/prebuilt_path.rs +++ b/installer/src/prebuilt_path.rs @@ -24,13 +24,13 @@ pub fn prebuilt_library_dir( toolchain: &str, target: &str, ) -> Result { - let base_dir = dirs - .whitaker_data_dir() + let data_dir = dirs + .whitaker_data() .ok_or_else(|| InstallerError::StagingFailed { reason: "could not determine Whitaker data directory".to_owned(), })?; let base_dir = - Utf8PathBuf::from_path_buf(base_dir).map_err(|path| InstallerError::StagingFailed { + Utf8PathBuf::from_path_buf(data_dir).map_err(|path| InstallerError::StagingFailed { reason: format!( "Whitaker data directory is not valid UTF-8: {}", path.display() @@ -45,6 +45,8 @@ pub fn prebuilt_library_dir( #[cfg(test)] mod tests { + //! Tests for prebuilt artefact path resolution. + use super::*; use crate::dirs::MockBaseDirs; use rstest::rstest; @@ -53,7 +55,7 @@ mod tests { #[test] fn prebuilt_library_dir_builds_expected_path() { let mut dirs = MockBaseDirs::new(); - dirs.expect_whitaker_data_dir() + dirs.expect_whitaker_data() .returning(|| Some(PathBuf::from("/home/test/.local/share/whitaker"))); let result = prebuilt_library_dir(&dirs, "nightly-2026-05-28", "x86_64-unknown-linux-gnu") @@ -74,7 +76,7 @@ mod tests { #[case] expected_reason: &str, ) { let mut dirs = MockBaseDirs::new(); - dirs.expect_whitaker_data_dir() + dirs.expect_whitaker_data() .return_once(move || data_dir.clone()); let err = prebuilt_library_dir(&dirs, "nightly-2026-05-28", "x86_64-unknown-linux-gnu") @@ -92,7 +94,7 @@ mod tests { use std::os::unix::ffi::OsStringExt; let mut dirs = MockBaseDirs::new(); - dirs.expect_whitaker_data_dir().return_once(|| { + dirs.expect_whitaker_data().return_once(|| { Some(PathBuf::from(OsString::from_vec(vec![ b'/', b't', b'm', b'p', b'/', 0xff, ]))) diff --git a/installer/src/resolution.rs b/installer/src/resolution.rs index 371443856..dd7970ab0 100644 --- a/installer/src/resolution.rs +++ b/installer/src/resolution.rs @@ -68,8 +68,7 @@ pub fn resolve_crates( // Assumes names have been validated via validate_crate_names(). debug!( target: "whitaker_installer::resolution", - "using explicit lint crate selection: {:?}", - specific_lints + "using explicit lint crate selection: {specific_lints:?}" ); return specific_lints.to_vec(); } @@ -147,52 +146,70 @@ pub fn validate_crate_names(names: &[CrateName], options: &CrateResolutionOption #[cfg(test)] mod tests { + //! Tests for release resolution. + use super::*; use rstest::rstest; - /// Test configuration for resolve_crates variants. + /// Test configuration for `resolve_crates` variants. struct ResolveCratesCase { - individual_lints: bool, - experimental: bool, - expect_lint: bool, - expect_suite: bool, - expect_bumpy_road: bool, - expect_experimental_lint: bool, + /// Resolution flags under test. + options: CrateResolutionOptions, + /// Crate names that must appear in the resolved set. + expected_present: &'static [&'static str], + /// Crate names that must be absent from the resolved set. + expected_absent: &'static [&'static str], } - /// Parameterized tests for resolve_crates variants. + /// Parameterized tests for `resolve_crates` variants. #[rstest] - #[case::default_suite_only(ResolveCratesCase { individual_lints: false, experimental: false, expect_lint: false, expect_suite: true, expect_bumpy_road: false, expect_experimental_lint: false })] - #[case::individual_lints(ResolveCratesCase { individual_lints: true, experimental: false, expect_lint: true, expect_suite: false, expect_bumpy_road: true, expect_experimental_lint: false })] - #[case::individual_with_experimental(ResolveCratesCase { individual_lints: true, experimental: true, expect_lint: true, expect_suite: false, expect_bumpy_road: true, expect_experimental_lint: true })] - #[case::suite_with_experimental(ResolveCratesCase { individual_lints: false, experimental: true, expect_lint: false, expect_suite: true, expect_bumpy_road: false, expect_experimental_lint: false })] + #[case::default_suite_only(ResolveCratesCase { + options: CrateResolutionOptions { individual_lints: false, experimental: false }, + expected_present: &[SUITE_CRATE], + expected_absent: &[ + "module_max_lines", + "bumpy_road_function", + "rstest_helper_should_be_fixture", + ], + })] + #[case::individual_lints(ResolveCratesCase { + options: CrateResolutionOptions { individual_lints: true, experimental: false }, + expected_present: &["module_max_lines", "bumpy_road_function"], + expected_absent: &[SUITE_CRATE, "rstest_helper_should_be_fixture"], + })] + #[case::individual_with_experimental(ResolveCratesCase { + options: CrateResolutionOptions { individual_lints: true, experimental: true }, + expected_present: &[ + "module_max_lines", + "bumpy_road_function", + "rstest_helper_should_be_fixture", + ], + expected_absent: &[SUITE_CRATE], + })] + #[case::suite_with_experimental(ResolveCratesCase { + options: CrateResolutionOptions { individual_lints: false, experimental: true }, + expected_present: &[SUITE_CRATE], + expected_absent: &[ + "module_max_lines", + "bumpy_road_function", + "rstest_helper_should_be_fixture", + ], + })] fn resolve_crates_variants(#[case] case: ResolveCratesCase) { - let options = CrateResolutionOptions { - individual_lints: case.individual_lints, - experimental: case.experimental, - }; - let crates = resolve_crates(&[], &options); + let crates = resolve_crates(&[], &case.options); - assert_eq!( - crates.contains(&CrateName::from("module_max_lines")), - case.expect_lint, - "lint crate inclusion mismatch" - ); - assert_eq!( - crates.contains(&CrateName::from(SUITE_CRATE)), - case.expect_suite, - "suite crate inclusion mismatch" - ); - assert_eq!( - crates.contains(&CrateName::from("bumpy_road_function")), - case.expect_bumpy_road, - "bumpy_road_function inclusion mismatch" - ); - assert_eq!( - crates.contains(&CrateName::from("rstest_helper_should_be_fixture")), - case.expect_experimental_lint, - "rstest_helper_should_be_fixture inclusion mismatch" - ); + for name in case.expected_present { + assert!( + crates.contains(&CrateName::from(*name)), + "{name} should be resolved, got {crates:?}" + ); + } + for name in case.expected_absent { + assert!( + !crates.contains(&CrateName::from(*name)), + "{name} should not be resolved, got {crates:?}" + ); + } } #[test] @@ -223,12 +240,15 @@ mod tests { assert!(res.is_ok()); } else { let err = res.expect_err("expected validation failure"); + let expected_name = crate_names + .first() + .expect("case should supply at least one crate name"); assert!( matches!( &err, InstallerError::LintCrateNotFound { name } | InstallerError::ExperimentalLintRequiresFlag { name } - if *name == crate_names[0] + if name == expected_name ), "unexpected error: {err:?}" ); diff --git a/installer/src/scanner.rs b/installer/src/scanner.rs index 4f26b660b..f62894362 100644 --- a/installer/src/scanner.rs +++ b/installer/src/scanner.rs @@ -57,8 +57,8 @@ pub fn scan_installed(target_dir: &Utf8Path) -> io::Result { } // Iterate over toolchain subdirectories - for entry in target_dir.read_dir_utf8()? { - let entry = entry?; + for entry_result in target_dir.read_dir_utf8()? { + let entry = entry_result?; let toolchain_path = entry.path(); if !toolchain_path.is_dir() { @@ -84,8 +84,8 @@ fn scan_toolchain_layouts( if release_path.is_dir() { libraries.extend(scan_toolchain_release(&release_path, toolchain)?); } - for entry in toolchain_path.read_dir_utf8()? { - let entry = entry?; + for entry_result in toolchain_path.read_dir_utf8()? { + let entry = entry_result?; if !entry.path().is_dir() || entry.file_name() == "release" { continue; } @@ -99,8 +99,8 @@ fn scan_toolchain_layouts( } fn contains_libraries_in_layout(lib_path: &Utf8Path) -> io::Result { - for entry in lib_path.read_dir_utf8()? { - let entry = entry?; + for entry_result in lib_path.read_dir_utf8()? { + let entry = entry_result?; if entry.path().is_file() && parse_library_filename(entry.file_name()).is_some() { return Ok(true); } @@ -115,8 +115,8 @@ fn scan_toolchain_release( ) -> io::Result> { let mut libraries = Vec::new(); - for entry in release_path.read_dir_utf8()? { - let entry = entry?; + for entry_result in release_path.read_dir_utf8()? { + let entry = entry_result?; let file_name = entry.file_name(); if let Some((crate_name, parsed_toolchain)) = parse_library_filename(file_name) { @@ -161,9 +161,7 @@ pub fn parse_library_filename(filename: &str) -> Option<(CrateName, String)> { let without_ext = without_prefix.strip_suffix(extension)?; // Split on @ to get crate name and toolchain - let at_pos = without_ext.find('@')?; - let crate_name = &without_ext[..at_pos]; - let toolchain = &without_ext[at_pos + 1..]; + let (crate_name, toolchain) = without_ext.split_once('@')?; if crate_name.is_empty() || toolchain.is_empty() { return None; @@ -241,6 +239,8 @@ pub fn lints_for_library_with_experimental( #[cfg(test)] mod tests { + //! Tests for scanning installed lint libraries. + use super::*; use rstest::rstest; use tempfile::TempDir; @@ -395,7 +395,8 @@ mod tests { .get(toolchain) .expect("toolchain should exist"); assert_eq!(libs.len(), 1); - assert_eq!(libs[0].crate_name.as_str(), "whitaker_suite"); - assert_eq!(libs[0].toolchain, toolchain); + let library = libs.first().expect("library should be recorded"); + assert_eq!(library.crate_name.as_str(), "whitaker_suite"); + assert_eq!(library.toolchain, toolchain); } } diff --git a/installer/src/staged_suite.rs b/installer/src/staged_suite.rs index 777b56b3c..2a2180ec9 100644 --- a/installer/src/staged_suite.rs +++ b/installer/src/staged_suite.rs @@ -72,7 +72,7 @@ mod tests { } impl StagedSuiteSetup { - fn requested_suite_crates(&self) -> Vec { + fn requested_suite_crates() -> Vec { vec![CrateName::from(SUITE_CRATE)] } @@ -80,18 +80,15 @@ mod tests { Stager::new(self.target_dir.clone(), self.toolchain.channel()) } - fn create_blocked_suite_output(&self) -> Utf8PathBuf { + fn create_blocked_suite_output(&self) -> Result { let stager = self.stager(); - stager - .prepare() - .expect("expected staging directory to be writable for test setup"); + stager.prepare()?; let blocked_path = stager .staging_path() .join(stager.staged_filename(&CrateName::from(SUITE_CRATE))); - std::fs::create_dir_all(blocked_path.as_std_path()) - .expect("expected to pre-create staged filename as a directory"); - blocked_path + std::fs::create_dir_all(blocked_path.as_std_path())?; + Ok(blocked_path) } } @@ -99,24 +96,24 @@ mod tests { Toolchain::with_override(Utf8Path::new("."), "nightly-2026-05-28") } - fn utf8_temp_dir(temp_dir: &TempDir) -> Utf8PathBuf { + fn utf8_temp_dir(temp_dir: &TempDir) -> std::io::Result { Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) - .expect("expected UTF-8 temp path for staged suite tests") + .map_err(|_| std::io::Error::other("temporary directory path must be UTF-8")) } #[fixture] - fn staged_suite_setup() -> StagedSuiteSetup { + fn staged_suite_setup() -> std::io::Result { let guard = env_test_guard(); - let temp_dir = tempfile::tempdir().expect("expected temp dir for staged suite tests"); - let target_dir = utf8_temp_dir(&temp_dir); + let temp_dir = tempfile::tempdir()?; + let target_dir = utf8_temp_dir(&temp_dir)?; let toolchain = test_toolchain(); - StagedSuiteSetup { + Ok(StagedSuiteSetup { _guard: guard, _temp_dir: temp_dir, toolchain, target_dir, - } + }) } #[rstest] @@ -139,9 +136,11 @@ mod tests { #[case::env_disabled((vec![CrateName::from(SUITE_CRATE)], Some("0")))] #[case::non_suite((vec![CrateName::from("module_max_lines")], Some("1")))] fn staged_suite_installation_skips_non_staging_requests( - staged_suite_setup: StagedSuiteSetup, + #[from(staged_suite_setup)] setup_res: std::io::Result, #[case] case: (Vec, Option<&str>), ) { + let staged_suite_setup = setup_res.expect("staged suite setup should be created"); + let (requested_crates, env_val) = case; let run = || { let result = try_test_staged_suite_installation( @@ -170,9 +169,11 @@ mod tests { #[cfg(debug_assertions)] #[rstest] fn staged_suite_installation_writes_placeholder_library_for_suite_requests( - staged_suite_setup: StagedSuiteSetup, + #[from(staged_suite_setup)] setup_res: std::io::Result, ) { - let requested_crates = staged_suite_setup.requested_suite_crates(); + let staged_suite_setup = setup_res.expect("staged suite setup should be created"); + + let requested_crates = StagedSuiteSetup::requested_suite_crates(); let stager = staged_suite_setup.stager(); with_var(TEST_STAGE_SUITE_ENV, Some("1"), || { @@ -195,9 +196,15 @@ mod tests { #[cfg(debug_assertions)] #[rstest] - fn staged_suite_installation_surfaces_write_failures(staged_suite_setup: StagedSuiteSetup) { - let requested_crates = staged_suite_setup.requested_suite_crates(); - let blocked_path = staged_suite_setup.create_blocked_suite_output(); + fn staged_suite_installation_surfaces_write_failures( + #[from(staged_suite_setup)] setup_res: std::io::Result, + ) { + let staged_suite_setup = setup_res.expect("staged suite setup should be created"); + + let requested_crates = StagedSuiteSetup::requested_suite_crates(); + let blocked_path = staged_suite_setup + .create_blocked_suite_output() + .expect("blocked staged output should be pre-created"); with_var(TEST_STAGE_SUITE_ENV, Some("1"), || { let err = try_test_staged_suite_installation( @@ -218,9 +225,11 @@ mod tests { #[cfg(not(debug_assertions))] #[rstest] fn staged_suite_installation_is_disabled_in_release_builds( - staged_suite_setup: StagedSuiteSetup, + #[from(staged_suite_setup)] setup_res: std::io::Result, ) { - let requested_crates = staged_suite_setup.requested_suite_crates(); + let staged_suite_setup = setup_res.expect("staged suite setup should be created"); + + let requested_crates = StagedSuiteSetup::requested_suite_crates(); let staging_dir = staged_suite_setup .target_dir .join(staged_suite_setup.toolchain.channel()) diff --git a/installer/src/stager.rs b/installer/src/stager.rs index fd6772777..0d9b43dab 100644 --- a/installer/src/stager.rs +++ b/installer/src/stager.rs @@ -39,7 +39,9 @@ impl Stager { let test_path = staging_dir.join(".whitaker-installer-test"); match fs::write(&test_path, b"test") { Ok(()) => { - let _ = fs::remove_file(&test_path); + // Removing the probe file is best effort; a leftover probe + // does not affect staging correctness. + drop(fs::remove_file(&test_path)); Ok(()) } Err(e) => Err(InstallerError::TargetNotWritable { @@ -123,7 +125,7 @@ impl Stager { /// directory. This function creates a `directories_next::BaseDirs` instance and /// calls its `data_local_dir()` method to obtain the base path (for example, /// `~/.local/share` on many Linux distributions, `~/Library/Application Support` -/// on macOS, and the Local AppData directory on Windows). The installer appends +/// on macOS, and the local `AppData` directory on Windows). The installer appends /// `dylint/lib` under that directory. #[must_use] pub fn default_target_dir() -> Option { @@ -135,6 +137,8 @@ pub fn default_target_dir() -> Option { #[cfg(test)] mod tests { + //! Tests for staging lint libraries into the target directory. + use super::*; #[test] diff --git a/installer/src/test_utils/dependency_binary_helpers_fakes.rs b/installer/src/test_utils/dependency_binary_helpers_fakes.rs index ec91be59a..bacbba51a 100644 --- a/installer/src/test_utils/dependency_binary_helpers_fakes.rs +++ b/installer/src/test_utils/dependency_binary_helpers_fakes.rs @@ -38,24 +38,37 @@ impl DependencyBinaryInstaller for AlwaysNotFoundRepositoryInstaller { } /// Writes a fake binary at `path` that exits successfully. -pub fn write_fake_binary(path: &Path, is_executable: bool) { - write_fake_binary_with_status(path, is_executable, 0); +/// +/// # Errors +/// +/// Returns any I/O error raised while writing the binary or setting its +/// permissions. +pub fn write_fake_binary(path: &Path, is_executable: bool) -> std::io::Result<()> { + write_fake_binary_with_status(path, is_executable, 0) } /// Writes a fake binary at `path` that exits with the supplied status code. -pub fn write_fake_binary_with_status(path: &Path, is_executable: bool, exit_code: i32) { - fs::write(path, fake_binary_contents(exit_code)).expect("write fake binary"); +/// +/// # Errors +/// +/// Returns any I/O error raised while writing the binary or setting its +/// permissions. +pub fn write_fake_binary_with_status( + path: &Path, + is_executable: bool, + exit_code: i32, +) -> std::io::Result<()> { + fs::write(path, fake_binary_contents(exit_code))?; #[cfg(unix)] { let mode = if is_executable { 0o755 } else { 0o644 }; - let mut permissions = fs::metadata(path) - .expect("read fake binary metadata") - .permissions(); + let mut permissions = fs::metadata(path)?.permissions(); permissions.set_mode(mode); - fs::set_permissions(path, permissions).expect("set fake binary permissions"); + fs::set_permissions(path, permissions)?; } #[cfg(not(unix))] let _ = is_executable; + Ok(()) } fn fake_binary_contents(exit_code: i32) -> Vec { @@ -70,32 +83,51 @@ fn fake_binary_contents(exit_code: i32) -> Vec { } /// Runs a closure with `PATH` pointing at one or more fake directories. -pub fn with_fake_path(setup: impl FnOnce(&[PathBuf]), run: impl FnOnce() -> T) -> T { +/// +/// # Errors +/// +/// Returns an I/O error when the fake directories cannot be created, the +/// `setup` closure fails, or the fake `PATH` cannot be joined. +pub fn with_fake_path( + setup: impl FnOnce(&[PathBuf]) -> std::io::Result<()>, + run: impl FnOnce() -> T, +) -> std::io::Result { let _guard = env_test_guard(); - let temp_dirs = [ - tempfile::tempdir().expect("create temp dir"), - tempfile::tempdir().expect("create temp dir"), - ]; + let temp_dirs = [tempfile::tempdir()?, tempfile::tempdir()?]; let path_dirs = temp_dirs .iter() .map(|dir| dir.path().to_path_buf()) .collect::>(); - setup(&path_dirs); + setup(&path_dirs)?; let path = std::env::join_paths(path_dirs.iter().map(PathBuf::as_path)) - .expect("join fake PATH directories"); - temp_env::with_var("PATH", Some(path), run) + .map_err(std::io::Error::other)?; + Ok(temp_env::with_var("PATH", Some(path), run)) } /// Runs a closure with `PATH` containing a fake executable in the first entry. -pub fn with_fake_binary_on_path(binary_name: &str, run: impl FnOnce() -> T) -> T { +/// +/// # Errors +/// +/// Returns an I/O error when the fake `PATH` cannot be prepared or the fake +/// binary cannot be written. +pub fn with_fake_binary_on_path( + binary_name: &str, + run: impl FnOnce() -> T, +) -> std::io::Result { with_fake_path( - |directories| write_fake_binary(&path_binary_location(&directories[0], binary_name), true), + |directories| { + let first_dir = directories.first().ok_or_else(|| { + std::io::Error::other("fake PATH should contain at least one directory") + })?; + write_fake_binary(&path_binary_location(first_dir, binary_name), true) + }, run, ) } /// Joins `binary_name` onto `directory` with the platform executable suffix, /// so directly probed fakes are runnable on Windows as well as Unix. +#[must_use] pub fn path_binary_location(directory: &Path, binary_name: &str) -> PathBuf { #[cfg(windows)] { diff --git a/installer/src/test_utils/mod.rs b/installer/src/test_utils/mod.rs index 6aa6de198..3d5c77adc 100644 --- a/installer/src/test_utils/mod.rs +++ b/installer/src/test_utils/mod.rs @@ -120,20 +120,20 @@ pub fn failure_output(stderr: impl AsRef) -> Output { /// Minimal directory stub for tests that only care about the binary path. #[derive(Debug, Clone, Default)] pub struct StubDirs { - /// Directory returned by [`BaseDirs::bin_dir`]. + /// Directory returned by [`BaseDirs::executables`]. pub bin_dir: Option, } impl BaseDirs for StubDirs { - fn home_dir(&self) -> Option { + fn home(&self) -> Option { None } - fn bin_dir(&self) -> Option { + fn executables(&self) -> Option { self.bin_dir.clone() } - fn whitaker_data_dir(&self) -> Option { + fn whitaker_data(&self) -> Option { None } } diff --git a/installer/src/tests/fast_path.rs b/installer/src/tests/fast_path.rs index 3efbbb3c5..685a5c7e5 100644 --- a/installer/src/tests/fast_path.rs +++ b/installer/src/tests/fast_path.rs @@ -4,6 +4,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use rstest::{fixture, rstest}; use temp_env::with_var_unset; use whitaker_installer::{ + cli::ExecutionFlags, crate_name::CrateName, test_support::{TEST_STAGE_SUITE_ENV, env_test_guard}, toolchain::Toolchain, @@ -51,7 +52,10 @@ fn fast_path_fixture() -> FastPathFixture { #[case::with_cranelift(true, &["rustc-codegen-cranelift"])] fn resolve_additional_components_parametrized(#[case] cranelift: bool, #[case] expected: &[&str]) { let args = InstallArgs { - cranelift, + execution: ExecutionFlags { + cranelift, + ..ExecutionFlags::default() + }, ..InstallArgs::default() }; @@ -63,7 +67,7 @@ fn fast_path_context_holds_supplied_values(fast_path_fixture: FastPathFixture) { let ctx = fast_path_fixture.context(); assert!(std::ptr::eq(ctx.args, &raw const fast_path_fixture.args)); - assert_eq!(ctx.dirs.home_dir(), Some(PathBuf::from("/tmp"))); + assert_eq!(ctx.dirs.home(), Some(PathBuf::from("/tmp"))); assert_eq!(ctx.toolchain.channel(), "nightly-2026-05-28"); assert_eq!(ctx.target_dir, &Utf8PathBuf::from("/tmp/target")); assert!(ctx.requested_crates.is_empty()); diff --git a/installer/src/tests/mod.rs b/installer/src/tests/mod.rs index f1e8e1f50..72181f870 100644 --- a/installer/src/tests/mod.rs +++ b/installer/src/tests/mod.rs @@ -6,7 +6,7 @@ use std::{path::PathBuf, time::Duration}; use rstest::{fixture, rstest}; use whitaker_installer::{ - cli::InstallArgs, + cli::{InstallArgs, LintSelectionFlags}, dependency_binaries::DependencyBinaryInstaller, deps::DependencyInstallOptions, dirs::BaseDirs, @@ -73,7 +73,10 @@ fn exit_code_for_run_result_prints_error_and_returns_one() { #[case::default_suite_only(InstallArgs::default(), false, true)] #[case::individual_lints( InstallArgs { - individual_lints: true, + lint_selection: LintSelectionFlags { + individual_lints: true, + ..LintSelectionFlags::default() + }, ..InstallArgs::default() }, true, @@ -130,12 +133,13 @@ fn ensure_dylint_tools_skips_install_when_installed(test_base_dirs: TestBaseDirs let mut stderr = Vec::new(); let options = dependency_install_options(&test_base_dirs, &repository_installer, false) .expect("dependency install options should build"); - let result = ensure_dylint_tools_with_options(&executor, &mut stderr, options); + let result = ensure_dylint_tools_with_options(&executor, &mut stderr, &options); assert!(result.is_ok()); assert!(stderr.is_empty()); executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[rstest] @@ -176,7 +180,7 @@ fn ensure_dylint_tools_installs_missing_tools( let mut stderr = Vec::new(); let options = dependency_install_options(&test_base_dirs, &repository_installer, quiet) .expect("dependency install options should build"); - let result = ensure_dylint_tools_with_options(&executor, &mut stderr, options); + let result = ensure_dylint_tools_with_options(&executor, &mut stderr, &options); assert!(result.is_ok()); let stderr_text = String::from_utf8(stderr).expect("stderr was not UTF-8"); @@ -196,7 +200,8 @@ fn ensure_dylint_tools_installs_missing_tools( ); } executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[rstest] @@ -225,7 +230,7 @@ fn ensure_dylint_tools_propagates_install_failures(test_base_dirs: TestBaseDirs) let mut stderr = Vec::new(); let options = dependency_install_options(&test_base_dirs, &repository_installer, false) .expect("dependency install options should build"); - let err = ensure_dylint_tools_with_options(&executor, &mut stderr, options) + let err = ensure_dylint_tools_with_options(&executor, &mut stderr, &options) .expect_err("expected install failure"); assert!(matches!( @@ -235,7 +240,8 @@ fn ensure_dylint_tools_propagates_install_failures(test_base_dirs: TestBaseDirs) && message == "cargo install failed" )); executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[derive(Debug, Clone)] @@ -246,15 +252,15 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home_dir(&self) -> Option { + fn home(&self) -> Option { self.home.clone() } - fn bin_dir(&self) -> Option { + fn executables(&self) -> Option { self.bin.clone() } - fn whitaker_data_dir(&self) -> Option { + fn whitaker_data(&self) -> Option { self.data.clone() } } diff --git a/installer/src/toolchain/mod.rs b/installer/src/toolchain/mod.rs index 9615cd5bf..5865ce98f 100644 --- a/installer/src/toolchain/mod.rs +++ b/installer/src/toolchain/mod.rs @@ -30,7 +30,7 @@ pub struct ToolchainInstallStatus { impl ToolchainInstallStatus { /// Returns true if the toolchain was installed during this run. #[must_use] - pub fn installed_toolchain(&self) -> bool { + pub const fn installed_toolchain(&self) -> bool { self.installed_toolchain } } diff --git a/installer/src/version.rs b/installer/src/version.rs index 8208b9987..9face4b47 100644 --- a/installer/src/version.rs +++ b/installer/src/version.rs @@ -58,6 +58,8 @@ impl fmt::Display for Version { #[cfg(test)] mod tests { + //! Tests for installer version reporting. + use super::*; #[test] diff --git a/installer/src/workspace.rs b/installer/src/workspace.rs index 674a07641..1a6bbac99 100644 --- a/installer/src/workspace.rs +++ b/installer/src/workspace.rs @@ -29,6 +29,7 @@ const WHITAKER_PACKAGE_NAME: &str = "whitaker"; /// println!("This is a Whitaker workspace"); /// } /// ``` +#[must_use] pub fn is_whitaker_workspace(dir: &Utf8Path) -> bool { let cargo_toml = dir.join("Cargo.toml"); if !cargo_toml.exists() { @@ -57,7 +58,7 @@ pub fn is_whitaker_workspace(dir: &Utf8Path) -> bool { /// /// Returns `None` if the platform's data directory cannot be determined. pub fn clone_directory(dirs: &dyn BaseDirs) -> Option { - dirs.whitaker_data_dir() + dirs.whitaker_data() .and_then(|p| Utf8PathBuf::try_from(p).ok()) } @@ -80,6 +81,7 @@ pub enum WorkspaceAction { /// operation (if any) is needed. Returns `UseCurrentDir` if `cwd` is a /// Whitaker workspace, `CloneTo` if `clone_dir` doesn't exist, `UpdateAt` /// if `update` is true and the clone exists, or `UseExisting` otherwise. +#[must_use] pub fn decide_workspace_action( cwd: &Utf8Path, clone_dir: &Utf8Path, @@ -132,6 +134,11 @@ pub fn ensure_workspace(dirs: &dyn BaseDirs, update: bool) -> Result Result { let cwd = current_dir_utf8()?; diff --git a/installer/src/workspace_tests.rs b/installer/src/workspace_tests.rs index c08f5d888..fa51facdf 100644 --- a/installer/src/workspace_tests.rs +++ b/installer/src/workspace_tests.rs @@ -14,19 +14,19 @@ struct TempWorkspace { } #[fixture] -fn temp_workspace() -> TempWorkspace { - let temp = TempDir::new().expect("failed to create temp dir"); - let path = Utf8PathBuf::try_from(temp.path().to_owned()).expect("non-UTF8 temp path"); - TempWorkspace { _temp: temp, path } +fn temp_workspace() -> std::io::Result { + let temp = TempDir::new()?; + let path = Utf8PathBuf::try_from(temp.path().to_owned()) + .map_err(|_| std::io::Error::other("temporary directory path must be UTF-8"))?; + Ok(TempWorkspace { _temp: temp, path }) } -fn write_cargo_toml(dir: &Utf8Path, package_name: &str) { +fn write_cargo_toml(dir: &Utf8Path, package_name: &str) -> std::io::Result<()> { let cargo_toml = dir.join("Cargo.toml"); fs::write( cargo_toml, format!("[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\n"), ) - .expect("failed to write Cargo.toml"); } #[rstest] @@ -34,12 +34,14 @@ fn write_cargo_toml(dir: &Utf8Path, package_name: &str) { #[case::other_project(Some("other-project"), false)] #[case::empty_dir(None, false)] fn is_whitaker_workspace_detection( - temp_workspace: TempWorkspace, + #[from(temp_workspace)] temp_workspace_res: std::io::Result, #[case] package_name: Option<&str>, #[case] expected: bool, ) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + if let Some(name) = package_name { - write_cargo_toml(&temp_workspace.path, name); + write_cargo_toml(&temp_workspace.path, name).expect("Cargo.toml should be written"); } assert_eq!(is_whitaker_workspace(&temp_workspace.path), expected); } @@ -59,8 +61,12 @@ fn clone_directory_returns_some_on_supported_platforms() { } #[rstest] -fn decide_workspace_action_uses_cwd_when_whitaker(temp_workspace: TempWorkspace) { - write_cargo_toml(&temp_workspace.path, "whitaker"); +fn decide_workspace_action_uses_cwd_when_whitaker( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + + write_cargo_toml(&temp_workspace.path, "whitaker").expect("Cargo.toml should be written"); let clone_dir = Utf8PathBuf::from("/nonexistent/clone/dir"); let action = decide_workspace_action(&temp_workspace.path, &clone_dir, true); @@ -69,7 +75,11 @@ fn decide_workspace_action_uses_cwd_when_whitaker(temp_workspace: TempWorkspace) } #[rstest] -fn decide_workspace_action_clones_when_empty(temp_workspace: TempWorkspace) { +fn decide_workspace_action_clones_when_empty( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + // temp_workspace.path is empty (no Cargo.toml), clone_dir doesn't exist let clone_dir = temp_workspace.path.join("clone_target"); @@ -79,7 +89,11 @@ fn decide_workspace_action_clones_when_empty(temp_workspace: TempWorkspace) { } #[rstest] -fn decide_workspace_action_updates_when_clone_exists(temp_workspace: TempWorkspace) { +fn decide_workspace_action_updates_when_clone_exists( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + // Create a clone directory (not a whitaker workspace, just exists) let clone_dir = temp_workspace.path.join("clone_target"); fs::create_dir(&clone_dir).expect("failed to create clone dir"); @@ -90,7 +104,11 @@ fn decide_workspace_action_updates_when_clone_exists(temp_workspace: TempWorkspa } #[rstest] -fn decide_workspace_action_uses_existing_when_no_update(temp_workspace: TempWorkspace) { +fn decide_workspace_action_uses_existing_when_no_update( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + let clone_dir = temp_workspace.path.join("clone_target"); fs::create_dir(&clone_dir).expect("failed to create clone dir"); @@ -105,31 +123,37 @@ fn decide_workspace_action_uses_existing_when_no_update(temp_workspace: TempWork fn mock_dirs_returning(data_dir: Option) -> MockBaseDirs { let mut mock = MockBaseDirs::new(); - mock.expect_whitaker_data_dir().return_const(data_dir); + mock.expect_whitaker_data().return_const(data_dir); mock } #[rstest] -fn resolve_workspace_path_returns_clone_dir_when_not_in_workspace(temp_workspace: TempWorkspace) { +fn resolve_workspace_path_returns_clone_dir_when_not_in_workspace( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + // Mock returns a data directory inside temp workspace let expected_dir = temp_workspace.path.join("data").join("whitaker"); let mock = mock_dirs_returning(Some(expected_dir.clone().into_std_path_buf())); let result = resolve_workspace_path(&mock); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), expected_dir); + assert_eq!(result.expect("workspace path should resolve"), expected_dir); } #[rstest] -fn resolve_workspace_path_errors_when_data_dir_unavailable(temp_workspace: TempWorkspace) { +fn resolve_workspace_path_errors_when_data_dir_unavailable( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + let _ = temp_workspace; // Ensure fixture is used let mock = mock_dirs_returning(None); let result = resolve_workspace_path(&mock); - assert!(result.is_err()); - let err = result.unwrap_err(); + let err = result.expect_err("resolution should fail without a data directory"); assert!( matches!(err, InstallerError::WorkspaceNotFound { .. }), "expected WorkspaceNotFound error, got: {err:?}" @@ -143,7 +167,11 @@ fn clone_directory_returns_none_when_data_dir_unavailable() { } #[rstest] -fn clone_directory_returns_path_from_mock(temp_workspace: TempWorkspace) { +fn clone_directory_returns_path_from_mock( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + let expected = temp_workspace.path.join("data").join("whitaker"); let mock = mock_dirs_returning(Some(expected.clone().into_std_path_buf())); assert_eq!(clone_directory(&mock), Some(expected)); @@ -151,37 +179,52 @@ fn clone_directory_returns_path_from_mock(temp_workspace: TempWorkspace) { // Tests for find_workspace_root -fn write_workspace_cargo_toml(dir: &Utf8Path) { +fn write_workspace_cargo_toml(dir: &Utf8Path) -> std::io::Result<()> { fs::write( dir.join("Cargo.toml"), "[workspace]\nmembers = [\"crates/*\"]\n", ) - .expect("failed to write workspace Cargo.toml"); } #[rstest] -fn find_workspace_root_finds_workspace_in_current_dir(temp_workspace: TempWorkspace) { - write_workspace_cargo_toml(&temp_workspace.path); +fn find_workspace_root_finds_workspace_in_current_dir( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + + write_workspace_cargo_toml(&temp_workspace.path).expect("Cargo.toml should be written"); assert_eq!( - find_workspace_root(&temp_workspace.path).unwrap(), + find_workspace_root(&temp_workspace.path).expect("workspace root should be found"), temp_workspace.path ); } #[rstest] -fn find_workspace_root_finds_workspace_in_parent_dir(temp_workspace: TempWorkspace) { - write_workspace_cargo_toml(&temp_workspace.path); +fn find_workspace_root_finds_workspace_in_parent_dir( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + + write_workspace_cargo_toml(&temp_workspace.path).expect("Cargo.toml should be written"); let subdir = temp_workspace.path.join("crates").join("my_crate"); fs::create_dir_all(&subdir).expect("failed to create subdirs"); - assert_eq!(find_workspace_root(&subdir).unwrap(), temp_workspace.path); + assert_eq!( + find_workspace_root(&subdir).expect("workspace root should be found"), + temp_workspace.path + ); } #[rstest] -fn find_workspace_root_errors_when_no_workspace_found(temp_workspace: TempWorkspace) { - write_cargo_toml(&temp_workspace.path, "not_a_workspace"); +fn find_workspace_root_errors_when_no_workspace_found( + #[from(temp_workspace)] temp_workspace_res: std::io::Result, +) { + let temp_workspace = temp_workspace_res.expect("temporary workspace should be created"); + + write_cargo_toml(&temp_workspace.path, "not_a_workspace") + .expect("Cargo.toml should be written"); let result = find_workspace_root(&temp_workspace.path); assert!(matches!( - result.unwrap_err(), + result.expect_err("workspace lookup should fail"), InstallerError::WorkspaceNotFound { .. } )); } diff --git a/installer/src/wrapper.rs b/installer/src/wrapper.rs index 8e9a190aa..5f279d9a0 100644 --- a/installer/src/wrapper.rs +++ b/installer/src/wrapper.rs @@ -63,7 +63,7 @@ pub fn generate_wrapper_scripts( dirs: &dyn BaseDirs, library_path: &Utf8Path, ) -> Result { - let bin_dir = dirs.bin_dir().ok_or_else(|| { + let bin_dir = dirs.executables().ok_or_else(|| { InstallerError::WrapperGeneration("could not determine bin directory".to_owned()) })?; @@ -111,9 +111,8 @@ exec cargo dylint "$@" r#"#!/usr/bin/env bash set -euo pipefail export DYLINT_LIBRARY_PATH="{library_path}" -cargo dylint list --color never | awk -v suite="{suite_crate}" '$0 ~ "^" suite "([[:space:]]|$)" {{ print }}' -"#, - suite_crate = SUITE_CRATE, +cargo dylint list --color never | awk -v suite="{SUITE_CRATE}" '$0 ~ "^" suite "([[:space:]]|$)" {{ print }}' +"# ); write_unix_script(&whitaker_ls_path, &whitaker_ls_content)?; @@ -158,12 +157,11 @@ cargo dylint @args let whitaker_ls_path = bin_dir.join("whitaker-ls.ps1"); let whitaker_ls_content = format!( r#"$env:DYLINT_LIBRARY_PATH = "{library_path}" -$suite = "{suite_crate}" +$suite = "{SUITE_CRATE}" cargo dylint list --color never | Where-Object {{ $_ -match ("^\\s*" + [regex]::Escape($suite) + "(\\s|$)") }} -"#, - suite_crate = SUITE_CRATE, +"# ); std::fs::write(&whitaker_ls_path, whitaker_ls_content) @@ -174,12 +172,11 @@ cargo dylint list --color never | Where-Object {{ /// Checks if a directory is in the PATH environment variable. fn is_directory_in_path(dir: &Path) -> bool { - std::env::var_os("PATH") - .map(|path| std::env::split_paths(&path).any(|p| p == dir)) - .unwrap_or(false) + std::env::var_os("PATH").is_some_and(|path| std::env::split_paths(&path).any(|p| p == dir)) } /// Returns instructions for adding a directory to PATH. +#[must_use] pub fn path_instructions(bin_dir: &Path) -> String { #[cfg(unix)] { @@ -213,6 +210,8 @@ pub fn path_instructions(bin_dir: &Path) -> String { #[cfg(test)] mod tests { + //! Tests for the generated cargo-whitaker wrapper script. + use super::*; use tempfile::TempDir; @@ -222,11 +221,44 @@ mod tests { assert!(!is_directory_in_path(temp.path())); } + /// Asserts that every user, group, and other execute bit is set on `$path`. + /// + /// Expressed as a macro so failures point at the calling test, and so the + /// fallible metadata read stays inside the test body. + #[cfg(unix)] + macro_rules! assert_script_is_executable { + ($path:expr) => {{ + use std::os::unix::fs::PermissionsExt as _; + + let metadata = std::fs::metadata($path).expect("script metadata should be readable"); + assert_eq!( + metadata.permissions().mode() & 0o111, + 0o111, + "script should be executable" + ); + }}; + } + + /// Asserts that the script at `$path` contains every fragment in `$fragments`. + #[cfg(unix)] + macro_rules! assert_script_contains { + ($path:expr, $fragments:expr) => {{ + let path: &Path = $path; + let content = std::fs::read_to_string(path).expect("script should be readable"); + for fragment in $fragments { + assert!( + content.contains(fragment), + "script {} should contain {fragment}", + path.display() + ); + } + }}; + } + #[cfg(unix)] #[test] fn generate_unix_scripts_create_executables() { use camino::Utf8PathBuf; - use std::os::unix::fs::PermissionsExt; let temp = TempDir::new().expect("failed to create temp dir"); let library_path = Utf8PathBuf::from("/tmp/dylint/lib"); @@ -236,23 +268,15 @@ mod tests { assert!(whitaker_path.exists()); assert!(whitaker_ls_path.exists()); - - let perms = std::fs::metadata(&whitaker_path) - .expect("failed to read metadata") - .permissions(); - assert_eq!(perms.mode() & 0o111, 0o111, "script should be executable"); - - let whitaker_content = - std::fs::read_to_string(&whitaker_path).expect("failed to read script"); - assert!(whitaker_content.contains("DYLINT_LIBRARY_PATH")); - assert!(whitaker_content.contains("cargo dylint")); - assert!(whitaker_content.contains("$@")); - - let whitaker_ls_content = - std::fs::read_to_string(&whitaker_ls_path).expect("failed to read script"); - assert!(whitaker_ls_content.contains("DYLINT_LIBRARY_PATH")); - assert!(whitaker_ls_content.contains("cargo dylint list")); - assert!(whitaker_ls_content.contains("whitaker_suite")); + assert_script_is_executable!(&whitaker_path); + assert_script_contains!( + &whitaker_path, + &["DYLINT_LIBRARY_PATH", "cargo dylint", "$@"] + ); + assert_script_contains!( + &whitaker_ls_path, + &["DYLINT_LIBRARY_PATH", "cargo dylint list", "whitaker_suite"] + ); } #[test] diff --git a/installer/tests/behaviour_artefact_packaging.rs b/installer/tests/behaviour_artefact_packaging.rs index 189bfa750..7cc0d5afc 100644 --- a/installer/tests/behaviour_artefact_packaging.rs +++ b/installer/tests/behaviour_artefact_packaging.rs @@ -88,7 +88,7 @@ fn run_packaging(world: &mut PackagingWorld) -> Result<(), String> { generated_at: GeneratedAt::new("2026-02-11T00:00:00Z"), }; - match package_artefact(params) { + match package_artefact(¶ms) { Ok(output) => world.output = Some(output), Err(e) => world.packaging_error = Some(e), } diff --git a/installer/tests/behaviour_binstall.rs b/installer/tests/behaviour_binstall.rs index 2fdcd7c06..12b79cbc6 100644 --- a/installer/tests/behaviour_binstall.rs +++ b/installer/tests/behaviour_binstall.rs @@ -81,8 +81,8 @@ fn assert_binstall_value_equals( #[given("the installer Cargo.toml is loaded")] fn given_cargo_toml_loaded(world: &mut BinstallWorld) -> Result<(), String> { - let table = load_cargo_toml(); - world.binstall_table = Some(extract_binstall_table(&table)); + let table = load_cargo_toml()?; + world.binstall_table = Some(extract_binstall_table(&table)?); Ok(()) } diff --git a/installer/tests/behaviour_dependency_binaries.rs b/installer/tests/behaviour_dependency_binaries.rs index e0cc9219a..babdb2f26 100644 --- a/installer/tests/behaviour_dependency_binaries.rs +++ b/installer/tests/behaviour_dependency_binaries.rs @@ -57,7 +57,7 @@ impl DependencyBinaryInstaller for StubRepositoryInstaller { match &self.behaviour { RepositoryInstallerBehaviour::Success | RepositoryInstallerBehaviour::SuccessWithFailedVerification => { - dirs.bin_dir().map_or_else( + dirs.executables().map_or_else( || Err(DependencyBinaryInstallError::MissingBinDir), |bin_dir| { // Stage a runnable fake at the returned path: dylint-link @@ -67,7 +67,7 @@ impl DependencyBinaryInstaller for StubRepositoryInstaller { &bin_dir, &format!("{}-{}", dependency.package(), target), ); - write_fake_binary(&installed_path, true); + write_fake_binary(&installed_path, true)?; Ok(installed_path) }, ) @@ -212,8 +212,8 @@ fn stage_dylint_link_on_path(bin_dir: &Path) -> Result<(), String> { let dylint_link_path = bin_dir.join("dylint-link.cmd"); #[cfg(not(windows))] let dylint_link_path = bin_dir.join("dylint-link"); - write_fake_binary(&dylint_link_path, true); - Ok(()) + write_fake_binary(&dylint_link_path, true) + .map_err(|error| format!("write fake dylint-link: {error}")) } fn run_install_with_dylint_link_on_path( @@ -264,7 +264,7 @@ fn when_dependency_installation_runs(world: &mut DependencyBinaryWorld) -> Resul &executor, &status, &mut world.stderr, - DependencyInstallOptions { + &DependencyInstallOptions { dirs: &dirs, repository_installer: &repository_installer, target, diff --git a/installer/tests/behaviour_install_metrics.rs b/installer/tests/behaviour_install_metrics.rs index 3749f88db..26357d725 100644 --- a/installer/tests/behaviour_install_metrics.rs +++ b/installer/tests/behaviour_install_metrics.rs @@ -9,9 +9,6 @@ use whitaker_installer::install_metrics::{ InstallMetrics, InstallMode, RecordOutcome, record_install_at_path, }; -/// Tolerance for comparing floating-point install rates. -const FLOAT_RATE_TOLERANCE: f64 = 1e-6; - #[derive(Default)] struct InstallMetricsWorld { /// Owns the scenario's temporary metrics directory so it outlives the run. @@ -165,23 +162,22 @@ fn then_build_installs(world: &mut InstallMetricsWorld, expected: u64) -> Result ) } -/// Compares a floating-point rate against its expected value with tolerance. -fn ensure_rate(actual: f64, expected: f64, context: &str) -> Result<(), String> { - if (actual - expected).abs() < FLOAT_RATE_TOLERANCE { - Ok(()) - } else { - Err(format!("{context}: expected {expected}, got {actual}")) - } -} - -#[then("download rate is {expected:f64}")] -fn then_download_rate(world: &mut InstallMetricsWorld, expected: f64) -> Result<(), String> { - ensure_rate(metrics(world)?.download_rate(), expected, "download rate") +#[then("download rate is {expected:u64} permille")] +fn then_download_rate(world: &mut InstallMetricsWorld, expected: u64) -> Result<(), String> { + ensure_eq( + &metrics(world)?.download_rate_permille(), + &expected, + "download rate permille", + ) } -#[then("build rate is {expected:f64}")] -fn then_build_rate(world: &mut InstallMetricsWorld, expected: f64) -> Result<(), String> { - ensure_rate(metrics(world)?.build_rate(), expected, "build rate") +#[then("build rate is {expected:u64} permille")] +fn then_build_rate(world: &mut InstallMetricsWorld, expected: u64) -> Result<(), String> { + ensure_eq( + &metrics(world)?.build_rate_permille(), + &expected, + "build rate permille", + ) } #[then("total installation time is {expected:u64} milliseconds")] diff --git a/installer/tests/behaviour_installer_release.rs b/installer/tests/behaviour_installer_release.rs index 4b6b36c6b..1ec28e587 100644 --- a/installer/tests/behaviour_installer_release.rs +++ b/installer/tests/behaviour_installer_release.rs @@ -100,7 +100,7 @@ fn attempt_packaging(world: &mut InstallerReleaseWorld) -> Result<(), String> { output_dir: temp_dir.path().to_path_buf(), }; - match installer_packaging::package_installer(params) { + match installer_packaging::package_installer(¶ms) { Ok(output) => world.package_output = Some(output), Err(e) => world.packaging_error = Some(e), } diff --git a/installer/tests/behaviour_prebuilt.rs b/installer/tests/behaviour_prebuilt.rs index f02181c69..5ca3a6466 100644 --- a/installer/tests/behaviour_prebuilt.rs +++ b/installer/tests/behaviour_prebuilt.rs @@ -274,8 +274,8 @@ fn when_prebuilt_attempted(world: &mut PrebuiltWorld) -> Result<(), String> { fn when_install_config_checked(world: &mut PrebuiltWorld) { let install_args = world.install_args.clone().unwrap_or_default(); let options = CrateResolutionOptions { - individual_lints: install_args.individual_lints, - experimental: install_args.experimental, + individual_lints: install_args.lint_selection.individual_lints, + experimental: install_args.lint_selection.experimental, }; let requested_crates = resolve_crates(&[], &options); world.should_attempt_prebuilt = Some(install_args.should_attempt_prebuilt(&requested_crates)); diff --git a/installer/tests/behaviour_staging.rs b/installer/tests/behaviour_staging.rs index 2f27601e6..8009d2e04 100644 --- a/installer/tests/behaviour_staging.rs +++ b/installer/tests/behaviour_staging.rs @@ -191,9 +191,7 @@ mod staging_failure { // Skip this assertion when running as root (uid 0) since root can bypass // filesystem permissions. This is similar to how CI containers often run. - // SAFETY: `libc::geteuid()` is a simple FFI call with no preconditions; - // it returns the effective user ID without modifying any state. - if unsafe { libc::geteuid() } == 0 { + if rustix::process::geteuid().is_root() { return Ok(()); } diff --git a/installer/tests/behaviour_toolchain.rs b/installer/tests/behaviour_toolchain.rs index a4a67caed..ff344cec2 100644 --- a/installer/tests/behaviour_toolchain.rs +++ b/installer/tests/behaviour_toolchain.rs @@ -5,8 +5,8 @@ //! //! The tests include: //! - Dry-run scenarios that test toolchain detection (skipped if toolchain missing) -//! - Install scenarios that exercise auto-install using an isolated rustup environment -//! (`RUSTUP_HOME/CARGO_HOME` set to temp directories) +//! - Install scenarios that exercise auto-install using an isolated rustup +//! environment (`RUSTUP_HOME/CARGO_HOME` set to temp directories) //! - Failure scenarios that test error handling with a non-existent toolchain mod prebuilt_markers; diff --git a/installer/tests/features/install_metrics.feature b/installer/tests/features/install_metrics.feature index 695191378..693724d11 100644 --- a/installer/tests/features/install_metrics.feature +++ b/installer/tests/features/install_metrics.feature @@ -9,8 +9,8 @@ Feature: Installer metrics recording Then total installs is 1 And download installs is 1 And build installs is 0 - And download rate is 1.0 - And build rate is 0.0 + And download rate is 1000 permille + And build rate is 0 permille And total installation time is 1200 milliseconds And summary line contains "download 1/1 (100.0%)" And summary line contains "build 0/1 (0.0%)" @@ -21,8 +21,8 @@ Feature: Installer metrics recording Then total installs is 1 And download installs is 0 And build installs is 1 - And download rate is 0.0 - And build rate is 1.0 + And download rate is 0 permille + And build rate is 1000 permille And total installation time is 900 milliseconds And summary line contains "download 0/1 (0.0%)" And summary line contains "build 1/1 (100.0%)" @@ -34,8 +34,8 @@ Feature: Installer metrics recording Then total installs is 2 And download installs is 1 And build installs is 1 - And download rate is 0.5 - And build rate is 0.5 + And download rate is 500 permille + And build rate is 500 permille And total installation time is 3000 milliseconds And summary line contains "download 1/2 (50.0%)" And summary line contains "build 1/2 (50.0%)" @@ -57,5 +57,5 @@ Feature: Installer metrics recording Scenario: Zero-state rates are zero Given an in-memory zero metrics aggregate When download and build rates are calculated - Then download rate is 0.0 - And build rate is 0.0 + Then download rate is 0 permille + And build rate is 0 permille diff --git a/typos.local.toml b/typos.local.toml index 25588330c..876a0bfb3 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -58,7 +58,7 @@ ignore = [ "(?m)^bumpy_road_function = Tha iomadh cruinneachadh de loidsig chumhachail neadaichte ann an `\\{ \\$name \\}`\\.$", "(?m)^careful setup before a test can execute and, sometimes, teardown afterward\\.$", "(?m)^cargo dylint list \\-\\-color never \\| Where\\-Object \\{\\{$", - "(?m)^cargo dylint list \\-\\-color never \\| awk \\-v suite=\"\\{suite_crate\\}\" '\\$0 ~ \"\\^\" suite \"\\(\\[\\[:space:\\]\\]\\|\\$\\)\" \\{\\{ print \\}\\}'$", + "(?m)^cargo dylint list \\-\\-color never \\| awk \\-v suite=\"\\{SUITE_CRATE\\}\" '\\$0 ~ \"\\^\" suite \"\\(\\[\\[:space:\\]\\]\\|\\$\\)\" \\{\\{ print \\}\\}'$", "(?m)^error: Tha gnìomh std::fs `std::fs::read` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", "(?m)^module_max_lines = Tha mòideal \\{ \\$module \\} a’ leudachadh gu \\{ \\$lines \\} loidhnichean agus a’ briseadh an crìoch \\{ \\$limit \\}\\.$", "(?m)^no_std_fs_operations = Tha gnìomh std::fs `\\{ \\$operation \\}` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", diff --git a/typos.toml b/typos.toml index 0f9af87af..5a0c01a2d 100644 --- a/typos.toml +++ b/typos.toml @@ -76,7 +76,7 @@ extend-ignore-re = [ "(?m)^bumpy_road_function = Tha iomadh cruinneachadh de loidsig chumhachail neadaichte ann an `\\{ \\$name \\}`\\.$", "(?m)^careful setup before a test can execute and, sometimes, teardown afterward\\.$", "(?m)^cargo dylint list \\-\\-color never \\| Where\\-Object \\{\\{$", - "(?m)^cargo dylint list \\-\\-color never \\| awk \\-v suite=\"\\{suite_crate\\}\" '\\$0 ~ \"\\^\" suite \"\\(\\[\\[:space:\\]\\]\\|\\$\\)\" \\{\\{ print \\}\\}'$", + "(?m)^cargo dylint list \\-\\-color never \\| awk \\-v suite=\"\\{SUITE_CRATE\\}\" '\\$0 ~ \"\\^\" suite \"\\(\\[\\[:space:\\]\\]\\|\\$\\)\" \\{\\{ print \\}\\}'$", "(?m)^error: Tha gnìomh std::fs `std::fs::read` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", "(?m)^module_max_lines = Tha mòideal \\{ \\$module \\} a’ leudachadh gu \\{ \\$lines \\} loidhnichean agus a’ briseadh an crìoch \\{ \\$limit \\}\\.$", "(?m)^no_std_fs_operations = Tha gnìomh std::fs `\\{ \\$operation \\}` a’ seachnadh a’ phoileasaidh comasan airson an t\\-siostaim fhaidhlichean\\.$", From d6af16316b88db83b508c015a32fa90b351a811c Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 12 Oct 2026 21:32:19 +0200 Subject: [PATCH 8/9] Fix developer guide markdown spacing Reformat the affected guidance and remove duplicate blank lines so the repository Markdown checks pass. --- docs/developers-guide.md | 97 ++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 53d792a50..03fb01395 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -294,26 +294,27 @@ make fmt # Apply formatting ## Mutation-testing workflow contract tests Whitaker runs scheduled, informational mutation testing through a thin caller -workflow, [`.github/workflows/mutation-testing.yml`](../.github/workflows/mutation-testing.yml), +workflow, +[`.github/workflows/mutation-testing.yml`](../.github/workflows/mutation-testing.yml), which delegates to the shared reusable workflow -`leynos/shared-actions/.github/workflows/mutation-cargo.yml`. The heavy -lifting — running `cargo-mutants`, sharding, and summarizing survivors — lives -in `shared-actions`; this repository carries only declarative configuration. -The run is **informational only**: it never gates a pull request. Survivors -are reported through the job summary and downloadable artefacts so they can be +`leynos/shared-actions/.github/workflows/mutation-cargo.yml`. The heavy lifting +— running `cargo-mutants`, sharding, and summarizing survivors — lives in +`shared-actions`; this repository carries only declarative configuration. The +run is **informational only**: it never gates a pull request. Survivors are +reported through the job summary and downloadable artefacts so they can be triaged into tests, not enforced as a blocking check. The workflow runs in two modes. A **daily schedule** (04:50 UTC) fires a change-scoped run that mutates only the source files touched within the detection window, so quiet days are cheap no-ops. A **manual dispatch** (the -Actions "Run workflow" control) mutates the whole workspace; select a branch -in that control to exercise a feature branch. +Actions "Run workflow" control) mutates the whole workspace; select a branch in +that control to exercise a feature branch. The caller passes a small set of configuration inputs, each carrying intent: - `paths` — `src/,common/,crates/,installer/,suite/`, the workspace member - prefixes rooted at the repository root; there are no top-level `examples/` - or `benches/` directories to add. + prefixes rooted at the repository root; there are no top-level `examples/` or + `benches/` directories to add. - `exclude-globs` — scaffolding whose surviving mutants would be noise rather than genuine test gaps: the `rustc_*` proxy crates and the `clippy_utils` stub (both re-export compiler internals), each lint's `ui/` and `examples/` @@ -332,16 +333,16 @@ excluded). Those crates enable `feature(rustc_private)` under the (`-C prefer-dynamic -Z force-unstable-if-unmarked`) that the `test` and `typecheck` Makefile targets inject per invocation — see the note in [`.cargo/config.toml`](../.cargo/config.toml), which deliberately keeps that -flag out of workspace-wide configuration because it would break `cargo -install` for `whitaker-installer`. The shared mutation workflow has no -equivalent per-crate RUSTFLAGS step, so it cannot reproduce -`TEST_CARGO_FLAGS` faithfully across the whole workspace. Consequently this -adoption is **pin-only**: the caller declares the best approximation of the CI -scope it safely can, rather than a `--test-workspace` run that mirrors -`make test` crate-for-crate, and the contract test asserts only that this -declared configuration holds — not that a full workspace mutation baseline -passes. [ADR 004](adr-004-pin-only-mutation-testing-contract.md) records this -decision, its alternatives, and the accepted limitations. +flag out of workspace-wide configuration because it would break `cargo install` +for `whitaker-installer`. The shared mutation workflow has no equivalent +per-crate RUSTFLAGS step, so it cannot reproduce `TEST_CARGO_FLAGS` faithfully +across the whole workspace. Consequently this adoption is **pin-only**: the +caller declares the best approximation of the CI scope it safely can, rather +than a `--test-workspace` run that mirrors `make test` crate-for-crate, and the +contract test asserts only that this declared configuration holds — not that a +full workspace mutation baseline passes. +[ADR 004](adr-004-pin-only-mutation-testing-contract.md) records this decision, +its alternatives, and the accepted limitations. The `uses:` reference pins the shared workflow to a full 40-character commit SHA rather than a branch or tag, so a force-push upstream cannot silently @@ -353,17 +354,19 @@ without any accompanying test edit. Because the caller is configuration rather than code, a contract test suite, [`tests/workflow_contracts/mutation_testing_test.py`](../tests/workflow_contracts/mutation_testing_test.py), -pins the shape it must uphold, failing the pull request when the caller -drifts — repointing the pin at a branch, widening the token scope, or -dropping a configuration input — rather than letting the breakage surface -only in a scheduled run. Run it locally with: +pins the shape it must uphold, failing the pull request when the caller drifts +— repointing the pin at a branch, widening the token scope, or dropping a +configuration input — rather than letting the breakage surface only in a +scheduled run. Run it locally with: ```sh make test-workflow-contracts ``` -which wraps `uv run --with 'pytest>=8' --with 'pyyaml>=6' pytest -tests/workflow_contracts -q`. The suite validates: +which wraps +`uv run --with 'pytest>=8' --with 'pyyaml>=6' pytest +tests/workflow_contracts -q`. +The suite validates: - the `uses:` reference targets `mutation-cargo.yml` pinned to a full commit SHA; @@ -1747,24 +1750,23 @@ the regression suite: `temp-env` provides scoped environment overrides, `tempfile` provides isolated target directories, and `rstest` powers the fixture-based test setup used by the staged-suite coverage. - #### Shared UI harness environment overrides -Workspace-level UI harness tests that mutate process-wide environment -variables must use the scoped helpers in `whitaker_common::test_support`: -`with_env_var` installs a temporary value, `with_env_var_removed` makes a -variable absent, and `with_locale` overrides (or clears) `DYLINT_LOCALE` for -the duration of a callback. The helpers delegate to `temp_env`, whose -re-entrant global lock serializes scoped mutations while permitting nesting -from the same thread, and every prior value is restored when the callback -returns or panics. The workspace forbids `unsafe` code, so tests must never -call `std::env::set_var`/`remove_var` directly. - -`whitaker::testing::ui::run_with_runner` wraps the Dylint UI runner in the -same scoped helpers. On every platform it clears `RUSTC_WRAPPER` while the -runner needs bare `rustc` invocations for `dylint_testing::Test::example`. On -Windows it also sets `VCPKG_ROOT` to `C:\vcpkg` when that directory exists -and the variable is otherwise absent. +Workspace-level UI harness tests that mutate process-wide environment variables +must use the scoped helpers in `whitaker_common::test_support`: `with_env_var` +installs a temporary value, `with_env_var_removed` makes a variable absent, and +`with_locale` overrides (or clears) `DYLINT_LOCALE` for the duration of a +callback. The helpers delegate to `temp_env`, whose re-entrant global lock +serializes scoped mutations while permitting nesting from the same thread, and +every prior value is restored when the callback returns or panics. The +workspace forbids `unsafe` code, so tests must never call +`std::env::set_var`/`remove_var` directly. + +`whitaker::testing::ui::run_with_runner` wraps the Dylint UI runner in the same +scoped helpers. On every platform it clears `RUSTC_WRAPPER` while the runner +needs bare `rustc` invocations for `dylint_testing::Test::example`. On Windows +it also sets `VCPKG_ROOT` to `C:\vcpkg` when that directory exists and the +variable is otherwise absent. Example-based UI tests in `rstest_helper_should_be_fixture` also use a cross-process directory lock under the system temporary directory. `nextest` @@ -2089,17 +2091,16 @@ It can be promoted to standard by: `suite/Cargo.toml` 3. Updating documentation to reflect the change - ### Suite entry point -Dylint resolves a library's lints through an exported `register_lints` -symbol, which requires `#[unsafe(no_mangle)]`. The workspace forbids in-crate -`unsafe` code, so the suite declares its entry point through +Dylint resolves a library's lints through an exported `register_lints` symbol, +which requires `#[unsafe(no_mangle)]`. The workspace forbids in-crate `unsafe` +code, so the suite declares its entry point through `whitaker_common::declare_dylint_register_entry!`, which expands the unsafe attribute from an external macro — the same escape hatch `dylint_linting::impl_late_lint!` relies on for single-lint crates. The macro -is reserved for Dylint driver crates; it must not be used for any other -symbol export. +is reserved for Dylint driver crates; it must not be used for any other symbol +export. ## Creating a New Lint From f3392ba8e05b04a106cd4b2dd15d19b777d95118 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 30 Aug 2026 11:11:29 +0200 Subject: [PATCH 9/9] Standardize Rust formatting Adopt the estate rustfmt profile and reformat the workspace under it. The profile requires a nightly rustfmt: it enables crate-granular, std-external-crate-grouped imports, doc-comment code formatting, comment and string wrapping, single-line functions, and try shorthand. The Dylint UI fixtures under `examples/` are excluded explicitly. Their `.stderr` expectations match compiler output byte for byte, including line and column numbers, so reformatting them silently shifts those spans and breaks the suite. Fixtures under `ui/` are already immune because they are not Cargo targets. `fn_single_line` collapses short rstest fixtures onto one line, and the `#[fixture]` expansion then trips `unused_braces` on the collapsed body. The three affected fixtures carry the existing `allow_fixture_expansion_lints` attribute, which scopes the allowance to the generated code rather than to the surrounding module. This commit is mechanical: no behaviour changes accompany it, which is why it is isolated from the fix PRs below it in the stack. --- .rustfmt.toml | 26 ++++ common/src/attributes/attribute.rs | 31 ++--- common/src/attributes/helpers.rs | 23 +++- common/src/attributes/kind.rs | 8 +- common/src/attributes/mod.rs | 5 +- common/src/attributes/path.rs | 3 +- common/src/attributes/tests.rs | 3 +- common/src/brain_trait_metrics/diagnostic.rs | 85 +++++-------- common/src/brain_trait_metrics/evaluation.rs | 32 ++--- common/src/brain_trait_metrics/item.rs | 24 +--- common/src/brain_trait_metrics/metrics.rs | 32 ++--- common/src/brain_trait_metrics/mod.rs | 18 ++- common/src/brain_trait_metrics/tests.rs | 3 +- .../cognitive_complexity.rs | 31 ++--- common/src/brain_type_metrics/diagnostic.rs | 73 +++++------ common/src/brain_type_metrics/evaluation.rs | 36 ++---- .../src/brain_type_metrics/foreign_reach.rs | 22 ++-- common/src/brain_type_metrics/mod.rs | 48 ++----- common/src/brain_type_metrics/tests.rs | 3 +- common/src/complexity_signal.rs | 29 ++--- common/src/context.rs | 104 ++++++++------- common/src/decomposition_advice/community.rs | 22 ++-- .../community_kani/propagate_labels.rs | 4 +- common/src/decomposition_advice/note.rs | 5 +- common/src/decomposition_advice/note_tests.rs | 5 +- common/src/decomposition_advice/profile.rs | 33 ++--- common/src/decomposition_advice/suggestion.rs | 39 +++--- .../tests/cosine_threshold.rs | 6 +- common/src/decomposition_advice/tests/mod.rs | 8 +- .../decomposition_advice/tests/propagation.rs | 16 +-- .../tests/test_fixtures.rs | 7 +- common/src/decomposition_advice/vector.rs | 30 ++--- common/src/diagnostics.rs | 58 ++++----- common/src/expr.rs | 27 ++-- common/src/i18n/diagnostics.rs | 51 ++------ common/src/i18n/helpers.rs | 29 +++-- common/src/i18n/loader.rs | 31 ++--- common/src/i18n/locales.rs | 4 +- common/src/i18n/mod.rs | 15 ++- common/src/i18n/selection.rs | 34 ++--- common/src/i18n/testing.rs | 11 +- common/src/i18n/tests.rs | 10 +- common/src/lcom4/extract.rs | 3 +- common/src/lcom4/mod.rs | 25 ++-- common/src/lcom4/tests.rs | 6 +- common/src/lib.rs | 119 ++++++++++++++---- common/src/path.rs | 14 +-- common/src/rstest/argument_fingerprint.rs | 35 +++--- common/src/rstest/detection.rs | 67 ++++++---- common/src/rstest/mod.rs | 20 ++- common/src/rstest/paragraph_fingerprint.rs | 53 +++----- common/src/rstest/parameter.rs | 47 ++++--- common/src/rstest/span.rs | 35 +++--- common/src/rstest/tests/fingerprint.rs | 10 +- common/src/rstest/tests/mod.rs | 16 ++- common/src/span.rs | 36 +++--- common/src/test_support/decomposition.rs | 8 +- .../test_support/decomposition_adjacency.rs | 4 +- .../decomposition_label_propagation.rs | 19 +-- .../decomposition_vector_algebra.rs | 16 +-- common/src/test_support/fixtures.rs | 3 +- common/src/test_support/mod.rs | 10 +- common/src/test_support/ui.rs | 11 +- .../tests/brain_trait_evaluation_behaviour.rs | 47 +++---- common/tests/brain_trait_metrics_behaviour.rs | 28 ++--- .../tests/brain_type_evaluation_behaviour.rs | 56 +++------ common/tests/brain_type_metrics_behaviour.rs | 66 +++------- .../tests/cognitive_complexity_behaviour.rs | 36 ++---- common/tests/complexity_signal_behaviour.rs | 54 +++----- common/tests/context_behaviour.rs | 24 +--- common/tests/cosine_threshold_behaviour.rs | 23 ++-- .../decomposition_adjacency_behaviour.rs | 25 ++-- .../tests/decomposition_advice_behaviour.rs | 40 +++--- ...ecomposition_diagnostic_notes_behaviour.rs | 45 +++---- ...composition_label_propagation_behaviour.rs | 29 ++--- .../decomposition_vector_algebra_behaviour.rs | 16 +-- common/tests/i18n_behaviour.rs | 68 +++------- common/tests/i18n_packaging.rs | 3 +- common/tests/lcom4_behaviour.rs | 48 ++----- common/tests/localizer_helpers.rs | 76 ++++------- common/tests/method_extraction_behaviour.rs | 44 ++----- common/tests/rstest_detection_behaviour.rs | 44 +++---- common/tests/rstest_fingerprint_behaviour.rs | 58 +++------ .../tests/rstest_span_recovery_behaviour.rs | 36 ++---- crates/bumpy_road_function/src/analysis.rs | 30 ++--- .../bumpy_road_function/src/driver/config.rs | 2 +- .../src/driver/diagnostic.rs | 13 +- crates/bumpy_road_function/src/driver/mod.rs | 24 ++-- .../src/driver/segment_builder.rs | 18 ++- .../tests/analysis_behaviour.rs | 31 ++--- .../conditional_max_n_branches/src/driver.rs | 19 +-- .../src/tests/behaviour.rs | 40 ++---- .../function_attrs_follow_docs/src/driver.rs | 34 ++--- .../src/tests/localization.rs | 46 +++---- .../src/tests/order_detection.rs | 34 ++--- .../src/tests/ui.rs | 8 +- crates/module_max_lines/src/driver.rs | 74 +++++------ .../src/driver/inner_attr.rs | 3 +- .../src/driver/mod.rs | 19 +-- .../src/driver/parser.rs | 23 ++-- .../src/tests/behaviour.rs | 56 +++------ .../src/tests/span_to_snippet.rs | 7 +- .../no_expect_outside_tests/src/behaviour.rs | 50 +++----- .../src/context/mod.rs | 31 +++-- .../src/context/tests.rs | 21 ++-- .../src/dependency_rlib_tests.rs | 8 +- .../src/diagnostics.rs | 46 +++---- .../no_expect_outside_tests/src/driver/mod.rs | 13 +- .../src/driver/tests.rs | 44 ++++--- crates/no_expect_outside_tests/src/tests.rs | 10 +- .../src/tests/localization.rs | 59 ++++----- .../src/tests/receiver_type_edge_cases.rs | 7 +- crates/no_std_fs_operations/src/behaviour.rs | 45 +++---- crates/no_std_fs_operations/src/config.rs | 6 +- .../no_std_fs_operations/src/diagnostics.rs | 23 ++-- crates/no_std_fs_operations/src/driver.rs | 31 +++-- crates/no_std_fs_operations/src/exclusion.rs | 20 +-- .../src/exclusion_behaviour.rs | 19 ++- crates/no_std_fs_operations/src/tests/ui.rs | 16 +-- crates/no_std_fs_operations/src/usage/mod.rs | 15 +-- .../no_std_fs_operations/src/usage/tests.rs | 3 +- .../tests/test_support/mod.rs | 4 +- .../src/context/tests.rs | 5 +- .../src/diagnostics.rs | 16 ++- crates/no_unwrap_or_else_panic/src/driver.rs | 20 +-- .../src/panic_detector.rs | 3 +- crates/no_unwrap_or_else_panic/src/policy.rs | 10 +- .../src/tests/behaviour.rs | 48 ++----- .../src/collector.rs | 18 ++- .../src/collector_tests.rs | 17 ++- .../src/driver.rs | 30 +++-- .../src/visitor.rs | 35 ++++-- .../tests/ui/harness_lock.rs | 4 +- .../src/behaviour.rs | 46 +++---- .../test_must_not_have_example/src/driver.rs | 26 ++-- .../src/heuristics.rs | 3 +- crates/whitaker_clones_core/build.rs | 5 +- .../whitaker_clones_core/src/ast/features.rs | 26 ++-- crates/whitaker_clones_core/src/ast/hash.rs | 21 ++-- crates/whitaker_clones_core/src/ast/kani.rs | 12 +- .../whitaker_clones_core/src/ast/lowering.rs | 25 ++-- .../src/ast/lowering_tests.rs | 16 ++- crates/whitaker_clones_core/src/ast/mod.rs | 11 +- crates/whitaker_clones_core/src/ast/tests.rs | 23 +++- crates/whitaker_clones_core/src/ast/tree.rs | 52 ++------ .../src/index/fragment_id.rs | 24 +--- crates/whitaker_clones_core/src/index/kani.rs | 57 ++++----- crates/whitaker_clones_core/src/index/lsh.rs | 11 +- .../whitaker_clones_core/src/index/minhash.rs | 21 ++-- .../whitaker_clones_core/src/index/tests.rs | 26 ++-- .../whitaker_clones_core/src/index/types.rs | 38 ++---- crates/whitaker_clones_core/src/lib.rs | 57 +++++++-- crates/whitaker_clones_core/src/run0/emit.rs | 17 ++- crates/whitaker_clones_core/src/run0/error.rs | 6 +- crates/whitaker_clones_core/src/run0/score.rs | 22 +--- .../src/run0/test_helpers.rs | 3 +- crates/whitaker_clones_core/src/run0/tests.rs | 9 +- .../src/run0/tests_emit.rs | 7 +- crates/whitaker_clones_core/src/run0/types.rs | 67 +++------- .../src/token/fingerprint.rs | 14 ++- crates/whitaker_clones_core/src/token/mod.rs | 10 +- .../src/token/normalize.rs | 8 +- .../whitaker_clones_core/src/token/tests.rs | 16 ++- .../whitaker_clones_core/src/token/types.rs | 12 +- .../tests/ast_boundary.rs | 4 +- .../tests/ast_feature_extraction_behaviour.rs | 11 +- .../tests/build_script_parsing.rs | 5 +- .../tests/candidate_pair_behaviour.rs | 20 +-- .../tests/min_hash_lsh_behaviour.rs | 44 +++---- .../tests/run0_sarif_behaviour.rs | 35 ++---- .../tests/token_pass_behaviour.rs | 36 +++--- .../src/builders/location_builder.rs | 16 ++- .../src/builders/log_builder.rs | 12 +- .../src/builders/result_builder.rs | 15 ++- .../src/builders/run_builder.rs | 27 ++-- crates/whitaker_sarif/src/lib.rs | 61 +++++---- crates/whitaker_sarif/src/merge.rs | 39 +++--- crates/whitaker_sarif/src/model/descriptor.rs | 4 +- crates/whitaker_sarif/src/model/location.rs | 2 +- crates/whitaker_sarif/src/model/log.rs | 4 +- crates/whitaker_sarif/src/model/mod.rs | 10 +- crates/whitaker_sarif/src/model/result.rs | 10 +- crates/whitaker_sarif/src/model/run.rs | 9 +- crates/whitaker_sarif/src/rules.rs | 4 +- crates/whitaker_sarif/src/test_support.rs | 11 +- .../whitaker_sarif/src/whitaker_properties.rs | 2 +- .../whitaker_sarif/tests/sarif_behaviour.rs | 54 ++++---- installer/src/artefact/download.rs | 7 +- installer/src/artefact/extraction.rs | 6 +- installer/src/artefact/git_sha.rs | 25 ++-- installer/src/artefact/manifest.rs | 90 ++++++------- installer/src/artefact/manifest_parser.rs | 3 +- installer/src/artefact/naming.rs | 34 +++-- installer/src/artefact/packaging.rs | 37 +++--- installer/src/artefact/packaging_tests.rs | 8 +- installer/src/artefact/schema_version.rs | 22 ++-- installer/src/artefact/sha256_digest.rs | 29 ++--- installer/src/artefact/target.rs | 51 +++----- installer/src/artefact/toolchain_channel.rs | 29 ++--- installer/src/artefact/verification.rs | 4 +- .../src/bin/package_dependency_binary.rs | 28 +++-- installer/src/bin/package_installer_bin.rs | 15 +-- installer/src/bin/package_lints.rs | 37 +++--- installer/src/builder.rs | 39 +++--- installer/src/cli.rs | 12 +- installer/src/crate_name.rs | 31 ++--- .../dependency_binaries/install/checksum.rs | 20 +-- .../dependency_binaries/install/downloader.rs | 32 +++-- .../downloader_boundary_non_utf8_tests.rs | 6 +- .../install/downloader_boundary_tests.rs | 12 +- .../install/downloader_tests.rs | 8 +- .../dependency_binaries/install/extractor.rs | 12 +- .../install/http_test_server.rs | 26 ++-- .../dependency_binaries/install/installer.rs | 27 ++-- .../dependency_binaries/install/metadata.rs | 7 +- .../src/dependency_binaries/install/mod.rs | 4 +- .../src/dependency_binaries/install/tests.rs | 24 ++-- installer/src/dependency_binaries/manifest.rs | 59 ++++----- installer/src/dependency_binaries/mod.rs | 18 ++- installer/src/dependency_packaging.rs | 17 +-- installer/src/dependency_packaging_tests.rs | 13 +- installer/src/deps/install.rs | 25 ++-- installer/src/deps/install_repository.rs | 12 +- installer/src/deps/install_tests.rs | 3 +- installer/src/deps/mod.rs | 43 ++++--- installer/src/deps/path_tests.rs | 14 ++- installer/src/deps/tests.rs | 29 +++-- installer/src/deps/tests_repository.rs | 31 +++-- installer/src/dirs.rs | 11 +- installer/src/error.rs | 3 +- installer/src/git.rs | 18 ++- installer/src/install_flow/mod.rs | 36 +++--- installer/src/install_flow/tests.rs | 22 ++-- installer/src/install_metrics.rs | 48 ++++--- installer/src/install_metrics_tests.rs | 5 +- installer/src/installer_packaging.rs | 27 ++-- installer/src/lib.rs | 10 +- installer/src/list.rs | 19 +-- installer/src/list_output.rs | 15 ++- installer/src/list_tests.rs | 6 +- installer/src/main.rs | 52 ++++---- installer/src/output.rs | 20 ++- installer/src/pipeline.rs | 29 +++-- installer/src/pipeline_staging_tests.rs | 13 +- installer/src/pipeline_tests.rs | 4 +- installer/src/prebuilt.rs | 30 +++-- installer/src/prebuilt_path.rs | 15 ++- installer/src/resolution.rs | 23 ++-- installer/src/scanner.rs | 24 ++-- installer/src/staged_suite.rs | 24 ++-- installer/src/stager.rs | 16 +-- .../test_utils/dependency_binary_helpers.rs | 17 ++- .../dependency_binary_helpers_fakes.rs | 20 +-- .../dependency_binary_helpers_tests.rs | 5 +- installer/src/test_utils/mod.rs | 12 +- installer/src/tests/mod.rs | 16 +-- installer/src/toolchain/mod.rs | 18 ++- .../src/toolchain/tests/failure_mocks/mod.rs | 12 +- installer/src/toolchain/tests/mod.rs | 17 ++- installer/src/version.rs | 28 ++--- installer/src/workspace.rs | 7 +- installer/src/workspace_tests.rs | 9 +- installer/src/wrapper.rs | 21 ++-- installer/tests/behaviour_artefact.rs | 36 ++---- .../tests/behaviour_artefact_packaging.rs | 46 +++---- installer/tests/behaviour_binstall.rs | 41 +++--- installer/tests/behaviour_cli.rs | 50 ++++---- installer/tests/behaviour_cli/scenarios.rs | 12 +- installer/tests/behaviour_cli/support.rs | 18 +-- installer/tests/behaviour_core.rs | 54 +++----- .../tests/behaviour_dependency_binaries.rs | 44 +++---- installer/tests/behaviour_docs.rs | 28 ++--- installer/tests/behaviour_install_metrics.rs | 37 ++---- .../tests/behaviour_installer_release.rs | 37 +++--- installer/tests/behaviour_prebuilt.rs | 28 ++--- installer/tests/behaviour_staging.rs | 12 +- installer/tests/behaviour_toolchain.rs | 59 +++++---- installer/tests/behaviour_workflows.rs | 20 +-- installer/tests/toolchain_steps/mod.rs | 13 +- .../tests/toolchain_steps/scenario_setup.rs | 4 +- src/config.rs | 14 +-- src/hir/mod.rs | 24 ++-- src/hir/tests.rs | 33 +++-- src/lib.rs | 9 +- src/lints/template/mod.rs | 35 ++---- src/lints/template/validation.rs | 4 +- src/testing/ui/mod.rs | 11 +- src/testing/ui/tests.rs | 10 +- src/testing/ui/toolchain.rs | 24 ++-- suite/src/driver.rs | 14 +-- suite/tests/registration.rs | 16 +-- tests/build_config.rs | 3 +- tests/config_loading.rs | 34 +++-- tests/lint_template.rs | 96 ++++---------- tests/locale_resolution.rs | 43 ++----- tests/nextest_ui_filter.rs | 9 +- tests/support/locale.rs | 7 +- tests/ui_harness.rs | 48 ++----- 298 files changed, 3311 insertions(+), 4054 deletions(-) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..ddaf6641b --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,26 @@ +unstable_features = true +comment_width = 100 +format_code_in_doc_comments = true +imports_granularity = "Crate" +imports_layout = "HorizontalVertical" +wrap_comments = true +group_imports = "StdExternalCrate" +use_try_shorthand = true +hex_literal_case = "Lower" +format_strings = true +format_macro_matchers = true +fn_single_line = true +condense_wildcard_suffixes = true +use_field_init_shorthand = true + +# Dylint UI fixtures are test data, not source: their `.stderr` expectations +# match compiler output byte for byte, including line and column numbers. +# Reformatting them (for example `format_strings` splitting a long `reason` +# across lines) silently shifts those spans and breaks the UI tests. Fixtures +# under `ui/` are already immune because they are not Cargo targets; these are +# targets, so exclude them explicitly. +ignore = [ + "crates/no_expect_outside_tests/examples", + "crates/no_unwrap_or_else_panic/examples", + "crates/rstest_helper_should_be_fixture/examples", +] diff --git a/common/src/attributes/attribute.rs b/common/src/attributes/attribute.rs index b1d25e4c7..2bed4d69f 100644 --- a/common/src/attributes/attribute.rs +++ b/common/src/attributes/attribute.rs @@ -87,9 +87,7 @@ impl Attribute { /// assert!(attribute.path().is_doc()); /// ``` #[must_use] - pub const fn path(&self) -> &AttributePath { - &self.path - } + pub const fn path(&self) -> &AttributePath { &self.path } /// Returns the attachment kind (inner or outer). /// @@ -102,9 +100,7 @@ impl Attribute { /// assert!(attribute.kind().is_inner()); /// ``` #[must_use] - pub const fn kind(&self) -> AttributeKind { - self.kind - } + pub const fn kind(&self) -> AttributeKind { self.kind } /// Returns the attribute arguments. /// @@ -121,9 +117,7 @@ impl Attribute { /// assert_eq!(attribute.arguments(), &["dead_code"]); /// ``` #[must_use] - pub fn arguments(&self) -> &[String] { - &self.arguments - } + pub fn arguments(&self) -> &[String] { &self.arguments } /// Indicates whether the attribute is a doc comment (`#[doc = ...]`). /// @@ -136,9 +130,7 @@ impl Attribute { /// assert!(attribute.is_doc()); /// ``` #[must_use] - pub fn is_doc(&self) -> bool { - self.path.is_doc() - } + pub fn is_doc(&self) -> bool { self.path.is_doc() } /// Indicates whether the attribute marks a test-like context. /// @@ -157,9 +149,7 @@ impl Attribute { /// assert!(rstest.is_test_like()); /// ``` #[must_use] - pub fn is_test_like(&self) -> bool { - self.is_test_like_with(&[]) - } + pub fn is_test_like(&self) -> bool { self.is_test_like_with(&[]) } /// Indicates whether the attribute marks a test-like context when supplied /// with additional recognized paths. @@ -205,9 +195,7 @@ impl Attribute { /// assert!(attribute.is_inner()); /// ``` #[must_use] - pub const fn is_inner(&self) -> bool { - self.kind.is_inner() - } + pub const fn is_inner(&self) -> bool { self.kind.is_inner() } /// Returns `true` when the attribute is an outer attribute. /// @@ -220,9 +208,7 @@ impl Attribute { /// assert!(attribute.is_outer()); /// ``` #[must_use] - pub const fn is_outer(&self) -> bool { - self.kind.is_outer() - } + pub const fn is_outer(&self) -> bool { self.kind.is_outer() } } fn matches_builtin_test_like_path(path: &AttributePath) -> bool { @@ -246,9 +232,10 @@ fn is_prelude_test_attribute(path: &AttributePath) -> bool { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::core_v1("core::prelude::v1::test", true)] #[case::absolute_core_v1("::core::prelude::v1::test", true)] diff --git a/common/src/attributes/helpers.rs b/common/src/attributes/helpers.rs index ecce64f2f..340c4f8a3 100644 --- a/common/src/attributes/helpers.rs +++ b/common/src/attributes/helpers.rs @@ -7,7 +7,12 @@ use super::{Attribute, AttributePath}; /// # Examples /// /// ``` -/// use whitaker_common::attributes::{split_doc_attributes, Attribute, AttributeKind, AttributePath}; +/// use whitaker_common::attributes::{ +/// Attribute, +/// AttributeKind, +/// AttributePath, +/// split_doc_attributes, +/// }; /// /// let doc = Attribute::new(AttributePath::from("doc"), AttributeKind::Outer); /// let allow = Attribute::new(AttributePath::from("allow"), AttributeKind::Outer); @@ -26,7 +31,7 @@ pub fn split_doc_attributes(attrs: &[Attribute]) -> (Vec<&Attribute>, Vec<&Attri /// # Examples /// /// ``` -/// use whitaker_common::attributes::{outer_attributes, Attribute, AttributeKind, AttributePath}; +/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath, outer_attributes}; /// /// let inner = Attribute::new(AttributePath::from("doc"), AttributeKind::Inner); /// let outer = Attribute::new(AttributePath::from("test"), AttributeKind::Outer); @@ -44,7 +49,12 @@ pub fn outer_attributes(attrs: &[Attribute]) -> Vec<&Attribute> { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{has_test_like_attribute, Attribute, AttributeKind, AttributePath}; +/// use whitaker_common::attributes::{ +/// Attribute, +/// AttributeKind, +/// AttributePath, +/// has_test_like_attribute, +/// }; /// /// let attr = Attribute::new(AttributePath::from("tokio::test"), AttributeKind::Outer); /// assert!(has_test_like_attribute(&[attr])); @@ -60,7 +70,12 @@ pub fn has_test_like_attribute(attrs: &[Attribute]) -> bool { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{has_test_like_attribute_with, Attribute, AttributeKind, AttributePath}; +/// use whitaker_common::attributes::{ +/// Attribute, +/// AttributeKind, +/// AttributePath, +/// has_test_like_attribute_with, +/// }; /// /// let attr = Attribute::new(AttributePath::from("custom::test"), AttributeKind::Outer); /// let additional = vec![AttributePath::from("custom::test")]; diff --git a/common/src/attributes/kind.rs b/common/src/attributes/kind.rs index 9fac08ae5..4f7be7445 100644 --- a/common/src/attributes/kind.rs +++ b/common/src/attributes/kind.rs @@ -21,9 +21,7 @@ impl AttributeKind { /// assert!(!AttributeKind::Outer.is_inner()); /// ``` #[must_use] - pub const fn is_inner(self) -> bool { - matches!(self, Self::Inner) - } + pub const fn is_inner(self) -> bool { matches!(self, Self::Inner) } /// Returns `true` when the attribute is an outer attribute. /// @@ -36,7 +34,5 @@ impl AttributeKind { /// assert!(!AttributeKind::Inner.is_outer()); /// ``` #[must_use] - pub const fn is_outer(self) -> bool { - matches!(self, Self::Outer) - } + pub const fn is_outer(self) -> bool { matches!(self, Self::Outer) } } diff --git a/common/src/attributes/mod.rs b/common/src/attributes/mod.rs index e6169e6cd..e62c5d1aa 100644 --- a/common/src/attributes/mod.rs +++ b/common/src/attributes/mod.rs @@ -28,7 +28,10 @@ mod path; pub use attribute::Attribute; pub use helpers::{ - has_test_like_attribute, has_test_like_attribute_with, outer_attributes, split_doc_attributes, + has_test_like_attribute, + has_test_like_attribute_with, + outer_attributes, + split_doc_attributes, }; pub use kind::AttributeKind; pub use path::AttributePath; diff --git a/common/src/attributes/path.rs b/common/src/attributes/path.rs index 6bb85459a..36bfb306c 100644 --- a/common/src/attributes/path.rs +++ b/common/src/attributes/path.rs @@ -7,9 +7,10 @@ pub type AttributePath = SimplePath; #[cfg(test)] mod tests { - use super::AttributePath; use rstest::rstest; + use super::AttributePath; + #[rstest] fn parses_paths() { let path = AttributePath::from("tokio::test"); diff --git a/common/src/attributes/tests.rs b/common/src/attributes/tests.rs index bdf17cd8b..d7aede023 100644 --- a/common/src/attributes/tests.rs +++ b/common/src/attributes/tests.rs @@ -1,8 +1,9 @@ //! Tests for attribute helpers. -use super::*; use rstest::rstest; +use super::*; + #[rstest] #[case::empty(Vec::::new())] #[case::single(vec!["dead_code".to_owned()])] diff --git a/common/src/brain_trait_metrics/diagnostic.rs b/common/src/brain_trait_metrics/diagnostic.rs index 5bb9a0407..4cb3f9d42 100644 --- a/common/src/brain_trait_metrics/diagnostic.rs +++ b/common/src/brain_trait_metrics/diagnostic.rs @@ -7,10 +7,12 @@ //! See `docs/brain-trust-lints-design.md` §Diagnostic output for the //! full format specification. -use super::TraitMetrics; -use super::evaluation::BrainTraitDisposition; +use super::{TraitMetrics, evaluation::BrainTraitDisposition}; use crate::decomposition_advice::{ - DecompositionContext, DecompositionSuggestion, SubjectKind, format_diagnostic_note, + DecompositionContext, + DecompositionSuggestion, + SubjectKind, + format_diagnostic_note, }; #[cfg(test)] @@ -29,10 +31,10 @@ mod tests; /// # Examples /// /// ``` -/// use whitaker_common::brain_trait_metrics::evaluation::{ -/// BrainTraitDiagnostic, BrainTraitDisposition, +/// use whitaker_common::brain_trait_metrics::{ +/// TraitMetricsBuilder, +/// evaluation::{BrainTraitDiagnostic, BrainTraitDisposition}, /// }; -/// use whitaker_common::brain_trait_metrics::TraitMetricsBuilder; /// /// let metrics = TraitMetricsBuilder::new("Foo").build(); /// let diag = BrainTraitDiagnostic::new(&metrics, BrainTraitDisposition::Pass); @@ -66,27 +68,19 @@ impl BrainTraitDiagnostic { /// Returns the trait name. #[must_use] - pub fn trait_name(&self) -> &str { - &self.trait_name - } + pub fn trait_name(&self) -> &str { &self.trait_name } /// Returns the evaluation disposition. #[must_use] - pub const fn disposition(&self) -> BrainTraitDisposition { - self.disposition - } + pub const fn disposition(&self) -> BrainTraitDisposition { self.disposition } /// Returns the number of required methods. #[must_use] - pub const fn required_method_count(&self) -> usize { - self.required_method_count - } + pub const fn required_method_count(&self) -> usize { self.required_method_count } /// Returns the number of default methods. #[must_use] - pub const fn default_method_count(&self) -> usize { - self.default_method_count - } + pub const fn default_method_count(&self) -> usize { self.default_method_count } /// Returns the total method count (required + default). #[must_use] @@ -96,22 +90,16 @@ impl BrainTraitDiagnostic { /// Returns the sum of default method cognitive complexity values. #[must_use] - pub const fn default_method_cc_sum(&self) -> usize { - self.default_method_cc_sum - } + pub const fn default_method_cc_sum(&self) -> usize { self.default_method_cc_sum } /// Returns the total number of trait items (methods + associated /// types + associated consts). #[must_use] - pub const fn total_item_count(&self) -> usize { - self.total_item_count - } + pub const fn total_item_count(&self) -> usize { self.total_item_count } /// Returns implementor burden (required method count). #[must_use] - pub const fn implementor_burden(&self) -> usize { - self.implementor_burden - } + pub const fn implementor_burden(&self) -> usize { self.implementor_burden } } // --------------------------------------------------------------------------- @@ -126,10 +114,10 @@ impl BrainTraitDiagnostic { /// # Examples /// /// ``` -/// use whitaker_common::brain_trait_metrics::evaluation::{ -/// BrainTraitDiagnostic, BrainTraitDisposition, format_primary_message, +/// use whitaker_common::brain_trait_metrics::{ +/// TraitMetricsBuilder, +/// evaluation::{BrainTraitDiagnostic, BrainTraitDisposition, format_primary_message}, /// }; -/// use whitaker_common::brain_trait_metrics::TraitMetricsBuilder; /// /// let mut builder = TraitMetricsBuilder::new("Parser"); /// builder.add_required_method("parse"); @@ -149,8 +137,8 @@ pub fn format_primary_message(diagnostic: &BrainTraitDiagnostic) -> String { if cc > 0 { format!( - "`{name}` has {total} methods ({req} required, \ - {def} default) with default method complexity CC={cc}." + "`{name}` has {total} methods ({req} required, {def} default) with default method \ + complexity CC={cc}." ) } else { format!("`{name}` has {total} methods ({req} required, {def} default).") @@ -165,10 +153,10 @@ pub fn format_primary_message(diagnostic: &BrainTraitDiagnostic) -> String { /// # Examples /// /// ``` -/// use whitaker_common::brain_trait_metrics::evaluation::{ -/// BrainTraitDiagnostic, BrainTraitDisposition, format_note, +/// use whitaker_common::brain_trait_metrics::{ +/// TraitMetricsBuilder, +/// evaluation::{BrainTraitDiagnostic, BrainTraitDisposition, format_note}, /// }; -/// use whitaker_common::brain_trait_metrics::TraitMetricsBuilder; /// /// let metrics = TraitMetricsBuilder::new("Foo").build(); /// let diag = BrainTraitDiagnostic::new(&metrics, BrainTraitDisposition::Pass); @@ -181,14 +169,13 @@ pub fn format_note(diagnostic: &BrainTraitDiagnostic) -> String { String::from("Total method count measures interface size and implementation surface area."); if diagnostic.default_method_cc_sum() > 0 { note.push_str( - " Default method CC sum measures complexity hidden behind \ - the trait's default implementations.", + " Default method CC sum measures complexity hidden behind the trait's default \ + implementations.", ); } if diagnostic.required_method_count() > 0 { note.push_str( - " Implementor burden indicates how many methods each \ - implementor must provide.", + " Implementor burden indicates how many methods each implementor must provide.", ); } note @@ -201,10 +188,10 @@ pub fn format_note(diagnostic: &BrainTraitDiagnostic) -> String { /// # Examples /// /// ``` -/// use whitaker_common::brain_trait_metrics::evaluation::{ -/// BrainTraitDiagnostic, BrainTraitDisposition, format_decomposition_note, +/// use whitaker_common::brain_trait_metrics::{ +/// TraitMetricsBuilder, +/// evaluation::{BrainTraitDiagnostic, BrainTraitDisposition, format_decomposition_note}, /// }; -/// use whitaker_common::brain_trait_metrics::TraitMetricsBuilder; /// /// let metrics = TraitMetricsBuilder::new("Foo").build(); /// let diagnostic = BrainTraitDiagnostic::new(&metrics, BrainTraitDisposition::Pass); @@ -231,10 +218,10 @@ pub fn format_decomposition_note( /// # Examples /// /// ``` -/// use whitaker_common::brain_trait_metrics::evaluation::{ -/// BrainTraitDiagnostic, BrainTraitDisposition, format_help, +/// use whitaker_common::brain_trait_metrics::{ +/// TraitMetricsBuilder, +/// evaluation::{BrainTraitDiagnostic, BrainTraitDisposition, format_help}, /// }; -/// use whitaker_common::brain_trait_metrics::TraitMetricsBuilder; /// /// let metrics = TraitMetricsBuilder::new("Foo").build(); /// let diag = BrainTraitDiagnostic::new(&metrics, BrainTraitDisposition::Pass); @@ -249,10 +236,7 @@ pub fn format_help(diagnostic: &BrainTraitDiagnostic) -> String { parts.push("splitting the trait into focused sub-traits"); } if diagnostic.default_method_cc_sum() > 0 { - parts.push( - "extracting complex default method bodies into free \ - functions or helper traits", - ); + parts.push("extracting complex default method bodies into free functions or helper traits"); } if diagnostic.required_method_count() > 0 { parts.push("providing more default implementations to reduce implementor burden"); @@ -260,8 +244,7 @@ pub fn format_help(diagnostic: &BrainTraitDiagnostic) -> String { if parts.is_empty() { return String::from( - "Consider splitting the trait into smaller, focused \ - sub-traits to reduce complexity.", + "Consider splitting the trait into smaller, focused sub-traits to reduce complexity.", ); } diff --git a/common/src/brain_trait_metrics/evaluation.rs b/common/src/brain_trait_metrics/evaluation.rs index 992ac8490..555d7820a 100644 --- a/common/src/brain_trait_metrics/evaluation.rs +++ b/common/src/brain_trait_metrics/evaluation.rs @@ -15,9 +15,11 @@ //! the full design rationale. use super::TraitMetrics; - pub use super::diagnostic::{ - BrainTraitDiagnostic, format_decomposition_note, format_help, format_note, + BrainTraitDiagnostic, + format_decomposition_note, + format_help, + format_note, format_primary_message, }; @@ -68,9 +70,7 @@ pub enum BrainTraitDisposition { /// ``` /// use whitaker_common::brain_trait_metrics::evaluation::BrainTraitThresholdsBuilder; /// -/// let thresholds = BrainTraitThresholdsBuilder::new() -/// .methods_warn(25) -/// .build(); +/// let thresholds = BrainTraitThresholdsBuilder::new().methods_warn(25).build(); /// assert_eq!(thresholds.methods_warn(), 25); /// ``` #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -84,22 +84,16 @@ impl BrainTraitThresholds { /// Total method count at or above which the warn rule's method /// condition is met. #[must_use] - pub const fn methods_warn(&self) -> usize { - self.methods_warn - } + pub const fn methods_warn(&self) -> usize { self.methods_warn } /// Total method count at or above which the deny rule triggers. #[must_use] - pub const fn methods_deny(&self) -> usize { - self.methods_deny - } + pub const fn methods_deny(&self) -> usize { self.methods_deny } /// Default method CC sum at or above which the warn rule's /// complexity condition is met. #[must_use] - pub const fn default_cc_warn(&self) -> usize { - self.default_cc_warn - } + pub const fn default_cc_warn(&self) -> usize { self.default_cc_warn } } // --------------------------------------------------------------------------- @@ -179,9 +173,7 @@ impl BrainTraitThresholdsBuilder { } impl Default for BrainTraitThresholdsBuilder { - fn default() -> Self { - Self::new() - } + fn default() -> Self { Self::new() } } // --------------------------------------------------------------------------- @@ -219,10 +211,10 @@ const fn is_warn_triggered(metrics: &TraitMetrics, thresholds: &BrainTraitThresh /// # Examples /// /// ``` -/// use whitaker_common::brain_trait_metrics::evaluation::{ -/// BrainTraitThresholdsBuilder, evaluate_brain_trait, +/// use whitaker_common::brain_trait_metrics::{ +/// TraitMetricsBuilder, +/// evaluation::{BrainTraitThresholdsBuilder, evaluate_brain_trait}, /// }; -/// use whitaker_common::brain_trait_metrics::TraitMetricsBuilder; /// /// let thresholds = BrainTraitThresholdsBuilder::new().build(); /// let metrics = TraitMetricsBuilder::new("Safe").build(); diff --git a/common/src/brain_trait_metrics/item.rs b/common/src/brain_trait_metrics/item.rs index a592ed798..819821076 100644 --- a/common/src/brain_trait_metrics/item.rs +++ b/common/src/brain_trait_metrics/item.rs @@ -132,9 +132,7 @@ impl TraitItemMetrics { /// assert_eq!(item.name(), "parse"); /// ``` #[must_use] - pub fn name(&self) -> &str { - &self.name - } + pub fn name(&self) -> &str { &self.name } /// Returns the trait item kind. /// @@ -147,9 +145,7 @@ impl TraitItemMetrics { /// assert_eq!(item.kind(), TraitItemKind::RequiredMethod); /// ``` #[must_use] - pub const fn kind(&self) -> TraitItemKind { - self.kind - } + pub const fn kind(&self) -> TraitItemKind { self.kind } /// Returns default method cognitive complexity when present. /// @@ -162,9 +158,7 @@ impl TraitItemMetrics { /// assert_eq!(item.default_method_cc(), Some(9)); /// ``` #[must_use] - pub const fn default_method_cc(&self) -> Option { - self.default_method_cc - } + pub const fn default_method_cc(&self) -> Option { self.default_method_cc } /// Returns `true` when this item is a required method. /// @@ -177,9 +171,7 @@ impl TraitItemMetrics { /// assert!(item.is_required_method()); /// ``` #[must_use] - pub fn is_required_method(&self) -> bool { - self.kind == TraitItemKind::RequiredMethod - } + pub fn is_required_method(&self) -> bool { self.kind == TraitItemKind::RequiredMethod } /// Returns `true` when this item is a default method. /// @@ -192,9 +184,7 @@ impl TraitItemMetrics { /// assert!(item.is_default_method()); /// ``` #[must_use] - pub fn is_default_method(&self) -> bool { - self.kind == TraitItemKind::DefaultMethod - } + pub fn is_default_method(&self) -> bool { self.kind == TraitItemKind::DefaultMethod } } /// Returns the total number of trait items. @@ -211,9 +201,7 @@ impl TraitItemMetrics { /// assert_eq!(trait_item_count(&items), 2); /// ``` #[must_use] -pub const fn trait_item_count(items: &[TraitItemMetrics]) -> usize { - items.len() -} +pub const fn trait_item_count(items: &[TraitItemMetrics]) -> usize { items.len() } /// Returns the number of required methods. /// diff --git a/common/src/brain_trait_metrics/metrics.rs b/common/src/brain_trait_metrics/metrics.rs index b1fd8e780..91c98a42b 100644 --- a/common/src/brain_trait_metrics/metrics.rs +++ b/common/src/brain_trait_metrics/metrics.rs @@ -40,9 +40,7 @@ impl TraitMetrics { /// assert_eq!(metrics.trait_name(), "Parser"); /// ``` #[must_use] - pub fn trait_name(&self) -> &str { - &self.trait_name - } + pub fn trait_name(&self) -> &str { &self.trait_name } /// Returns the total number of trait items. /// @@ -56,9 +54,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().total_item_count(), 1); /// ``` #[must_use] - pub const fn total_item_count(&self) -> usize { - self.total_item_count - } + pub const fn total_item_count(&self) -> usize { self.total_item_count } /// Returns the number of required methods. /// @@ -72,9 +68,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().required_method_count(), 1); /// ``` #[must_use] - pub const fn required_method_count(&self) -> usize { - self.required_method_count - } + pub const fn required_method_count(&self) -> usize { self.required_method_count } /// Returns the number of default methods. /// @@ -88,9 +82,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().default_method_count(), 1); /// ``` #[must_use] - pub const fn default_method_count(&self) -> usize { - self.default_method_count - } + pub const fn default_method_count(&self) -> usize { self.default_method_count } /// Returns the sum of default method cognitive complexity values. /// @@ -105,9 +97,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().default_method_cc_sum(), 12); /// ``` #[must_use] - pub const fn default_method_cc_sum(&self) -> usize { - self.default_method_cc_sum - } + pub const fn default_method_cc_sum(&self) -> usize { self.default_method_cc_sum } /// Returns implementor burden as the required method count. /// @@ -122,9 +112,7 @@ impl TraitMetrics { /// assert_eq!(builder.build().implementor_burden(), 2); /// ``` #[must_use] - pub const fn implementor_burden(&self) -> usize { - self.required_method_count - } + pub const fn implementor_burden(&self) -> usize { self.required_method_count } } /// Incremental builder for [`TraitMetrics`]. @@ -164,9 +152,7 @@ impl TraitMetricsBuilder { /// builder.add_item(TraitItemMetrics::required_method("parse")); /// assert_eq!(builder.build().required_method_count(), 1); /// ``` - pub fn add_item(&mut self, item: TraitItemMetrics) { - self.items.push(item); - } + pub fn add_item(&mut self, item: TraitItemMetrics) { self.items.push(item); } /// Adds a required method. /// @@ -255,9 +241,7 @@ impl TraitMetricsBuilder { /// assert!(TraitMetricsBuilder::new("Parser").is_empty()); /// ``` #[must_use] - pub const fn is_empty(&self) -> bool { - self.items.is_empty() - } + pub const fn is_empty(&self) -> bool { self.items.is_empty() } /// Consumes the builder and returns aggregated trait metrics. /// diff --git a/common/src/brain_trait_metrics/mod.rs b/common/src/brain_trait_metrics/mod.rs index e0790c748..2ee48f832 100644 --- a/common/src/brain_trait_metrics/mod.rs +++ b/common/src/brain_trait_metrics/mod.rs @@ -24,11 +24,21 @@ mod metrics; mod tests; pub use evaluation::{ - BrainTraitDiagnostic, BrainTraitDisposition, BrainTraitThresholds, BrainTraitThresholdsBuilder, - evaluate_brain_trait, format_help, format_note, format_primary_message, + BrainTraitDiagnostic, + BrainTraitDisposition, + BrainTraitThresholds, + BrainTraitThresholdsBuilder, + evaluate_brain_trait, + format_help, + format_note, + format_primary_message, }; pub use item::{ - TraitItemKind, TraitItemMetrics, default_method_cc_sum, default_method_count, - required_method_count, trait_item_count, + TraitItemKind, + TraitItemMetrics, + default_method_cc_sum, + default_method_count, + required_method_count, + trait_item_count, }; pub use metrics::{TraitMetrics, TraitMetricsBuilder}; diff --git a/common/src/brain_trait_metrics/tests.rs b/common/src/brain_trait_metrics/tests.rs index f1890d7de..54b46ce8a 100644 --- a/common/src/brain_trait_metrics/tests.rs +++ b/common/src/brain_trait_metrics/tests.rs @@ -1,8 +1,9 @@ //! Unit tests for brain trait metric collection. -use super::*; use rstest::rstest; +use super::*; + fn mixed_items() -> Vec { vec![ TraitItemMetrics::required_method("parse"), diff --git a/common/src/brain_type_metrics/cognitive_complexity.rs b/common/src/brain_type_metrics/cognitive_complexity.rs index bb6c842c4..28a5f0d91 100644 --- a/common/src/brain_type_metrics/cognitive_complexity.rs +++ b/common/src/brain_type_metrics/cognitive_complexity.rs @@ -28,12 +28,11 @@ mod tests; /// /// # Three increment categories /// -/// - **Structural** (+1): `if`, `else if`, `else`, `match`, `for`, -/// `while`, `loop`, `?` operator, catch-equivalent constructs. -/// - **Nesting** (+`effective_depth)`: applied alongside structural for -/// constructs that also incur a nesting penalty. -/// - **Fundamental** (+1): boolean operator sequence breaks (`&&`, -/// `||`). +/// - **Structural** (+1): `if`, `else if`, `else`, `match`, `for`, `while`, `loop`, `?` operator, +/// catch-equivalent constructs. +/// - **Nesting** (+`effective_depth)`: applied alongside structural for constructs that also incur +/// a nesting penalty. +/// - **Fundamental** (+1): boolean operator sequence breaks (`&&`, `||`). /// /// # Examples /// @@ -42,12 +41,12 @@ mod tests; /// /// let mut cc = CognitiveComplexityBuilder::new(); /// // Simulate: if condition { ... } -/// cc.record_structural_increment(false); // +1 -/// cc.record_nesting_increment(false); // +0 (depth is 0) +/// cc.record_structural_increment(false); // +1 +/// cc.record_nesting_increment(false); // +0 (depth is 0) /// cc.push_nesting(false); /// // Simulate: nested if { ... } -/// cc.record_structural_increment(false); // +1 -/// cc.record_nesting_increment(false); // +1 (depth is 1) +/// cc.record_structural_increment(false); // +1 +/// cc.record_nesting_increment(false); // +1 (depth is 1) /// cc.push_nesting(false); /// cc.pop_nesting(); /// cc.pop_nesting(); @@ -224,9 +223,7 @@ impl CognitiveComplexityBuilder { /// cc.pop_nesting(); /// ``` #[must_use] - pub const fn effective_depth(&self) -> usize { - self.effective_depth - } + pub const fn effective_depth(&self) -> usize { self.effective_depth } /// Returns the accumulated cognitive complexity score so far. /// @@ -240,9 +237,7 @@ impl CognitiveComplexityBuilder { /// assert_eq!(cc.score(), 1); /// ``` #[must_use] - pub const fn score(&self) -> usize { - self.score - } + pub const fn score(&self) -> usize { self.score } /// Consumes the builder and returns the final complexity score. /// @@ -271,7 +266,5 @@ impl CognitiveComplexityBuilder { } impl Default for CognitiveComplexityBuilder { - fn default() -> Self { - Self::new() - } + fn default() -> Self { Self::new() } } diff --git a/common/src/brain_type_metrics/diagnostic.rs b/common/src/brain_type_metrics/diagnostic.rs index 0008fccf4..66fa07b65 100644 --- a/common/src/brain_type_metrics/diagnostic.rs +++ b/common/src/brain_type_metrics/diagnostic.rs @@ -7,10 +7,12 @@ //! See `docs/brain-trust-lints-design.md` §Diagnostic output for the //! full format specification. -use super::evaluation::BrainTypeDisposition; -use super::{MethodMetrics, TypeMetrics}; +use super::{MethodMetrics, TypeMetrics, evaluation::BrainTypeDisposition}; use crate::decomposition_advice::{ - DecompositionContext, DecompositionSuggestion, SubjectKind, format_diagnostic_note, + DecompositionContext, + DecompositionSuggestion, + SubjectKind, + format_diagnostic_note, }; #[cfg(test)] @@ -29,10 +31,10 @@ mod tests; /// # Examples /// /// ``` -/// use whitaker_common::brain_type_metrics::evaluation::{ -/// BrainTypeDiagnostic, BrainTypeDisposition, +/// use whitaker_common::brain_type_metrics::{ +/// TypeMetricsBuilder, +/// evaluation::{BrainTypeDiagnostic, BrainTypeDisposition}, /// }; -/// use whitaker_common::brain_type_metrics::TypeMetricsBuilder; /// /// let metrics = TypeMetricsBuilder::new("Foo", 25, 80).build(); /// let diag = BrainTypeDiagnostic::new(&metrics, BrainTypeDisposition::Pass); @@ -64,39 +66,27 @@ impl BrainTypeDiagnostic { /// Returns the type name. #[must_use] - pub fn type_name(&self) -> &str { - &self.type_name - } + pub fn type_name(&self) -> &str { &self.type_name } /// Returns the evaluation disposition. #[must_use] - pub const fn disposition(&self) -> BrainTypeDisposition { - self.disposition - } + pub const fn disposition(&self) -> BrainTypeDisposition { self.disposition } /// Returns the Weighted Methods Count. #[must_use] - pub const fn wmc(&self) -> usize { - self.wmc - } + pub const fn wmc(&self) -> usize { self.wmc } /// Returns the LCOM4 connected component count. #[must_use] - pub const fn lcom4(&self) -> usize { - self.lcom4 - } + pub const fn lcom4(&self) -> usize { self.lcom4 } /// Returns the foreign reach count. #[must_use] - pub const fn foreign_reach(&self) -> usize { - self.foreign_reach - } + pub const fn foreign_reach(&self) -> usize { self.foreign_reach } /// Returns brain methods with their full metric details. #[must_use] - pub fn brain_methods(&self) -> &[MethodMetrics] { - &self.brain_methods - } + pub fn brain_methods(&self) -> &[MethodMetrics] { &self.brain_methods } } // --------------------------------------------------------------------------- @@ -114,10 +104,10 @@ impl BrainTypeDiagnostic { /// # Examples /// /// ``` -/// use whitaker_common::brain_type_metrics::evaluation::{ -/// BrainTypeDiagnostic, BrainTypeDisposition, format_primary_message, +/// use whitaker_common::brain_type_metrics::{ +/// TypeMetricsBuilder, +/// evaluation::{BrainTypeDiagnostic, BrainTypeDisposition, format_primary_message}, /// }; -/// use whitaker_common::brain_type_metrics::TypeMetricsBuilder; /// /// let mut builder = TypeMetricsBuilder::new("Foo", 25, 80); /// builder.add_method("parse", 31, 140); @@ -157,8 +147,8 @@ fn format_primary_with_one_brain_method( let lcom4 = diagnostic.lcom4(); let fr_suffix = foreign_reach_suffix(diagnostic); format!( - "`{name}` has WMC={wmc}, LCOM4={lcom4}{fr_suffix}, \ - and a brain method `{}` (CC={}, LOC={}).", + "`{name}` has WMC={wmc}, LCOM4={lcom4}{fr_suffix}, and a brain method `{}` (CC={}, \ + LOC={}).", bm.name(), bm.cognitive_complexity(), bm.lines_of_code(), @@ -188,8 +178,7 @@ fn format_primary_with_many_brain_methods( .collect::>() .join(", "); format!( - "`{name}` has WMC={wmc}, LCOM4={lcom4}{fr_suffix}, \ - and {n} brain methods: {method_list}.", + "`{name}` has WMC={wmc}, LCOM4={lcom4}{fr_suffix}, and {n} brain methods: {method_list}.", ) } @@ -212,10 +201,10 @@ fn foreign_reach_suffix(diagnostic: &BrainTypeDiagnostic) -> String { /// # Examples /// /// ``` -/// use whitaker_common::brain_type_metrics::evaluation::{ -/// BrainTypeDiagnostic, BrainTypeDisposition, format_note, +/// use whitaker_common::brain_type_metrics::{ +/// TypeMetricsBuilder, +/// evaluation::{BrainTypeDiagnostic, BrainTypeDisposition, format_note}, /// }; -/// use whitaker_common::brain_type_metrics::TypeMetricsBuilder; /// /// let metrics = TypeMetricsBuilder::new("Foo", 25, 80).build(); /// let diag = BrainTypeDiagnostic::new(&metrics, BrainTypeDisposition::Pass); @@ -253,10 +242,10 @@ pub fn format_note(diagnostic: &BrainTypeDiagnostic) -> String { /// # Examples /// /// ``` -/// use whitaker_common::brain_type_metrics::evaluation::{ -/// BrainTypeDiagnostic, BrainTypeDisposition, format_decomposition_note, +/// use whitaker_common::brain_type_metrics::{ +/// TypeMetricsBuilder, +/// evaluation::{BrainTypeDiagnostic, BrainTypeDisposition, format_decomposition_note}, /// }; -/// use whitaker_common::brain_type_metrics::TypeMetricsBuilder; /// /// let metrics = TypeMetricsBuilder::new("Foo", 25, 80).build(); /// let diagnostic = BrainTypeDiagnostic::new(&metrics, BrainTypeDisposition::Pass); @@ -283,10 +272,10 @@ pub fn format_decomposition_note( /// # Examples /// /// ``` -/// use whitaker_common::brain_type_metrics::evaluation::{ -/// BrainTypeDiagnostic, BrainTypeDisposition, format_help, +/// use whitaker_common::brain_type_metrics::{ +/// TypeMetricsBuilder, +/// evaluation::{BrainTypeDiagnostic, BrainTypeDisposition, format_help}, /// }; -/// use whitaker_common::brain_type_metrics::TypeMetricsBuilder; /// /// let metrics = TypeMetricsBuilder::new("Foo", 25, 80).build(); /// let diag = BrainTypeDiagnostic::new(&metrics, BrainTypeDisposition::Pass); @@ -309,8 +298,8 @@ pub fn format_help(diagnostic: &BrainTypeDiagnostic) -> String { if parts.is_empty() { return String::from( - "Consider extracting related methods into separate types or \ - modules to reduce complexity and improve cohesion.", + "Consider extracting related methods into separate types or modules to reduce \ + complexity and improve cohesion.", ); } diff --git a/common/src/brain_type_metrics/evaluation.rs b/common/src/brain_type_metrics/evaluation.rs index bc708c00e..8a3cf981f 100644 --- a/common/src/brain_type_metrics/evaluation.rs +++ b/common/src/brain_type_metrics/evaluation.rs @@ -14,9 +14,11 @@ //! the full design rationale. use super::TypeMetrics; - pub use super::diagnostic::{ - BrainTypeDiagnostic, format_decomposition_note, format_help, format_note, + BrainTypeDiagnostic, + format_decomposition_note, + format_help, + format_note, format_primary_message, }; @@ -80,33 +82,23 @@ pub struct BrainTypeThresholds { impl BrainTypeThresholds { /// WMC at or above which the warn rule's WMC condition is met. #[must_use] - pub const fn wmc_warn(&self) -> usize { - self.wmc_warn - } + pub const fn wmc_warn(&self) -> usize { self.wmc_warn } /// WMC at or above which the deny rule triggers (OR-based). #[must_use] - pub const fn wmc_deny(&self) -> usize { - self.wmc_deny - } + pub const fn wmc_deny(&self) -> usize { self.wmc_deny } /// LCOM4 at or above which the warn rule's cohesion condition is met. #[must_use] - pub const fn lcom4_warn(&self) -> usize { - self.lcom4_warn - } + pub const fn lcom4_warn(&self) -> usize { self.lcom4_warn } /// LCOM4 at or above which the deny rule triggers (OR-based). #[must_use] - pub const fn lcom4_deny(&self) -> usize { - self.lcom4_deny - } + pub const fn lcom4_deny(&self) -> usize { self.lcom4_deny } /// Brain method count at or above which the deny rule triggers. #[must_use] - pub const fn brain_method_deny_count(&self) -> usize { - self.brain_method_deny_count - } + pub const fn brain_method_deny_count(&self) -> usize { self.brain_method_deny_count } } // --------------------------------------------------------------------------- @@ -208,9 +200,7 @@ impl BrainTypeThresholdsBuilder { } impl Default for BrainTypeThresholdsBuilder { - fn default() -> Self { - Self::new() - } + fn default() -> Self { Self::new() } } // --------------------------------------------------------------------------- @@ -243,10 +233,10 @@ const fn is_warn_triggered(metrics: &TypeMetrics, thresholds: &BrainTypeThreshol /// # Examples /// /// ``` -/// use whitaker_common::brain_type_metrics::evaluation::{ -/// BrainTypeThresholdsBuilder, evaluate_brain_type, +/// use whitaker_common::brain_type_metrics::{ +/// TypeMetricsBuilder, +/// evaluation::{BrainTypeThresholdsBuilder, evaluate_brain_type}, /// }; -/// use whitaker_common::brain_type_metrics::TypeMetricsBuilder; /// /// let thresholds = BrainTypeThresholdsBuilder::new().build(); /// let metrics = TypeMetricsBuilder::new("Safe", 25, 80).build(); diff --git a/common/src/brain_type_metrics/foreign_reach.rs b/common/src/brain_type_metrics/foreign_reach.rs index efff8dfb4..03d15b59d 100644 --- a/common/src/brain_type_metrics/foreign_reach.rs +++ b/common/src/brain_type_metrics/foreign_reach.rs @@ -50,9 +50,7 @@ impl ForeignReferenceSet { /// assert!(refs.is_empty()); /// ``` #[must_use] - pub fn new() -> Self { - Self::default() - } + pub fn new() -> Self { Self::default() } /// Records a reference to an external module or type path. /// @@ -67,7 +65,7 @@ impl ForeignReferenceSet { /// use whitaker_common::brain_type_metrics::ForeignReferenceSet; /// /// let mut refs = ForeignReferenceSet::new(); - /// refs.record_reference("std::fmt", true); // macro — filtered + /// refs.record_reference("std::fmt", true); // macro — filtered /// refs.record_reference("serde::Serialize", false); /// /// assert_eq!(refs.count(), 1); @@ -90,9 +88,7 @@ impl ForeignReferenceSet { /// assert_eq!(refs.count(), 1); /// ``` #[must_use] - pub fn count(&self) -> usize { - self.references.len() - } + pub fn count(&self) -> usize { self.references.len() } /// Returns `true` when no references have been recorded. /// @@ -105,9 +101,7 @@ impl ForeignReferenceSet { /// assert!(refs.is_empty()); /// ``` #[must_use] - pub fn is_empty(&self) -> bool { - self.references.is_empty() - } + pub fn is_empty(&self) -> bool { self.references.is_empty() } /// Returns the set of recorded references, for diagnostic display. /// @@ -121,9 +115,7 @@ impl ForeignReferenceSet { /// assert!(refs.references().contains("std::io")); /// ``` #[must_use] - pub const fn references(&self) -> &BTreeSet { - &self.references - } + pub const fn references(&self) -> &BTreeSet { &self.references } } /// Counts distinct foreign references from an iterator of @@ -140,9 +132,9 @@ impl ForeignReferenceSet { /// /// let refs = vec![ /// ("std::io".into(), false), -/// ("std::io".into(), false), // duplicate +/// ("std::io".into(), false), // duplicate /// ("serde::de".into(), false), -/// ("macro_gen".into(), true), // macro — filtered +/// ("macro_gen".into(), true), // macro — filtered /// ]; /// assert_eq!(foreign_reach_count(refs), 2); /// ``` diff --git a/common/src/brain_type_metrics/mod.rs b/common/src/brain_type_metrics/mod.rs index bb4fc6a2c..d87e8c530 100644 --- a/common/src/brain_type_metrics/mod.rs +++ b/common/src/brain_type_metrics/mod.rs @@ -75,21 +75,15 @@ impl MethodMetrics { /// Returns the method name. #[must_use] - pub fn name(&self) -> &str { - &self.name - } + pub fn name(&self) -> &str { &self.name } /// Returns the cognitive complexity (CC) value. #[must_use] - pub const fn cognitive_complexity(&self) -> usize { - self.cognitive_complexity - } + pub const fn cognitive_complexity(&self) -> usize { self.cognitive_complexity } /// Returns the lines of code (LOC) count. #[must_use] - pub const fn lines_of_code(&self) -> usize { - self.lines_of_code - } + pub const fn lines_of_code(&self) -> usize { self.lines_of_code } /// Returns `true` when this method qualifies as a "brain method". /// @@ -209,21 +203,15 @@ pub struct TypeMetrics { impl TypeMetrics { /// Returns the type name. #[must_use] - pub fn type_name(&self) -> &str { - &self.type_name - } + pub fn type_name(&self) -> &str { &self.type_name } /// Weighted Methods Count (sum of CC across all methods). #[must_use] - pub const fn wmc(&self) -> usize { - self.wmc - } + pub const fn wmc(&self) -> usize { self.wmc } /// Brain methods with their full metric details. #[must_use] - pub fn brain_methods(&self) -> &[MethodMetrics] { - &self.brain_methods - } + pub fn brain_methods(&self) -> &[MethodMetrics] { &self.brain_methods } /// Returns an iterator over the names of brain methods. /// @@ -234,27 +222,19 @@ impl TypeMetrics { /// Number of brain methods detected. #[must_use] - pub const fn brain_method_count(&self) -> usize { - self.brain_methods.len() - } + pub const fn brain_method_count(&self) -> usize { self.brain_methods.len() } /// LCOM4 connected component count (1 = cohesive, >= 2 = low cohesion). #[must_use] - pub const fn lcom4(&self) -> usize { - self.lcom4 - } + pub const fn lcom4(&self) -> usize { self.lcom4 } /// Count of distinct external modules or types referenced. #[must_use] - pub const fn foreign_reach(&self) -> usize { - self.foreign_reach - } + pub const fn foreign_reach(&self) -> usize { self.foreign_reach } /// Total number of methods in the type. #[must_use] - pub const fn method_count(&self) -> usize { - self.method_count - } + pub const fn method_count(&self) -> usize { self.method_count } } // --------------------------------------------------------------------------- @@ -326,14 +306,10 @@ impl TypeMetricsBuilder { } /// Records the LCOM4 value (connected component count). - pub const fn set_lcom4(&mut self, lcom4: usize) { - self.lcom4 = Some(lcom4); - } + pub const fn set_lcom4(&mut self, lcom4: usize) { self.lcom4 = Some(lcom4); } /// Records the foreign reach count. - pub const fn set_foreign_reach(&mut self, count: usize) { - self.foreign_reach = Some(count); - } + pub const fn set_foreign_reach(&mut self, count: usize) { self.foreign_reach = Some(count); } /// Consumes the builder and returns the completed [`TypeMetrics`]. /// diff --git a/common/src/brain_type_metrics/tests.rs b/common/src/brain_type_metrics/tests.rs index 75e3c066b..5d85e8af2 100644 --- a/common/src/brain_type_metrics/tests.rs +++ b/common/src/brain_type_metrics/tests.rs @@ -1,8 +1,9 @@ //! Unit tests for brain type metric collection. -use super::*; use rstest::rstest; +use super::*; + // --------------------------------------------------------------------------- // MethodMetrics // --------------------------------------------------------------------------- diff --git a/common/src/complexity_signal.rs b/common/src/complexity_signal.rs index 835d36798..eeae4df56 100644 --- a/common/src/complexity_signal.rs +++ b/common/src/complexity_signal.rs @@ -63,21 +63,15 @@ impl LineSegment { /// Returns the first line covered by the segment (inclusive). #[must_use] - pub const fn start_line(self) -> usize { - self.start_line - } + pub const fn start_line(self) -> usize { self.start_line } /// Returns the last line covered by the segment (inclusive). #[must_use] - pub const fn end_line(self) -> usize { - self.end_line - } + pub const fn end_line(self) -> usize { self.end_line } /// Returns the per-line contribution. #[must_use] - pub const fn value(self) -> f64 { - self.value - } + pub const fn value(self) -> f64 { self.value } } /// Errors emitted when constructing a [`LineSegment`]. @@ -131,7 +125,8 @@ pub enum SignalBuildError { /// A segment does not intersect the function's line range. #[error( - "segment lies outside function range (segment={segment_start}..={segment_end}, function={function_start}..={function_end})" + "segment lies outside function range (segment={segment_start}..={segment_end}, \ + function={function_start}..={function_end})" )] SegmentOutsideFunctionRange { /// One-based first line covered by the offending segment. @@ -221,12 +216,11 @@ fn accumulate_signal_from_diff(diff: &[f64], len: usize) -> Vec { /// /// # Errors /// -/// - Returns [`SignalBuildError::FunctionLineRangeMustBeOneBased`] when the -/// provided range includes line `0`. -/// - Returns [`SignalBuildError::FunctionStartAfterEnd`] when the range is -/// inverted. -/// - Returns [`SignalBuildError::SegmentOutsideFunctionRange`] when any segment -/// does not overlap the function range. +/// - Returns [`SignalBuildError::FunctionLineRangeMustBeOneBased`] when the provided range includes +/// line `0`. +/// - Returns [`SignalBuildError::FunctionStartAfterEnd`] when the range is inverted. +/// - Returns [`SignalBuildError::SegmentOutsideFunctionRange`] when any segment does not overlap +/// the function range. /// /// # Examples /// @@ -341,9 +335,10 @@ pub fn smooth_moving_average(signal: &[f64], window: usize) -> Result, #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] fn rasterize_signal_accumulates_overlapping_segments() { let segments = vec![ diff --git a/common/src/context.rs b/common/src/context.rs index 43b33db64..bb5c8e2c6 100644 --- a/common/src/context.rs +++ b/common/src/context.rs @@ -1,7 +1,10 @@ //! Context tracking utilities for analysing traversal stacks. use crate::attributes::{ - Attribute, AttributePath, has_test_like_attribute, has_test_like_attribute_with, + Attribute, + AttributePath, + has_test_like_attribute, + has_test_like_attribute_with, }; /// Categorizes a frame within the traversal stack. @@ -31,13 +34,18 @@ impl ContextEntry { /// # Examples /// /// ``` - /// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; - /// use whitaker_common::context::{ContextEntry, ContextKind}; + /// use whitaker_common::{ + /// attributes::{Attribute, AttributeKind, AttributePath}, + /// context::{ContextEntry, ContextKind}, + /// }; /// /// let entry = ContextEntry::new( /// "demo", /// ContextKind::Function, - /// vec![Attribute::new(AttributePath::from("test"), AttributeKind::Outer)], + /// vec![Attribute::new( + /// AttributePath::from("test"), + /// AttributeKind::Outer, + /// )], /// ); /// assert_eq!(entry.name(), "demo"); /// ``` @@ -55,12 +63,17 @@ impl ContextEntry { /// # Examples /// /// ``` - /// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; - /// use whitaker_common::context::ContextEntry; + /// use whitaker_common::{ + /// attributes::{Attribute, AttributeKind, AttributePath}, + /// context::ContextEntry, + /// }; /// /// let entry = ContextEntry::function( /// "demo", - /// vec![Attribute::new(AttributePath::from("test"), AttributeKind::Outer)], + /// vec![Attribute::new( + /// AttributePath::from("test"), + /// AttributeKind::Outer, + /// )], /// ); /// assert!(entry.kind().matches_function()); /// ``` @@ -71,40 +84,28 @@ impl ContextEntry { /// Returns the entry name. #[must_use] - pub fn name(&self) -> &str { - &self.name - } + pub fn name(&self) -> &str { &self.name } /// Returns the entry kind. #[must_use] - pub const fn kind(&self) -> &ContextKind { - &self.kind - } + pub const fn kind(&self) -> &ContextKind { &self.kind } /// Returns a snapshot of the entry attributes. #[must_use] - pub fn attributes(&self) -> &[Attribute] { - &self.attributes - } + pub fn attributes(&self) -> &[Attribute] { &self.attributes } /// Returns a mutable reference to the attributes for in-place updates. #[must_use] - pub const fn attributes_mut(&mut self) -> &mut Vec { - &mut self.attributes - } + pub const fn attributes_mut(&mut self) -> &mut Vec { &mut self.attributes } /// Adds an attribute to the entry. - pub fn push_attribute(&mut self, attribute: Attribute) { - self.attributes.push(attribute); - } + pub fn push_attribute(&mut self, attribute: Attribute) { self.attributes.push(attribute); } } impl ContextKind { /// Returns `true` when the kind is [`ContextKind::Function`]. #[must_use] - pub const fn matches_function(&self) -> bool { - matches!(self, Self::Function) - } + pub const fn matches_function(&self) -> bool { matches!(self, Self::Function) } } /// Tests whether a slice of attributes marks an item as a test function. @@ -112,16 +113,19 @@ impl ContextKind { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::context::is_test_fn; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// context::is_test_fn, +/// }; /// -/// let attrs = vec![Attribute::new(AttributePath::from("rstest"), AttributeKind::Outer)]; +/// let attrs = vec![Attribute::new( +/// AttributePath::from("rstest"), +/// AttributeKind::Outer, +/// )]; /// assert!(is_test_fn(&attrs)); /// ``` #[must_use] -pub fn is_test_fn(attrs: &[Attribute]) -> bool { - has_test_like_attribute(attrs) -} +pub fn is_test_fn(attrs: &[Attribute]) -> bool { has_test_like_attribute(attrs) } /// Tests whether a slice of attributes marks an item as a test function while /// honouring custom attribute paths. @@ -129,10 +133,15 @@ pub fn is_test_fn(attrs: &[Attribute]) -> bool { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::context::is_test_fn_with; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// context::is_test_fn_with, +/// }; /// -/// let attrs = vec![Attribute::new(AttributePath::from("custom::test"), AttributeKind::Outer)]; +/// let attrs = vec![Attribute::new( +/// AttributePath::from("custom::test"), +/// AttributeKind::Outer, +/// )]; /// let additional = vec![AttributePath::from("custom::test")]; /// assert!(is_test_fn_with(&attrs, &additional)); /// ``` @@ -146,11 +155,16 @@ pub fn is_test_fn_with(attrs: &[Attribute], additional: &[AttributePath]) -> boo /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::context::{in_test_like_context, ContextEntry}; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// context::{ContextEntry, in_test_like_context}, +/// }; /// /// let mut entry = ContextEntry::function("demo", Vec::new()); -/// entry.push_attribute(Attribute::new(AttributePath::from("test"), AttributeKind::Outer)); +/// entry.push_attribute(Attribute::new( +/// AttributePath::from("test"), +/// AttributeKind::Outer, +/// )); /// assert!(in_test_like_context(&[entry])); /// ``` #[must_use] @@ -164,11 +178,16 @@ pub fn in_test_like_context(stack: &[ContextEntry]) -> bool { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::context::{in_test_like_context_with, ContextEntry}; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// context::{ContextEntry, in_test_like_context_with}, +/// }; /// /// let mut entry = ContextEntry::function("demo", Vec::new()); -/// entry.push_attribute(Attribute::new(AttributePath::from("custom::test"), AttributeKind::Outer)); +/// entry.push_attribute(Attribute::new( +/// AttributePath::from("custom::test"), +/// AttributeKind::Outer, +/// )); /// let additional = vec![AttributePath::from("custom::test")]; /// assert!(in_test_like_context_with(&[entry], &additional)); /// ``` @@ -187,7 +206,7 @@ pub fn in_test_like_context_with(stack: &[ContextEntry], additional: &[Attribute /// # Examples /// /// ``` -/// use whitaker_common::context::{is_in_main_fn, ContextEntry}; +/// use whitaker_common::context::{ContextEntry, is_in_main_fn}; /// /// let stack = vec![ContextEntry::function("main", Vec::new())]; /// assert!(is_in_main_fn(&stack)); @@ -202,9 +221,10 @@ pub fn is_in_main_fn(stack: &[ContextEntry]) -> bool { #[cfg(test)] mod tests { + use rstest::rstest; + use super::*; use crate::attributes::{Attribute, AttributeKind, AttributePath}; - use rstest::rstest; fn test_attribute() -> Attribute { Attribute::new(AttributePath::from("test"), AttributeKind::Outer) diff --git a/common/src/decomposition_advice/community.rs b/common/src/decomposition_advice/community.rs index f2b15df9a..1ee1f61b5 100644 --- a/common/src/decomposition_advice/community.rs +++ b/common/src/decomposition_advice/community.rs @@ -3,8 +3,11 @@ use std::collections::BTreeMap; use super::vector::{ - MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED, MIN_COSINE_THRESHOLD_NUMERATOR_SQUARED, - MethodFeatureVector, cosine_threshold_met, dot_product, + MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED, + MIN_COSINE_THRESHOLD_NUMERATOR_SQUARED, + MethodFeatureVector, + cosine_threshold_met, + dot_product, }; #[derive(Clone, Debug, Eq, PartialEq)] @@ -25,19 +28,13 @@ impl SimilarityEdge { } #[cfg(test)] - pub(crate) const fn left(&self) -> usize { - self.left - } + pub(crate) const fn left(&self) -> usize { self.left } #[cfg(test)] - pub(crate) const fn right(&self) -> usize { - self.right - } + pub(crate) const fn right(&self) -> usize { self.right } #[cfg(test)] - pub(crate) const fn weight(&self) -> u64 { - self.weight - } + pub(crate) const fn weight(&self) -> u64 { self.weight } } pub(crate) fn build_similarity_edges(vectors: &[MethodFeatureVector]) -> Vec { @@ -216,7 +213,8 @@ pub(crate) fn propagate_labels_report( if iteration_count == max_iterations { log::debug!( - "label propagation reached iteration limit: nodes={}, active_nodes={}, max_iterations={}", + "label propagation reached iteration limit: nodes={}, active_nodes={}, \ + max_iterations={}", vectors.len(), active_nodes.len(), max_iterations, diff --git a/common/src/decomposition_advice/community_kani/propagate_labels.rs b/common/src/decomposition_advice/community_kani/propagate_labels.rs index 809dc4ffe..e69749ab3 100644 --- a/common/src/decomposition_advice/community_kani/propagate_labels.rs +++ b/common/src/decomposition_advice/community_kani/propagate_labels.rs @@ -1,10 +1,8 @@ //! Bounded model-checking harnesses for `propagate_labels`. +use super::{super::propagate_labels_report, shared::bounded_iteration_count}; use crate::decomposition_advice::minimal_feature_vector; -use super::super::propagate_labels_report; -use super::shared::bounded_iteration_count; - const NODE_COUNT: usize = 3; fn symbolic_vectors() -> [crate::decomposition_advice::vector::MethodFeatureVector; NODE_COUNT] { diff --git a/common/src/decomposition_advice/note.rs b/common/src/decomposition_advice/note.rs index ad2664696..a3d596035 100644 --- a/common/src/decomposition_advice/note.rs +++ b/common/src/decomposition_advice/note.rs @@ -25,7 +25,10 @@ const MAX_METHODS_PER_SUGGESTION: usize = 3; /// /// ``` /// use whitaker_common::decomposition_advice::{ -/// DecompositionContext, MethodProfileBuilder, SubjectKind, format_diagnostic_note, +/// DecompositionContext, +/// MethodProfileBuilder, +/// SubjectKind, +/// format_diagnostic_note, /// suggest_decomposition, /// }; /// diff --git a/common/src/decomposition_advice/note_tests.rs b/common/src/decomposition_advice/note_tests.rs index bb6acd638..3e7cdbe1d 100644 --- a/common/src/decomposition_advice/note_tests.rs +++ b/common/src/decomposition_advice/note_tests.rs @@ -4,7 +4,10 @@ use super::format_diagnostic_note; use crate::{ decomposition_advice::{DecompositionContext, MethodProfile, SubjectKind}, test_support::decomposition::{ - MethodInput, decomposition_suggestions, parser_serde_fs_fixture, profile, + MethodInput, + decomposition_suggestions, + parser_serde_fs_fixture, + profile, transport_trait_fixture, }, }; diff --git a/common/src/decomposition_advice/profile.rs b/common/src/decomposition_advice/profile.rs index 1a304c04a..5bf0192ff 100644 --- a/common/src/decomposition_advice/profile.rs +++ b/common/src/decomposition_advice/profile.rs @@ -61,15 +61,11 @@ impl DecompositionContext { /// Returns the analysed subject name. #[must_use] - pub fn subject_name(&self) -> &str { - &self.subject_name - } + pub fn subject_name(&self) -> &str { &self.subject_name } /// Returns the analysed subject kind. #[must_use] - pub const fn subject_kind(&self) -> SubjectKind { - self.subject_kind - } + pub const fn subject_kind(&self) -> SubjectKind { self.subject_kind } } /// Immutable per-method metadata used to build feature vectors. @@ -106,33 +102,23 @@ pub struct MethodProfile { impl MethodProfile { /// Returns the method name. #[must_use] - pub fn name(&self) -> &str { - &self.name - } + pub fn name(&self) -> &str { &self.name } /// Returns accessed fields. #[must_use] - pub const fn accessed_fields(&self) -> &BTreeSet { - &self.accessed_fields - } + pub const fn accessed_fields(&self) -> &BTreeSet { &self.accessed_fields } /// Returns types used in the method signature. #[must_use] - pub const fn signature_types(&self) -> &BTreeSet { - &self.signature_types - } + pub const fn signature_types(&self) -> &BTreeSet { &self.signature_types } /// Returns types used in local variables. #[must_use] - pub const fn local_types(&self) -> &BTreeSet { - &self.local_types - } + pub const fn local_types(&self) -> &BTreeSet { &self.local_types } /// Returns external domains used by the method. #[must_use] - pub const fn external_domains(&self) -> &BTreeSet { - &self.external_domains - } + pub const fn external_domains(&self) -> &BTreeSet { &self.external_domains } } /// Mutable builder for [`MethodProfile`]. @@ -151,7 +137,10 @@ impl MethodProfile { /// .record_local_type("PathBuf"); /// /// let profile = builder.build(); -/// assert_eq!(profile.external_domains().iter().next().map(String::as_str), Some("std::fs")); +/// assert_eq!( +/// profile.external_domains().iter().next().map(String::as_str), +/// Some("std::fs") +/// ); /// ``` #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct MethodProfileBuilder { diff --git a/common/src/decomposition_advice/suggestion.rs b/common/src/decomposition_advice/suggestion.rs index 255380979..4ab9e33b0 100644 --- a/common/src/decomposition_advice/suggestion.rs +++ b/common/src/decomposition_advice/suggestion.rs @@ -2,9 +2,11 @@ use std::collections::BTreeMap; -use super::community::detect_communities; -use super::profile::{DecompositionContext, MethodProfile, SubjectKind}; -use super::vector::{FeatureCategory, MethodFeatureVector, build_feature_vector}; +use super::{ + community::detect_communities, + profile::{DecompositionContext, MethodProfile, SubjectKind}, + vector::{FeatureCategory, MethodFeatureVector, build_feature_vector}, +}; /// The extraction shape suggested for a method community. /// @@ -57,7 +59,10 @@ impl std::fmt::Display for SuggestedExtractionKind { /// /// ``` /// use whitaker_common::decomposition_advice::{ -/// DecompositionContext, MethodProfileBuilder, SubjectKind, SuggestedExtractionKind, +/// DecompositionContext, +/// MethodProfileBuilder, +/// SubjectKind, +/// SuggestedExtractionKind, /// suggest_decomposition, /// }; /// @@ -81,7 +86,10 @@ impl std::fmt::Display for SuggestedExtractionKind { /// ); /// /// assert_eq!(suggestions.len(), 2); -/// assert_eq!(suggestions[0].extraction_kind(), SuggestedExtractionKind::Module); +/// assert_eq!( +/// suggestions[0].extraction_kind(), +/// SuggestedExtractionKind::Module +/// ); /// ``` #[derive(Clone, Debug, Eq, PartialEq)] pub struct DecompositionSuggestion { @@ -94,27 +102,19 @@ pub struct DecompositionSuggestion { impl DecompositionSuggestion { /// Returns the community label. #[must_use] - pub fn label(&self) -> &str { - &self.label - } + pub fn label(&self) -> &str { &self.label } /// Returns the suggested extraction kind. #[must_use] - pub const fn extraction_kind(&self) -> SuggestedExtractionKind { - self.extraction_kind - } + pub const fn extraction_kind(&self) -> SuggestedExtractionKind { self.extraction_kind } /// Returns method names in the community. #[must_use] - pub fn methods(&self) -> &[String] { - &self.methods - } + pub fn methods(&self) -> &[String] { &self.methods } /// Returns the dominant features that motivated the suggestion. #[must_use] - pub fn rationale(&self) -> &[String] { - &self.rationale - } + pub fn rationale(&self) -> &[String] { &self.rationale } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -133,7 +133,10 @@ struct AggregatedFeature { /// /// ``` /// use whitaker_common::decomposition_advice::{ -/// DecompositionContext, MethodProfileBuilder, SubjectKind, suggest_decomposition, +/// DecompositionContext, +/// MethodProfileBuilder, +/// SubjectKind, +/// suggest_decomposition, /// }; /// /// let context = DecompositionContext::new("Parser", SubjectKind::Type); diff --git a/common/src/decomposition_advice/tests/cosine_threshold.rs b/common/src/decomposition_advice/tests/cosine_threshold.rs index e5e2cb164..35abe26c4 100644 --- a/common/src/decomposition_advice/tests/cosine_threshold.rs +++ b/common/src/decomposition_advice/tests/cosine_threshold.rs @@ -7,8 +7,10 @@ //! and `MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED` constants. use crate::decomposition_advice::vector::{ - MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED, MIN_COSINE_THRESHOLD_NUMERATOR_SQUARED, - cosine_threshold_met, test_feature_vector, + MIN_COSINE_THRESHOLD_DENOMINATOR_SQUARED, + MIN_COSINE_THRESHOLD_NUMERATOR_SQUARED, + cosine_threshold_met, + test_feature_vector, }; fn check_cosine_threshold(left_weights: &[(&str, u64)], right_weights: &[(&str, u64)]) -> bool { diff --git a/common/src/decomposition_advice/tests/mod.rs b/common/src/decomposition_advice/tests/mod.rs index e7f1cd8cd..8c791f04e 100644 --- a/common/src/decomposition_advice/tests/mod.rs +++ b/common/src/decomposition_advice/tests/mod.rs @@ -9,8 +9,12 @@ mod vector_algebra; use std::str::FromStr; use self::test_fixtures::{ - ExpectedSuggestion, MethodInput, assert_suggestion, assert_type_decomposition_is_empty, - parser_serde_fs_fixture, profile, + ExpectedSuggestion, + MethodInput, + assert_suggestion, + assert_type_decomposition_is_empty, + parser_serde_fs_fixture, + profile, }; use super::{ community::{build_similarity_edges, detect_communities}, diff --git a/common/src/decomposition_advice/tests/propagation.rs b/common/src/decomposition_advice/tests/propagation.rs index 4240402c2..ca91e2536 100644 --- a/common/src/decomposition_advice/tests/propagation.rs +++ b/common/src/decomposition_advice/tests/propagation.rs @@ -22,9 +22,7 @@ fn edge(left: usize, right: usize, weight: u64) -> SimilarityEdge { #[allow_fixture_expansion_lints] #[fixture] -fn connected_triplet_vectors() -> Vec { - vectors(&["alpha", "beta", "gamma"]) -} +fn connected_triplet_vectors() -> Vec { vectors(&["alpha", "beta", "gamma"]) } #[fixture] fn connected_triplet_adjacency() -> Vec> { @@ -43,21 +41,15 @@ fn linear_quartet_adjacency() -> Vec> { #[allow_fixture_expansion_lints] #[fixture] -fn isolated_tail_vectors() -> Vec { - vectors(&["alpha", "beta", "gamma"]) -} +fn isolated_tail_vectors() -> Vec { vectors(&["alpha", "beta", "gamma"]) } #[allow_fixture_expansion_lints] #[fixture] -fn isolated_tail_adjacency() -> Vec> { - build_adjacency(3, &[edge(0, 1, 5)]) -} +fn isolated_tail_adjacency() -> Vec> { build_adjacency(3, &[edge(0, 1, 5)]) } #[allow_fixture_expansion_lints] #[fixture] -fn lexical_tie_vectors() -> Vec { - vectors(&["gamma", "alpha", "beta"]) -} +fn lexical_tie_vectors() -> Vec { vectors(&["gamma", "alpha", "beta"]) } #[fixture] fn lexical_tie_adjacency() -> Vec> { diff --git a/common/src/decomposition_advice/tests/test_fixtures.rs b/common/src/decomposition_advice/tests/test_fixtures.rs index b09fba956..d58767b20 100644 --- a/common/src/decomposition_advice/tests/test_fixtures.rs +++ b/common/src/decomposition_advice/tests/test_fixtures.rs @@ -1,12 +1,15 @@ //! Shared fixture builders for decomposition-advice unit tests. use super::super::{ - DecompositionSuggestion, SuggestedExtractionKind, + DecompositionSuggestion, + SuggestedExtractionKind, profile::{DecompositionContext, MethodProfile, SubjectKind}, suggest_decomposition, }; pub(super) use crate::test_support::decomposition::{ - MethodInput, parser_serde_fs_fixture, profile, + MethodInput, + parser_serde_fs_fixture, + profile, }; // `Copy` keeps the expectation cheap to pass by value; it only holds diff --git a/common/src/decomposition_advice/vector.rs b/common/src/decomposition_advice/vector.rs index 3122b326d..9f9967381 100644 --- a/common/src/decomposition_advice/vector.rs +++ b/common/src/decomposition_advice/vector.rs @@ -70,13 +70,9 @@ struct FeatureIdentity { } impl FeatureMetadata { - pub(crate) const fn category(&self) -> FeatureCategory { - self.category - } + pub(crate) const fn category(&self) -> FeatureCategory { self.category } - pub(crate) fn display(&self) -> &str { - &self.display - } + pub(crate) fn display(&self) -> &str { &self.display } } #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -87,17 +83,11 @@ pub(crate) struct MethodFeatureVector { } impl MethodFeatureVector { - pub(crate) fn method_name(&self) -> &str { - &self.method_name - } + pub(crate) fn method_name(&self) -> &str { &self.method_name } - pub(crate) const fn weights(&self) -> &BTreeMap { - &self.weights - } + pub(crate) const fn weights(&self) -> &BTreeMap { &self.weights } - pub(crate) const fn metadata(&self) -> &BTreeMap { - &self.metadata - } + pub(crate) const fn metadata(&self) -> &BTreeMap { &self.metadata } pub(crate) fn norm_squared(&self) -> u64 { self.weights.values().map(|weight| weight * weight).sum() @@ -199,8 +189,10 @@ pub(crate) fn build_feature_vector(profile: &MethodProfile) -> MethodFeatureVect /// For testing and integration purposes, use the public test support wrapper: /// /// ``` -/// use whitaker_common::MethodProfileBuilder; -/// use whitaker_common::test_support::decomposition::methods_meet_cosine_threshold; +/// use whitaker_common::{ +/// MethodProfileBuilder, +/// test_support::decomposition::methods_meet_cosine_threshold, +/// }; /// /// let mut left_builder = MethodProfileBuilder::new("parse_tokens"); /// left_builder.record_accessed_field("grammar"); @@ -322,9 +314,7 @@ fn add_feature( }); } -fn canonical_feature_value(value: &str) -> String { - value.trim().to_lowercase() -} +fn canonical_feature_value(value: &str) -> String { value.trim().to_lowercase() } fn feature_identity(value: &str) -> FeatureIdentity { let canonical = canonical_feature_value(value); diff --git a/common/src/diagnostics.rs b/common/src/diagnostics.rs index aeadc3aa7..25a7b10ac 100644 --- a/common/src/diagnostics.rs +++ b/common/src/diagnostics.rs @@ -49,21 +49,15 @@ impl Suggestion { /// Returns the human-readable message. #[must_use] - pub fn message(&self) -> &str { - &self.message - } + pub fn message(&self) -> &str { &self.message } /// Returns the replacement snippet. #[must_use] - pub fn replacement(&self) -> &str { - &self.replacement - } + pub fn replacement(&self) -> &str { &self.replacement } /// Returns the applicability classification. #[must_use] - pub const fn applicability(&self) -> Applicability { - self.applicability - } + pub const fn applicability(&self) -> Applicability { self.applicability } } /// Represents a lint diagnostic with optional notes and suggestions. @@ -80,39 +74,27 @@ pub struct Diagnostic { impl Diagnostic { /// Returns the lint code. #[must_use] - pub fn code(&self) -> &str { - &self.code - } + pub fn code(&self) -> &str { &self.code } /// Returns the primary message. #[must_use] - pub fn message(&self) -> &str { - &self.message - } + pub fn message(&self) -> &str { &self.message } /// Returns the primary span. #[must_use] - pub const fn span(&self) -> SourceSpan { - self.span - } + pub const fn span(&self) -> SourceSpan { self.span } /// Returns additional diagnostic notes. #[must_use] - pub fn notes(&self) -> &[String] { - &self.notes - } + pub fn notes(&self) -> &[String] { &self.notes } /// Returns help messages. #[must_use] - pub fn helps(&self) -> &[String] { - &self.helps - } + pub fn helps(&self) -> &[String] { &self.helps } /// Returns collected suggestions. #[must_use] - pub fn suggestions(&self) -> &[Suggestion] { - &self.suggestions - } + pub fn suggestions(&self) -> &[Suggestion] { &self.suggestions } } /// Builder for [`Diagnostic`] instances. @@ -157,9 +139,7 @@ impl DiagnosticBuilder { /// Completes the builder and returns the diagnostic. #[must_use] - pub fn build(self) -> Diagnostic { - self.diagnostic - } + pub fn build(self) -> Diagnostic { self.diagnostic } } /// Starts building a lint diagnostic for a given span. @@ -167,13 +147,20 @@ impl DiagnosticBuilder { /// # Examples /// /// ``` -/// use whitaker_common::diagnostics::{span_lint, Applicability, Suggestion}; -/// use whitaker_common::span::{SourceLocation, SourceSpan}; +/// use whitaker_common::{ +/// diagnostics::{Applicability, Suggestion, span_lint}, +/// span::{SourceLocation, SourceSpan}, +/// }; /// -/// let span = SourceSpan::new(SourceLocation::new(1, 0), SourceLocation::new(1, 4)).expect("valid span for example"); +/// let span = SourceSpan::new(SourceLocation::new(1, 0), SourceLocation::new(1, 4)) +/// .expect("valid span for example"); /// let diagnostic = span_lint("demo", "Example", span) /// .help("Consider refactoring") -/// .suggestion(Suggestion::new("Use helper", "helper()", Applicability::MaybeIncorrect)) +/// .suggestion(Suggestion::new( +/// "Use helper", +/// "helper()", +/// Applicability::MaybeIncorrect, +/// )) /// .build(); /// assert_eq!(diagnostic.code(), "demo"); /// ``` @@ -188,9 +175,10 @@ pub fn span_lint( #[cfg(test)] mod tests { + use rstest::rstest; + use super::*; use crate::span::{SourceLocation, SourceSpan}; - use rstest::rstest; #[rstest] fn builds_diagnostic() { diff --git a/common/src/expr.rs b/common/src/expr.rs index 23a3dd531..68773e86e 100644 --- a/common/src/expr.rs +++ b/common/src/expr.rs @@ -21,12 +21,18 @@ pub enum Expr { /// # Examples /// /// ``` -/// use whitaker_common::expr::{Expr, def_id_of_expr_callee}; -/// use whitaker_common::path::SimplePath; +/// use whitaker_common::{ +/// expr::{Expr, def_id_of_expr_callee}, +/// path::SimplePath, +/// }; /// -/// let expr = Expr::Call { callee: SimplePath::from("std::mem::drop") }; +/// let expr = Expr::Call { +/// callee: SimplePath::from("std::mem::drop"), +/// }; /// assert_eq!( -/// def_id_of_expr_callee(&expr).expect("call expression has callee path").segments(), +/// def_id_of_expr_callee(&expr) +/// .expect("call expression has callee path") +/// .segments(), /// &["std", "mem", "drop"] /// ); /// ``` @@ -43,8 +49,7 @@ pub const fn def_id_of_expr_callee(expr: &Expr) -> Option<&SimplePath> { /// # Examples /// /// ``` -/// use whitaker_common::expr::is_path_to; -/// use whitaker_common::path::SimplePath; +/// use whitaker_common::{expr::is_path_to, path::SimplePath}; /// /// let path = SimplePath::from("core::option::Option"); /// assert!(is_path_to(&path, ["core", "option", "Option"])); @@ -63,10 +68,11 @@ where /// # Examples /// /// ``` -/// use whitaker_common::expr::recv_is_option_or_result; -/// use whitaker_common::path::SimplePath; +/// use whitaker_common::{expr::recv_is_option_or_result, path::SimplePath}; /// -/// assert!(recv_is_option_or_result(&SimplePath::from("std::option::Option"))); +/// assert!(recv_is_option_or_result(&SimplePath::from( +/// "std::option::Option" +/// ))); /// assert!(recv_is_option_or_result(&SimplePath::from("Result"))); /// assert!(!recv_is_option_or_result(&SimplePath::from("crate::Thing"))); /// ``` @@ -77,9 +83,10 @@ pub fn recv_is_option_or_result(path: &SimplePath) -> bool { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] fn callee_extraction() { let expr = Expr::Call { diff --git a/common/src/i18n/diagnostics.rs b/common/src/i18n/diagnostics.rs index 219ecbc9f..ba4f9aa8e 100644 --- a/common/src/i18n/diagnostics.rs +++ b/common/src/i18n/diagnostics.rs @@ -8,19 +8,16 @@ //! # }; //! # fn demo(localizer: &Localizer) -> Result<(), whitaker_common::i18n::I18nError> { //! # let args = Arguments::default(); -//! let messages = resolve_message_set( -//! localizer, -//! MessageKey::new("my-lint.message"), -//! &args, -//! )?; +//! let messages = resolve_message_set(localizer, MessageKey::new("my-lint.message"), &args)?; //! # assert!(!messages.primary().is_empty()); //! # Ok(()) //! # } //! ``` -use super::{Arguments, I18nError, Localizer}; use std::fmt; +use super::{Arguments, I18nError, Localizer}; + /// Identifier for a Fluent message within a localization bundle. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct MessageKey<'a>(&'a str); @@ -28,21 +25,15 @@ pub struct MessageKey<'a>(&'a str); impl<'a> MessageKey<'a> { /// Construct a new message key wrapper. #[must_use] - pub const fn new(value: &'a str) -> Self { - Self(value) - } + pub const fn new(value: &'a str) -> Self { Self(value) } } impl AsRef for MessageKey<'_> { - fn as_ref(&self) -> &str { - self.0 - } + fn as_ref(&self) -> &str { self.0 } } impl fmt::Display for MessageKey<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) } } /// Identifier for a Fluent attribute attached to a message. @@ -52,21 +43,15 @@ pub struct AttrKey<'a>(&'a str); impl<'a> AttrKey<'a> { /// Construct a new attribute key wrapper. #[must_use] - pub const fn new(value: &'a str) -> Self { - Self(value) - } + pub const fn new(value: &'a str) -> Self { Self(value) } } impl AsRef for AttrKey<'_> { - fn as_ref(&self) -> &str { - self.0 - } + fn as_ref(&self) -> &str { self.0 } } impl fmt::Display for AttrKey<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) } } /// Lookup trait used by lint crates to resolve translated diagnostic strings. @@ -124,11 +109,7 @@ impl DiagnosticMessageSet { /// /// ``` /// # use whitaker_common::i18n::DiagnosticMessageSet; - /// let messages = DiagnosticMessageSet::new( - /// "primary".into(), - /// "note".into(), - /// "help".into(), - /// ); + /// let messages = DiagnosticMessageSet::new("primary".into(), "note".into(), "help".into()); /// assert_eq!(messages.primary(), "primary"); /// ``` pub const fn new(primary: String, note: String, help: String) -> Self { @@ -153,9 +134,7 @@ impl DiagnosticMessageSet { /// # ); /// assert_eq!(messages.primary(), "primary"); /// ``` - pub fn primary(&self) -> &str { - &self.primary - } + pub fn primary(&self) -> &str { &self.primary } /// Access the note attached to the diagnostic. #[must_use] @@ -171,9 +150,7 @@ impl DiagnosticMessageSet { /// # ); /// assert_eq!(messages.note(), "note"); /// ``` - pub fn note(&self) -> &str { - &self.note - } + pub fn note(&self) -> &str { &self.note } /// Access the help text attached to the diagnostic. #[must_use] @@ -189,9 +166,7 @@ impl DiagnosticMessageSet { /// # ); /// assert_eq!(messages.help(), "help"); /// ``` - pub fn help(&self) -> &str { - &self.help - } + pub fn help(&self) -> &str { &self.help } /// Remove Unicode isolating marks inserted by Fluent placeholders. pub(crate) fn strip_isolating_marks(self) -> Self { diff --git a/common/src/i18n/helpers.rs b/common/src/i18n/helpers.rs index 88be8edd9..d0d7f9502 100644 --- a/common/src/i18n/helpers.rs +++ b/common/src/i18n/helpers.rs @@ -9,7 +9,12 @@ use std::env; use log::debug; use super::{ - Arguments, DiagnosticMessageSet, Localizer, MessageKey, resolve_localizer, resolve_message_set, + Arguments, + DiagnosticMessageSet, + Localizer, + MessageKey, + resolve_localizer, + resolve_message_set, }; /// Construct a [`Localizer`] for `lint_name` using workspace configuration. @@ -95,8 +100,13 @@ fn welsh_branch_phrase(branches: usize) -> String { /// /// ``` /// use whitaker_common::i18n::{ -/// Arguments, DiagnosticMessageSet, Localizer, MessageKey, MessageResolution, -/// noop_reporter, safe_resolve_message_set, +/// Arguments, +/// DiagnosticMessageSet, +/// Localizer, +/// MessageKey, +/// MessageResolution, +/// noop_reporter, +/// safe_resolve_message_set, /// }; /// /// let localizer = Localizer::new(Some("en-GB")); @@ -126,13 +136,18 @@ pub fn noop_reporter(_message: String) {} /// # Examples /// /// ``` -/// use whitaker_common::i18n::testing::RecordingEmitter; +/// use std::borrow::Cow; +/// +/// use fluent_templates::fluent_bundle::FluentValue; /// use whitaker_common::i18n::{ -/// Arguments, DiagnosticMessageSet, Localizer, MessageKey, MessageResolution, +/// Arguments, +/// DiagnosticMessageSet, +/// Localizer, +/// MessageKey, +/// MessageResolution, /// safe_resolve_message_set, +/// testing::RecordingEmitter, /// }; -/// use fluent_templates::fluent_bundle::FluentValue; -/// use std::borrow::Cow; /// /// let mut args: Arguments<'static> = Arguments::default(); /// args.insert(Cow::Borrowed("subject"), FluentValue::from("demo")); diff --git a/common/src/i18n/loader.rs b/common/src/i18n/loader.rs index af85a178e..30921b062 100644 --- a/common/src/i18n/loader.rs +++ b/common/src/i18n/loader.rs @@ -4,15 +4,18 @@ //! messages from Fluent bundles, along with supporting types for arguments //! and lookup errors. -use std::borrow::Cow; -use std::collections::HashMap; -use std::str::FromStr; +use std::{borrow::Cow, collections::HashMap, str::FromStr}; use fluent_templates::{Loader, fluent_bundle::FluentValue}; use thiserror::Error; -use super::locales::supports_locale; -use super::{FALLBACK_LANGUAGE, FALLBACK_LITERAL, LOADER, LanguageIdentifier}; +use super::{ + FALLBACK_LANGUAGE, + FALLBACK_LITERAL, + LOADER, + LanguageIdentifier, + locales::supports_locale, +}; /// `HashMap` wrapper used when passing Fluent arguments to lookups. pub type Arguments<'a> = HashMap, FluentValue<'a>>; @@ -46,7 +49,7 @@ impl Localizer { /// Create a localizer for `locale`, falling back to [`crate::i18n::FALLBACK_LOCALE`]. /// /// ``` - /// use whitaker_common::i18n::{available_locales, Localizer}; + /// use whitaker_common::i18n::{Localizer, available_locales}; /// /// let locale = Localizer::new(Some("cy")); /// assert!(available_locales().contains(&"cy".to_string())); @@ -79,21 +82,15 @@ impl Localizer { /// Return the resolved locale identifier. #[must_use] - pub const fn language(&self) -> &LanguageIdentifier { - &self.language - } + pub const fn language(&self) -> &LanguageIdentifier { &self.language } /// Return the resolved locale as a string slice. #[must_use] - pub fn locale(&self) -> &str { - &self.language_tag - } + pub fn locale(&self) -> &str { &self.language_tag } /// Whether the fallback locale was used. #[must_use] - pub const fn used_fallback(&self) -> bool { - self.fallback_used - } + pub const fn used_fallback(&self) -> bool { self.fallback_used } /// Fetch the translated message for `key`. /// @@ -101,9 +98,7 @@ impl Localizer { /// /// Returns [`I18nError::MissingMessage`] when `key` is not defined for the /// resolved locale. - pub fn message(&self, key: &str) -> Result { - self.lookup(key, None, None) - } + pub fn message(&self, key: &str) -> Result { self.lookup(key, None, None) } /// Fetch the translated message with Fluent arguments. /// diff --git a/common/src/i18n/locales.rs b/common/src/i18n/locales.rs index 6a534888a..f7f8362ef 100644 --- a/common/src/i18n/locales.rs +++ b/common/src/i18n/locales.rs @@ -19,9 +19,7 @@ static ALL_LOCALES: std::sync::LazyLock> = std::sync::LazyLock::new( /// Return a sorted slice of the available locales. #[must_use] -pub fn available_locales() -> &'static [String] { - ALL_LOCALES.as_slice() -} +pub fn available_locales() -> &'static [String] { ALL_LOCALES.as_slice() } /// Check whether a locale tag is supported by the embedded bundles. #[must_use] diff --git a/common/src/i18n/mod.rs b/common/src/i18n/mod.rs index d001f24dc..d4dc5ad6c 100644 --- a/common/src/i18n/mod.rs +++ b/common/src/i18n/mod.rs @@ -12,15 +12,15 @@ //! //! See [`resolve_message_set`] for fetching a lint’s primary/note/help trio. -use fluent_templates::static_loader; use std::path::PathBuf; -use unic_langid::langid; /// Re-export the Fluent value type for constructing diagnostic arguments. /// See [`resolve_message_set`] for loading messages that consume these /// arguments. pub use fluent_templates::fluent_bundle::FluentValue; pub(crate) use fluent_templates::loader::LanguageIdentifier; +use fluent_templates::static_loader; +use unic_langid::langid; const FALLBACK_LITERAL: &str = "en-GB"; /// Directory name used for Fluent locale resources. @@ -74,10 +74,17 @@ pub mod testing; /// Diagnostic localization helpers. /// See [`resolve_message_set`] for fetching primary, note, and help strings. pub use diagnostics::{ - AttrKey, BundleLookup, DiagnosticMessageSet, MessageKey, resolve_message_set, + AttrKey, + BundleLookup, + DiagnosticMessageSet, + MessageKey, + resolve_message_set, }; pub use helpers::{ - MessageResolution, branch_phrase, get_localizer_for_lint, noop_reporter, + MessageResolution, + branch_phrase, + get_localizer_for_lint, + noop_reporter, safe_resolve_message_set, }; pub use loader::{Arguments, I18nError, Localizer}; diff --git a/common/src/i18n/selection.rs b/common/src/i18n/selection.rs index ddb6277a3..738ae1fbc 100644 --- a/common/src/i18n/selection.rs +++ b/common/src/i18n/selection.rs @@ -1,8 +1,7 @@ //! Locale resolver wiring explicit overrides, environment variables, and //! configuration before falling back to the bundled localizer. -use std::borrow::Cow; -use std::fmt; +use std::{borrow::Cow, fmt}; use log::{debug, warn}; @@ -51,39 +50,27 @@ impl LocaleSelection { /// Returns the effective locale source. #[must_use] - pub const fn source(&self) -> LocaleSource { - self.source - } + pub const fn source(&self) -> LocaleSource { self.source } /// Returns the locale requested by the resolved source, if any. #[must_use] - pub fn requested(&self) -> Option<&str> { - self.requested.as_deref() - } + pub fn requested(&self) -> Option<&str> { self.requested.as_deref() } /// Returns the resolved locale tag. #[must_use] - pub fn locale(&self) -> &str { - self.localizer.locale() - } + pub fn locale(&self) -> &str { self.localizer.locale() } /// Whether the fallback locale was used. #[must_use] - pub const fn used_fallback(&self) -> bool { - self.localizer.used_fallback() - } + pub const fn used_fallback(&self) -> bool { self.localizer.used_fallback() } /// Returns the resolved [`Localizer`]. #[must_use] - pub const fn localizer(&self) -> &Localizer { - &self.localizer - } + pub const fn localizer(&self) -> &Localizer { &self.localizer } /// Consumes the selection, yielding the [`Localizer`]. #[must_use] - pub fn into_localizer(self) -> Localizer { - self.localizer - } + pub fn into_localizer(self) -> Localizer { self.localizer } /// Emit a debug log summarizing the resolved locale. pub fn log_outcome(&self, target: &str) { @@ -156,9 +143,10 @@ pub fn normalize_locale(input: Option<&str>) -> Option<&str> { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[derive(Clone, Copy, Debug)] struct ResolutionCase { explicit: Option<&'static str>, @@ -170,9 +158,7 @@ mod tests { } impl ResolutionCase { - fn environment(&self) -> Option { - self.environment.map(String::from) - } + fn environment(&self) -> Option { self.environment.map(String::from) } } #[rstest] diff --git a/common/src/i18n/testing.rs b/common/src/i18n/testing.rs index 212faa01e..dc88abc13 100644 --- a/common/src/i18n/testing.rs +++ b/common/src/i18n/testing.rs @@ -4,8 +4,7 @@ //! [`RecordingEmitter`] for exercising error paths and verifying diagnostic //! output during localization tests. -use std::borrow::Cow; -use std::cell::RefCell; +use std::{borrow::Cow, cell::RefCell}; pub use super::helpers::{MessageResolution, safe_resolve_message_set}; use super::{Arguments, AttrKey, BundleLookup, I18nError, MessageKey}; @@ -70,12 +69,8 @@ pub struct RecordingEmitter { impl RecordingEmitter { /// Access the recorded messages emitted during localization failures. #[must_use] - pub fn recorded_messages(&self) -> Vec { - self.messages.borrow().clone() - } + pub fn recorded_messages(&self) -> Vec { self.messages.borrow().clone() } /// Record a delayed bug message for later assertions. - pub fn record(&self, message: String) { - self.messages.borrow_mut().push(message); - } + pub fn record(&self, message: String) { self.messages.borrow_mut().push(message); } } diff --git a/common/src/i18n/tests.rs b/common/src/i18n/tests.rs index c21611037..7b348c90d 100644 --- a/common/src/i18n/tests.rs +++ b/common/src/i18n/tests.rs @@ -1,9 +1,15 @@ use std::borrow::Cow; -use super::FluentValue; use rstest::rstest; -use super::{Arguments, FALLBACK_LOCALE, Localizer, available_locales, supports_locale}; +use super::{ + Arguments, + FALLBACK_LOCALE, + FluentValue, + Localizer, + available_locales, + supports_locale, +}; #[rstest] #[case(None, FALLBACK_LOCALE, true)] diff --git a/common/src/lcom4/extract.rs b/common/src/lcom4/extract.rs index 8c13aa877..507be690c 100644 --- a/common/src/lcom4/extract.rs +++ b/common/src/lcom4/extract.rs @@ -195,9 +195,10 @@ mod tests { //! rstest-based unit tests for [`super::MethodInfoBuilder`] and //! [`super::collect_method_infos`]. - use super::*; use rstest::rstest; + use super::*; + /// Applies field and call records to a builder and asserts against /// expected sets. fn assert_extraction( diff --git a/common/src/lcom4/mod.rs b/common/src/lcom4/mod.rs index ec3604cd2..ac9961cec 100644 --- a/common/src/lcom4/mod.rs +++ b/common/src/lcom4/mod.rs @@ -28,6 +28,7 @@ use std::collections::{BTreeSet, HashMap}; /// /// ``` /// use std::collections::BTreeSet; +/// /// use whitaker_common::lcom4::MethodInfo; /// /// let method = MethodInfo::new( @@ -55,6 +56,7 @@ impl MethodInfo { /// /// ``` /// use std::collections::BTreeSet; + /// /// use whitaker_common::lcom4::MethodInfo; /// /// let m = MethodInfo::new( @@ -83,15 +85,14 @@ impl MethodInfo { /// /// ``` /// use std::collections::BTreeSet; + /// /// use whitaker_common::lcom4::MethodInfo; /// /// let m = MethodInfo::new("read", BTreeSet::new(), BTreeSet::new()); /// assert_eq!(m.name(), "read"); /// ``` #[must_use] - pub fn name(&self) -> &str { - &self.name - } + pub fn name(&self) -> &str { &self.name } /// Returns the set of field names accessed by this method. /// @@ -99,19 +100,14 @@ impl MethodInfo { /// /// ``` /// use std::collections::BTreeSet; + /// /// use whitaker_common::lcom4::MethodInfo; /// - /// let m = MethodInfo::new( - /// "read", - /// BTreeSet::from(["buf".into()]), - /// BTreeSet::new(), - /// ); + /// let m = MethodInfo::new("read", BTreeSet::from(["buf".into()]), BTreeSet::new()); /// assert!(m.accessed_fields().contains("buf")); /// ``` #[must_use] - pub const fn accessed_fields(&self) -> &BTreeSet { - &self.accessed_fields - } + pub const fn accessed_fields(&self) -> &BTreeSet { &self.accessed_fields } /// Returns the set of method names called directly by this method. /// @@ -119,6 +115,7 @@ impl MethodInfo { /// /// ``` /// use std::collections::BTreeSet; + /// /// use whitaker_common::lcom4::MethodInfo; /// /// let m = MethodInfo::new( @@ -129,9 +126,7 @@ impl MethodInfo { /// assert!(m.called_methods().contains("validate")); /// ``` #[must_use] - pub const fn called_methods(&self) -> &BTreeSet { - &self.called_methods - } + pub const fn called_methods(&self) -> &BTreeSet { &self.called_methods } } /// Disjoint-set forest for connected component counting. @@ -285,6 +280,7 @@ fn union_by_method_calls(methods: &[MethodInfo], uf: &mut UnionFind) { /// /// ``` /// use std::collections::BTreeSet; +/// /// use whitaker_common::lcom4::{MethodInfo, cohesion_components}; /// /// let methods = vec![ @@ -297,6 +293,7 @@ fn union_by_method_calls(methods: &[MethodInfo], uf: &mut UnionFind) { /// /// ``` /// use std::collections::BTreeSet; +/// /// use whitaker_common::lcom4::{MethodInfo, cohesion_components}; /// /// let methods = vec![ diff --git a/common/src/lcom4/tests.rs b/common/src/lcom4/tests.rs index 311296807..1105e210b 100644 --- a/common/src/lcom4/tests.rs +++ b/common/src/lcom4/tests.rs @@ -1,9 +1,11 @@ //! rstest-based unit tests for [`super::cohesion_components`] and supporting types. -use super::*; -use rstest::rstest; use std::collections::BTreeSet; +use rstest::rstest; + +use super::*; + // --- Fixtures (shared setup) --- #[rstest::fixture] diff --git a/common/src/lib.rs b/common/src/lib.rs index 1dc994ac6..4f102dd6b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -19,47 +19,114 @@ pub mod span; pub mod test_support; pub use attributes::{ - Attribute, AttributeKind, AttributePath, PARSED_ATTRIBUTE_PLACEHOLDER, has_test_like_attribute, - has_test_like_attribute_with, outer_attributes, split_doc_attributes, -}; -pub use brain_trait_metrics::evaluation::{ - BrainTraitDiagnostic, BrainTraitDisposition, BrainTraitThresholds, BrainTraitThresholdsBuilder, - evaluate_brain_trait, + Attribute, + AttributeKind, + AttributePath, + PARSED_ATTRIBUTE_PLACEHOLDER, + has_test_like_attribute, + has_test_like_attribute_with, + outer_attributes, + split_doc_attributes, }; pub use brain_trait_metrics::{ - TraitItemKind, TraitItemMetrics, TraitMetrics, TraitMetricsBuilder, default_method_cc_sum, - default_method_count, required_method_count, trait_item_count, -}; -pub use brain_type_metrics::evaluation::{ - BrainTypeDiagnostic, BrainTypeDisposition, BrainTypeThresholds, BrainTypeThresholdsBuilder, - evaluate_brain_type, format_help, format_note, format_primary_message, + TraitItemKind, + TraitItemMetrics, + TraitMetrics, + TraitMetricsBuilder, + default_method_cc_sum, + default_method_count, + evaluation::{ + BrainTraitDiagnostic, + BrainTraitDisposition, + BrainTraitThresholds, + BrainTraitThresholdsBuilder, + evaluate_brain_trait, + }, + required_method_count, + trait_item_count, }; pub use brain_type_metrics::{ - CognitiveComplexityBuilder, ForeignReferenceSet, MethodMetrics, TypeMetrics, - TypeMetricsBuilder, brain_methods, foreign_reach_count, weighted_methods_count, + CognitiveComplexityBuilder, + ForeignReferenceSet, + MethodMetrics, + TypeMetrics, + TypeMetricsBuilder, + brain_methods, + evaluation::{ + BrainTypeDiagnostic, + BrainTypeDisposition, + BrainTypeThresholds, + BrainTypeThresholdsBuilder, + evaluate_brain_type, + format_help, + format_note, + format_primary_message, + }, + foreign_reach_count, + weighted_methods_count, }; pub use context::{ - ContextEntry, ContextKind, in_test_like_context, in_test_like_context_with, is_in_main_fn, - is_test_fn, is_test_fn_with, + ContextEntry, + ContextKind, + in_test_like_context, + in_test_like_context_with, + is_in_main_fn, + is_test_fn, + is_test_fn_with, }; pub use decomposition_advice::{ - DecompositionContext, DecompositionSuggestion, MethodProfile, MethodProfileBuilder, - SubjectKind, SuggestedExtractionKind, format_diagnostic_note, suggest_decomposition, + DecompositionContext, + DecompositionSuggestion, + MethodProfile, + MethodProfileBuilder, + SubjectKind, + SuggestedExtractionKind, + format_diagnostic_note, + suggest_decomposition, }; pub use diagnostics::{Applicability, Diagnostic, DiagnosticBuilder, Suggestion, span_lint}; pub use expr::{Expr, def_id_of_expr_callee, is_path_to, recv_is_option_or_result}; pub use i18n::{ - Arguments, FALLBACK_LOCALE, I18nError, LocaleSelection, LocaleSource, Localizer, - MessageResolution, available_locales, branch_phrase, get_localizer_for_lint, noop_reporter, - normalize_locale, resolve_localizer, safe_resolve_message_set, supports_locale, + Arguments, + FALLBACK_LOCALE, + I18nError, + LocaleSelection, + LocaleSource, + Localizer, + MessageResolution, + available_locales, + branch_phrase, + get_localizer_for_lint, + noop_reporter, + normalize_locale, + resolve_localizer, + safe_resolve_message_set, + supports_locale, }; pub use lcom4::{MethodInfo, MethodInfoBuilder, cohesion_components, collect_method_infos}; pub use path::SimplePath; pub use rstest::{ - ArgAtom, ArgFingerprint, CalleeShape, ExpansionTrace, ExprShape, LocalSlot, - ParagraphFingerprint, ParagraphNormalizer, ParameterBinding, RstestDetectionOptions, - RstestParameter, RstestParameterKind, SpanRecoveryFrame, StmtShape, UserEditableSpan, - classify_rstest_parameter, fixture_local_names, is_rstest_fixture, is_rstest_fixture_with, - is_rstest_test, is_rstest_test_with, recover_user_editable_span, + ArgAtom, + ArgFingerprint, + CalleeShape, + ExpansionTrace, + ExprShape, + LocalSlot, + ParagraphFingerprint, + ParagraphNormalizer, + ParameterBinding, + RstestDetectionOptions, + RstestParameter, + RstestParameterKind, + SpanRecoveryFrame, + StmtShape, + UserEditableSpan, + classify_rstest_parameter, + fixture_local_names, + is_rstest_fixture, + is_rstest_fixture_with, + is_rstest_test, + is_rstest_test_with, + recover_user_editable_span, }; pub use span::{SourceLocation, SourceSpan, SpanError, span_line_count, span_to_lines}; diff --git a/common/src/path.rs b/common/src/path.rs index 3d268abf3..8960b003f 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -88,15 +88,11 @@ impl SimplePath { } impl From<&str> for SimplePath { - fn from(path: &str) -> Self { - Self::parse(path) - } + fn from(path: &str) -> Self { Self::parse(path) } } impl From for SimplePath { - fn from(path: String) -> Self { - Self::parse(&path) - } + fn from(path: String) -> Self { Self::parse(&path) } } impl fmt::Display for SimplePath { @@ -107,10 +103,12 @@ impl fmt::Display for SimplePath { #[cfg(test)] mod tests { - use super::*; - use rstest::rstest; use std::collections::VecDeque; + use rstest::rstest; + + use super::*; + #[rstest] fn filters_empty_segments() { let path = SimplePath::from("::crate::::Item::"); diff --git a/common/src/rstest/argument_fingerprint.rs b/common/src/rstest/argument_fingerprint.rs index 8742b2bc0..5df065d0b 100644 --- a/common/src/rstest/argument_fingerprint.rs +++ b/common/src/rstest/argument_fingerprint.rs @@ -31,7 +31,12 @@ impl ArgAtom { /// use whitaker_common::rstest::ArgAtom; /// /// let atom = ArgAtom::fixture_local("db"); - /// assert_eq!(atom, ArgAtom::FixtureLocal { name: "db".to_string() }); + /// assert_eq!( + /// atom, + /// ArgAtom::FixtureLocal { + /// name: "db".to_string() + /// } + /// ); /// ``` #[must_use] pub fn fixture_local(name: impl Into) -> Self { @@ -46,12 +51,15 @@ impl ArgAtom { /// use whitaker_common::rstest::ArgAtom; /// /// let atom = ArgAtom::const_lit("42"); - /// assert_eq!(atom, ArgAtom::ConstLit { text: "42".to_string() }); + /// assert_eq!( + /// atom, + /// ArgAtom::ConstLit { + /// text: "42".to_string() + /// } + /// ); /// ``` #[must_use] - pub fn const_lit(text: impl Into) -> Self { - Self::ConstLit { text: text.into() } - } + pub fn const_lit(text: impl Into) -> Self { Self::ConstLit { text: text.into() } } /// Builds a stable constant-path argument atom. /// @@ -85,9 +93,7 @@ impl ArgAtom { /// assert_eq!(ArgAtom::unsupported(), ArgAtom::Unsupported); /// ``` #[must_use] - pub const fn unsupported() -> Self { - Self::Unsupported - } + pub const fn unsupported() -> Self { Self::Unsupported } } /// A positional fingerprint for one helper-call argument list. @@ -104,10 +110,7 @@ impl ArgFingerprint { /// ``` /// use whitaker_common::rstest::{ArgAtom, ArgFingerprint}; /// - /// let fingerprint = ArgFingerprint::new([ - /// ArgAtom::fixture_local("db"), - /// ArgAtom::const_lit("42"), - /// ]); + /// let fingerprint = ArgFingerprint::new([ArgAtom::fixture_local("db"), ArgAtom::const_lit("42")]); /// /// assert_eq!(fingerprint.atoms().len(), 2); /// ``` @@ -123,13 +126,9 @@ impl ArgFingerprint { /// Returns the stored atoms in positional order. #[must_use] - pub fn atoms(&self) -> &[ArgAtom] { - &self.atoms - } + pub fn atoms(&self) -> &[ArgAtom] { &self.atoms } /// Consumes the fingerprint and returns the stored atoms. #[must_use] - pub fn into_atoms(self) -> Vec { - self.atoms - } + pub fn into_atoms(self) -> Vec { self.atoms } } diff --git a/common/src/rstest/detection.rs b/common/src/rstest/detection.rs index c5c9ba059..7dc80e80d 100644 --- a/common/src/rstest/detection.rs +++ b/common/src/rstest/detection.rs @@ -30,8 +30,7 @@ impl ExpansionTrace { /// # Examples /// /// ``` - /// use whitaker_common::attributes::AttributePath; - /// use whitaker_common::rstest::ExpansionTrace; + /// use whitaker_common::{attributes::AttributePath, rstest::ExpansionTrace}; /// /// let trace = ExpansionTrace::new([AttributePath::from("rstest")]); /// assert_eq!(trace.frames(), &[AttributePath::from("rstest")]); @@ -48,9 +47,7 @@ impl ExpansionTrace { /// Returns the stored expansion frames. #[must_use] - pub fn frames(&self) -> &[AttributePath] { - &self.frames - } + pub fn frames(&self) -> &[AttributePath] { &self.frames } } /// Runtime options for strict `rstest` detection. @@ -66,11 +63,13 @@ impl RstestDetectionOptions { /// # Examples /// /// ``` - /// use whitaker_common::attributes::AttributePath; - /// use whitaker_common::rstest::RstestDetectionOptions; + /// use whitaker_common::{attributes::AttributePath, rstest::RstestDetectionOptions}; /// /// let options = RstestDetectionOptions::new( - /// vec![AttributePath::from("case"), AttributePath::from("rstest::case")], + /// vec![ + /// AttributePath::from("case"), + /// AttributePath::from("rstest::case"), + /// ], /// true, /// ); /// assert!(options.use_expansion_trace_fallback()); @@ -88,15 +87,11 @@ impl RstestDetectionOptions { /// Returns the configured provider-parameter attribute paths. #[must_use] - pub fn provider_param_attributes(&self) -> &[AttributePath] { - &self.provider_param_attributes - } + pub fn provider_param_attributes(&self) -> &[AttributePath] { &self.provider_param_attributes } /// Returns whether expansion-trace fallback is enabled. #[must_use] - pub const fn use_expansion_trace_fallback(&self) -> bool { - self.use_expansion_trace_fallback - } + pub const fn use_expansion_trace_fallback(&self) -> bool { self.use_expansion_trace_fallback } } impl Default for RstestDetectionOptions { @@ -117,10 +112,15 @@ impl Default for RstestDetectionOptions { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::rstest::is_rstest_test; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// rstest::is_rstest_test, +/// }; /// -/// let attrs = vec![Attribute::new(AttributePath::from("rstest"), AttributeKind::Outer)]; +/// let attrs = vec![Attribute::new( +/// AttributePath::from("rstest"), +/// AttributeKind::Outer, +/// )]; /// assert!(is_rstest_test(&attrs)); /// ``` #[must_use] @@ -134,10 +134,15 @@ pub fn is_rstest_test(attrs: &[Attribute]) -> bool { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::rstest::{ExpansionTrace, RstestDetectionOptions, is_rstest_test_with}; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// rstest::{ExpansionTrace, RstestDetectionOptions, is_rstest_test_with}, +/// }; /// -/// let attrs = vec![Attribute::new(AttributePath::from("allow"), AttributeKind::Outer)]; +/// let attrs = vec![Attribute::new( +/// AttributePath::from("allow"), +/// AttributeKind::Outer, +/// )]; /// let trace = ExpansionTrace::new([AttributePath::from("rstest")]); /// let options = RstestDetectionOptions::new(Vec::new(), true); /// assert!(is_rstest_test_with(&attrs, Some(&trace), &options)); @@ -157,10 +162,15 @@ pub fn is_rstest_test_with( /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::rstest::is_rstest_fixture; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// rstest::is_rstest_fixture, +/// }; /// -/// let attrs = vec![Attribute::new(AttributePath::from("fixture"), AttributeKind::Outer)]; +/// let attrs = vec![Attribute::new( +/// AttributePath::from("fixture"), +/// AttributeKind::Outer, +/// )]; /// assert!(is_rstest_fixture(&attrs)); /// ``` #[must_use] @@ -174,10 +184,15 @@ pub fn is_rstest_fixture(attrs: &[Attribute]) -> bool { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::rstest::{ExpansionTrace, RstestDetectionOptions, is_rstest_fixture_with}; +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// rstest::{ExpansionTrace, RstestDetectionOptions, is_rstest_fixture_with}, +/// }; /// -/// let attrs = vec![Attribute::new(AttributePath::from("allow"), AttributeKind::Outer)]; +/// let attrs = vec![Attribute::new( +/// AttributePath::from("allow"), +/// AttributeKind::Outer, +/// )]; /// let trace = ExpansionTrace::new([AttributePath::from("rstest::fixture")]); /// let options = RstestDetectionOptions::new(Vec::new(), true); /// assert!(is_rstest_fixture_with(&attrs, Some(&trace), &options)); diff --git a/common/src/rstest/mod.rs b/common/src/rstest/mod.rs index 279ba5479..0be2cdd8a 100644 --- a/common/src/rstest/mod.rs +++ b/common/src/rstest/mod.rs @@ -18,14 +18,26 @@ mod span; pub use argument_fingerprint::{ArgAtom, ArgFingerprint}; pub use detection::{ - ExpansionTrace, RstestDetectionOptions, is_rstest_fixture, is_rstest_fixture_with, - is_rstest_test, is_rstest_test_with, + ExpansionTrace, + RstestDetectionOptions, + is_rstest_fixture, + is_rstest_fixture_with, + is_rstest_test, + is_rstest_test_with, }; pub use paragraph_fingerprint::{ - CalleeShape, ExprShape, LocalSlot, ParagraphFingerprint, ParagraphNormalizer, StmtShape, + CalleeShape, + ExprShape, + LocalSlot, + ParagraphFingerprint, + ParagraphNormalizer, + StmtShape, }; pub use parameter::{ - ParameterBinding, RstestParameter, RstestParameterKind, classify_rstest_parameter, + ParameterBinding, + RstestParameter, + RstestParameterKind, + classify_rstest_parameter, fixture_local_names, }; pub use span::{SpanRecoveryFrame, UserEditableSpan, recover_user_editable_span}; diff --git a/common/src/rstest/paragraph_fingerprint.rs b/common/src/rstest/paragraph_fingerprint.rs index 8aa9a62d2..6dff0fdd7 100644 --- a/common/src/rstest/paragraph_fingerprint.rs +++ b/common/src/rstest/paragraph_fingerprint.rs @@ -18,15 +18,11 @@ impl LocalSlot { /// assert_eq!(slot.index(), 0); /// ``` #[must_use] - pub const fn new(index: u32) -> Self { - Self(index) - } + pub const fn new(index: u32) -> Self { Self(index) } /// Returns the stable slot ordinal. #[must_use] - pub const fn index(self) -> u32 { - self.0 - } + pub const fn index(self) -> u32 { self.0 } } /// Assigns deterministic local slots by first appearance order. @@ -97,9 +93,7 @@ impl CalleeShape { /// assert_eq!(callee, CalleeShape::DefPath("crate::make_user".to_string())); /// ``` #[must_use] - pub fn def_path(def_path: impl Into) -> Self { - Self::DefPath(def_path.into()) - } + pub fn def_path(def_path: impl Into) -> Self { Self::DefPath(def_path.into()) } /// Builds an unknown callee shape. /// @@ -111,9 +105,7 @@ impl CalleeShape { /// assert_eq!(CalleeShape::unknown(), CalleeShape::Unknown); /// ``` #[must_use] - pub const fn unknown() -> Self { - Self::Unknown - } + pub const fn unknown() -> Self { Self::Unknown } } /// A normalized expression shape used by paragraph statements. @@ -159,9 +151,7 @@ impl ExprShape { /// ); /// ``` #[must_use] - pub const fn call(callee: CalleeShape, argc: usize) -> Self { - Self::Call { callee, argc } - } + pub const fn call(callee: CalleeShape, argc: usize) -> Self { Self::Call { callee, argc } } /// Builds a method-call expression shape. #[must_use] @@ -174,21 +164,15 @@ impl ExprShape { /// Builds a path expression shape. #[must_use] - pub const fn path() -> Self { - Self::Path - } + pub const fn path() -> Self { Self::Path } /// Builds a literal expression shape. #[must_use] - pub const fn lit() -> Self { - Self::Lit - } + pub const fn lit() -> Self { Self::Lit } /// Builds an explicit unsupported expression shape. #[must_use] - pub const fn other() -> Self { - Self::Other - } + pub const fn other() -> Self { Self::Other } } /// A normalized statement shape used for paragraph grouping. @@ -219,13 +203,13 @@ impl StmtShape { /// /// assert_eq!( /// StmtShape::let_binding(ExprShape::lit()), - /// StmtShape::Let { init: ExprShape::Lit }, + /// StmtShape::Let { + /// init: ExprShape::Lit + /// }, /// ); /// ``` #[must_use] - pub const fn let_binding(init: ExprShape) -> Self { - Self::Let { init } - } + pub const fn let_binding(init: ExprShape) -> Self { Self::Let { init } } /// Builds a mutating-call statement shape. #[must_use] @@ -247,7 +231,10 @@ impl ParagraphFingerprint { /// /// ``` /// use whitaker_common::rstest::{ - /// CalleeShape, ExprShape, ParagraphFingerprint, ParagraphNormalizer, + /// CalleeShape, + /// ExprShape, + /// ParagraphFingerprint, + /// ParagraphNormalizer, /// StmtShape, /// }; /// @@ -277,13 +264,9 @@ impl ParagraphFingerprint { /// Returns the stored statement shapes in paragraph order. #[must_use] - pub fn shapes(&self) -> &[StmtShape] { - &self.shapes - } + pub fn shapes(&self) -> &[StmtShape] { &self.shapes } /// Consumes the fingerprint and returns the stored statement shapes. #[must_use] - pub fn into_shapes(self) -> Vec { - self.shapes - } + pub fn into_shapes(self) -> Vec { self.shapes } } diff --git a/common/src/rstest/parameter.rs b/common/src/rstest/parameter.rs index a576948f8..2a2f37690 100644 --- a/common/src/rstest/parameter.rs +++ b/common/src/rstest/parameter.rs @@ -1,8 +1,9 @@ //! Pure parameter classification helpers for `rstest`-driven functions. +use std::collections::BTreeSet; + use super::RstestDetectionOptions; use crate::attributes::Attribute; -use std::collections::BTreeSet; /// Represents the supported parameter binding shapes for version-one `rstest` /// classification. @@ -27,12 +28,17 @@ impl RstestParameter { /// # Examples /// /// ``` - /// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; - /// use whitaker_common::rstest::{ParameterBinding, RstestParameter}; + /// use whitaker_common::{ + /// attributes::{Attribute, AttributeKind, AttributePath}, + /// rstest::{ParameterBinding, RstestParameter}, + /// }; /// /// let parameter = RstestParameter::new( /// ParameterBinding::Ident("db".to_string()), - /// vec![Attribute::new(AttributePath::from("case"), AttributeKind::Outer)], + /// vec![Attribute::new( + /// AttributePath::from("case"), + /// AttributeKind::Outer, + /// )], /// ); /// assert_eq!(parameter.attributes().len(), 1); /// ``` @@ -70,21 +76,15 @@ impl RstestParameter { /// assert_eq!(parameter.binding_name(), None); /// ``` #[must_use] - pub const fn unsupported() -> Self { - Self::new(ParameterBinding::Unsupported, Vec::new()) - } + pub const fn unsupported() -> Self { Self::new(ParameterBinding::Unsupported, Vec::new()) } /// Returns the binding metadata. #[must_use] - pub const fn binding(&self) -> &ParameterBinding { - &self.binding - } + pub const fn binding(&self) -> &ParameterBinding { &self.binding } /// Returns the parameter attributes. #[must_use] - pub fn attributes(&self) -> &[Attribute] { - &self.attributes - } + pub fn attributes(&self) -> &[Attribute] { &self.attributes } /// Returns the identifier binding name when available. #[must_use] @@ -115,14 +115,22 @@ pub enum RstestParameterKind { /// # Examples /// /// ``` -/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -/// use whitaker_common::rstest::{ -/// RstestDetectionOptions, RstestParameter, RstestParameterKind, classify_rstest_parameter, +/// use whitaker_common::{ +/// attributes::{Attribute, AttributeKind, AttributePath}, +/// rstest::{ +/// RstestDetectionOptions, +/// RstestParameter, +/// RstestParameterKind, +/// classify_rstest_parameter, +/// }, /// }; /// /// let parameter = RstestParameter::new( /// whitaker_common::rstest::ParameterBinding::Ident("db".to_string()), -/// vec![Attribute::new(AttributePath::from("case"), AttributeKind::Outer)], +/// vec![Attribute::new( +/// AttributePath::from("case"), +/// AttributeKind::Outer, +/// )], /// ); /// let kind = classify_rstest_parameter(¶meter, &RstestDetectionOptions::default()); /// assert_eq!(kind, RstestParameterKind::Provider); @@ -151,7 +159,10 @@ pub fn classify_rstest_parameter( /// ``` /// use whitaker_common::rstest::{RstestDetectionOptions, RstestParameter, fixture_local_names}; /// -/// let parameters = vec![RstestParameter::ident("db"), RstestParameter::ident("clock")]; +/// let parameters = vec![ +/// RstestParameter::ident("db"), +/// RstestParameter::ident("clock"), +/// ]; /// let names = fixture_local_names(¶meters, &RstestDetectionOptions::default()); /// assert!(names.contains("db")); /// assert!(names.contains("clock")); diff --git a/common/src/rstest/span.rs b/common/src/rstest/span.rs index 7dfc41758..fa521e26d 100644 --- a/common/src/rstest/span.rs +++ b/common/src/rstest/span.rs @@ -13,8 +13,10 @@ impl SpanRecoveryFrame { /// # Examples /// /// ``` - /// use whitaker_common::rstest::SpanRecoveryFrame; - /// use whitaker_common::span::{SourceLocation, SourceSpan}; + /// use whitaker_common::{ + /// rstest::SpanRecoveryFrame, + /// span::{SourceLocation, SourceSpan}, + /// }; /// /// let span = SourceSpan::new(SourceLocation::new(3, 1), SourceLocation::new(3, 8)) /// .expect("example span should be valid"); @@ -32,21 +34,15 @@ impl SpanRecoveryFrame { /// Returns the stored frame value. #[must_use] - pub const fn value(&self) -> &T { - &self.value - } + pub const fn value(&self) -> &T { &self.value } /// Consumes the frame and returns the stored value. #[must_use] - pub fn into_value(self) -> T { - self.value - } + pub fn into_value(self) -> T { self.value } /// Returns whether the frame still originates from macro expansion. #[must_use] - pub const fn from_expansion(&self) -> bool { - self.from_expansion - } + pub const fn from_expansion(&self) -> bool { self.from_expansion } } /// The result of recovering a user-editable span from an ordered frame chain. @@ -66,14 +62,19 @@ impl UserEditableSpan { /// # Examples /// /// ``` - /// use whitaker_common::rstest::UserEditableSpan; - /// use whitaker_common::span::{SourceLocation, SourceSpan}; + /// use whitaker_common::{ + /// rstest::UserEditableSpan, + /// span::{SourceLocation, SourceSpan}, + /// }; /// /// let span = SourceSpan::new(SourceLocation::new(5, 1), SourceLocation::new(5, 7)) /// .expect("example span should be valid"); /// /// assert_eq!(UserEditableSpan::Recovered(span).into_option(), Some(span)); - /// assert_eq!(UserEditableSpan::::MacroOnly.into_option(), None); + /// assert_eq!( + /// UserEditableSpan::::MacroOnly.into_option(), + /// None + /// ); /// ``` #[must_use] pub fn into_option(self) -> Option { @@ -92,8 +93,10 @@ impl UserEditableSpan { /// # Examples /// /// ``` -/// use whitaker_common::rstest::{SpanRecoveryFrame, UserEditableSpan, recover_user_editable_span}; -/// use whitaker_common::span::{SourceLocation, SourceSpan}; +/// use whitaker_common::{ +/// rstest::{SpanRecoveryFrame, UserEditableSpan, recover_user_editable_span}, +/// span::{SourceLocation, SourceSpan}, +/// }; /// /// let macro_span = SourceSpan::new(SourceLocation::new(2, 1), SourceLocation::new(2, 5)) /// .expect("example span should be valid"); diff --git a/common/src/rstest/tests/fingerprint.rs b/common/src/rstest/tests/fingerprint.rs index 1b9bbfae3..fdbf96d1d 100644 --- a/common/src/rstest/tests/fingerprint.rs +++ b/common/src/rstest/tests/fingerprint.rs @@ -3,8 +3,14 @@ use rstest::rstest; use crate::rstest::{ - ArgAtom, ArgFingerprint, CalleeShape, ExprShape, LocalSlot, ParagraphFingerprint, - ParagraphNormalizer, StmtShape, + ArgAtom, + ArgFingerprint, + CalleeShape, + ExprShape, + LocalSlot, + ParagraphFingerprint, + ParagraphNormalizer, + StmtShape, }; #[rstest] diff --git a/common/src/rstest/tests/mod.rs b/common/src/rstest/tests/mod.rs index 18aa0b383..009ae8017 100644 --- a/common/src/rstest/tests/mod.rs +++ b/common/src/rstest/tests/mod.rs @@ -8,9 +8,19 @@ use std::collections::BTreeSet; use rstest::rstest; use super::{ - ExpansionTrace, ParameterBinding, RstestDetectionOptions, RstestParameter, RstestParameterKind, - SpanRecoveryFrame, UserEditableSpan, classify_rstest_parameter, fixture_local_names, - is_rstest_fixture, is_rstest_fixture_with, is_rstest_test, is_rstest_test_with, + ExpansionTrace, + ParameterBinding, + RstestDetectionOptions, + RstestParameter, + RstestParameterKind, + SpanRecoveryFrame, + UserEditableSpan, + classify_rstest_parameter, + fixture_local_names, + is_rstest_fixture, + is_rstest_fixture_with, + is_rstest_test, + is_rstest_test_with, recover_user_editable_span, }; use crate::{ diff --git a/common/src/span.rs b/common/src/span.rs index f53cb7e92..359e80e2b 100644 --- a/common/src/span.rs +++ b/common/src/span.rs @@ -28,21 +28,15 @@ impl SourceLocation { /// assert_eq!(location.line(), 3); /// ``` #[must_use] - pub const fn new(line: usize, column: usize) -> Self { - Self { line, column } - } + pub const fn new(line: usize, column: usize) -> Self { Self { line, column } } /// Returns the one-based line number. #[must_use] - pub const fn line(self) -> usize { - self.line - } + pub const fn line(self) -> usize { self.line } /// Returns the one-based column number. #[must_use] - pub const fn column(self) -> usize { - self.column - } + pub const fn column(self) -> usize { self.column } /// Returns true when this location is positioned after other. #[must_use] @@ -70,7 +64,8 @@ impl SourceSpan { /// ``` /// use whitaker_common::span::{SourceLocation, SourceSpan}; /// - /// let span = SourceSpan::new(SourceLocation::new(1, 0), SourceLocation::new(3, 2)).expect("valid span for example"); + /// let span = SourceSpan::new(SourceLocation::new(1, 0), SourceLocation::new(3, 2)) + /// .expect("valid span for example"); /// assert_eq!(span.start().line(), 1); /// ``` #[must_use = "Inspect the span creation result to handle invalid ranges"] @@ -84,15 +79,11 @@ impl SourceSpan { /// Returns the start location. #[must_use] - pub const fn start(self) -> SourceLocation { - self.start - } + pub const fn start(self) -> SourceLocation { self.start } /// Returns the end location. #[must_use] - pub const fn end(self) -> SourceLocation { - self.end - } + pub const fn end(self) -> SourceLocation { self.end } } /// Converts a span into the inclusive range of line numbers it covers. @@ -102,7 +93,8 @@ impl SourceSpan { /// ``` /// use whitaker_common::span::{SourceLocation, SourceSpan, span_to_lines}; /// -/// let span = SourceSpan::new(SourceLocation::new(4, 0), SourceLocation::new(6, 5)).expect("valid span for example"); +/// let span = SourceSpan::new(SourceLocation::new(4, 0), SourceLocation::new(6, 5)) +/// .expect("valid span for example"); /// assert_eq!(span_to_lines(span), 4..=6); /// ``` #[must_use] @@ -117,19 +109,19 @@ pub const fn span_to_lines(span: SourceSpan) -> RangeInclusive { /// ``` /// use whitaker_common::span::{SourceLocation, SourceSpan, span_line_count}; /// -/// let span = SourceSpan::new(SourceLocation::new(2, 0), SourceLocation::new(5, 1)).expect("valid span for example"); +/// let span = SourceSpan::new(SourceLocation::new(2, 0), SourceLocation::new(5, 1)) +/// .expect("valid span for example"); /// assert_eq!(span_line_count(span), 4); /// ``` #[must_use] -pub const fn span_line_count(span: SourceSpan) -> usize { - span.end.line() - span.start.line() + 1 -} +pub const fn span_line_count(span: SourceSpan) -> usize { span.end.line() - span.start.line() + 1 } #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] fn span_construction_validates_order() { let err = SourceSpan::new(SourceLocation::new(3, 1), SourceLocation::new(2, 0)); diff --git a/common/src/test_support/decomposition.rs b/common/src/test_support/decomposition.rs index cc31a9998..b3987900c 100644 --- a/common/src/test_support/decomposition.rs +++ b/common/src/test_support/decomposition.rs @@ -17,8 +17,12 @@ pub use self::{ vector_algebra::{MethodVectorAlgebraReport, method_vector_algebra}, }; use crate::decomposition_advice::{ - DecompositionContext, DecompositionSuggestion, MethodProfile, MethodProfileBuilder, - SubjectKind, methods_meet_cosine_threshold as runtime_methods_meet_cosine_threshold, + DecompositionContext, + DecompositionSuggestion, + MethodProfile, + MethodProfileBuilder, + SubjectKind, + methods_meet_cosine_threshold as runtime_methods_meet_cosine_threshold, suggest_decomposition, }; diff --git a/common/src/test_support/decomposition_adjacency.rs b/common/src/test_support/decomposition_adjacency.rs index fcf0c0e6d..6e196f9ea 100644 --- a/common/src/test_support/decomposition_adjacency.rs +++ b/common/src/test_support/decomposition_adjacency.rs @@ -90,9 +90,7 @@ impl AdjacencyReport { /// assert_eq!(report.node_count(), 4); /// ``` #[must_use] - pub const fn node_count(&self) -> usize { - self.node_count - } + pub const fn node_count(&self) -> usize { self.node_count } /// Returns the neighbour list for `node`, sorted by neighbour index. /// diff --git a/common/src/test_support/decomposition_label_propagation.rs b/common/src/test_support/decomposition_label_propagation.rs index cc37f947e..f57596719 100644 --- a/common/src/test_support/decomposition_label_propagation.rs +++ b/common/src/test_support/decomposition_label_propagation.rs @@ -2,7 +2,8 @@ use super::adjacency::{AdjacencyError, EdgeInput, validate_edges}; use crate::decomposition_advice::{ - MethodProfileBuilder, build_feature_vector, + MethodProfileBuilder, + build_feature_vector, community::{ LabelPropagationReport as RuntimeLabelPropagationReport, propagate_labels_report as runtime_propagate_labels_report, @@ -38,28 +39,20 @@ pub struct LabelPropagationReport { impl LabelPropagationReport { /// Returns the final label vector. #[must_use] - pub fn labels(&self) -> &[usize] { - &self.runtime.labels - } + pub fn labels(&self) -> &[usize] { &self.runtime.labels } /// Returns the propagated label for `node`, or `None` if it is out of /// range. #[must_use] - pub fn label_of(&self, node: usize) -> Option { - self.labels().get(node).copied() - } + pub fn label_of(&self, node: usize) -> Option { self.labels().get(node).copied() } /// Returns the number of propagation passes performed. #[must_use] - pub const fn iteration_count(&self) -> usize { - self.runtime.iteration_count - } + pub const fn iteration_count(&self) -> usize { self.runtime.iteration_count } /// Returns `true` when the graph contains at least one active node. #[must_use] - pub const fn has_active_nodes(&self) -> bool { - self.has_active_nodes - } + pub const fn has_active_nodes(&self) -> bool { self.has_active_nodes } /// Returns `true` when every label is a valid node index. #[must_use] diff --git a/common/src/test_support/decomposition_vector_algebra.rs b/common/src/test_support/decomposition_vector_algebra.rs index 8b2079af8..3aa9f5bf1 100644 --- a/common/src/test_support/decomposition_vector_algebra.rs +++ b/common/src/test_support/decomposition_vector_algebra.rs @@ -70,9 +70,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.left_dot_right(), 40); /// ``` #[must_use] - pub const fn left_dot_right(self) -> u64 { - self.left_dot_right - } + pub const fn left_dot_right(self) -> u64 { self.left_dot_right } /// Returns the result of [`MethodVectorAlgebraReport::right_dot_left`]. /// @@ -104,9 +102,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.right_dot_left(), 40); /// ``` #[must_use] - pub const fn right_dot_left(self) -> u64 { - self.right_dot_left - } + pub const fn right_dot_left(self) -> u64 { self.right_dot_left } /// Returns the result of [`MethodVectorAlgebraReport::left_norm_squared`]. /// @@ -138,9 +134,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.left_norm_squared(), 44); /// ``` #[must_use] - pub const fn left_norm_squared(self) -> u64 { - self.left_norm_squared - } + pub const fn left_norm_squared(self) -> u64 { self.left_norm_squared } /// Returns the result of [`MethodVectorAlgebraReport::right_norm_squared`]. /// @@ -172,9 +166,7 @@ impl MethodVectorAlgebraReport { /// assert_eq!(report.right_norm_squared(), 44); /// ``` #[must_use] - pub const fn right_norm_squared(self) -> u64 { - self.right_norm_squared - } + pub const fn right_norm_squared(self) -> u64 { self.right_norm_squared } } /// Computes the shipped vector-algebra helper values for two methods. diff --git a/common/src/test_support/fixtures.rs b/common/src/test_support/fixtures.rs index 9f3408e3e..88ff45ef7 100644 --- a/common/src/test_support/fixtures.rs +++ b/common/src/test_support/fixtures.rs @@ -172,7 +172,8 @@ mod tests { //! Tests for fixture staging helpers used by lint test suites. use std::{ - fs, io, + fs, + io, path::{Path, PathBuf}, }; diff --git a/common/src/test_support/mod.rs b/common/src/test_support/mod.rs index 66c0739ed..52cdf777f 100644 --- a/common/src/test_support/mod.rs +++ b/common/src/test_support/mod.rs @@ -28,8 +28,14 @@ use std::{ pub use fixtures::{copy_directory, copy_fixture}; pub use ui::{ - FixtureEnvironment, discover_fixtures, prepare_fixture, read_directory_config, - read_fixture_config, resolve_fixture_config, run_fixtures_with, run_test_runner, + FixtureEnvironment, + discover_fixtures, + prepare_fixture, + read_directory_config, + read_fixture_config, + resolve_fixture_config, + run_fixtures_with, + run_test_runner, }; /// Serializes tests that mutate process-wide environment variables. diff --git a/common/src/test_support/ui.rs b/common/src/test_support/ui.rs index 5773a105d..a27c39691 100644 --- a/common/src/test_support/ui.rs +++ b/common/src/test_support/ui.rs @@ -5,7 +5,8 @@ //! `dylint_testing` while capturing panics into deterministic error messages. use std::{ - fs, io, + fs, + io, path::{Path, PathBuf}, }; @@ -25,14 +26,10 @@ pub struct FixtureEnvironment { impl FixtureEnvironment { /// Returns the root directory containing the cloned fixture files. #[must_use] - pub fn workdir(&self) -> &Path { - &self.workdir - } + pub fn workdir(&self) -> &Path { &self.workdir } /// Moves the optional `dylint.toml` contents out of the environment. - pub const fn take_config(&mut self) -> Option { - self.config.take() - } + pub const fn take_config(&mut self) -> Option { self.config.take() } } /// Discovers `.rs` fixtures inside `directory`, returning the paths unsorted. diff --git a/common/tests/brain_trait_evaluation_behaviour.rs b/common/tests/brain_trait_evaluation_behaviour.rs index b7b35899a..5be7074b7 100644 --- a/common/tests/brain_trait_evaluation_behaviour.rs +++ b/common/tests/brain_trait_evaluation_behaviour.rs @@ -5,10 +5,15 @@ use std::cell::{Cell, RefCell}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::brain_trait_metrics::{ - TraitMetrics, TraitMetricsBuilder, + TraitMetrics, + TraitMetricsBuilder, evaluation::{ - BrainTraitDiagnostic, BrainTraitDisposition, BrainTraitThresholds, - BrainTraitThresholdsBuilder, evaluate_brain_trait, format_primary_message, + BrainTraitDiagnostic, + BrainTraitDisposition, + BrainTraitThresholds, + BrainTraitThresholdsBuilder, + evaluate_brain_trait, + format_primary_message, }, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -101,9 +106,7 @@ impl EvaluationWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> EvaluationWorld { - EvaluationWorld::default() -} +fn world() -> EvaluationWorld { EvaluationWorld::default() } // --- Given steps --- @@ -209,41 +212,25 @@ fn then_primary_message_contains(world: &EvaluationWorld, text: String) -> Resul // here. #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 0)] -fn scenario_within_limits_passes(world: EvaluationWorld) { - let _ = world; -} +fn scenario_within_limits_passes(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 1)] -fn scenario_all_warn_conditions(world: EvaluationWorld) { - let _ = world; -} +fn scenario_all_warn_conditions(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 2)] -fn scenario_many_methods_alone(world: EvaluationWorld) { - let _ = world; -} +fn scenario_many_methods_alone(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 3)] -fn scenario_high_cc_alone(world: EvaluationWorld) { - let _ = world; -} +fn scenario_high_cc_alone(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 4)] -fn scenario_deny_threshold(world: EvaluationWorld) { - let _ = world; -} +fn scenario_deny_threshold(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 5)] -fn scenario_deny_supersedes_warn(world: EvaluationWorld) { - let _ = world; -} +fn scenario_deny_supersedes_warn(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 6)] -fn scenario_associated_items_excluded(world: EvaluationWorld) { - let _ = world; -} +fn scenario_associated_items_excluded(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_evaluation.feature", index = 7)] -fn scenario_diagnostic_surfaces_values(world: EvaluationWorld) { - let _ = world; -} +fn scenario_diagnostic_surfaces_values(world: EvaluationWorld) { let _ = world; } diff --git a/common/tests/brain_trait_metrics_behaviour.rs b/common/tests/brain_trait_metrics_behaviour.rs index 8e630f393..05eb7b9fa 100644 --- a/common/tests/brain_trait_metrics_behaviour.rs +++ b/common/tests/brain_trait_metrics_behaviour.rs @@ -28,9 +28,7 @@ struct TraitMetricsWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> TraitMetricsWorld { - TraitMetricsWorld::default() -} +fn world() -> TraitMetricsWorld { TraitMetricsWorld::default() } fn with_metrics(world: &TraitMetricsWorld, assert_fn: impl FnOnce(&TraitMetrics)) { let metrics_ref = world.metrics.borrow(); @@ -183,31 +181,19 @@ fn then_implementor_burden(world: &TraitMetricsWorld, count: usize) { // `tests/features/brain_trait_metrics.feature`. #[scenario(path = "tests/features/brain_trait_metrics.feature", index = 0)] -fn scenario_mixed_trait_items(world: TraitMetricsWorld) { - let _ = world; -} +fn scenario_mixed_trait_items(world: TraitMetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_metrics.feature", index = 1)] -fn scenario_without_default_methods(world: TraitMetricsWorld) { - let _ = world; -} +fn scenario_without_default_methods(world: TraitMetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_metrics.feature", index = 2)] -fn scenario_empty_trait(world: TraitMetricsWorld) { - let _ = world; -} +fn scenario_empty_trait(world: TraitMetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_metrics.feature", index = 3)] -fn scenario_expansion_filter(world: TraitMetricsWorld) { - let _ = world; -} +fn scenario_expansion_filter(world: TraitMetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_metrics.feature", index = 4)] -fn scenario_implementor_burden(world: TraitMetricsWorld) { - let _ = world; -} +fn scenario_implementor_burden(world: TraitMetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_trait_metrics.feature", index = 5)] -fn scenario_only_default_methods(world: TraitMetricsWorld) { - let _ = world; -} +fn scenario_only_default_methods(world: TraitMetricsWorld) { let _ = world; } diff --git a/common/tests/brain_type_evaluation_behaviour.rs b/common/tests/brain_type_evaluation_behaviour.rs index 89b5f8237..22811c1bb 100644 --- a/common/tests/brain_type_evaluation_behaviour.rs +++ b/common/tests/brain_type_evaluation_behaviour.rs @@ -5,10 +5,16 @@ use std::cell::{Cell, RefCell}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::brain_type_metrics::{ - MethodMetrics, TypeMetrics, TypeMetricsBuilder, + MethodMetrics, + TypeMetrics, + TypeMetricsBuilder, evaluation::{ - BrainTypeDiagnostic, BrainTypeDisposition, BrainTypeThresholds, BrainTypeThresholdsBuilder, - evaluate_brain_type, format_primary_message, + BrainTypeDiagnostic, + BrainTypeDisposition, + BrainTypeThresholds, + BrainTypeThresholdsBuilder, + evaluate_brain_type, + format_primary_message, }, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -91,9 +97,7 @@ impl EvaluationWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> EvaluationWorld { - EvaluationWorld::default() -} +fn world() -> EvaluationWorld { EvaluationWorld::default() } // --- Given steps --- @@ -105,9 +109,7 @@ fn given_type_metrics(world: &EvaluationWorld, name: String, wmc: usize, brain_c } #[given("the type has LCOM4 {lcom4}")] -fn given_lcom4(world: &EvaluationWorld, lcom4: usize) { - world.lcom4.set(lcom4); -} +fn given_lcom4(world: &EvaluationWorld, lcom4: usize) { world.lcom4.set(lcom4); } #[given("the default brain type thresholds")] fn given_default_thresholds(world: &EvaluationWorld) { @@ -205,46 +207,28 @@ fn then_primary_message_contains(world: &EvaluationWorld, text: String) { // here. #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 0)] -fn scenario_within_limits_passes(world: EvaluationWorld) { - let _ = world; -} +fn scenario_within_limits_passes(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 1)] -fn scenario_all_warn_conditions(world: EvaluationWorld) { - let _ = world; -} +fn scenario_all_warn_conditions(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 2)] -fn scenario_high_wmc_alone(world: EvaluationWorld) { - let _ = world; -} +fn scenario_high_wmc_alone(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 3)] -fn scenario_brain_method_without_wmc(world: EvaluationWorld) { - let _ = world; -} +fn scenario_brain_method_without_wmc(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 4)] -fn scenario_wmc_deny_threshold(world: EvaluationWorld) { - let _ = world; -} +fn scenario_wmc_deny_threshold(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 5)] -fn scenario_multiple_brain_methods_deny(world: EvaluationWorld) { - let _ = world; -} +fn scenario_multiple_brain_methods_deny(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 6)] -fn scenario_high_lcom4_deny(world: EvaluationWorld) { - let _ = world; -} +fn scenario_high_lcom4_deny(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 7)] -fn scenario_deny_supersedes_warn(world: EvaluationWorld) { - let _ = world; -} +fn scenario_deny_supersedes_warn(world: EvaluationWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_evaluation.feature", index = 8)] -fn scenario_diagnostic_surfaces_values(world: EvaluationWorld) { - let _ = world; -} +fn scenario_diagnostic_surfaces_values(world: EvaluationWorld) { let _ = world; } diff --git a/common/tests/brain_type_metrics_behaviour.rs b/common/tests/brain_type_metrics_behaviour.rs index c01213f3a..ea98687d4 100644 --- a/common/tests/brain_type_metrics_behaviour.rs +++ b/common/tests/brain_type_metrics_behaviour.rs @@ -5,7 +5,11 @@ use std::cell::{Cell, RefCell}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::brain_type_metrics::{ - ForeignReferenceSet, MethodMetrics, TypeMetricsBuilder, brain_methods, foreign_reach_count, + ForeignReferenceSet, + MethodMetrics, + TypeMetricsBuilder, + brain_methods, + foreign_reach_count, weighted_methods_count, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -51,9 +55,7 @@ impl Default for MetricsWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> MetricsWorld { - MetricsWorld::default() -} +fn world() -> MetricsWorld { MetricsWorld::default() } // --- Helpers --- @@ -87,9 +89,7 @@ fn given_method(world: &MetricsWorld, name: String, cc: usize, loc: usize) { } #[given("the brain method CC threshold is {threshold}")] -fn given_cc_threshold(world: &MetricsWorld, threshold: usize) { - world.cc_threshold.set(threshold); -} +fn given_cc_threshold(world: &MetricsWorld, threshold: usize) { world.cc_threshold.set(threshold); } #[given("the brain method LOC threshold is {threshold}")] fn given_loc_threshold(world: &MetricsWorld, threshold: usize) { @@ -97,9 +97,7 @@ fn given_loc_threshold(world: &MetricsWorld, threshold: usize) { } #[given("the LCOM4 value is {value}")] -fn given_lcom4(world: &MetricsWorld, value: usize) { - world.lcom4.set(Some(value)); -} +fn given_lcom4(world: &MetricsWorld, value: usize) { world.lcom4.set(Some(value)); } #[given("the foreign reach count is {count}")] fn given_foreign_reach_count(world: &MetricsWorld, count: usize) { @@ -209,14 +207,10 @@ fn then_type_wmc(world: &MetricsWorld, value: usize) { } #[then("the type has {n} brain method")] -fn then_type_brain_count_singular(world: &MetricsWorld, n: usize) { - assert_brain_count(world, n); -} +fn then_type_brain_count_singular(world: &MetricsWorld, n: usize) { assert_brain_count(world, n); } #[then("the type has {n} brain methods")] -fn then_type_brain_count_plural(world: &MetricsWorld, n: usize) { - assert_brain_count(world, n); -} +fn then_type_brain_count_plural(world: &MetricsWorld, n: usize) { assert_brain_count(world, n); } #[then("the type LCOM4 is {value}")] fn then_type_lcom4(world: &MetricsWorld, value: usize) { @@ -239,51 +233,31 @@ fn then_foreign_reach(world: &MetricsWorld, count: usize) { // here. #[scenario(path = "tests/features/brain_type_metrics.feature", index = 0)] -fn scenario_wmc_sum(world: MetricsWorld) { - let _ = world; -} +fn scenario_wmc_sum(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 1)] -fn scenario_brain_method_qualifies(world: MetricsWorld) { - let _ = world; -} +fn scenario_brain_method_qualifies(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 2)] -fn scenario_below_both_thresholds(world: MetricsWorld) { - let _ = world; -} +fn scenario_below_both_thresholds(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 3)] -fn scenario_only_cc_threshold(world: MetricsWorld) { - let _ = world; -} +fn scenario_only_cc_threshold(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 4)] -fn scenario_only_loc_threshold(world: MetricsWorld) { - let _ = world; -} +fn scenario_only_loc_threshold(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 5)] -fn scenario_empty_type_zero_wmc(world: MetricsWorld) { - let _ = world; -} +fn scenario_empty_type_zero_wmc(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 6)] -fn scenario_type_metrics_aggregate(world: MetricsWorld) { - let _ = world; -} +fn scenario_type_metrics_aggregate(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 7)] -fn scenario_foreign_refs_deduplicated(world: MetricsWorld) { - let _ = world; -} +fn scenario_foreign_refs_deduplicated(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 8)] -fn scenario_macro_expanded_foreign_filtered(world: MetricsWorld) { - let _ = world; -} +fn scenario_macro_expanded_foreign_filtered(world: MetricsWorld) { let _ = world; } #[scenario(path = "tests/features/brain_type_metrics.feature", index = 9)] -fn scenario_foreign_reach_convenience(world: MetricsWorld) { - let _ = world; -} +fn scenario_foreign_reach_convenience(world: MetricsWorld) { let _ = world; } diff --git a/common/tests/cognitive_complexity_behaviour.rs b/common/tests/cognitive_complexity_behaviour.rs index 9b0876384..9d715ec3c 100644 --- a/common/tests/cognitive_complexity_behaviour.rs +++ b/common/tests/cognitive_complexity_behaviour.rs @@ -24,9 +24,7 @@ impl Default for CcWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> CcWorld { - CcWorld::default() -} +fn world() -> CcWorld { CcWorld::default() } // --- Helpers --- @@ -120,41 +118,25 @@ fn then_score_is(world: &CcWorld, expected: usize) { // here. #[scenario(path = "tests/features/cognitive_complexity.feature", index = 0)] -fn scenario_empty_function(world: CcWorld) { - let _ = world; -} +fn scenario_empty_function(world: CcWorld) { let _ = world; } #[scenario(path = "tests/features/cognitive_complexity.feature", index = 1)] -fn scenario_single_if(world: CcWorld) { - let _ = world; -} +fn scenario_single_if(world: CcWorld) { let _ = world; } #[scenario(path = "tests/features/cognitive_complexity.feature", index = 2)] -fn scenario_nested_if(world: CcWorld) { - let _ = world; -} +fn scenario_nested_if(world: CcWorld) { let _ = world; } #[scenario(path = "tests/features/cognitive_complexity.feature", index = 3)] -fn scenario_macro_structural_excluded(world: CcWorld) { - let _ = world; -} +fn scenario_macro_structural_excluded(world: CcWorld) { let _ = world; } #[scenario(path = "tests/features/cognitive_complexity.feature", index = 4)] -fn scenario_macro_nesting_no_inflate(world: CcWorld) { - let _ = world; -} +fn scenario_macro_nesting_no_inflate(world: CcWorld) { let _ = world; } #[scenario(path = "tests/features/cognitive_complexity.feature", index = 5)] -fn scenario_boolean_operators(world: CcWorld) { - let _ = world; -} +fn scenario_boolean_operators(world: CcWorld) { let _ = world; } #[scenario(path = "tests/features/cognitive_complexity.feature", index = 6)] -fn scenario_mixed_real_and_expansion(world: CcWorld) { - let _ = world; -} +fn scenario_mixed_real_and_expansion(world: CcWorld) { let _ = world; } #[scenario(path = "tests/features/cognitive_complexity.feature", index = 7)] -fn scenario_fundamental_from_expansion_excluded(world: CcWorld) { - let _ = world; -} +fn scenario_fundamental_from_expansion_excluded(world: CcWorld) { let _ = world; } diff --git a/common/tests/complexity_signal_behaviour.rs b/common/tests/complexity_signal_behaviour.rs index 7091f462f..72c8c7f85 100644 --- a/common/tests/complexity_signal_behaviour.rs +++ b/common/tests/complexity_signal_behaviour.rs @@ -5,7 +5,11 @@ use std::cell::{Cell, RefCell}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::complexity_signal::{ - LineSegment, SignalBuildError, SmoothingError, rasterize_signal, smooth_moving_average, + LineSegment, + SignalBuildError, + SmoothingError, + rasterize_signal, + smooth_moving_average, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -26,17 +30,11 @@ impl SignalWorld { self.function_end.set(Some(end)); } - fn push_segment(&self, segment: LineSegment) { - self.segments.borrow_mut().push(segment); - } + fn push_segment(&self, segment: LineSegment) { self.segments.borrow_mut().push(segment); } - fn set_raw_signal(&self, signal: Vec) { - self.raw_signal.replace(Some(signal)); - } + fn set_raw_signal(&self, signal: Vec) { self.raw_signal.replace(Some(signal)); } - fn set_smoothing_window(&self, window: usize) { - self.smoothing_window.set(Some(window)); - } + fn set_smoothing_window(&self, window: usize) { self.smoothing_window.set(Some(window)); } #[expect( clippy::expect_used, @@ -101,9 +99,7 @@ impl SignalWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> SignalWorld { - SignalWorld::default() -} +fn world() -> SignalWorld { SignalWorld::default() } /// Parses a comma-separated list of floating-point values. /// @@ -195,19 +191,13 @@ fn given_raw_signal(world: &SignalWorld, values: String) -> Result<(), String> { } #[given("the smoothing window is {window}")] -fn given_window(world: &SignalWorld, window: usize) { - world.set_smoothing_window(window); -} +fn given_window(world: &SignalWorld, window: usize) { world.set_smoothing_window(window); } #[when("I build the per-line signal")] -fn when_build(world: &SignalWorld) { - world.build_signal(); -} +fn when_build(world: &SignalWorld) { world.build_signal(); } #[when("I smooth the signal")] -fn when_smooth(world: &SignalWorld) { - world.smooth(); -} +fn when_smooth(world: &SignalWorld) { world.smooth(); } #[then("the built signal equals {expected}")] fn then_built_signal(world: &SignalWorld, expected: String) -> Result<(), String> { @@ -246,26 +236,16 @@ fn then_smoothing_fails(world: &SignalWorld) { } #[scenario(path = "tests/features/complexity_signal.feature", index = 0)] -fn scenario_overlapping_segments(world: SignalWorld) { - let _ = world; -} +fn scenario_overlapping_segments(world: SignalWorld) { let _ = world; } #[scenario(path = "tests/features/complexity_signal.feature", index = 1)] -fn scenario_out_of_range_segments(world: SignalWorld) { - let _ = world; -} +fn scenario_out_of_range_segments(world: SignalWorld) { let _ = world; } #[scenario(path = "tests/features/complexity_signal.feature", index = 2)] -fn scenario_smoothing_happy_path(world: SignalWorld) { - let _ = world; -} +fn scenario_smoothing_happy_path(world: SignalWorld) { let _ = world; } #[scenario(path = "tests/features/complexity_signal.feature", index = 3)] -fn scenario_smoothing_even_window(world: SignalWorld) { - let _ = world; -} +fn scenario_smoothing_even_window(world: SignalWorld) { let _ = world; } #[scenario(path = "tests/features/complexity_signal.feature", index = 4)] -fn scenario_smoothing_zero_window(world: SignalWorld) { - let _ = world; -} +fn scenario_smoothing_zero_window(world: SignalWorld) { let _ = world; } diff --git a/common/tests/context_behaviour.rs b/common/tests/context_behaviour.rs index 5d0acd154..80c9190bd 100644 --- a/common/tests/context_behaviour.rs +++ b/common/tests/context_behaviour.rs @@ -44,17 +44,11 @@ impl FunctionFixture { self.additional.borrow_mut().clear(); } - fn attributes(&self) -> std::cell::Ref<'_, Vec> { - self.attributes.borrow() - } + fn attributes(&self) -> std::cell::Ref<'_, Vec> { self.attributes.borrow() } - fn context(&self) -> std::cell::Ref<'_, Vec> { - self.context.borrow() - } + fn context(&self) -> std::cell::Ref<'_, Vec> { self.context.borrow() } - fn additional(&self) -> std::cell::Ref<'_, Vec> { - self.additional.borrow() - } + fn additional(&self) -> std::cell::Ref<'_, Vec> { self.additional.borrow() } fn configure_additional(&self, path: &str) { self.additional.borrow_mut().push(AttributePath::from(path)); @@ -63,9 +57,7 @@ impl FunctionFixture { #[allow_fixture_expansion_lints] #[fixture] -fn function() -> FunctionFixture { - FunctionFixture::new() -} +fn function() -> FunctionFixture { FunctionFixture::new() } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] struct Evaluation { is_test: bool, @@ -74,9 +66,7 @@ struct Evaluation { #[allow_fixture_expansion_lints] #[fixture] -fn evaluation() -> Evaluation { - Evaluation::default() -} +fn evaluation() -> Evaluation { Evaluation::default() } #[given("a function annotated with rstest")] fn given_rstest(function: &FunctionFixture) { @@ -91,9 +81,7 @@ fn given_tokio(function: &FunctionFixture) { } #[given("a function without test attributes")] -fn given_plain(function: &FunctionFixture) { - function.clear(); -} +fn given_plain(function: &FunctionFixture) { function.clear(); } #[given("the lint recognizes {path} as a test attribute")] fn given_custom_attribute(function: &FunctionFixture, path: String) { diff --git a/common/tests/cosine_threshold_behaviour.rs b/common/tests/cosine_threshold_behaviour.rs index 18dcf6bb9..ddf9c78ac 100644 --- a/common/tests/cosine_threshold_behaviour.rs +++ b/common/tests/cosine_threshold_behaviour.rs @@ -5,7 +5,8 @@ use std::{cell::RefCell, collections::BTreeMap, str::FromStr}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::{ - MethodProfileBuilder, test_support::decomposition::methods_meet_cosine_threshold, + MethodProfileBuilder, + test_support::decomposition::methods_meet_cosine_threshold, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -40,9 +41,7 @@ impl FromStr for MethodSide { struct CsvList(Vec); impl CsvList { - fn into_vec(self) -> Vec { - self.0 - } + fn into_vec(self) -> Vec { self.0 } } impl FromStr for CsvList { @@ -67,9 +66,7 @@ struct CosineThresholdWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> CosineThresholdWorld { - CosineThresholdWorld::default() -} +fn world() -> CosineThresholdWorld { CosineThresholdWorld::default() } fn ensure_method_builder(world: &CosineThresholdWorld, side: MethodSide, method_name: &str) { world.methods.borrow_mut().insert( @@ -191,16 +188,10 @@ fn then_methods_are_not_similar(world: &CosineThresholdWorld) -> Result<(), Stri // `tests/features/cosine_threshold.feature`. #[scenario(path = "tests/features/cosine_threshold.feature", index = 0)] -fn scenario_strong_overlap(world: CosineThresholdWorld) { - let _ = world; -} +fn scenario_strong_overlap(world: CosineThresholdWorld) { let _ = world; } #[scenario(path = "tests/features/cosine_threshold.feature", index = 1)] -fn scenario_below_threshold(world: CosineThresholdWorld) { - let _ = world; -} +fn scenario_below_threshold(world: CosineThresholdWorld) { let _ = world; } #[scenario(path = "tests/features/cosine_threshold.feature", index = 2)] -fn scenario_zero_vector(world: CosineThresholdWorld) { - let _ = world; -} +fn scenario_zero_vector(world: CosineThresholdWorld) { let _ = world; } diff --git a/common/tests/decomposition_adjacency_behaviour.rs b/common/tests/decomposition_adjacency_behaviour.rs index d257c31a3..8c67c27dc 100644 --- a/common/tests/decomposition_adjacency_behaviour.rs +++ b/common/tests/decomposition_adjacency_behaviour.rs @@ -5,7 +5,10 @@ use std::cell::RefCell; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::test_support::decomposition::{ - AdjacencyError, AdjacencyReport, EdgeInput, adjacency_report, + AdjacencyError, + AdjacencyReport, + EdgeInput, + adjacency_report, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -18,9 +21,7 @@ struct AdjacencyWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> AdjacencyWorld { - AdjacencyWorld::default() -} +fn world() -> AdjacencyWorld { AdjacencyWorld::default() } #[given("a graph with {count} nodes")] fn given_graph_with_nodes(world: &AdjacencyWorld, count: usize) { @@ -134,21 +135,13 @@ fn then_neighbours_of_node_are_sorted(world: &AdjacencyWorld, node: usize) -> Re // `tests/features/decomposition_adjacency.feature`. #[scenario(path = "tests/features/decomposition_adjacency.feature", index = 0)] -fn scenario_valid_edges_produce_symmetric_neighbour_lists(world: AdjacencyWorld) { - let _ = world; -} +fn scenario_valid_edges_produce_symmetric_neighbour_lists(world: AdjacencyWorld) { let _ = world; } #[scenario(path = "tests/features/decomposition_adjacency.feature", index = 1)] -fn scenario_malformed_edge_input_rejected_canonical_order(world: AdjacencyWorld) { - let _ = world; -} +fn scenario_malformed_edge_input_rejected_canonical_order(world: AdjacencyWorld) { let _ = world; } #[scenario(path = "tests/features/decomposition_adjacency.feature", index = 2)] -fn scenario_isolated_nodes_have_empty_neighbour_lists(world: AdjacencyWorld) { - let _ = world; -} +fn scenario_isolated_nodes_have_empty_neighbour_lists(world: AdjacencyWorld) { let _ = world; } #[scenario(path = "tests/features/decomposition_adjacency.feature", index = 3)] -fn scenario_multiple_neighbours_appear_in_sorted_order(world: AdjacencyWorld) { - let _ = world; -} +fn scenario_multiple_neighbours_appear_in_sorted_order(world: AdjacencyWorld) { let _ = world; } diff --git a/common/tests/decomposition_advice_behaviour.rs b/common/tests/decomposition_advice_behaviour.rs index d2798aa28..e67e2592c 100644 --- a/common/tests/decomposition_advice_behaviour.rs +++ b/common/tests/decomposition_advice_behaviour.rs @@ -5,8 +5,12 @@ use std::{cell::RefCell, collections::BTreeMap}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::decomposition_advice::{ - DecompositionContext, DecompositionSuggestion, MethodProfileBuilder, SubjectKind, - SuggestedExtractionKind, suggest_decomposition, + DecompositionContext, + DecompositionSuggestion, + MethodProfileBuilder, + SubjectKind, + SuggestedExtractionKind, + suggest_decomposition, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -14,9 +18,7 @@ use whitaker_test_macros::allow_fixture_expansion_lints; struct CsvList(Vec); impl CsvList { - fn into_vec(self) -> Vec { - self.0 - } + fn into_vec(self) -> Vec { self.0 } } impl std::str::FromStr for CsvList { @@ -44,9 +46,7 @@ struct DecompositionWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> DecompositionWorld { - DecompositionWorld::default() -} +fn world() -> DecompositionWorld { DecompositionWorld::default() } fn create_method_builder(world: &DecompositionWorld, method_name: &str) -> usize { let mut next_method_id = world.next_method_id.borrow_mut(); @@ -152,9 +152,7 @@ fn given_context(world: &DecompositionWorld, kind: SubjectKind, name: String) { } #[given("a method named {name}")] -fn given_method(world: &DecompositionWorld, name: String) { - create_method_builder(world, &name); -} +fn given_method(world: &DecompositionWorld, name: String) { create_method_builder(world, &name); } #[given("method {name} accesses fields {fields}")] fn given_fields(world: &DecompositionWorld, name: String, fields: CsvList) { @@ -301,26 +299,16 @@ fn then_suggestion_has_rationale( // `tests/features/decomposition_advice.feature` file. #[scenario(path = "tests/features/decomposition_advice.feature", index = 0)] -fn scenario_type_method_groups(world: DecompositionWorld) { - let _ = world; -} +fn scenario_type_method_groups(world: DecompositionWorld) { let _ = world; } #[scenario(path = "tests/features/decomposition_advice.feature", index = 1)] -fn scenario_trait_sub_traits(world: DecompositionWorld) { - let _ = world; -} +fn scenario_trait_sub_traits(world: DecompositionWorld) { let _ = world; } #[scenario(path = "tests/features/decomposition_advice.feature", index = 2)] -fn scenario_no_suggestions(world: DecompositionWorld) { - let _ = world; -} +fn scenario_no_suggestions(world: DecompositionWorld) { let _ = world; } #[scenario(path = "tests/features/decomposition_advice.feature", index = 3)] -fn scenario_singleton_noise(world: DecompositionWorld) { - let _ = world; -} +fn scenario_singleton_noise(world: DecompositionWorld) { let _ = world; } #[scenario(path = "tests/features/decomposition_advice.feature", index = 4)] -fn scenario_local_type_groups(world: DecompositionWorld) { - let _ = world; -} +fn scenario_local_type_groups(world: DecompositionWorld) { let _ = world; } diff --git a/common/tests/decomposition_diagnostic_notes_behaviour.rs b/common/tests/decomposition_diagnostic_notes_behaviour.rs index f701f7aa5..c94fce259 100644 --- a/common/tests/decomposition_diagnostic_notes_behaviour.rs +++ b/common/tests/decomposition_diagnostic_notes_behaviour.rs @@ -6,7 +6,10 @@ use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::{ decomposition_advice::{ - DecompositionContext, MethodProfileBuilder, SubjectKind, format_diagnostic_note, + DecompositionContext, + MethodProfileBuilder, + SubjectKind, + format_diagnostic_note, suggest_decomposition, }, test_support::decomposition::{parser_serde_fs_fixture, transport_trait_fixture}, @@ -17,18 +20,14 @@ use whitaker_test_macros::allow_fixture_expansion_lints; struct CsvList(Vec); impl CsvList { - fn into_vec(self) -> Vec { - self.0 - } + fn into_vec(self) -> Vec { self.0 } } #[derive(Debug, Clone)] struct QuotedString(String); impl QuotedString { - fn into_inner(self) -> String { - self.0 - } + fn into_inner(self) -> String { self.0 } } impl std::str::FromStr for CsvList { @@ -48,9 +47,7 @@ impl std::str::FromStr for CsvList { impl std::str::FromStr for QuotedString { type Err = std::convert::Infallible; - fn from_str(s: &str) -> Result { - Ok(Self(s.trim_matches('"').to_owned())) - } + fn from_str(s: &str) -> Result { Ok(Self(s.trim_matches('"').to_owned())) } } #[derive(Debug, Default)] @@ -65,9 +62,7 @@ struct DiagnosticNoteWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> DiagnosticNoteWorld { - DiagnosticNoteWorld::default() -} +fn world() -> DiagnosticNoteWorld { DiagnosticNoteWorld::default() } fn create_method_builder(world: &DiagnosticNoteWorld, method_name: &str) -> usize { let mut next_method_id = world.next_method_id.borrow_mut(); @@ -164,9 +159,7 @@ fn given_context(world: &DiagnosticNoteWorld, kind: SubjectKind, name: String) { } #[given("a method named {name}")] -fn given_method(world: &DiagnosticNoteWorld, name: String) { - create_method_builder(world, &name); -} +fn given_method(world: &DiagnosticNoteWorld, name: String) { create_method_builder(world, &name); } #[given("the parser, serde, and filesystem methods are tracked")] fn given_parser_serde_fs_fixture(world: &DiagnosticNoteWorld) { @@ -280,38 +273,28 @@ fn then_note_does_not_contain( path = "tests/features/decomposition_diagnostic_notes.feature", index = 0 )] -fn scenario_type_note_renders_three_areas(world: DiagnosticNoteWorld) { - let _ = world; -} +fn scenario_type_note_renders_three_areas(world: DiagnosticNoteWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_diagnostic_notes.feature", index = 1 )] -fn scenario_trait_note_renders_sub_traits(world: DiagnosticNoteWorld) { - let _ = world; -} +fn scenario_trait_note_renders_sub_traits(world: DiagnosticNoteWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_diagnostic_notes.feature", index = 2 )] -fn scenario_no_suggestions_yield_no_note(world: DiagnosticNoteWorld) { - let _ = world; -} +fn scenario_no_suggestions_yield_no_note(world: DiagnosticNoteWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_diagnostic_notes.feature", index = 3 )] -fn scenario_large_subjects_cap_rendered_areas(world: DiagnosticNoteWorld) { - let _ = world; -} +fn scenario_large_subjects_cap_rendered_areas(world: DiagnosticNoteWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_diagnostic_notes.feature", index = 4 )] -fn scenario_large_communities_cap_method_names(world: DiagnosticNoteWorld) { - let _ = world; -} +fn scenario_large_communities_cap_method_names(world: DiagnosticNoteWorld) { let _ = world; } diff --git a/common/tests/decomposition_label_propagation_behaviour.rs b/common/tests/decomposition_label_propagation_behaviour.rs index c46d6e765..f69744517 100644 --- a/common/tests/decomposition_label_propagation_behaviour.rs +++ b/common/tests/decomposition_label_propagation_behaviour.rs @@ -5,7 +5,10 @@ use std::{cell::RefCell, str::FromStr}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::test_support::decomposition::{ - AdjacencyError, EdgeInput, LabelPropagationReport, label_propagation_report, + AdjacencyError, + EdgeInput, + LabelPropagationReport, + label_propagation_report, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -13,9 +16,7 @@ use whitaker_test_macros::allow_fixture_expansion_lints; struct CsvList(Vec); impl CsvList { - fn into_vec(self) -> Vec { - self.0 - } + fn into_vec(self) -> Vec { self.0 } } impl FromStr for CsvList { @@ -37,9 +38,7 @@ impl FromStr for CsvList { struct CsvLabels(Vec); impl CsvLabels { - fn as_slice(&self) -> &[usize] { - &self.0 - } + fn as_slice(&self) -> &[usize] { &self.0 } } impl FromStr for CsvLabels { @@ -70,9 +69,7 @@ struct LabelPropagationWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> LabelPropagationWorld { - LabelPropagationWorld::default() -} +fn world() -> LabelPropagationWorld { LabelPropagationWorld::default() } #[given("methods named {method_names} are tracked")] fn given_methods(world: &LabelPropagationWorld, method_names: CsvList) { @@ -201,9 +198,7 @@ fn scenario_disconnected_pairs_settle_to_shared_labels(world: LabelPropagationWo path = "tests/features/decomposition_label_propagation.feature", index = 1 )] -fn scenario_isolated_nodes_keep_their_own_labels(world: LabelPropagationWorld) { - let _ = world; -} +fn scenario_isolated_nodes_keep_their_own_labels(world: LabelPropagationWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_label_propagation.feature", @@ -217,14 +212,10 @@ fn scenario_zero_iteration_bound_keeps_initial_labels(world: LabelPropagationWor path = "tests/features/decomposition_label_propagation.feature", index = 3 )] -fn scenario_equal_scores_break_ties_lexically(world: LabelPropagationWorld) { - let _ = world; -} +fn scenario_equal_scores_break_ties_lexically(world: LabelPropagationWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_label_propagation.feature", index = 4 )] -fn scenario_invalid_edge_input_is_rejected(world: LabelPropagationWorld) { - let _ = world; -} +fn scenario_invalid_edge_input_is_rejected(world: LabelPropagationWorld) { let _ = world; } diff --git a/common/tests/decomposition_vector_algebra_behaviour.rs b/common/tests/decomposition_vector_algebra_behaviour.rs index feb08aac8..03a019000 100644 --- a/common/tests/decomposition_vector_algebra_behaviour.rs +++ b/common/tests/decomposition_vector_algebra_behaviour.rs @@ -41,9 +41,7 @@ impl FromStr for MethodSide { struct CsvList(Vec); impl CsvList { - fn into_vec(self) -> Vec { - self.0 - } + fn into_vec(self) -> Vec { self.0 } } impl FromStr for CsvList { @@ -68,9 +66,7 @@ struct VectorAlgebraWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> VectorAlgebraWorld { - VectorAlgebraWorld::default() -} +fn world() -> VectorAlgebraWorld { VectorAlgebraWorld::default() } fn ensure_method_builder(world: &VectorAlgebraWorld, side: MethodSide, method_name: &str) { world.methods.borrow_mut().insert( @@ -223,17 +219,13 @@ fn then_dot_product_is_zero(world: &VectorAlgebraWorld) -> Result<(), String> { path = "tests/features/decomposition_vector_algebra.feature", index = 0 )] -fn scenario_shared_field_preserves_commutativity(world: VectorAlgebraWorld) { - let _ = world; -} +fn scenario_shared_field_preserves_commutativity(world: VectorAlgebraWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_vector_algebra.feature", index = 1 )] -fn scenario_empty_method_has_non_negative_norm(world: VectorAlgebraWorld) { - let _ = world; -} +fn scenario_empty_method_has_non_negative_norm(world: VectorAlgebraWorld) { let _ = world; } #[scenario( path = "tests/features/decomposition_vector_algebra.feature", diff --git a/common/tests/i18n_behaviour.rs b/common/tests/i18n_behaviour.rs index a5a9a6c5b..1e4bdf600 100644 --- a/common/tests/i18n_behaviour.rs +++ b/common/tests/i18n_behaviour.rs @@ -23,9 +23,7 @@ struct I18nFixture { } impl I18nFixture { - fn set_locale(&self, locale: Option) { - *self.locale.borrow_mut() = locale; - } + fn set_locale(&self, locale: Option) { *self.locale.borrow_mut() = locale; } fn ensure_localizer(&self) -> Localizer { let locale = self.locale.borrow().clone(); @@ -63,19 +61,13 @@ fn branch_count_from_key(key: &str) -> Option<(String, u32)> { #[allow_fixture_expansion_lints] #[fixture] -fn fixture() -> I18nFixture { - I18nFixture::default() -} +fn fixture() -> I18nFixture { I18nFixture::default() } #[given("no locale preference")] -fn given_no_locale(fixture: &I18nFixture) { - fixture.set_locale(None); -} +fn given_no_locale(fixture: &I18nFixture) { fixture.set_locale(None); } #[given("the locale preference {locale}")] -fn given_locale(fixture: &I18nFixture, locale: String) { - fixture.set_locale(Some(locale)); -} +fn given_locale(fixture: &I18nFixture, locale: String) { fixture.set_locale(Some(locale)); } #[when("I request the message for {key}")] fn when_message(fixture: &I18nFixture, key: String) { @@ -166,69 +158,43 @@ fn then_missing(fixture: &I18nFixture) { } #[scenario(path = "tests/features/i18n_loader.feature", index = 0)] -fn scenario_falls_back(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_falls_back(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 1)] -fn scenario_secondary_locale(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_secondary_locale(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 2)] -fn scenario_gaelic_plural(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_gaelic_plural(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 3)] -fn scenario_welsh_lint_count_zero(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_lint_count_zero(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 4)] -fn scenario_welsh_lint_count_large(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_lint_count_large(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 5)] -fn scenario_welsh_lint_count_one(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_lint_count_one(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 6)] -fn scenario_welsh_lint_count_two(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_lint_count_two(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 7)] -fn scenario_welsh_lint_count_three(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_lint_count_three(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 8)] -fn scenario_welsh_lint_count_six(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_lint_count_six(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 9)] -fn scenario_welsh_lint_count_eleven(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_lint_count_eleven(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 10)] -fn scenario_attribute_falls_back(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_attribute_falls_back(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 11)] -fn scenario_missing_message(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_missing_message(fixture: I18nFixture) { let _ = fixture; } #[scenario(path = "tests/features/i18n_loader.feature", index = 12)] -fn scenario_welsh_conditional_note_lenition(fixture: I18nFixture) { - let _ = fixture; -} +fn scenario_welsh_conditional_note_lenition(fixture: I18nFixture) { let _ = fixture; } #[cfg(test)] mod tests { diff --git a/common/tests/i18n_packaging.rs b/common/tests/i18n_packaging.rs index b3c853d78..7c9ee7eb5 100644 --- a/common/tests/i18n_packaging.rs +++ b/common/tests/i18n_packaging.rs @@ -14,7 +14,8 @@ mod unix { use std::{ error::Error, - fs, io, + fs, + io, path::{Path, PathBuf}, process::Command, }; diff --git a/common/tests/lcom4_behaviour.rs b/common/tests/lcom4_behaviour.rs index 903a6d1ed..609978190 100644 --- a/common/tests/lcom4_behaviour.rs +++ b/common/tests/lcom4_behaviour.rs @@ -17,25 +17,19 @@ struct LcomWorld { } impl LcomWorld { - fn push_method(&self, method: MethodInfo) { - self.methods.borrow_mut().push(method); - } + fn push_method(&self, method: MethodInfo) { self.methods.borrow_mut().push(method); } fn compute(&self) { let methods = self.methods.borrow(); self.result.set(Some(cohesion_components(&methods))); } - const fn result(&self) -> Option { - self.result.get() - } + const fn result(&self) -> Option { self.result.get() } } #[allow_fixture_expansion_lints] #[fixture] -fn world() -> LcomWorld { - LcomWorld::default() -} +fn world() -> LcomWorld { LcomWorld::default() } /// Parses a comma-separated list of field names into a `BTreeSet`. /// @@ -73,9 +67,7 @@ fn given_method_no_fields_calling(world: &LcomWorld, name: String, callee: Strin } #[when("I compute LCOM4")] -fn when_compute(world: &LcomWorld) { - world.compute(); -} +fn when_compute(world: &LcomWorld) { world.compute(); } #[then("the component count is {count}")] fn then_component_count(world: &LcomWorld, count: usize) { @@ -87,44 +79,28 @@ fn then_component_count(world: &LcomWorld, count: usize) { // scenarios in the feature file requires updating the indices here. #[scenario(path = "tests/features/lcom4.feature", index = 0)] -fn scenario_single_method(world: LcomWorld) { - let _ = world; -} +fn scenario_single_method(world: LcomWorld) { let _ = world; } #[scenario(path = "tests/features/lcom4.feature", index = 1)] -fn scenario_shared_field(world: LcomWorld) { - let _ = world; -} +fn scenario_shared_field(world: LcomWorld) { let _ = world; } #[scenario(path = "tests/features/lcom4.feature", index = 2)] -fn scenario_direct_call(world: LcomWorld) { - let _ = world; -} +fn scenario_direct_call(world: LcomWorld) { let _ = world; } #[scenario(path = "tests/features/lcom4.feature", index = 3)] -fn scenario_disjoint_methods(world: LcomWorld) { - let _ = world; -} +fn scenario_disjoint_methods(world: LcomWorld) { let _ = world; } #[scenario(path = "tests/features/lcom4.feature", index = 4)] -fn scenario_transitive_sharing(world: LcomWorld) { - let _ = world; -} +fn scenario_transitive_sharing(world: LcomWorld) { let _ = world; } #[scenario(path = "tests/features/lcom4.feature", index = 5)] -fn scenario_empty_type(world: LcomWorld) { - let _ = world; -} +fn scenario_empty_type(world: LcomWorld) { let _ = world; } #[scenario(path = "tests/features/lcom4.feature", index = 6)] -fn scenario_isolated_methods(world: LcomWorld) { - let _ = world; -} +fn scenario_isolated_methods(world: LcomWorld) { let _ = world; } #[scenario(path = "tests/features/lcom4.feature", index = 7)] -fn scenario_self_call(world: LcomWorld) { - let _ = world; -} +fn scenario_self_call(world: LcomWorld) { let _ = world; } // --- Unit tests for parse_field_set --- diff --git a/common/tests/localizer_helpers.rs b/common/tests/localizer_helpers.rs index 754beedfb..c7d45827f 100644 --- a/common/tests/localizer_helpers.rs +++ b/common/tests/localizer_helpers.rs @@ -14,8 +14,16 @@ use rstest::{fixture, rstest}; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::{ i18n::{ - Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, - get_localizer_for_lint, noop_reporter, safe_resolve_message_set, testing::RecordingEmitter, + Arguments, + DiagnosticMessageSet, + FluentValue, + Localizer, + MessageKey, + MessageResolution, + get_localizer_for_lint, + noop_reporter, + safe_resolve_message_set, + testing::RecordingEmitter, }, test_support::with_locale, }; @@ -100,9 +108,7 @@ impl HelperWorld { } } - fn set_message_key(&self, key: String) { - self.message_key.borrow_mut().replace(key); - } + fn set_message_key(&self, key: String) { self.message_key.borrow_mut().replace(key); } fn set_fallback_messages(&self) { let fallback = DiagnosticMessageSet::new( @@ -126,13 +132,9 @@ impl HelperWorld { .clone() } - fn ensure_arguments(&self) -> Arguments<'static> { - self.arguments.borrow().clone() - } + fn ensure_arguments(&self) -> Arguments<'static> { self.arguments.borrow().clone() } - fn clear_arguments(&self) { - *self.arguments.borrow_mut() = Arguments::default(); - } + fn clear_arguments(&self) { *self.arguments.borrow_mut() = Arguments::default(); } fn prepare_doc_arguments(&self) { let mut args: Arguments<'static> = Arguments::default(); @@ -180,21 +182,15 @@ impl HelperWorld { .ok_or_else(|| String::from("diagnostic messages should be resolved")) } - fn recorded_messages(&self) -> Vec { - self.emitter.recorded_messages() - } + fn recorded_messages(&self) -> Vec { self.emitter.recorded_messages() } } #[allow_fixture_expansion_lints] #[fixture] -fn world() -> HelperWorld { - HelperWorld::new() -} +fn world() -> HelperWorld { HelperWorld::new() } #[given("DYLINT_LOCALE is not set")] -fn given_env_cleared(world: &HelperWorld) { - world.set_environment(None); -} +fn given_env_cleared(world: &HelperWorld) { world.set_environment(None); } #[given("DYLINT_LOCALE is {locale}")] fn given_env(world: &HelperWorld, locale: String) { @@ -202,9 +198,7 @@ fn given_env(world: &HelperWorld, locale: String) { } #[given("no configuration locale is provided")] -fn given_no_config(world: &HelperWorld) { - world.set_configuration(None); -} +fn given_no_config(world: &HelperWorld) { world.set_configuration(None); } #[given("the configuration locale is {locale}")] fn given_config(world: &HelperWorld, locale: String) { @@ -223,9 +217,7 @@ fn then_locale(world: &HelperWorld, locale: String) -> Result<(), String> { } #[given("fallback messages are defined")] -fn given_fallback(world: &HelperWorld) { - world.set_fallback_messages(); -} +fn given_fallback(world: &HelperWorld) { world.set_fallback_messages(); } #[given("a missing message key {key} is requested")] fn given_missing_key(world: &HelperWorld, key: String) { @@ -238,19 +230,13 @@ fn given_message_key(world: &HelperWorld, key: String) { } #[given("I prepare arguments for the doc attribute diagnostic")] -fn given_doc_arguments(world: &HelperWorld) { - world.prepare_doc_arguments(); -} +fn given_doc_arguments(world: &HelperWorld) { world.prepare_doc_arguments(); } #[given("I do not prepare arguments for the doc attribute diagnostic")] -fn given_no_doc_arguments(world: &HelperWorld) { - world.clear_arguments(); -} +fn given_no_doc_arguments(world: &HelperWorld) { world.clear_arguments(); } #[when("I resolve the diagnostic message set")] -fn when_resolve_messages(world: &HelperWorld) -> Result<(), String> { - world.resolve_messages() -} +fn when_resolve_messages(world: &HelperWorld) -> Result<(), String> { world.resolve_messages() } #[then("the fallback primary message contains {snippet}")] fn then_fallback_primary(world: &HelperWorld, snippet: String) -> Result<(), String> { @@ -300,29 +286,19 @@ fn then_no_bug(world: &HelperWorld) { } #[scenario("tests/features/localizer_helpers.feature", index = 0)] -fn scenario_fallback_to_default(world: HelperWorld) { - let _ = world; -} +fn scenario_fallback_to_default(world: HelperWorld) { let _ = world; } #[scenario("tests/features/localizer_helpers.feature", index = 1)] -fn scenario_environment_locale(world: HelperWorld) { - let _ = world; -} +fn scenario_environment_locale(world: HelperWorld) { let _ = world; } #[scenario("tests/features/localizer_helpers.feature", index = 2)] -fn scenario_localization_fallback(world: HelperWorld) { - let _ = world; -} +fn scenario_localization_fallback(world: HelperWorld) { let _ = world; } #[scenario("tests/features/localizer_helpers.feature", index = 3)] -fn scenario_localization_success(world: HelperWorld) { - let _ = world; -} +fn scenario_localization_success(world: HelperWorld) { let _ = world; } #[scenario("tests/features/localizer_helpers.feature", index = 4)] -fn scenario_interpolation_failure(world: HelperWorld) { - let _ = world; -} +fn scenario_interpolation_failure(world: HelperWorld) { let _ = world; } #[test] fn invalid_locale_warns_and_falls_back() { diff --git a/common/tests/method_extraction_behaviour.rs b/common/tests/method_extraction_behaviour.rs index 93a4e4edb..c7717f11b 100644 --- a/common/tests/method_extraction_behaviour.rs +++ b/common/tests/method_extraction_behaviour.rs @@ -49,20 +49,14 @@ impl ExtractionWorld { self.with_result(|info| info.called_methods().contains(method)) } - fn fields_empty(&self) -> bool { - self.with_result(|info| info.accessed_fields().is_empty()) - } + fn fields_empty(&self) -> bool { self.with_result(|info| info.accessed_fields().is_empty()) } - fn calls_empty(&self) -> bool { - self.with_result(|info| info.called_methods().is_empty()) - } + fn calls_empty(&self) -> bool { self.with_result(|info| info.called_methods().is_empty()) } } #[allow_fixture_expansion_lints] #[fixture] -fn world() -> ExtractionWorld { - ExtractionWorld::default() -} +fn world() -> ExtractionWorld { ExtractionWorld::default() } // --- Given steps --- @@ -94,9 +88,7 @@ fn given_call_expanded(world: &ExtractionWorld, method: String) { // --- When steps --- #[when("the method info is built")] -fn when_build(world: &ExtractionWorld) { - world.build(); -} +fn when_build(world: &ExtractionWorld) { world.build(); } // --- Then steps --- @@ -148,36 +140,22 @@ fn then_calls_empty(world: &ExtractionWorld) { // here. #[scenario(path = "tests/features/method_extraction.feature", index = 0)] -fn scenario_field_access_recorded(world: ExtractionWorld) { - let _ = world; -} +fn scenario_field_access_recorded(world: ExtractionWorld) { let _ = world; } #[scenario(path = "tests/features/method_extraction.feature", index = 1)] -fn scenario_method_call_recorded(world: ExtractionWorld) { - let _ = world; -} +fn scenario_method_call_recorded(world: ExtractionWorld) { let _ = world; } #[scenario(path = "tests/features/method_extraction.feature", index = 2)] -fn scenario_macro_field_filtered(world: ExtractionWorld) { - let _ = world; -} +fn scenario_macro_field_filtered(world: ExtractionWorld) { let _ = world; } #[scenario(path = "tests/features/method_extraction.feature", index = 3)] -fn scenario_macro_call_filtered(world: ExtractionWorld) { - let _ = world; -} +fn scenario_macro_call_filtered(world: ExtractionWorld) { let _ = world; } #[scenario(path = "tests/features/method_extraction.feature", index = 4)] -fn scenario_all_expansion_empty(world: ExtractionWorld) { - let _ = world; -} +fn scenario_all_expansion_empty(world: ExtractionWorld) { let _ = world; } #[scenario(path = "tests/features/method_extraction.feature", index = 5)] -fn scenario_empty_builder(world: ExtractionWorld) { - let _ = world; -} +fn scenario_empty_builder(world: ExtractionWorld) { let _ = world; } #[scenario(path = "tests/features/method_extraction.feature", index = 6)] -fn scenario_multiple_accumulate(world: ExtractionWorld) { - let _ = world; -} +fn scenario_multiple_accumulate(world: ExtractionWorld) { let _ = world; } diff --git a/common/tests/rstest_detection_behaviour.rs b/common/tests/rstest_detection_behaviour.rs index b68d386a3..ac83dcad9 100644 --- a/common/tests/rstest_detection_behaviour.rs +++ b/common/tests/rstest_detection_behaviour.rs @@ -7,9 +7,15 @@ use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::{ attributes::{Attribute, AttributeKind, AttributePath}, rstest::{ - ExpansionTrace, ParameterBinding, RstestDetectionOptions, RstestParameter, - RstestParameterKind, classify_rstest_parameter, fixture_local_names, - is_rstest_fixture_with, is_rstest_test_with, + ExpansionTrace, + ParameterBinding, + RstestDetectionOptions, + RstestParameter, + RstestParameterKind, + classify_rstest_parameter, + fixture_local_names, + is_rstest_fixture_with, + is_rstest_test_with, }, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -106,19 +112,13 @@ impl DetectionWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> DetectionWorld { - DetectionWorld::default() -} +fn world() -> DetectionWorld { DetectionWorld::default() } #[given("a function annotated with rstest")] -fn given_rstest_function(world: &DetectionWorld) { - world.push_attribute("rstest"); -} +fn given_rstest_function(world: &DetectionWorld) { world.push_attribute("rstest"); } #[given("a function annotated with rstest::fixture")] -fn given_rstest_fixture(world: &DetectionWorld) { - world.push_attribute("rstest::fixture"); -} +fn given_rstest_fixture(world: &DetectionWorld) { world.push_attribute("rstest::fixture"); } #[given("a parameter named db")] fn given_fixture_local_parameter(world: &DetectionWorld) { @@ -142,14 +142,10 @@ fn given_unsupported_parameter(world: &DetectionWorld) { } #[given("the expansion trace contains rstest")] -fn given_trace(world: &DetectionWorld) { - world.set_trace("rstest"); -} +fn given_trace(world: &DetectionWorld) { world.set_trace("rstest"); } #[given("expansion fallback is enabled")] -fn given_fallback_enabled(world: &DetectionWorld) { - world.enable_trace_fallback(); -} +fn given_fallback_enabled(world: &DetectionWorld) { world.enable_trace_fallback(); } #[given("a function annotated with rstest and allow")] fn given_rstest_and_allow(world: &DetectionWorld) { @@ -179,14 +175,10 @@ fn given_multi_frame_trace(world: &DetectionWorld) { } #[when("I check whether the function is an rstest test")] -fn when_check_test(world: &DetectionWorld) { - world.evaluate_test(); -} +fn when_check_test(world: &DetectionWorld) { world.evaluate_test(); } #[when("I check whether the function is an rstest fixture")] -fn when_check_fixture(world: &DetectionWorld) { - world.evaluate_fixture(); -} +fn when_check_fixture(world: &DetectionWorld) { world.evaluate_fixture(); } #[when("I classify the parameter")] fn when_classify_parameter(world: &DetectionWorld) -> Result<(), String> { @@ -194,9 +186,7 @@ fn when_classify_parameter(world: &DetectionWorld) -> Result<(), String> { } #[when("I evaluate fixture-local names")] -fn when_fixture_names_evaluated(world: &DetectionWorld) { - world.evaluate_fixture_names(); -} +fn when_fixture_names_evaluated(world: &DetectionWorld) { world.evaluate_fixture_names(); } #[then("the function is recognized as an rstest test")] fn then_test_positive(world: &DetectionWorld) { diff --git a/common/tests/rstest_fingerprint_behaviour.rs b/common/tests/rstest_fingerprint_behaviour.rs index 8666c5901..82e5a0876 100644 --- a/common/tests/rstest_fingerprint_behaviour.rs +++ b/common/tests/rstest_fingerprint_behaviour.rs @@ -5,8 +5,14 @@ use std::cell::RefCell; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::rstest::{ - ArgAtom, ArgFingerprint, CalleeShape, ExprShape, LocalSlot, ParagraphFingerprint, - ParagraphNormalizer, StmtShape, + ArgAtom, + ArgFingerprint, + CalleeShape, + ExprShape, + LocalSlot, + ParagraphFingerprint, + ParagraphNormalizer, + StmtShape, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -52,19 +58,13 @@ impl FingerprintWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> FingerprintWorld { - FingerprintWorld::default() -} +fn world() -> FingerprintWorld { FingerprintWorld::default() } #[given("helper-call arguments for fixture db and literal 42")] -fn given_helper_args(world: &FingerprintWorld) { - world.set_first_args(helper_args()); -} +fn given_helper_args(world: &FingerprintWorld) { world.set_first_args(helper_args()); } #[given("matching helper-call arguments for fixture db and literal 42")] -fn given_matching_helper_args(world: &FingerprintWorld) { - world.set_second_args(helper_args()); -} +fn given_matching_helper_args(world: &FingerprintWorld) { world.set_second_args(helper_args()); } #[given("helper-call arguments containing an unsupported argument")] fn given_unsupported_args(world: &FingerprintWorld) { @@ -95,24 +95,16 @@ fn given_two_arg_paragraph(world: &FingerprintWorld) { } #[when("I compare the argument fingerprints")] -fn when_compare_args(world: &FingerprintWorld) { - world.compare_args(); -} +fn when_compare_args(world: &FingerprintWorld) { world.compare_args(); } #[when("I compare the paragraph fingerprints")] -fn when_compare_paragraphs(world: &FingerprintWorld) { - world.compare_paragraphs(); -} +fn when_compare_paragraphs(world: &FingerprintWorld) { world.compare_paragraphs(); } #[when("I inspect the argument fingerprint")] -fn when_inspect_args(world: &FingerprintWorld) { - let _ = world; -} +fn when_inspect_args(world: &FingerprintWorld) { let _ = world; } #[when("I inspect the paragraph fingerprint")] -fn when_inspect_paragraph(world: &FingerprintWorld) { - let _ = world; -} +fn when_inspect_paragraph(world: &FingerprintWorld) { let _ = world; } #[then("the argument fingerprints match")] fn then_args_match(world: &FingerprintWorld) { @@ -175,26 +167,16 @@ fn setup_paragraph(first: &str, second: &str, constructor_argc: usize) -> Paragr } #[scenario(path = "tests/features/rstest_fingerprint.feature", index = 0)] -fn scenario_equivalent_arguments_match(world: FingerprintWorld) { - let _ = world; -} +fn scenario_equivalent_arguments_match(world: FingerprintWorld) { let _ = world; } #[scenario(path = "tests/features/rstest_fingerprint.feature", index = 1)] -fn scenario_renamed_paragraphs_match(world: FingerprintWorld) { - let _ = world; -} +fn scenario_renamed_paragraphs_match(world: FingerprintWorld) { let _ = world; } #[scenario(path = "tests/features/rstest_fingerprint.feature", index = 2)] -fn scenario_unsupported_arguments_remain_explicit(world: FingerprintWorld) { - let _ = world; -} +fn scenario_unsupported_arguments_remain_explicit(world: FingerprintWorld) { let _ = world; } #[scenario(path = "tests/features/rstest_fingerprint.feature", index = 3)] -fn scenario_structural_paragraphs_diverge(world: FingerprintWorld) { - let _ = world; -} +fn scenario_structural_paragraphs_diverge(world: FingerprintWorld) { let _ = world; } #[scenario(path = "tests/features/rstest_fingerprint.feature", index = 4)] -fn scenario_first_appearance_order_controls_slots(world: FingerprintWorld) { - let _ = world; -} +fn scenario_first_appearance_order_controls_slots(world: FingerprintWorld) { let _ = world; } diff --git a/common/tests/rstest_span_recovery_behaviour.rs b/common/tests/rstest_span_recovery_behaviour.rs index a98e73d02..1e7900d95 100644 --- a/common/tests/rstest_span_recovery_behaviour.rs +++ b/common/tests/rstest_span_recovery_behaviour.rs @@ -33,29 +33,19 @@ impl SpanRecoveryWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> SpanRecoveryWorld { - SpanRecoveryWorld::default() -} +fn world() -> SpanRecoveryWorld { SpanRecoveryWorld::default() } #[given("a direct user-editable span at line {line}")] -fn given_direct_span(world: &SpanRecoveryWorld, line: usize) { - world.push_frame(line, false); -} +fn given_direct_span(world: &SpanRecoveryWorld, line: usize) { world.push_frame(line, false); } #[given("a macro frame at line {line}")] -fn given_macro_frame(world: &SpanRecoveryWorld, line: usize) { - world.push_frame(line, true); -} +fn given_macro_frame(world: &SpanRecoveryWorld, line: usize) { world.push_frame(line, true); } #[given("a user-editable frame at line {line}")] -fn given_user_frame(world: &SpanRecoveryWorld, line: usize) { - world.push_frame(line, false); -} +fn given_user_frame(world: &SpanRecoveryWorld, line: usize) { world.push_frame(line, false); } #[when("I recover the user-editable span")] -fn when_recover(world: &SpanRecoveryWorld) { - world.evaluate(); -} +fn when_recover(world: &SpanRecoveryWorld) { world.evaluate(); } fn source_span(line: usize) -> SourceSpan { match SourceSpan::new(SourceLocation::new(line, 1), SourceLocation::new(line, 8)) { @@ -90,21 +80,13 @@ fn then_macro_only(world: &SpanRecoveryWorld) { } #[scenario(path = "tests/features/rstest_span_recovery.feature", index = 0)] -fn scenario_direct_span_is_kept(world: SpanRecoveryWorld) { - let _ = world; -} +fn scenario_direct_span_is_kept(world: SpanRecoveryWorld) { let _ = world; } #[scenario(path = "tests/features/rstest_span_recovery.feature", index = 1)] -fn scenario_nested_macro_chain_recovers(world: SpanRecoveryWorld) { - let _ = world; -} +fn scenario_nested_macro_chain_recovers(world: SpanRecoveryWorld) { let _ = world; } #[scenario(path = "tests/features/rstest_span_recovery.feature", index = 2)] -fn scenario_macro_only_is_skipped(world: SpanRecoveryWorld) { - let _ = world; -} +fn scenario_macro_only_is_skipped(world: SpanRecoveryWorld) { let _ = world; } #[scenario(path = "tests/features/rstest_span_recovery.feature", index = 3)] -fn scenario_first_user_frame_wins(world: SpanRecoveryWorld) { - let _ = world; -} +fn scenario_first_user_frame_wins(world: SpanRecoveryWorld) { let _ = world; } diff --git a/crates/bumpy_road_function/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index 4ba784ec7..953e09af0 100644 --- a/crates/bumpy_road_function/src/analysis.rs +++ b/crates/bumpy_road_function/src/analysis.rs @@ -69,7 +69,7 @@ impl Default for Settings { /// # Examples /// /// ``` -/// use bumpy_road_function::analysis::{normalize_settings, Settings, Weights}; +/// use bumpy_road_function::analysis::{Settings, Weights, normalize_settings}; /// /// let settings = Settings { /// threshold: -1.0, @@ -97,9 +97,7 @@ pub fn normalize_settings(settings: Settings) -> Settings { } } - const fn is_valid_window(window: usize) -> bool { - window != 0 && (window & 1) == 1 - } + const fn is_valid_window(window: usize) -> bool { window != 0 && (window & 1) == 1 } let defaults = Settings::default(); let threshold = if settings.threshold.is_finite() && settings.threshold >= 0.0 { @@ -141,21 +139,15 @@ pub struct BumpInterval { impl BumpInterval { /// First index covered by the bump (inclusive). #[must_use] - pub const fn start_index(self) -> usize { - self.start_index - } + pub const fn start_index(self) -> usize { self.start_index } /// Last index covered by the bump (inclusive). #[must_use] - pub const fn end_index(self) -> usize { - self.end_index - } + pub const fn end_index(self) -> usize { self.end_index } /// Number of samples spanned by the bump. #[must_use] - pub const fn len(self) -> usize { - self.end_index - self.start_index + 1 - } + pub const fn len(self) -> usize { self.end_index - self.start_index + 1 } /// Returns `true` when the interval contains no samples. /// @@ -163,15 +155,11 @@ impl BumpInterval { /// retained defensively so callers can validate values originating from /// other sources. #[must_use] - pub const fn is_empty(self) -> bool { - self.start_index > self.end_index - } + pub const fn is_empty(self) -> bool { self.start_index > self.end_index } /// Area above the threshold used for ranking bumps. #[must_use] - pub const fn area_above_threshold(self) -> f64 { - self.area_above_threshold - } + pub const fn area_above_threshold(self) -> f64 { self.area_above_threshold } } /// Mutable state maintained during bump detection. @@ -194,9 +182,7 @@ impl BumpDetectionContext { } } - fn into_intervals(self) -> Vec { - self.intervals - } + fn into_intervals(self) -> Vec { self.intervals } } /// Detects bump intervals in the smoothed signal. diff --git a/crates/bumpy_road_function/src/driver/config.rs b/crates/bumpy_road_function/src/driver/config.rs index b69785f93..276449316 100644 --- a/crates/bumpy_road_function/src/driver/config.rs +++ b/crates/bumpy_road_function/src/driver/config.rs @@ -3,11 +3,11 @@ //! The lint reads optional configuration from `dylint.toml`, applies defaults, //! and relies on `analysis::normalize_settings` to clamp invalid values. -use crate::analysis::{Settings, Weights}; use log::debug; use serde::Deserialize; use super::LINT_NAME; +use crate::analysis::{Settings, Weights}; #[derive(Clone, Copy, Debug, Deserialize)] #[serde(default, deny_unknown_fields)] diff --git a/crates/bumpy_road_function/src/driver/diagnostic.rs b/crates/bumpy_road_function/src/driver/diagnostic.rs index 61defdf65..64a277b87 100644 --- a/crates/bumpy_road_function/src/driver/diagnostic.rs +++ b/crates/bumpy_road_function/src/driver/diagnostic.rs @@ -3,19 +3,22 @@ //! The lint warns when it detects two or more separated bump intervals in the //! smoothed signal and highlights the two most severe bumps. -use std::borrow::Cow; -use std::ops::RangeInclusive; +use std::{borrow::Cow, ops::RangeInclusive}; -use crate::analysis::{BumpInterval, Settings, top_two_bumps}; use fluent_templates::fluent_bundle::FluentValue; use rustc_lint::{LateContext, LintContext}; use rustc_span::{BytePos, Span}; -use whitaker_common::i18n::DiagnosticMessageSet; use whitaker_common::{ - Arguments, Localizer, MessageResolution, noop_reporter, safe_resolve_message_set, + Arguments, + Localizer, + MessageResolution, + i18n::DiagnosticMessageSet, + noop_reporter, + safe_resolve_message_set, }; use super::{BUMPY_ROAD_FUNCTION, LINT_NAME, MESSAGE_KEY}; +use crate::analysis::{BumpInterval, Settings, top_two_bumps}; /// Payload describing a lint diagnostic to emit. pub(super) struct DiagnosticInput<'a> { diff --git a/crates/bumpy_road_function/src/driver/mod.rs b/crates/bumpy_road_function/src/driver/mod.rs index 434f537dd..af679b4cb 100644 --- a/crates/bumpy_road_function/src/driver/mod.rs +++ b/crates/bumpy_road_function/src/driver/mod.rs @@ -5,17 +5,19 @@ //! more separated bumps above a configurable threshold. The warning highlights //! the two largest bump intervals with labelled spans. -use crate::analysis::{Settings, detect_bumps, normalize_settings}; use rustc_hir as hir; use rustc_hir::ExprKind; use rustc_lint::{LateContext, LateLintPass}; -use rustc_span::Ident; -use rustc_span::Span; -use rustc_span::symbol::Symbol; +use rustc_span::{Ident, Span, symbol::Symbol}; use whitaker::SharedConfig; -use whitaker_common::complexity_signal::{rasterize_signal, smooth_moving_average}; -use whitaker_common::i18n::MessageKey; -use whitaker_common::{Localizer, get_localizer_for_lint}; +use whitaker_common::{ + Localizer, + complexity_signal::{rasterize_signal, smooth_moving_average}, + get_localizer_for_lint, + i18n::MessageKey, +}; + +use crate::analysis::{Settings, detect_bumps, normalize_settings}; const LINT_NAME: &str = "bumpy_road_function"; const MESSAGE_KEY: MessageKey<'static> = MessageKey::new(LINT_NAME); @@ -24,9 +26,11 @@ mod config; mod diagnostic; mod segment_builder; -use self::config::load_configuration; -use self::diagnostic::{DiagnosticInput, emit_diagnostic}; -use self::segment_builder::{SegmentBuilder, span_line_range}; +use self::{ + config::load_configuration, + diagnostic::{DiagnosticInput, emit_diagnostic}, + segment_builder::{SegmentBuilder, span_line_range}, +}; /// Dylint lint declaration and registration glue. /// diff --git a/crates/bumpy_road_function/src/driver/segment_builder.rs b/crates/bumpy_road_function/src/driver/segment_builder.rs index 00839f225..046868850 100644 --- a/crates/bumpy_road_function/src/driver/segment_builder.rs +++ b/crates/bumpy_road_function/src/driver/segment_builder.rs @@ -5,14 +5,14 @@ use std::ops::RangeInclusive; -use crate::analysis::Settings; use rustc_hir as hir; use rustc_hir::{BinOpKind, ExprKind, LoopSource, UnOp}; use rustc_lint::LateContext; -use rustc_span::source_map::SourceMap; -use rustc_span::{DesugaringKind, Span}; +use rustc_span::{DesugaringKind, Span, source_map::SourceMap}; use whitaker_common::complexity_signal::LineSegment; +use crate::analysis::Settings; + pub(super) struct SegmentBuilder<'a, 'tcx> { cx: &'a LateContext<'tcx>, settings: &'a Settings, @@ -164,7 +164,9 @@ impl<'a, 'tcx> SegmentBuilder<'a, 'tcx> { self.cx.tcx.sess.dcx().span_delayed_bug( span, format!( - "bumpy-road segment lines lie outside function range (segment={segment_start}..={segment_end}, function={function_start}..={function_end})", + "bumpy-road segment lines lie outside function range \ + (segment={segment_start}..={segment_end}, \ + function={function_start}..={function_end})", segment_start = lines.start(), segment_end = lines.end(), function_start = self.function_lines.start(), @@ -190,13 +192,9 @@ impl<'a, 'tcx> SegmentBuilder<'a, 'tcx> { } impl<'tcx> rustc_hir::intravisit::Visitor<'tcx> for SegmentBuilder<'_, 'tcx> { - fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) { - Self::visit_expr(self, expr); - } + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) { Self::visit_expr(self, expr); } - fn visit_block(&mut self, block: &'tcx hir::Block<'tcx>) { - Self::visit_block(self, block); - } + fn visit_block(&mut self, block: &'tcx hir::Block<'tcx>) { Self::visit_block(self, block); } } fn extract_while_components<'hir>( diff --git a/crates/bumpy_road_function/tests/analysis_behaviour.rs b/crates/bumpy_road_function/tests/analysis_behaviour.rs index 23255aa2f..c21342d6a 100644 --- a/crates/bumpy_road_function/tests/analysis_behaviour.rs +++ b/crates/bumpy_road_function/tests/analysis_behaviour.rs @@ -10,7 +10,12 @@ extern crate rustc_driver; use std::cell::RefCell; use bumpy_road_function::analysis::{ - DEFAULT_THRESHOLD, Settings, Weights, detect_bumps, normalize_settings, top_two_bumps, + DEFAULT_THRESHOLD, + Settings, + Weights, + detect_bumps, + normalize_settings, + top_two_bumps, }; use rstest::{fixture, rstest}; use rstest_bdd_macros::{given, scenario, then, when}; @@ -111,9 +116,7 @@ struct World { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> World { - World::default() -} +fn world() -> World { World::default() } #[given("a smoothed signal with two bumps")] fn given_signal_two_bumps(world: &World) { @@ -123,19 +126,13 @@ fn given_signal_two_bumps(world: &World) { } #[given("a smoothed signal with one bump")] -fn given_signal_one_bump(world: &World) { - world.signal.replace(vec![0.0, 3.0, 3.0, 0.0]); -} +fn given_signal_one_bump(world: &World) { world.signal.replace(vec![0.0, 3.0, 3.0, 0.0]); } #[given("a smoothed signal with a short spike")] -fn given_signal_short_spike(world: &World) { - world.signal.replace(vec![0.0, 4.0, 0.0]); -} +fn given_signal_short_spike(world: &World) { world.signal.replace(vec![0.0, 4.0, 0.0]); } #[given("the threshold is {threshold:f64}")] -fn given_threshold(world: &World, threshold: f64) { - world.threshold.replace(threshold); -} +fn given_threshold(world: &World, threshold: f64) { world.threshold.replace(threshold); } #[given("the minimum bump length is {min_lines}")] fn given_min_bump_lines(world: &World, min_lines: usize) { @@ -157,14 +154,10 @@ fn then_bump_count(world: &World, count: usize) { } #[given("default settings")] -fn given_default_settings(world: &World) { - world.settings.replace(Settings::default()); -} +fn given_default_settings(world: &World) { world.settings.replace(Settings::default()); } #[when("the smoothing window is set to {window}")] -fn when_set_window(world: &World, window: usize) { - world.settings.borrow_mut().window = window; -} +fn when_set_window(world: &World, window: usize) { world.settings.borrow_mut().window = window; } #[when("the threshold is set to {threshold:f64}")] fn when_set_threshold(world: &World, threshold: f64) { diff --git a/crates/conditional_max_n_branches/src/driver.rs b/crates/conditional_max_n_branches/src/driver.rs index 1dd499a40..ba7d27385 100644 --- a/crates/conditional_max_n_branches/src/driver.rs +++ b/crates/conditional_max_n_branches/src/driver.rs @@ -15,10 +15,16 @@ use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_span::{DesugaringKind, Span}; use serde::Deserialize; use whitaker::SharedConfig; -use whitaker_common::i18n::{DiagnosticMessageSet, MessageKey}; use whitaker_common::{ - Arguments, FALLBACK_LOCALE, Localizer, MessageResolution, branch_phrase, - get_localizer_for_lint, noop_reporter, safe_resolve_message_set, + Arguments, + FALLBACK_LOCALE, + Localizer, + MessageResolution, + branch_phrase, + get_localizer_for_lint, + i18n::{DiagnosticMessageSet, MessageKey}, + noop_reporter, + safe_resolve_message_set, }; const LINT_NAME: &str = "conditional_max_n_branches"; @@ -32,9 +38,7 @@ struct Config { } impl Config { - const fn default_max_branches() -> usize { - 2 - } + const fn default_max_branches() -> usize { 2 } } impl Default for Config { @@ -305,9 +309,10 @@ fn fallback_messages(kind: ConditionKind, branches: usize, limit: usize) -> Diag #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case(1, 2, ConditionDisposition::WithinLimit)] #[case(2, 2, ConditionDisposition::WithinLimit)] diff --git a/crates/conditional_max_n_branches/src/tests/behaviour.rs b/crates/conditional_max_n_branches/src/tests/behaviour.rs index b3bf13c49..b3715fb46 100644 --- a/crates/conditional_max_n_branches/src/tests/behaviour.rs +++ b/crates/conditional_max_n_branches/src/tests/behaviour.rs @@ -15,13 +15,9 @@ struct PredicateWorld { } impl PredicateWorld { - fn set_limit(&self, value: usize) { - self.limit.set(value); - } + fn set_limit(&self, value: usize) { self.limit.set(value); } - fn set_branches(&self, value: usize) { - self.branches.set(value); - } + fn set_branches(&self, value: usize) { self.branches.set(value); } fn evaluate(&self) { let outcome = evaluate_condition(self.branches.get(), self.limit.get()); @@ -37,24 +33,16 @@ impl PredicateWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> PredicateWorld { - PredicateWorld::default() -} +fn world() -> PredicateWorld { PredicateWorld::default() } #[given("the branch limit is {limit}")] -fn given_limit(world: &PredicateWorld, limit: usize) { - world.set_limit(limit); -} +fn given_limit(world: &PredicateWorld, limit: usize) { world.set_limit(limit); } #[given("the predicate declares {branches} branches")] -fn given_branches(world: &PredicateWorld, branches: usize) { - world.set_branches(branches); -} +fn given_branches(world: &PredicateWorld, branches: usize) { world.set_branches(branches); } #[when("I evaluate the predicate complexity")] -fn when_evaluate(world: &PredicateWorld) { - world.evaluate(); -} +fn when_evaluate(world: &PredicateWorld) { world.evaluate(); } #[then("the predicate is accepted")] fn then_accepted(world: &PredicateWorld) { @@ -67,21 +55,13 @@ fn then_rejected(world: &PredicateWorld) { } #[scenario(path = "tests/features/conditional_branches.feature", index = 0)] -fn scenario_within_limit(world: PredicateWorld) { - let _ = world; -} +fn scenario_within_limit(world: PredicateWorld) { let _ = world; } #[scenario(path = "tests/features/conditional_branches.feature", index = 1)] -fn scenario_at_limit(world: PredicateWorld) { - let _ = world; -} +fn scenario_at_limit(world: PredicateWorld) { let _ = world; } #[scenario(path = "tests/features/conditional_branches.feature", index = 2)] -fn scenario_exceeds_limit(world: PredicateWorld) { - let _ = world; -} +fn scenario_exceeds_limit(world: PredicateWorld) { let _ = world; } #[scenario(path = "tests/features/conditional_branches.feature", index = 3)] -fn scenario_custom_limit(world: PredicateWorld) { - let _ = world; -} +fn scenario_custom_limit(world: PredicateWorld) { let _ = world; } diff --git a/crates/function_attrs_follow_docs/src/driver.rs b/crates/function_attrs_follow_docs/src/driver.rs index 1ab6e4d7d..45285d235 100644 --- a/crates/function_attrs_follow_docs/src/driver.rs +++ b/crates/function_attrs_follow_docs/src/driver.rs @@ -4,17 +4,25 @@ //! free functions, inherent methods, and trait methods. Keeping doc comments at //! the front mirrors idiomatic Rust style and prevents them from being obscured //! by implementation details such as `#[inline]` or `#[allow]` attributes. -use rustc_ast::AttrStyle; -use rustc_ast::attr::AttributeExt; +use std::borrow::Cow; + +use rustc_ast::{AttrStyle, attr::AttributeExt}; use rustc_hir as hir; use rustc_hir::attrs::AttributeKind; use rustc_lint::{DiagDecorator, LateContext, LateLintPass, LintContext}; use rustc_span::Span; -use std::borrow::Cow; use whitaker::{SharedConfig, recover_user_editable_hir_span}; use whitaker_common::i18n::{ - Arguments, BundleLookup, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, - MessageResolution, get_localizer_for_lint, noop_reporter, safe_resolve_message_set, + Arguments, + BundleLookup, + DiagnosticMessageSet, + FluentValue, + Localizer, + MessageKey, + MessageResolution, + get_localizer_for_lint, + noop_reporter, + safe_resolve_message_set, }; #[cfg(test)] use whitaker_common::i18n::{I18nError, resolve_message_set}; @@ -213,23 +221,15 @@ impl AttrInfo { (span.lo(), span.hi()) } - const fn user_editable_span(&self) -> Option { - self.user_editable_span - } + const fn user_editable_span(&self) -> Option { self.user_editable_span } } impl OrderedAttribute for AttrInfo { - fn is_outer(&self) -> bool { - self.is_outer - } + fn is_outer(&self) -> bool { self.is_outer } - fn is_doc(&self) -> bool { - self.is_doc - } + fn is_doc(&self) -> bool { self.is_doc } - fn span(&self) -> Span { - self.span - } + fn span(&self) -> Span { self.span } } /// Context for checking function attributes. diff --git a/crates/function_attrs_follow_docs/src/tests/localization.rs b/crates/function_attrs_follow_docs/src/tests/localization.rs index 45d24741a..a643d2967 100644 --- a/crates/function_attrs_follow_docs/src/tests/localization.rs +++ b/crates/function_attrs_follow_docs/src/tests/localization.rs @@ -10,7 +10,11 @@ use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::i18n::{I18nError, testing::FailingLookup}; use super::{ - FunctionAttrsMessages, FunctionKind, Localizer, MESSAGE_KEY, attribute_fallback, + FunctionAttrsMessages, + FunctionKind, + Localizer, + MESSAGE_KEY, + attribute_fallback, localized_messages, }; @@ -56,14 +60,10 @@ impl LocalizationWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> LocalizationWorld { - LocalizationWorld::default() -} +fn world() -> LocalizationWorld { LocalizationWorld::default() } #[given("the locale {locale} is selected")] -fn given_locale(world: &LocalizationWorld, locale: String) { - world.use_localizer(&locale); -} +fn given_locale(world: &LocalizationWorld, locale: String) { world.use_localizer(&locale); } #[given("the subject kind is {kind}")] fn given_subject(world: &LocalizationWorld, kind: String) { @@ -81,14 +81,10 @@ fn given_attribute(world: &LocalizationWorld, label: String) { } #[given("the attribute snippet cannot be retrieved")] -fn given_attribute_fallback(world: &LocalizationWorld) { - world.use_attribute_fallback.set(true); -} +fn given_attribute_fallback(world: &LocalizationWorld) { world.use_attribute_fallback.set(true); } #[given("localization fails")] -fn given_failure(world: &LocalizationWorld) { - world.failing.set(true); -} +fn given_failure(world: &LocalizationWorld) { world.failing.set(true); } #[when("I localize the diagnostic")] fn when_localize(world: &LocalizationWorld) { @@ -149,30 +145,18 @@ fn then_failure(world: &LocalizationWorld, key: String) { } #[scenario(path = "tests/features/function_attrs_localization.feature", index = 0)] -fn scenario_fallback(world: LocalizationWorld) { - let _ = world; -} +fn scenario_fallback(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/function_attrs_localization.feature", index = 1)] -fn scenario_welsh(world: LocalizationWorld) { - let _ = world; -} +fn scenario_welsh(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/function_attrs_localization.feature", index = 2)] -fn scenario_attribute_fallback(world: LocalizationWorld) { - let _ = world; -} +fn scenario_attribute_fallback(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/function_attrs_localization.feature", index = 3)] -fn scenario_unknown_locale(world: LocalizationWorld) { - let _ = world; -} +fn scenario_unknown_locale(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/function_attrs_localization.feature", index = 4)] -fn scenario_failure(world: LocalizationWorld) { - let _ = world; -} +fn scenario_failure(world: LocalizationWorld) { let _ = world; } -fn failing_lookup() -> FailingLookup { - FailingLookup::new(MESSAGE_KEY.as_ref()) -} +fn failing_lookup() -> FailingLookup { FailingLookup::new(MESSAGE_KEY.as_ref()) } diff --git a/crates/function_attrs_follow_docs/src/tests/order_detection.rs b/crates/function_attrs_follow_docs/src/tests/order_detection.rs index 7a37b7433..0c736ffc5 100644 --- a/crates/function_attrs_follow_docs/src/tests/order_detection.rs +++ b/crates/function_attrs_follow_docs/src/tests/order_detection.rs @@ -13,21 +13,19 @@ use rustc_span::{BytePos, DUMMY_SP, Span}; use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; use super::{ - AttrInfo, OrderedAttribute, attribute_within_item, detect_misordered_doc, parsed_attribute_span, + AttrInfo, + OrderedAttribute, + attribute_within_item, + detect_misordered_doc, + parsed_attribute_span, }; impl OrderedAttribute for Attribute { - fn is_outer(&self) -> bool { - self.is_outer() - } + fn is_outer(&self) -> bool { self.is_outer() } - fn is_doc(&self) -> bool { - self.is_doc() - } + fn is_doc(&self) -> bool { self.is_doc() } - fn span(&self) -> Span { - DUMMY_SP - } + fn span(&self) -> Span { DUMMY_SP } } #[derive(Default)] @@ -49,15 +47,11 @@ impl AttributeWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> AttributeWorld { - AttributeWorld::default() -} +fn world() -> AttributeWorld { AttributeWorld::default() } #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn result() -> Option<(usize, usize)> { - None -} +fn result() -> Option<(usize, usize)> { None } #[given("a doc comment before other attributes")] fn doc_precedes(world: &AttributeWorld) { @@ -85,9 +79,7 @@ fn doc_after_inner(world: &AttributeWorld) { } #[when("I evaluate the attribute order")] -fn evaluate(world: &AttributeWorld) -> Option<(usize, usize)> { - world.result() -} +fn evaluate(world: &AttributeWorld) -> Option<(usize, usize)> { world.result() } #[then("the order is accepted")] fn order_ok(result: &Option<(usize, usize)>) { @@ -99,9 +91,7 @@ fn order_rejected(result: &Option<(usize, usize)>) { assert!(result.is_some()); } -fn test_span(lo: u32, hi: u32) -> Span { - Span::with_root_ctxt(BytePos(lo), BytePos(hi)) -} +fn test_span(lo: u32, hi: u32) -> Span { Span::with_root_ctxt(BytePos(lo), BytePos(hi)) } #[rstest] fn recovered_user_span_drives_source_ordering() { diff --git a/crates/function_attrs_follow_docs/src/tests/ui.rs b/crates/function_attrs_follow_docs/src/tests/ui.rs index d90bddc52..b0c391319 100644 --- a/crates/function_attrs_follow_docs/src/tests/ui.rs +++ b/crates/function_attrs_follow_docs/src/tests/ui.rs @@ -10,15 +10,11 @@ use whitaker_common::test_support::with_locale; #[test] #[serial] -fn ui() { - run_ui_with_locale("ui", None); -} +fn ui() { run_ui_with_locale("ui", None); } #[test] #[serial] -fn ui_runs_in_welsh_locale() { - run_ui_with_locale("ui-cy", Some("cy")); -} +fn ui_runs_in_welsh_locale() { run_ui_with_locale("ui-cy", Some("cy")); } fn run_ui_with_locale(directory: &str, locale: Option<&str>) { with_locale(locale, || { diff --git a/crates/module_max_lines/src/driver.rs b/crates/module_max_lines/src/driver.rs index 2dbf7afac..6b150debe 100644 --- a/crates/module_max_lines/src/driver.rs +++ b/crates/module_max_lines/src/driver.rs @@ -7,13 +7,17 @@ use log::debug; use rustc_hir as hir; use rustc_lint::{LateContext, LateLintPass, LintContext}; -use rustc_span::Span; -use rustc_span::source_map::SourceMap; -use rustc_span::symbol::Ident; +use rustc_span::{Span, source_map::SourceMap, symbol::Ident}; use whitaker::{ModuleMaxLinesConfig, SharedConfig, module_body_span, module_header_span}; use whitaker_common::i18n::{ - Arguments, DiagnosticMessageSet, Localizer, MessageKey, MessageResolution, - get_localizer_for_lint, noop_reporter, safe_resolve_message_set, + Arguments, + DiagnosticMessageSet, + Localizer, + MessageKey, + MessageResolution, + get_localizer_for_lint, + noop_reporter, + safe_resolve_message_set, }; const LINT_NAME: &str = "module_max_lines"; @@ -163,9 +167,10 @@ struct ModuleDiagnosticInfo { } fn emit_diagnostic(cx: &LateContext<'_>, info: &ModuleDiagnosticInfo, localizer: &Localizer) { - use fluent_templates::fluent_bundle::FluentValue; use std::borrow::Cow; + use fluent_templates::fluent_bundle::FluentValue; + let mut args: Arguments<'_> = Arguments::default(); let module_name = info.ident.name.as_str(); args.insert(Cow::Borrowed("module"), FluentValue::from(module_name)); @@ -209,9 +214,10 @@ fn fallback_messages(module: &str, lines: usize, limit: usize) -> DiagnosticMess #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case(4, 5, false, ModuleDisposition::WithinLimit)] #[case(6, 5, false, ModuleDisposition::ExceedsLimit)] @@ -229,10 +235,12 @@ mod tests { #[cfg(test)] mod behaviour { - use super::{ModuleDisposition, evaluate_module}; + use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; - use std::cell::RefCell; + + use super::{ModuleDisposition, evaluate_module}; #[derive(Default)] struct ModuleWorld { @@ -243,17 +251,11 @@ mod behaviour { } impl ModuleWorld { - fn set_lines(&self, value: usize) { - *self.lines.borrow_mut() = value; - } + fn set_lines(&self, value: usize) { *self.lines.borrow_mut() = value; } - fn set_limit(&self, value: usize) { - *self.limit.borrow_mut() = value; - } + fn set_limit(&self, value: usize) { *self.limit.borrow_mut() = value; } - fn mark_macro(&self) { - *self.from_macro.borrow_mut() = true; - } + fn mark_macro(&self) { *self.from_macro.borrow_mut() = true; } fn evaluate(&self) { let lines = *self.lines.borrow(); @@ -272,29 +274,19 @@ mod behaviour { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] - fn world() -> ModuleWorld { - ModuleWorld::default() - } + fn world() -> ModuleWorld { ModuleWorld::default() } #[given("the maximum module length is {limit}")] - fn given_limit(world: &ModuleWorld, limit: usize) { - world.set_limit(limit); - } + fn given_limit(world: &ModuleWorld, limit: usize) { world.set_limit(limit); } #[given("a module spans {lines} lines")] - fn given_lines(world: &ModuleWorld, lines: usize) { - world.set_lines(lines); - } + fn given_lines(world: &ModuleWorld, lines: usize) { world.set_lines(lines); } #[given("the module originates from a macro expansion")] - fn given_macro(world: &ModuleWorld) { - world.mark_macro(); - } + fn given_macro(world: &ModuleWorld) { world.mark_macro(); } #[when("I evaluate the module length")] - fn when_evaluate(world: &ModuleWorld) { - world.evaluate(); - } + fn when_evaluate(world: &ModuleWorld) { world.evaluate(); } #[then("the module is accepted")] fn then_accepted(world: &ModuleWorld) { @@ -312,24 +304,16 @@ mod behaviour { } #[scenario(path = "tests/features/module_length.feature", index = 0)] - fn scenario_within_limit(world: ModuleWorld) { - let _ = world; - } + fn scenario_within_limit(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_length.feature", index = 1)] - fn scenario_exceeds_limit(world: ModuleWorld) { - let _ = world; - } + fn scenario_exceeds_limit(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_length.feature", index = 2)] - fn scenario_exact_limit(world: ModuleWorld) { - let _ = world; - } + fn scenario_exact_limit(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_length.feature", index = 3)] - fn scenario_macro(world: ModuleWorld) { - let _ = world; - } + fn scenario_macro(world: ModuleWorld) { let _ = world; } } #[cfg(test)] diff --git a/crates/module_must_have_inner_docs/src/driver/inner_attr.rs b/crates/module_must_have_inner_docs/src/driver/inner_attr.rs index 1bdf4b3fc..b24afccf9 100644 --- a/crates/module_must_have_inner_docs/src/driver/inner_attr.rs +++ b/crates/module_must_have_inner_docs/src/driver/inner_attr.rs @@ -4,9 +4,8 @@ //! tied to the main lint flow, such as detecting case-mismatched `doc` //! identifiers and filtering `cfg_attr` wrappers that never supply docs. -use crate::{AttributeBody, ParseInput}; - use super::parser; +use crate::{AttributeBody, ParseInput}; fn segment_has_case_incorrect_doc(segment: &str) -> bool { let Some((ident, tail)) = parser::take_ident(ParseInput::from(segment)) else { diff --git a/crates/module_must_have_inner_docs/src/driver/mod.rs b/crates/module_must_have_inner_docs/src/driver/mod.rs index 994c395b4..6b7868360 100644 --- a/crates/module_must_have_inner_docs/src/driver/mod.rs +++ b/crates/module_must_have_inner_docs/src/driver/mod.rs @@ -13,13 +13,18 @@ use rustc_hir as hir; use rustc_lint::{LateContext, LateLintPass, LintContext}; #[cfg(test)] use rustc_span::DUMMY_SP; -use rustc_span::source_map::SourceMap; -use rustc_span::symbol::Ident; -use rustc_span::{BytePos, Span}; +use rustc_span::{BytePos, Span, source_map::SourceMap, symbol::Ident}; use whitaker::{SharedConfig, module_body_span, module_header_span}; use whitaker_common::i18n::{ - Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, - get_localizer_for_lint, noop_reporter, safe_resolve_message_set, + Arguments, + DiagnosticMessageSet, + FluentValue, + Localizer, + MessageKey, + MessageResolution, + get_localizer_for_lint, + noop_reporter, + safe_resolve_message_set, }; mod inner_attr; @@ -60,9 +65,7 @@ impl<'a> ParseInput<'a> { /// assert_eq!(input.as_str(), "example"); /// ``` #[must_use] - pub fn as_str(&self) -> &'a str { - **self - } + pub fn as_str(&self) -> &'a str { **self } } const LINT_NAME: &str = "module_must_have_inner_docs"; diff --git a/crates/module_must_have_inner_docs/src/driver/parser.rs b/crates/module_must_have_inner_docs/src/driver/parser.rs index 72a7402a7..a0215ba79 100644 --- a/crates/module_must_have_inner_docs/src/driver/parser.rs +++ b/crates/module_must_have_inner_docs/src/driver/parser.rs @@ -6,8 +6,8 @@ //! `#![doc = \"...\"]` style attributes) while ignoring commas inside nested //! parentheses when dissecting meta lists. Key helpers: //! - `skip_leading_whitespace`: advances a text cursor past Unicode whitespace. -//! - `is_doc_comment`: recognizes leading doc comments or doc attributes, -//! including those wrapped in `cfg_attr`. +//! - `is_doc_comment`: recognizes leading doc comments or doc attributes, including those wrapped +//! in `cfg_attr`. //! //! These utilities underpin the lint that determines whether a module has the //! required leading inner docs. @@ -88,9 +88,7 @@ pub(super) fn is_doc_comment(rest: ParseInput<'_>) -> bool { // Returns true for direct `doc` attributes and for `cfg_attr` wrappers that // contain a `doc` entry. -fn is_doc_attr(attr_body: AttributeBody<'_>) -> bool { - is_doc_ident(ParseInput::from(*attr_body)) -} +fn is_doc_attr(attr_body: AttributeBody<'_>) -> bool { is_doc_ident(ParseInput::from(*attr_body)) } /// Extracts the leading identifier from the input, skipping any leading /// whitespace. @@ -105,7 +103,9 @@ fn is_doc_attr(attr_body: AttributeBody<'_>) -> bool { /// # use module_must_have_inner_docs::ParseInput; /// # use module_must_have_inner_docs::parser::take_ident; /// let input = ParseInput::from(" foo_bar(baz)"); -/// let Some((ident, rest)) = take_ident(input) else { panic!() }; +/// let Some((ident, rest)) = take_ident(input) else { +/// panic!() +/// }; /// assert_eq!(*ident, "foo_bar"); /// assert_eq!(rest.as_str(), "(baz)"); /// @@ -156,9 +156,7 @@ const fn is_ident_start(ch: char) -> bool { ch == '_' || ch.is_ascii_alphabetic() } -const fn is_ident_continue(ch: char) -> bool { - ch == '_' || ch.is_ascii_alphanumeric() -} +const fn is_ident_continue(ch: char) -> bool { ch == '_' || ch.is_ascii_alphanumeric() } pub(super) fn cfg_attr_has_doc(tail: ParseInput<'_>) -> bool { let (_, trimmed) = skip_leading_whitespace(tail); @@ -231,17 +229,16 @@ fn process_char_for_doc_after_first( } } -fn segment_is_doc(segment: &str) -> bool { - is_doc_ident(ParseInput::from(segment)) -} +fn segment_is_doc(segment: &str) -> bool { is_doc_ident(ParseInput::from(segment)) } #[cfg(test)] mod tests { //! Unit tests for parsing helpers. + use rstest::rstest; + use super::skip_leading_whitespace; use crate::ParseInput; - use rstest::rstest; #[rstest] #[case("", 0, "")] diff --git a/crates/module_must_have_inner_docs/src/tests/behaviour.rs b/crates/module_must_have_inner_docs/src/tests/behaviour.rs index 90569f841..5bd99dc0e 100644 --- a/crates/module_must_have_inner_docs/src/tests/behaviour.rs +++ b/crates/module_must_have_inner_docs/src/tests/behaviour.rs @@ -17,9 +17,7 @@ struct ModuleWorld { } impl ModuleWorld { - fn push(&self, text: &str) { - self.prefix.borrow_mut().push_str(text); - } + fn push(&self, text: &str) { self.prefix.borrow_mut().push_str(text); } fn evaluate(&self) { let snippet = self.prefix.borrow(); @@ -38,29 +36,19 @@ impl ModuleWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> ModuleWorld { - ModuleWorld::default() -} +fn world() -> ModuleWorld { ModuleWorld::default() } #[given("the module begins with an inner doc comment")] -fn given_inner_doc(world: &ModuleWorld) { - world.push("//! module docs\n"); -} +fn given_inner_doc(world: &ModuleWorld) { world.push("//! module docs\n"); } #[given("the module body starts with code only")] -fn given_no_attributes(world: &ModuleWorld) { - world.push("pub fn demo() {}\n"); -} +fn given_no_attributes(world: &ModuleWorld) { world.push("pub fn demo() {}\n"); } #[given("the module contains an inner configuration attribute")] -fn given_inner_allow(world: &ModuleWorld) { - world.push("#![allow(dead_code)]\n"); -} +fn given_inner_allow(world: &ModuleWorld) { world.push("#![allow(dead_code)]\n"); } #[given("documentation follows that attribute")] -fn given_doc_after(world: &ModuleWorld) { - world.push("//! trailing docs\n"); -} +fn given_doc_after(world: &ModuleWorld) { world.push("//! trailing docs\n"); } #[given("the module contains an inner configuration attribute and inline documentation")] fn given_inline_attr_doc(world: &ModuleWorld) { @@ -68,14 +56,10 @@ fn given_inline_attr_doc(world: &ModuleWorld) { } #[given("the module declares only outer documentation")] -fn given_outer_doc(world: &ModuleWorld) { - world.push("/// outer docs\n"); -} +fn given_outer_doc(world: &ModuleWorld) { world.push("/// outer docs\n"); } #[when("I validate the module documentation requirements")] -fn when_detect(world: &ModuleWorld) { - world.evaluate(); -} +fn when_detect(world: &ModuleWorld) { world.evaluate(); } #[then("the module is accepted")] fn then_accept(world: &ModuleWorld) { @@ -96,31 +80,19 @@ fn then_misordered(world: &ModuleWorld) { } #[scenario(path = "tests/features/module_docs.feature", index = 0)] -fn scenario_docs_first(world: ModuleWorld) { - let _ = world; -} +fn scenario_docs_first(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_docs.feature", index = 1)] -fn scenario_missing_docs(world: ModuleWorld) { - let _ = world; -} +fn scenario_missing_docs(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_docs.feature", index = 2)] -fn scenario_misordered_docs(world: ModuleWorld) { - let _ = world; -} +fn scenario_misordered_docs(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_docs.feature", index = 3)] -fn scenario_inline_attr_doc(world: ModuleWorld) { - let _ = world; -} +fn scenario_inline_attr_doc(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_docs.feature", index = 4)] -fn scenario_inner_attribute_only(world: ModuleWorld) { - let _ = world; -} +fn scenario_inner_attribute_only(world: ModuleWorld) { let _ = world; } #[scenario(path = "tests/features/module_docs.feature", index = 5)] -fn scenario_outer_docs(world: ModuleWorld) { - let _ = world; -} +fn scenario_outer_docs(world: ModuleWorld) { let _ = world; } diff --git a/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs b/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs index 4bc544dcd..1039e57d6 100644 --- a/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs +++ b/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs @@ -2,7 +2,8 @@ use rstest::{fixture, rstest}; use rustc_span::{ - FileName, Span, + FileName, + Span, source_map::{FilePathMapping, SourceMap}, }; @@ -30,9 +31,7 @@ fn span_to_snippet_failure_skips_diagnostic(unresolvable_span_fixture: (SourceMa #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn unresolvable_span_fixture() -> (SourceMap, Span) { - unresolvable_span() -} +fn unresolvable_span_fixture() -> (SourceMap, Span) { unresolvable_span() } /// Builds a cross-file span (start in "first.rs", end in "second.rs") with the /// root context so the `SourceMap` cannot resolve it to a single file. This diff --git a/crates/no_expect_outside_tests/src/behaviour.rs b/crates/no_expect_outside_tests/src/behaviour.rs index 1f903e09a..8cfd7859e 100644 --- a/crates/no_expect_outside_tests/src/behaviour.rs +++ b/crates/no_expect_outside_tests/src/behaviour.rs @@ -1,12 +1,17 @@ //! Behaviour-driven tests covering context summarization for the lint's context //! world and BDD steps. -use crate::context::{ContextSummary, summarize_context}; +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -use whitaker_common::{ContextEntry, ContextKind}; +use whitaker_common::{ + ContextEntry, + ContextKind, + attributes::{Attribute, AttributeKind, AttributePath}, +}; + +use crate::context::{ContextSummary, summarize_context}; #[derive(Default)] struct ContextWorld { @@ -41,17 +46,13 @@ impl ContextWorld { .push(ContextEntry::new(name, ContextKind::Module, Vec::new())); } - fn enable_cfg_test(&self) { - *self.cfg_test.borrow_mut() = true; - } + fn enable_cfg_test(&self) { *self.cfg_test.borrow_mut() = true; } fn register_additional_attribute(&self, path: &str) { self.additional.borrow_mut().push(AttributePath::from(path)); } - fn mark_doctest(&self) { - *self.is_doctest.borrow_mut() = true; - } + fn mark_doctest(&self) { *self.is_doctest.borrow_mut() = true; } fn evaluate(&self) { let entries = self.entries.borrow(); @@ -64,29 +65,20 @@ impl ContextWorld { *self.summary.borrow_mut() = summary; } - fn summary_ref(&self) -> std::cell::Ref<'_, ContextSummary> { - self.summary.borrow() - } + fn summary_ref(&self) -> std::cell::Ref<'_, ContextSummary> { self.summary.borrow() } - fn should_skip_lint(&self) -> bool { - *self.skip_lint.borrow() - } + fn should_skip_lint(&self) -> bool { *self.skip_lint.borrow() } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> ContextWorld { - ContextWorld::default() -} +fn world() -> ContextWorld { ContextWorld::default() } #[given("a non-test function named {name}")] -fn given_plain_function(world: &ContextWorld, name: String) { - world.push_function(&name); -} +fn given_plain_function(world: &ContextWorld, name: String) { world.push_function(&name); } #[given("a test function named {name}")] -fn given_test_function(world: &ContextWorld, name: String) { - world.push_test_function(&name); -} +fn given_test_function(world: &ContextWorld, name: String) { world.push_test_function(&name); } #[given("a module with cfg(test)")] fn given_cfg_test_module(world: &ContextWorld) { @@ -111,14 +103,10 @@ fn given_function_with_additional_attribute(world: &ContextWorld, path: String) } #[given("the lint is running within a doctest")] -fn given_doctest(world: &ContextWorld) { - world.mark_doctest(); -} +fn given_doctest(world: &ContextWorld) { world.mark_doctest(); } #[when("I summarize the context")] -fn when_summarize(world: &ContextWorld) { - world.evaluate(); -} +fn when_summarize(world: &ContextWorld) { world.evaluate(); } #[then("the context is marked as production")] fn then_production(world: &ContextWorld) { diff --git a/crates/no_expect_outside_tests/src/context/mod.rs b/crates/no_expect_outside_tests/src/context/mod.rs index 5ae48079b..367b25a6f 100644 --- a/crates/no_expect_outside_tests/src/context/mod.rs +++ b/crates/no_expect_outside_tests/src/context/mod.rs @@ -2,17 +2,23 @@ //! guards (for example, `cfg(test)`), supporting the lint's context //! summarization. -use rustc_ast::AttrStyle; -use rustc_ast::ast::{MetaItem, MetaItemInner}; +use rustc_ast::{ + AttrStyle, + ast::{MetaItem, MetaItemInner}, +}; use rustc_hir as hir; -use rustc_hir::Node; -use rustc_hir::attrs::AttributeKind as HirAttributeKind; +use rustc_hir::{Node, attrs::AttributeKind as HirAttributeKind}; use rustc_lint::LateContext; use rustc_span::sym; use whitaker::hir::has_test_like_hir_attributes; use whitaker_common::{ - Attribute, AttributeKind, AttributePath, ContextEntry, ContextKind, - PARSED_ATTRIBUTE_PLACEHOLDER, in_test_like_context_with, + Attribute, + AttributeKind, + AttributePath, + ContextEntry, + ContextKind, + PARSED_ATTRIBUTE_PLACEHOLDER, + in_test_like_context_with, }; #[derive(Default, Debug, Clone, PartialEq, Eq)] @@ -31,8 +37,8 @@ pub(crate) struct ContextSummary { /// /// - `cx`: Lint context used to walk the HIR and inspect ancestor attributes. /// - `hir_id`: The HIR node whose ancestor chain should be summarized. -/// - `additional_test_attributes`: Extra user-configured attribute paths that -/// should be treated as test markers alongside Whitaker's built-in list. +/// - `additional_test_attributes`: Extra user-configured attribute paths that should be treated as +/// test markers alongside Whitaker's built-in list. /// /// # Returns /// @@ -95,11 +101,10 @@ fn has_test_ancestry( /// # Parameters /// /// - `entries`: Simplified ancestor contexts produced by `collect_context`. -/// - `has_test_context_ancestry`: Whether any ancestor already established -/// test-only ancestry via propagation, `cfg(test)`, or a recognized -/// test-marker attribute. -/// - `additional_test_attributes`: Extra user-configured attribute paths that -/// should be considered test markers during the final summary check. +/// - `has_test_context_ancestry`: Whether any ancestor already established test-only ancestry via +/// propagation, `cfg(test)`, or a recognized test-marker attribute. +/// - `additional_test_attributes`: Extra user-configured attribute paths that should be considered +/// test markers during the final summary check. /// /// # Returns /// diff --git a/crates/no_expect_outside_tests/src/context/tests.rs b/crates/no_expect_outside_tests/src/context/tests.rs index cd624e27c..35e8622fd 100644 --- a/crates/no_expect_outside_tests/src/context/tests.rs +++ b/crates/no_expect_outside_tests/src/context/tests.rs @@ -3,8 +3,6 @@ //! Verifies HIR attribute conversion to `whitaker_common::Attribute` and `cfg(test)` //! detection for both parsed and unparsed attribute variants. -#[cfg(feature = "dylint-driver")] -use super::{convert_attribute, has_test_ancestry, is_cfg_test_attribute, meta_contains_test_cfg}; #[cfg(feature = "dylint-driver")] use rstest::rstest; #[cfg(feature = "dylint-driver")] @@ -24,6 +22,9 @@ use rustc_span::{AttrId, DUMMY_SP, create_default_session_globals_then}; #[cfg(feature = "dylint-driver")] use whitaker_common::{AttributeKind, AttributePath, PARSED_ATTRIBUTE_PLACEHOLDER}; +#[cfg(feature = "dylint-driver")] +use super::{convert_attribute, has_test_ancestry, is_cfg_test_attribute, meta_contains_test_cfg}; + /// Type-safe wrapper for AST path segments. #[cfg(feature = "dylint-driver")] #[derive(Debug, Clone, Copy)] @@ -31,16 +32,12 @@ struct PathSegments(&'static [&'static str]); #[cfg(feature = "dylint-driver")] impl PathSegments { - const fn new(segments: &'static [&'static str]) -> Self { - Self(segments) - } + const fn new(segments: &'static [&'static str]) -> Self { Self(segments) } } #[cfg(feature = "dylint-driver")] impl AsRef<[&'static str]> for PathSegments { - fn as_ref(&self) -> &[&'static str] { - self.0 - } + fn as_ref(&self) -> &[&'static str] { self.0 } } #[cfg(feature = "dylint-driver")] @@ -202,9 +199,7 @@ fn meta_list(segments: PathSegments, children: Vec) -> MetaItem { } #[cfg(feature = "dylint-driver")] -fn meta_inner(meta: MetaItem) -> MetaItemInner { - MetaItemInner::MetaItem(meta) -} +fn meta_inner(meta: MetaItem) -> MetaItemInner { MetaItemInner::MetaItem(meta) } // --------------------------------------------------------------------------- // cfg pattern helpers @@ -276,9 +271,7 @@ fn build_cfg_all_test_unix() -> MetaItem { /// Builds `cfg(not(test))`. #[cfg(feature = "dylint-driver")] -fn build_cfg_not_test() -> MetaItem { - cfg_not(meta_word(PATH_TEST)) -} +fn build_cfg_not_test() -> MetaItem { cfg_not(meta_word(PATH_TEST)) } /// Builds `cfg_attr(test, cfg(test))`. #[cfg(feature = "dylint-driver")] diff --git a/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs index 8e5bdbe7b..5a5c9f004 100644 --- a/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs +++ b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs @@ -55,9 +55,7 @@ const TIED_ARTEFACTS: [ArtefactSpec<'static>; 2] = [ /// selection test. #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn selection_directory() -> TemporaryDirectory { - TemporaryDirectory::new("selection") -} +fn selection_directory() -> TemporaryDirectory { TemporaryDirectory::new("selection") } /// Creates `artefacts` inside `directory`, sets their modification times, then /// invokes `dependency_rlib` and returns both the expected and selected paths @@ -126,9 +124,7 @@ impl TemporaryDirectory { } /// Returns the path to the temporary directory. - fn path(&self) -> &Path { - &self.0 - } + fn path(&self) -> &Path { &self.0 } } impl Drop for TemporaryDirectory { diff --git a/crates/no_expect_outside_tests/src/diagnostics.rs b/crates/no_expect_outside_tests/src/diagnostics.rs index a8c7ce56d..0161f0a70 100644 --- a/crates/no_expect_outside_tests/src/diagnostics.rs +++ b/crates/no_expect_outside_tests/src/diagnostics.rs @@ -2,47 +2,45 @@ //! diagnostics; the driver detects violations and context supplies //! test-context evidence. -use crate::NO_EXPECT_OUTSIDE_TESTS; -use crate::context::ContextSummary; +use std::{borrow::Cow, fmt}; + use rustc_hir as hir; use rustc_lint::{DiagDecorator, LateContext, LintContext}; use rustc_middle::ty; use rustc_span::sym; -use std::borrow::Cow; -use std::fmt; use whitaker_common::i18n::{ - Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, - noop_reporter, safe_resolve_message_set, + Arguments, + DiagnosticMessageSet, + FluentValue, + Localizer, + MessageKey, + MessageResolution, + noop_reporter, + safe_resolve_message_set, }; #[cfg(test)] use whitaker_common::i18n::{BundleLookup, I18nError, resolve_message_set}; +use crate::{NO_EXPECT_OUTSIDE_TESTS, context::ContextSummary}; + /// A formatted label for the receiver type (e.g., "`Result`"). #[derive(Debug, Clone)] pub(crate) struct ReceiverLabel(String); impl ReceiverLabel { - pub(crate) fn new(label: impl Into) -> Self { - Self(label.into()) - } + pub(crate) fn new(label: impl Into) -> Self { Self(label.into()) } } impl Default for ReceiverLabel { - fn default() -> Self { - Self::new(String::new()) - } + fn default() -> Self { Self::new(String::new()) } } impl AsRef for ReceiverLabel { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for ReceiverLabel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -109,21 +107,15 @@ impl ReceiverCategory { pub(crate) struct ContextLabel(String); impl ContextLabel { - pub(crate) fn new(label: impl Into) -> Self { - Self(label.into()) - } + pub(crate) fn new(label: impl Into) -> Self { Self(label.into()) } } impl AsRef for ContextLabel { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for ContextLabel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } pub(crate) struct DiagnosticContext<'a> { diff --git a/crates/no_expect_outside_tests/src/driver/mod.rs b/crates/no_expect_outside_tests/src/driver/mod.rs index 8b6bf4c80..756dca676 100644 --- a/crates/no_expect_outside_tests/src/driver/mod.rs +++ b/crates/no_expect_outside_tests/src/driver/mod.rs @@ -9,9 +9,7 @@ //! extend the recognized test attributes through `dylint.toml` when bespoke //! macros are in play. -use std::collections::HashSet; -use std::ffi::OsStr; -use std::path::Path; +use std::{collections::HashSet, ffi::OsStr, path::Path}; use log::debug; use rustc_hir as hir; @@ -19,12 +17,13 @@ use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::{self, Ty}; use rustc_span::{RemapPathScopeComponents, sym}; use serde::Deserialize; -use whitaker::SharedConfig; -use whitaker::hir::has_test_like_hir_attributes; +use whitaker::{SharedConfig, hir::has_test_like_hir_attributes}; use whitaker_common::{AttributePath, Localizer, get_localizer_for_lint}; -use crate::context::{collect_context, is_cfg_test_attribute, summarize_context}; -use crate::diagnostics::{DiagnosticContext, emit_diagnostic}; +use crate::{ + context::{collect_context, is_cfg_test_attribute, summarize_context}, + diagnostics::{DiagnosticContext, emit_diagnostic}, +}; /// Dylint lint declaration and registration glue. /// diff --git a/crates/no_expect_outside_tests/src/driver/tests.rs b/crates/no_expect_outside_tests/src/driver/tests.rs index 0c01dbbca..26a0b8b79 100644 --- a/crates/no_expect_outside_tests/src/driver/tests.rs +++ b/crates/no_expect_outside_tests/src/driver/tests.rs @@ -1,11 +1,13 @@ //! Unit tests for test attribute detection helpers in the driver module. -use super::*; +use std::path::Path; + use rstest::rstest; use rustc_ast::AttrStyle; use rustc_hir::attrs::AttributeKind as HirAttributeKind; use rustc_span::{AttrId, DUMMY_SP, create_default_session_globals_then}; -use std::path::Path; + +use super::*; // ------------------------------------------------------------------------- // Test fixtures for HIR attributes @@ -170,36 +172,32 @@ fn has_test_like_hir_attributes_accepts_additional_test_attributes() { // // Behavioural coverage is achieved through: // -// 1. UI tests for attribute detection (is_test_attribute, -// has_test_like_hir_attributes): +// 1. UI tests for attribute detection (is_test_attribute, has_test_like_hir_attributes): // - pass_expect_in_test.rs, pass_expect_in_rstest.rs, pass_expect_in_tokio_test.rs // - These verify that test attributes are recognized without the fallback // // 2. UI tests for arbitrary cfg(test) ancestry detection: // - pass_expect_in_test_module.rs, pass_expect_in_tests_module.rs -// - These verify that `#[cfg(test)]` module ancestry marks nested contexts -// as test-only regardless of the exact module-name heuristic -// - fail_expect_in_file_backed_non_test_fn.rs confirms the ancestry does -// not leak into ordinary top-level functions next to a file-backed test -// module +// - These verify that `#[cfg(test)]` module ancestry marks nested contexts as test-only +// regardless of the exact module-name heuristic +// - fail_expect_in_file_backed_non_test_fn.rs confirms the ancestry does not leak into ordinary +// top-level functions next to a file-backed test module // // 3. Example-based regression coverage for the `rustc --test` harness path: -// - `pass_expect_in_tokio_test_harness` compiles a real `#[tokio::test]` -// example target under `--test`, placing `.expect(...)` calls inside -// nested closure and async-block bodies so the parent walk and sibling -// const descriptor fallback are both exercised. +// - `pass_expect_in_tokio_test_harness` compiles a real `#[tokio::test]` example target under +// `--test`, placing `.expect(...)` calls inside nested closure and async-block bodies so the +// parent walk and sibling const descriptor fallback are both exercised. // - `pass_expect_in_tokio_nonstandard_module_harness` and -// `pass_expect_in_tokio_path_module_harness` cover non-standard module -// names and `#[path]`-loaded Tokio tests under the harness path. -// - `pass_expect_in_tokio_path_module_harness_no_config` keeps the -// `cfg(test)` ancestor walk as the load-bearing path for file-backed -// Tokio tests without extra configuration. -// - `fail_expect_in_tokio_crate_non_test_fn` verifies that configured -// Tokio test attributes remain scoped to actual test functions. +// `pass_expect_in_tokio_path_module_harness` cover non-standard module names and +// `#[path]`-loaded Tokio tests under the harness path. +// - `pass_expect_in_tokio_path_module_harness_no_config` keeps the `cfg(test)` ancestor walk as +// the load-bearing path for file-backed Tokio tests without extra configuration. +// - `fail_expect_in_tokio_crate_non_test_fn` verifies that configured Tokio test attributes +// remain scoped to actual test functions. // -// 4. Real-world validation: The lint is used on this repository's own -// integration tests (compiled with --test), validating the fallback works -// correctly for `cfg(test)` ancestry checks and harness-based recovery. +// 4. Real-world validation: The lint is used on this repository's own integration tests (compiled +// with --test), validating the fallback works correctly for `cfg(test)` ancestry checks and +// harness-based recovery. // // The remaining helper with isolated unit coverage is straightforward: // - extract_function_item: matches `hir::Node::Item` values whose kind is `Fn` diff --git a/crates/no_expect_outside_tests/src/tests.rs b/crates/no_expect_outside_tests/src/tests.rs index a74000480..f6e842319 100644 --- a/crates/no_expect_outside_tests/src/tests.rs +++ b/crates/no_expect_outside_tests/src/tests.rs @@ -1,10 +1,14 @@ //! Unit tests validating context summarization outcomes across default and //! configured test attributes. -use crate::context::summarize_context; use rstest::rstest; -use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -use whitaker_common::{ContextEntry, ContextKind}; +use whitaker_common::{ + ContextEntry, + ContextKind, + attributes::{Attribute, AttributeKind, AttributePath}, +}; + +use crate::context::summarize_context; fn function_entry(name: &str, attrs: Vec) -> ContextEntry { ContextEntry::new(name, ContextKind::Function, attrs) diff --git a/crates/no_expect_outside_tests/src/tests/localization.rs b/crates/no_expect_outside_tests/src/tests/localization.rs index 3bfbc6ee1..33d65d318 100644 --- a/crates/no_expect_outside_tests/src/tests/localization.rs +++ b/crates/no_expect_outside_tests/src/tests/localization.rs @@ -12,8 +12,15 @@ use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::i18n::{BundleLookup, testing::FailingLookup}; use super::{ - I18nError, Localizer, MESSAGE_KEY, NoExpectMessages, ReceiverCategory, ReceiverLabel, - context_label, fallback_messages, localized_messages, + I18nError, + Localizer, + MESSAGE_KEY, + NoExpectMessages, + ReceiverCategory, + ReceiverLabel, + context_label, + fallback_messages, + localized_messages, }; use crate::context::ContextSummary; @@ -94,9 +101,7 @@ impl LocalizationWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> LocalizationWorld { - LocalizationWorld::default() -} +fn world() -> LocalizationWorld { LocalizationWorld::default() } #[given("the locale {locale} is selected")] fn given_locale(world: &LocalizationWorld, locale: String) { @@ -120,9 +125,7 @@ fn given_function(world: &LocalizationWorld, name: String) { } #[given("the receiver type is empty")] -fn given_receiver_type_empty(world: &LocalizationWorld) { - world.set_receiver_type(""); -} +fn given_receiver_type_empty(world: &LocalizationWorld) { world.set_receiver_type(""); } #[given("the receiver type is malformed")] fn given_receiver_type_malformed(world: &LocalizationWorld) { @@ -135,14 +138,10 @@ fn given_receiver_type_unexpected(world: &LocalizationWorld) { } #[given("the call occurs outside any function")] -fn given_no_function(world: &LocalizationWorld) { - world.set_function(None); -} +fn given_no_function(world: &LocalizationWorld) { world.set_function(None); } #[given("localization fails")] -fn given_failure(world: &LocalizationWorld) { - world.failing.set(true); -} +fn given_failure(world: &LocalizationWorld) { world.failing.set(true); } #[when("I localize the expect diagnostic")] fn when_localize(world: &LocalizationWorld) { @@ -211,39 +210,25 @@ fn then_failure(world: &LocalizationWorld, key: String) { } #[scenario(path = "tests/features/localization.feature", index = 0)] -fn scenario_fallback(world: LocalizationWorld) { - let _ = world; -} +fn scenario_fallback(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 1)] -fn scenario_cymraeg(world: LocalizationWorld) { - let _ = world; -} +fn scenario_cymraeg(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 2)] -fn scenario_unknown_locale(world: LocalizationWorld) { - let _ = world; -} +fn scenario_unknown_locale(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 3)] -fn scenario_receiver_empty(world: LocalizationWorld) { - let _ = world; -} +fn scenario_receiver_empty(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 4)] -fn scenario_receiver_malformed(world: LocalizationWorld) { - let _ = world; -} +fn scenario_receiver_malformed(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 5)] -fn scenario_receiver_unexpected(world: LocalizationWorld) { - let _ = world; -} +fn scenario_receiver_unexpected(world: LocalizationWorld) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 6)] -fn scenario_failure(world: LocalizationWorld) { - let _ = world; -} +fn scenario_failure(world: LocalizationWorld) { let _ = world; } #[then("the fallback help mentions {snippet}")] fn then_fallback(world: &LocalizationWorld, snippet: String) { @@ -270,6 +255,4 @@ fn execute_localization( localized_messages(lookup, receiver, &context, category) } -fn failing_lookup() -> FailingLookup { - FailingLookup::new(MESSAGE_KEY.as_ref()) -} +fn failing_lookup() -> FailingLookup { FailingLookup::new(MESSAGE_KEY.as_ref()) } diff --git a/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs b/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs index 769ab60af..57ce8dfc1 100644 --- a/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs +++ b/crates/no_expect_outside_tests/src/tests/receiver_type_edge_cases.rs @@ -3,7 +3,12 @@ use rstest::rstest; use super::{ - ContextLabel, Localizer, NoExpectMessages, ReceiverCategory, ReceiverLabel, localized_messages, + ContextLabel, + Localizer, + NoExpectMessages, + ReceiverCategory, + ReceiverLabel, + localized_messages, }; #[rstest] diff --git a/crates/no_std_fs_operations/src/behaviour.rs b/crates/no_std_fs_operations/src/behaviour.rs index 0a3488827..34a5680dd 100644 --- a/crates/no_std_fs_operations/src/behaviour.rs +++ b/crates/no_std_fs_operations/src/behaviour.rs @@ -1,11 +1,12 @@ //! Behaviour-driven localization tests for the `no_std_fs_operations` lint. -use crate::diagnostics::{StdFsMessages, localized_messages}; +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use whitaker_common::i18n::testing::FailingLookup; -use whitaker_common::i18n::{I18nError, Localizer}; +use whitaker_common::i18n::{I18nError, Localizer, testing::FailingLookup}; + +use crate::diagnostics::{StdFsMessages, localized_messages}; #[derive(Default)] struct LocalizationWorld { @@ -20,13 +21,9 @@ impl LocalizationWorld { self.localizer = Some(Localizer::new(Some(locale))); } - fn set_operation(&mut self, operation: &str) { - operation.clone_into(&mut self.operation); - } + fn set_operation(&mut self, operation: &str) { operation.clone_into(&mut self.operation); } - const fn mark_failure(&mut self) { - self.failing = true; - } + const fn mark_failure(&mut self) { self.failing = true; } fn resolve(&mut self) { let op = self.operation.clone(); @@ -79,14 +76,10 @@ fn given_operation(world: &WorldCell, operation: String) { } #[given("localization fails")] -fn given_failure(world: &WorldCell) { - world.borrow_mut().mark_failure(); -} +fn given_failure(world: &WorldCell) { world.borrow_mut().mark_failure(); } #[when("I localize the std::fs diagnostic")] -fn when_localize(world: &WorldCell) { - world.borrow_mut().resolve(); -} +fn when_localize(world: &WorldCell) { world.borrow_mut().resolve(); } #[then("the primary mentions {snippet}")] fn then_primary(world: &WorldCell, snippet: String) { @@ -133,26 +126,16 @@ fn then_failure(world: &WorldCell, key: String) { } #[scenario(path = "tests/features/localization.feature", index = 0)] -fn scenario_english(world: WorldCell) { - let _ = world; -} +fn scenario_english(world: WorldCell) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 1)] -fn scenario_welsh(world: WorldCell) { - let _ = world; -} +fn scenario_welsh(world: WorldCell) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 2)] -fn scenario_gaelic(world: WorldCell) { - let _ = world; -} +fn scenario_gaelic(world: WorldCell) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 3)] -fn scenario_fallback(world: WorldCell) { - let _ = world; -} +fn scenario_fallback(world: WorldCell) { let _ = world; } #[scenario(path = "tests/features/localization.feature", index = 4)] -fn scenario_failure(world: WorldCell) { - let _ = world; -} +fn scenario_failure(world: WorldCell) { let _ = world; } diff --git a/crates/no_std_fs_operations/src/config.rs b/crates/no_std_fs_operations/src/config.rs index 6ffdbad2f..a333aea9b 100644 --- a/crates/no_std_fs_operations/src/config.rs +++ b/crates/no_std_fs_operations/src/config.rs @@ -1,10 +1,12 @@ //! Configuration for the `no_std_fs_operations` lint: the settings schema and //! the `dylint.toml` loading path, kept separate from the lint pass itself. -use crate::exclusion::PathExclusions; +use std::collections::HashSet; + use log::warn; use serde::Deserialize; -use std::collections::HashSet; + +use crate::exclusion::PathExclusions; /// Lint name used for `dylint.toml` lookups, diagnostic codes, and log targets. pub(crate) const LINT_NAME: &str = "no_std_fs_operations"; diff --git a/crates/no_std_fs_operations/src/diagnostics.rs b/crates/no_std_fs_operations/src/diagnostics.rs index ea8ebcc6a..ee147ce40 100644 --- a/crates/no_std_fs_operations/src/diagnostics.rs +++ b/crates/no_std_fs_operations/src/diagnostics.rs @@ -1,17 +1,24 @@ //! Localized diagnostics for the `no_std_fs_operations` lint. -use crate::NO_STD_FS_OPERATIONS; -use crate::usage::StdFsUsage; +use std::borrow::Cow; + use rustc_lint::{LateContext, LintContext}; use rustc_span::Span; -use std::borrow::Cow; use whitaker_common::i18n::{ - Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, - noop_reporter, safe_resolve_message_set, + Arguments, + DiagnosticMessageSet, + FluentValue, + Localizer, + MessageKey, + MessageResolution, + noop_reporter, + safe_resolve_message_set, }; #[cfg(test)] use whitaker_common::i18n::{BundleLookup, I18nError, resolve_message_set}; +use crate::{NO_STD_FS_OPERATIONS, usage::StdFsUsage}; + /// Emit a diagnostic for a detected `std::fs` usage. pub(crate) fn emit_diagnostic( cx: &LateContext<'_>, @@ -61,9 +68,11 @@ fn fallback_messages(operation: &str) -> StdFsMessages { ) .to_owned(); let help = concat!( - "Pass `cap_std::fs::Dir` handles and camino::Utf8Path/Utf8PathBuf arguments down to the call ", + "Pass `cap_std::fs::Dir` handles and camino::Utf8Path/Utf8PathBuf arguments down to the \ + call ", "so only explicit capabilities touch the filesystem." - ).to_owned(); + ) + .to_owned(); DiagnosticMessageSet::new(primary, note, help) } diff --git a/crates/no_std_fs_operations/src/driver.rs b/crates/no_std_fs_operations/src/driver.rs index 94d593401..bf339cb1d 100644 --- a/crates/no_std_fs_operations/src/driver.rs +++ b/crates/no_std_fs_operations/src/driver.rs @@ -1,12 +1,6 @@ //! Lint crate enforcing capability-based filesystem access by forbidding //! `std::fs` operations. -use crate::config::{LINT_NAME, load_configuration}; -use crate::diagnostics::emit_diagnostic; -use crate::exclusion::PathExclusions; -use crate::usage::{ - StdFsUsage, UsageCategory, classify_def_id, classify_qpath, classify_res, label_is_std_fs, -}; use log::{debug, info}; use rustc_hir as hir; use rustc_hir::AmbigArg; @@ -14,9 +8,24 @@ use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty; use rustc_span::{Span, sym}; use whitaker::SharedConfig; -use whitaker_common::SimplePath; -use whitaker_common::i18n::Localizer; -use whitaker_common::i18n::get_localizer_for_lint; +use whitaker_common::{ + SimplePath, + i18n::{Localizer, get_localizer_for_lint}, +}; + +use crate::{ + config::{LINT_NAME, load_configuration}, + diagnostics::emit_diagnostic, + exclusion::PathExclusions, + usage::{ + StdFsUsage, + UsageCategory, + classify_def_id, + classify_qpath, + classify_res, + label_is_std_fs, + }, +}; /// Lint pass that tracks localization and exclusion state while checking /// `std::fs` usage. @@ -167,9 +176,7 @@ struct LintSite { impl NoStdFsOperations { /// Centralizes exclusion logic for all lint pass methods. #[inline] - const fn should_skip(&self) -> bool { - self.excluded - } + const fn should_skip(&self) -> bool { self.excluded } fn emit_optional(&self, cx: &LateContext<'_>, site: LintSite, usage: Option) { if self.should_skip() { diff --git a/crates/no_std_fs_operations/src/exclusion.rs b/crates/no_std_fs_operations/src/exclusion.rs index eb6d481f9..eb232e7fc 100644 --- a/crates/no_std_fs_operations/src/exclusion.rs +++ b/crates/no_std_fs_operations/src/exclusion.rs @@ -6,11 +6,13 @@ //! ordinary unit and behavioural tests; the driver supplies the enclosing item //! path resolved from the HIR. -use crate::config::LINT_NAME; -use log::warn; use std::collections::HashSet; + +use log::warn; use whitaker_common::SimplePath; +use crate::config::LINT_NAME; + /// Maximum length of a malformed entry echoed into a warning, so a pathological /// configuration value cannot produce an unbounded log line. const MAX_LOGGED_ENTRY_LEN: usize = 64; @@ -67,9 +69,7 @@ impl PathExclusions { /// /// The driver consults this before resolving an item's path so the common /// case pays no lookup cost. - pub(crate) const fn is_empty(&self) -> bool { - self.prefixes.is_empty() - } + pub(crate) const fn is_empty(&self) -> bool { self.prefixes.is_empty() } /// Returns `true` when `item_path` falls within a configured exclusion. /// @@ -114,12 +114,14 @@ mod tests { //! prefix matching (example-based and property-based), and the bounded //! rendering used when warning about rejected entries. - use super::PathExclusions; + use std::collections::HashSet; + use proptest::prelude::*; use rstest::rstest; - use std::collections::HashSet; use whitaker_common::SimplePath; + use super::PathExclusions; + fn exclusions(paths: &[&str]) -> PathExclusions { PathExclusions::new( &paths @@ -222,9 +224,7 @@ mod tests { // item paths that share segment *text* with a prefix but differ at a segment // boundary — exactly the `a::b` vs `a::bc` hazard segment-wise matching must // reject. - fn segment() -> impl Strategy { - "[a-c]{1,3}" - } + fn segment() -> impl Strategy { "[a-c]{1,3}" } proptest! { #[test] diff --git a/crates/no_std_fs_operations/src/exclusion_behaviour.rs b/crates/no_std_fs_operations/src/exclusion_behaviour.rs index f5dcd57b4..2eeb5c2f8 100644 --- a/crates/no_std_fs_operations/src/exclusion_behaviour.rs +++ b/crates/no_std_fs_operations/src/exclusion_behaviour.rs @@ -3,13 +3,14 @@ //! These scenarios exercise the pure `PathExclusions` decision the driver makes //! for each detected `std::fs` usage, without needing a live `rustc` session. -use crate::exclusion::PathExclusions; +use std::{cell::RefCell, collections::HashSet}; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; -use std::collections::HashSet; use whitaker_common::SimplePath; +use crate::exclusion::PathExclusions; + #[derive(Default)] struct ExclusionWorld { excluded_paths: HashSet, @@ -17,9 +18,7 @@ struct ExclusionWorld { } impl ExclusionWorld { - fn exclude_path(&mut self, path: &str) { - self.excluded_paths.insert(path.to_owned()); - } + fn exclude_path(&mut self, path: &str) { self.excluded_paths.insert(path.to_owned()); } fn evaluate(&mut self, item_path: &str) { let exclusions = PathExclusions::new(&self.excluded_paths); @@ -38,9 +37,7 @@ type WorldCell = RefCell; #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> WorldCell { - RefCell::new(ExclusionWorld::default()) -} +fn world() -> WorldCell { RefCell::new(ExclusionWorld::default()) } #[given("the module path {path} is excluded")] fn given_excluded_path(world: &WorldCell, path: String) { @@ -48,9 +45,7 @@ fn given_excluded_path(world: &WorldCell, path: String) { } #[given("no module paths are excluded")] -fn given_no_excluded_paths(world: &WorldCell) { - world.borrow_mut().excluded_paths.clear(); -} +fn given_no_excluded_paths(world: &WorldCell) { world.borrow_mut().excluded_paths.clear(); } #[when("a std::fs usage is found in item {item}")] fn when_usage_found(world: &WorldCell, item: String) { diff --git a/crates/no_std_fs_operations/src/tests/ui.rs b/crates/no_std_fs_operations/src/tests/ui.rs index 0ef049167..b0e05e715 100644 --- a/crates/no_std_fs_operations/src/tests/ui.rs +++ b/crates/no_std_fs_operations/src/tests/ui.rs @@ -5,27 +5,19 @@ use whitaker_common::test_support::with_locale; #[test] #[serial] -fn ui() { - run_with_locale("ui", None); -} +fn ui() { run_with_locale("ui", None); } #[test] #[serial] -fn ui_runs_in_welsh() { - run_with_locale("ui-cy", Some("cy")); -} +fn ui_runs_in_welsh() { run_with_locale("ui-cy", Some("cy")); } #[test] #[serial] -fn ui_runs_in_gaelic() { - run_with_locale("ui-gd", Some("gd")); -} +fn ui_runs_in_gaelic() { run_with_locale("ui-gd", Some("gd")); } #[test] #[serial] -fn ui_runs_in_fallback_locale() { - run_with_locale("ui-fallback", Some("zz")); -} +fn ui_runs_in_fallback_locale() { run_with_locale("ui-fallback", Some("zz")); } fn run_with_locale(directory: &str, locale: Option<&str>) { with_locale(locale, || { diff --git a/crates/no_std_fs_operations/src/usage/mod.rs b/crates/no_std_fs_operations/src/usage/mod.rs index c9ad6d70b..8cfd243cb 100644 --- a/crates/no_std_fs_operations/src/usage/mod.rs +++ b/crates/no_std_fs_operations/src/usage/mod.rs @@ -1,8 +1,7 @@ //! Classifies `std::fs` usages encountered by the lint into diagnostic inputs. use rustc_hir as hir; -use rustc_hir::def::Res; -use rustc_hir::def_id::DefId; +use rustc_hir::{def::Res, def_id::DefId}; use rustc_lint::LateContext; use rustc_span::sym; use whitaker_common::SimplePath; @@ -66,16 +65,12 @@ impl StdFsUsage { /// let usage = StdFsUsage::new(String::from("std::fs::remove_file"), UsageCategory::Call); /// assert_eq!(usage.operation(), "std::fs::remove_file"); /// ``` - pub fn operation(&self) -> &str { - &self.operation - } + pub fn operation(&self) -> &str { &self.operation } /// Returns the usage category. #[cfg(test)] #[must_use] - pub const fn category(&self) -> UsageCategory { - self.category - } + pub const fn category(&self) -> UsageCategory { self.category } } /// Classify a resolved path (expression, type, import) into a usage record. @@ -151,9 +146,7 @@ fn is_std_fs_path(path: &SimplePath) -> bool { } /// Returns true if the character should be rejected in a valid `std::fs` label. -const fn is_invalid_label_char(ch: char) -> bool { - ch.is_whitespace() || matches!(ch, '(' | ')') -} +const fn is_invalid_label_char(ch: char) -> bool { ch.is_whitespace() || matches!(ch, '(' | ')') } pub(crate) fn label_is_std_fs(label: &str) -> bool { if label != label.trim() { diff --git a/crates/no_std_fs_operations/src/usage/tests.rs b/crates/no_std_fs_operations/src/usage/tests.rs index bffc21005..d0680b7a9 100644 --- a/crates/no_std_fs_operations/src/usage/tests.rs +++ b/crates/no_std_fs_operations/src/usage/tests.rs @@ -1,7 +1,8 @@ //! Tests for classifying `std::fs` usage and its reporting metadata. -use super::{StdFsUsage, UsageCategory, label_is_std_fs}; use rstest::rstest; +use super::{StdFsUsage, UsageCategory, label_is_std_fs}; + #[rstest] #[case("std::fs", true)] #[case("std::fs::File::open", true)] diff --git a/crates/no_std_fs_operations/tests/test_support/mod.rs b/crates/no_std_fs_operations/tests/test_support/mod.rs index 6b132c873..84079d74b 100644 --- a/crates/no_std_fs_operations/tests/test_support/mod.rs +++ b/crates/no_std_fs_operations/tests/test_support/mod.rs @@ -17,9 +17,7 @@ pub(super) struct FixtureProject { impl FixtureProject { /// Returns the fixture project root directory. - pub(super) fn root(&self) -> &Path { - &self.root - } + pub(super) fn root(&self) -> &Path { &self.root } } /// Selects which suppression mechanism a fixture exercises. diff --git a/crates/no_unwrap_or_else_panic/src/context/tests.rs b/crates/no_unwrap_or_else_panic/src/context/tests.rs index d22d97079..6041e8690 100644 --- a/crates/no_unwrap_or_else_panic/src/context/tests.rs +++ b/crates/no_unwrap_or_else_panic/src/context/tests.rs @@ -3,8 +3,6 @@ //! Verifies that `convert_attribute` and `is_cfg_test_attribute` handle //! parsed attributes (e.g., `#[must_use]`) without panicking. -#[cfg(feature = "dylint-driver")] -use super::{convert_attribute, is_cfg_test_attribute}; #[cfg(feature = "dylint-driver")] use rustc_hir as hir; #[cfg(feature = "dylint-driver")] @@ -14,6 +12,9 @@ use rustc_span::DUMMY_SP; #[cfg(feature = "dylint-driver")] use whitaker_common::{AttributeKind, PARSED_ATTRIBUTE_PLACEHOLDER}; +#[cfg(feature = "dylint-driver")] +use super::{convert_attribute, is_cfg_test_attribute}; + /// Verify that `convert_attribute` handles parsed attributes without panicking. /// /// Parsed attributes (e.g., `#[must_use]`) are pre-processed by rustc and don't diff --git a/crates/no_unwrap_or_else_panic/src/diagnostics.rs b/crates/no_unwrap_or_else_panic/src/diagnostics.rs index d8a7628d3..a7515d0e0 100644 --- a/crates/no_unwrap_or_else_panic/src/diagnostics.rs +++ b/crates/no_unwrap_or_else_panic/src/diagnostics.rs @@ -1,14 +1,22 @@ //! Diagnostic emission for the lint, including localization fallbacks. -use crate::{LINT_NAME, NO_UNWRAP_OR_ELSE_PANIC}; +use std::borrow::Cow; + use rustc_hir as hir; use rustc_lint::{LateContext, LintContext}; -use std::borrow::Cow; use whitaker_common::i18n::{ - Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, - noop_reporter, safe_resolve_message_set, + Arguments, + DiagnosticMessageSet, + FluentValue, + Localizer, + MessageKey, + MessageResolution, + noop_reporter, + safe_resolve_message_set, }; +use crate::{LINT_NAME, NO_UNWRAP_OR_ELSE_PANIC}; + const MESSAGE_KEY: MessageKey<'static> = MessageKey::new(LINT_NAME); /// Emit the lint diagnostic using localized messages. diff --git a/crates/no_unwrap_or_else_panic/src/driver.rs b/crates/no_unwrap_or_else_panic/src/driver.rs index 377199f15..920d53589 100644 --- a/crates/no_unwrap_or_else_panic/src/driver.rs +++ b/crates/no_unwrap_or_else_panic/src/driver.rs @@ -1,19 +1,23 @@ //! Lint wiring that flags panicking `unwrap_or_else` fallbacks. -use crate::LINT_NAME; -use crate::context::ContextSummary; -use crate::diagnostics::emit_diagnostic; -use crate::panic_detector::{closure_panics, receiver_is_option_or_result}; -use crate::policy::{LintPolicy, should_flag}; +use std::collections::HashSet; + use log::debug; use rustc_hir as hir; use rustc_hir::ExprKind; use rustc_lint::{LateContext, LateLintPass}; use serde::Deserialize; -use std::collections::HashSet; use whitaker::SharedConfig; use whitaker_common::i18n::{Localizer, get_localizer_for_lint}; +use crate::{ + LINT_NAME, + context::ContextSummary, + diagnostics::emit_diagnostic, + panic_detector::{closure_panics, receiver_is_option_or_result}, + policy::{LintPolicy, should_flag}, +}; + /// Dylint lint declaration and registration glue. /// /// `impl_late_lint!` expands to the Dylint ABI entry point and the @@ -46,9 +50,7 @@ struct Config { } impl Config { - fn resolved_allow_in_main(&self) -> bool { - self.allow_in_main.unwrap_or(false) - } + fn resolved_allow_in_main(&self) -> bool { self.allow_in_main.unwrap_or(false) } } /// Lint pass that inspects `unwrap_or_else` fallbacks for panics. diff --git a/crates/no_unwrap_or_else_panic/src/panic_detector.rs b/crates/no_unwrap_or_else_panic/src/panic_detector.rs index c958aba99..01b1ad065 100644 --- a/crates/no_unwrap_or_else_panic/src/panic_detector.rs +++ b/crates/no_unwrap_or_else_panic/src/panic_detector.rs @@ -1,8 +1,7 @@ //! Detect panics inside `unwrap_or_else` fallback closures. use rustc_hir as hir; -use rustc_hir::def_id::DefId; -use rustc_hir::{Expr, ExprKind}; +use rustc_hir::{Expr, ExprKind, def_id::DefId}; use rustc_lint::LateContext; use rustc_middle::ty; use rustc_span::sym; diff --git a/crates/no_unwrap_or_else_panic/src/policy.rs b/crates/no_unwrap_or_else_panic/src/policy.rs index f837dad72..a979c37ea 100644 --- a/crates/no_unwrap_or_else_panic/src/policy.rs +++ b/crates/no_unwrap_or_else_panic/src/policy.rs @@ -1,7 +1,6 @@ //! Pure lint policy evaluation logic shared by driver and behaviour tests. -use crate::context::ContextSummary; -use crate::panic_detector::PanicInfo; +use crate::{context::ContextSummary, panic_detector::PanicInfo}; /// Configuration flags controlling when the lint should emit diagnostics. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -12,9 +11,7 @@ pub(crate) struct LintPolicy { impl LintPolicy { /// Create a policy with the given `allow_in_main` flag. #[must_use] - pub(crate) const fn new(allow_in_main: bool) -> Self { - Self { allow_in_main } - } + pub(crate) const fn new(allow_in_main: bool) -> Self { Self { allow_in_main } } } /// Decide whether the lint should emit based on context and closure behaviour. @@ -69,9 +66,10 @@ pub(crate) const fn should_flag( #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[derive(Clone, Copy, Debug)] struct PolicyCase { policy: LintPolicy, diff --git a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs index 4b7666b4a..96a89b0df 100644 --- a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs +++ b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs @@ -34,9 +34,7 @@ impl DecisionWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> DecisionWorld { - DecisionWorld::default() -} +fn world() -> DecisionWorld { DecisionWorld::default() } #[given("a panicking unwrap_or_else fallback outside tests")] fn given_panicking(world: &DecisionWorld) { @@ -51,9 +49,7 @@ fn given_panicking(world: &DecisionWorld) { } #[given("a panicking unwrap_or_else fallback")] -fn given_panicking_alias(world: &DecisionWorld) { - given_panicking(world); -} +fn given_panicking_alias(world: &DecisionWorld) { given_panicking(world); } #[given("the panic message interpolates a value")] fn given_interpolating(world: &DecisionWorld) { @@ -85,24 +81,16 @@ fn given_main(world: &DecisionWorld) { } #[given("allow in main is enabled")] -fn given_allow_main(world: &DecisionWorld) { - world.allow_in_main.set(true); -} +fn given_allow_main(world: &DecisionWorld) { world.allow_in_main.set(true); } #[given("the fallback is safe")] -fn given_safe_fallback(world: &DecisionWorld) { - world.panic_info.set(PanicInfo::default()); -} +fn given_safe_fallback(world: &DecisionWorld) { world.panic_info.set(PanicInfo::default()); } #[given("a doctest harness is active")] -fn given_doctest(world: &DecisionWorld) { - world.is_doctest.set(true); -} +fn given_doctest(world: &DecisionWorld) { world.is_doctest.set(true); } #[when("the lint policy is evaluated")] -fn when_policy_evaluated(world: &DecisionWorld) { - world.should_flag.set(Some(world.evaluate())); -} +fn when_policy_evaluated(world: &DecisionWorld) { world.should_flag.set(Some(world.evaluate())); } #[then("the lint triggers")] fn then_triggers(world: &DecisionWorld) { @@ -115,31 +103,19 @@ fn then_skipped(world: &DecisionWorld) { } #[scenario(path = "tests/features/policy.feature", index = 0)] -fn scenario_panicking_outside_tests(world: DecisionWorld) { - let _ = world; -} +fn scenario_panicking_outside_tests(world: DecisionWorld) { let _ = world; } #[scenario(path = "tests/features/policy.feature", index = 1)] -fn scenario_panicking_inside_test(world: DecisionWorld) { - let _ = world; -} +fn scenario_panicking_inside_test(world: DecisionWorld) { let _ = world; } #[scenario(path = "tests/features/policy.feature", index = 2)] -fn scenario_panicking_in_main_with_allow(world: DecisionWorld) { - let _ = world; -} +fn scenario_panicking_in_main_with_allow(world: DecisionWorld) { let _ = world; } #[scenario(path = "tests/features/policy.feature", index = 3)] -fn scenario_safe_fallback(world: DecisionWorld) { - let _ = world; -} +fn scenario_safe_fallback(world: DecisionWorld) { let _ = world; } #[scenario(path = "tests/features/policy.feature", index = 4)] -fn scenario_doctest(world: DecisionWorld) { - let _ = world; -} +fn scenario_doctest(world: DecisionWorld) { let _ = world; } #[scenario(path = "tests/features/policy.feature", index = 5)] -fn scenario_interpolated_panic_in_test(world: DecisionWorld) { - let _ = world; -} +fn scenario_interpolated_panic_in_test(world: DecisionWorld) { let _ = world; } diff --git a/crates/rstest_helper_should_be_fixture/src/collector.rs b/crates/rstest_helper_should_be_fixture/src/collector.rs index 70f54632f..f9dea57bd 100644 --- a/crates/rstest_helper_should_be_fixture/src/collector.rs +++ b/crates/rstest_helper_should_be_fixture/src/collector.rs @@ -8,8 +8,10 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use log::debug; use rustc_hir as hir; -use rustc_hir::def::{DefKind, Res}; -use rustc_hir::def_id::{DefId, LOCAL_CRATE}; +use rustc_hir::{ + def::{DefKind, Res}, + def_id::{DefId, LOCAL_CRATE}, +}; use rustc_lint::LateContext; use rustc_span::{BytePos, FileName, Span}; use whitaker_common::rstest::{ArgAtom, ArgFingerprint}; @@ -190,14 +192,10 @@ impl CallSiteCollector { } /// Returns the number of distinct callees with collected evidence. - pub(crate) fn callee_count(&self) -> usize { - self.by_callee.len() - } + pub(crate) fn callee_count(&self) -> usize { self.by_callee.len() } /// Returns the number of deduplicated call-site records. - pub(crate) fn record_count(&self) -> usize { - self.by_callee.values().map(Vec::len).sum() - } + pub(crate) fn record_count(&self) -> usize { self.by_callee.values().map(Vec::len).sum() } /// Removes all stored evidence from the collector. pub(crate) fn clear(&mut self) { @@ -304,9 +302,7 @@ fn literal_atom(cx: &LateContext<'_>, span: Span, lit: &hir::Lit) -> ArgAtom { literal_text_atom(text) } -fn literal_text_atom(text: String) -> ArgAtom { - ArgAtom::const_lit(text) -} +fn literal_text_atom(text: String) -> ArgAtom { ArgAtom::const_lit(text) } /// Resolves a helper call expression to a local function definition. #[must_use] diff --git a/crates/rstest_helper_should_be_fixture/src/collector_tests.rs b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs index 23bc3085a..51471a363 100644 --- a/crates/rstest_helper_should_be_fixture/src/collector_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs @@ -16,7 +16,10 @@ use rustc_span::{BytePos, DUMMY_SP, FileName, Span}; use whitaker_common::rstest::{ArgAtom, ArgFingerprint}; use super::{ - CallSiteCollector, CallSiteLocation, CallSiteRecord, literal_text_atom, + CallSiteCollector, + CallSiteLocation, + CallSiteRecord, + literal_text_atom, should_skip_arg_for_unrecoverable_span, }; @@ -140,17 +143,13 @@ fn collect_two_calls(lo2: u32, hi2: u32) -> (CallSiteCollector, [bool; 2]) { (collector, inserted) } -fn record(callee_def_id: DefId) -> CallSiteRecord { - record_at(callee_def_id, DUMMY_SP) -} +fn record(callee_def_id: DefId) -> CallSiteRecord { record_at(callee_def_id, DUMMY_SP) } fn record_at(callee_def_id: DefId, span: Span) -> CallSiteRecord { CallSiteRecord::new(callee_def_id, ArgFingerprint::default(), def_id(99), span) } -fn def_id(index: u32) -> DefId { - DefId::local(DefIndex::from_u32(index)) -} +fn def_id(index: u32) -> DefId { DefId::local(DefIndex::from_u32(index)) } fn location(callee: &str, lo: BytePos, hi: BytePos) -> CallSiteLocation { location_with_hir_id(callee, lo, hi, 0) @@ -170,9 +169,7 @@ fn location_with_hir_id( ) } -fn source_file() -> FileName { - FileName::Custom("src/lib.rs".to_owned()) -} +fn source_file() -> FileName { FileName::Custom("src/lib.rs".to_owned()) } proptest! { #[test] diff --git a/crates/rstest_helper_should_be_fixture/src/driver.rs b/crates/rstest_helper_should_be_fixture/src/driver.rs index 8d2426387..f5b4e4ae4 100644 --- a/crates/rstest_helper_should_be_fixture/src/driver.rs +++ b/crates/rstest_helper_should_be_fixture/src/driver.rs @@ -4,26 +4,30 @@ //! configuration normalization stays in small helper methods so it can be //! tested without constructing rustc contexts. -use crate::collector::CallSiteCollector; -use crate::visitor::{ - CallSiteVisitor, attribute_from_hir, fixture_local_ids, redacted_fingerprint_shape, -}; +use std::{collections::HashSet, io::Write}; + use camino::{Utf8Path, Utf8PathBuf}; -use cap_std::ambient_authority; -use cap_std::fs_utf8::{Dir, OpenOptions}; +use cap_std::{ + ambient_authority, + fs_utf8::{Dir, OpenOptions}, +}; use log::debug; use rustc_hir as hir; -use rustc_hir::def_id::LocalDefId; -use rustc_hir::intravisit::Visitor; +use rustc_hir::{def_id::LocalDefId, intravisit::Visitor}; use rustc_lint::{LateContext, LateLintPass}; use rustc_span::Span; use serde::Deserialize; -use std::collections::HashSet; -use std::io::Write; use whitaker::SharedConfig; -use whitaker_common::attributes::AttributePath; -use whitaker_common::i18n::{Localizer, get_localizer_for_lint}; -use whitaker_common::rstest::{RstestDetectionOptions, is_rstest_test_with}; +use whitaker_common::{ + attributes::AttributePath, + i18n::{Localizer, get_localizer_for_lint}, + rstest::{RstestDetectionOptions, is_rstest_test_with}, +}; + +use crate::{ + collector::CallSiteCollector, + visitor::{CallSiteVisitor, attribute_from_hir, fixture_local_ids, redacted_fingerprint_shape}, +}; const LINT_NAME: &str = "rstest_helper_should_be_fixture"; /// Internal test-only hook used by the UI harness to assert passive collection. diff --git a/crates/rstest_helper_should_be_fixture/src/visitor.rs b/crates/rstest_helper_should_be_fixture/src/visitor.rs index d890a199f..de7b835c3 100644 --- a/crates/rstest_helper_should_be_fixture/src/visitor.rs +++ b/crates/rstest_helper_should_be_fixture/src/visitor.rs @@ -3,21 +3,36 @@ //! This module keeps rustc HIR mechanics separate from the lint-pass bootstrap //! so the driver remains focused on configuration and crate-level lifecycle. -use crate::collector::{ - CallSiteCollector, CallSiteLocation, CallSiteRecord, lower_arg_atom, resolve_local_callee, -}; +use std::collections::HashSet; + use log::debug; use rustc_ast::AttrStyle; use rustc_hir as hir; -use rustc_hir::def_id::DefId; -use rustc_hir::intravisit::{self, Visitor}; +use rustc_hir::{ + def_id::DefId, + intravisit::{self, Visitor}, +}; use rustc_lint::LateContext; use rustc_span::Span; -use std::collections::HashSet; -use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; -use whitaker_common::rstest::{ - ArgAtom, ArgFingerprint, ParameterBinding, RstestDetectionOptions, RstestParameter, - RstestParameterKind, classify_rstest_parameter, +use whitaker_common::{ + attributes::{Attribute, AttributeKind, AttributePath}, + rstest::{ + ArgAtom, + ArgFingerprint, + ParameterBinding, + RstestDetectionOptions, + RstestParameter, + RstestParameterKind, + classify_rstest_parameter, + }, +}; + +use crate::collector::{ + CallSiteCollector, + CallSiteLocation, + CallSiteRecord, + lower_arg_atom, + resolve_local_callee, }; const LINT_NAME: &str = "rstest_helper_should_be_fixture"; diff --git a/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs b/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs index bc306996b..ac69cca26 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui/harness_lock.rs @@ -41,9 +41,7 @@ pub(crate) struct ExampleHarnessLock { impl ExampleHarnessLock { /// Returns the lock directory serializing the current example run. - pub(crate) fn path(&self) -> &Path { - &self.path - } + pub(crate) fn path(&self) -> &Path { &self.path } } #[derive(Clone, Debug, Eq, PartialEq)] struct ExampleHarnessLockOwner(String); diff --git a/crates/test_must_not_have_example/src/behaviour.rs b/crates/test_must_not_have_example/src/behaviour.rs index 5fd53dac7..7c82a288f 100644 --- a/crates/test_must_not_have_example/src/behaviour.rs +++ b/crates/test_must_not_have_example/src/behaviour.rs @@ -1,9 +1,11 @@ //! Behaviour-driven coverage for documentation example heuristics. -use crate::heuristics::{DocExampleViolation, detect_example_violation}; +use std::cell::RefCell; + use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; -use std::cell::RefCell; + +use crate::heuristics::{DocExampleViolation, detect_example_violation}; #[derive(Default)] struct DocumentationWorld { @@ -12,35 +14,25 @@ struct DocumentationWorld { } impl DocumentationWorld { - fn push_line(&self, line: &str) { - self.lines.borrow_mut().push(line.to_owned()); - } + fn push_line(&self, line: &str) { self.lines.borrow_mut().push(line.to_owned()); } fn evaluate(&self) { let doc = self.lines.borrow().join("\n"); self.outcome.replace(detect_example_violation(&doc)); } - fn outcome(&self) -> Option { - *self.outcome.borrow() - } + fn outcome(&self) -> Option { *self.outcome.borrow() } } #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> DocumentationWorld { - DocumentationWorld::default() -} +fn world() -> DocumentationWorld { DocumentationWorld::default() } #[given("documentation line {line}")] -fn given_line(world: &DocumentationWorld, line: String) { - world.push_line(line.trim_matches('"')); -} +fn given_line(world: &DocumentationWorld, line: String) { world.push_line(line.trim_matches('"')); } #[when("I evaluate the documentation")] -fn when_evaluate(world: &DocumentationWorld) { - world.evaluate(); -} +fn when_evaluate(world: &DocumentationWorld) { world.evaluate(); } #[then("the violation is examples heading")] fn then_examples_heading(world: &DocumentationWorld) { @@ -58,26 +50,16 @@ fn then_no_violation(world: &DocumentationWorld) { } #[scenario(path = "tests/features/doc_examples.feature", index = 0)] -fn scenario_examples_heading(world: DocumentationWorld) { - let _ = world; -} +fn scenario_examples_heading(world: DocumentationWorld) { let _ = world; } #[scenario(path = "tests/features/doc_examples.feature", index = 1)] -fn scenario_code_fence(world: DocumentationWorld) { - let _ = world; -} +fn scenario_code_fence(world: DocumentationWorld) { let _ = world; } #[scenario(path = "tests/features/doc_examples.feature", index = 2)] -fn scenario_inline_ticks(world: DocumentationWorld) { - let _ = world; -} +fn scenario_inline_ticks(world: DocumentationWorld) { let _ = world; } #[scenario(path = "tests/features/doc_examples.feature", index = 3)] -fn scenario_plain_prose(world: DocumentationWorld) { - let _ = world; -} +fn scenario_plain_prose(world: DocumentationWorld) { let _ = world; } #[scenario(path = "tests/features/doc_examples.feature", index = 4)] -fn scenario_source_order(world: DocumentationWorld) { - let _ = world; -} +fn scenario_source_order(world: DocumentationWorld) { let _ = world; } diff --git a/crates/test_must_not_have_example/src/driver.rs b/crates/test_must_not_have_example/src/driver.rs index 453c17def..dc46e3ae3 100644 --- a/crates/test_must_not_have_example/src/driver.rs +++ b/crates/test_must_not_have_example/src/driver.rs @@ -1,21 +1,31 @@ //! Lint crate enforcing example-free documentation for test functions. -use crate::heuristics::{DocExampleViolation, detect_example_violation}; +use std::borrow::Cow; + use log::debug; use rustc_hir as hir; use rustc_hir::Node; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_span::{Ident, Span, Symbol}; use serde::Deserialize; -use std::borrow::Cow; -use whitaker::SharedConfig; -use whitaker::hir::has_test_like_hir_attributes; -use whitaker_common::AttributePath; -use whitaker_common::i18n::{ - Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, - get_localizer_for_lint, noop_reporter, safe_resolve_message_set, +use whitaker::{SharedConfig, hir::has_test_like_hir_attributes}; +use whitaker_common::{ + AttributePath, + i18n::{ + Arguments, + DiagnosticMessageSet, + FluentValue, + Localizer, + MessageKey, + MessageResolution, + get_localizer_for_lint, + noop_reporter, + safe_resolve_message_set, + }, }; +use crate::heuristics::{DocExampleViolation, detect_example_violation}; + const LINT_NAME: &str = "test_must_not_have_example"; const MESSAGE_KEY: MessageKey<'static> = MessageKey::new("test_must_not_have_example"); diff --git a/crates/test_must_not_have_example/src/heuristics.rs b/crates/test_must_not_have_example/src/heuristics.rs index 18347195f..bba408d87 100644 --- a/crates/test_must_not_have_example/src/heuristics.rs +++ b/crates/test_must_not_have_example/src/heuristics.rs @@ -89,9 +89,10 @@ fn is_code_fence(line: &str) -> bool { #[cfg(test)] mod tests { - use super::{DocExampleViolation, detect_example_violation}; use rstest::rstest; + use super::{DocExampleViolation, detect_example_violation}; + #[rstest] #[case("No examples here.", None)] #[case("# Examples", Some(DocExampleViolation::ExamplesHeading))] diff --git a/crates/whitaker_clones_core/build.rs b/crates/whitaker_clones_core/build.rs index ffda4682d..4f193e054 100644 --- a/crates/whitaker_clones_core/build.rs +++ b/crates/whitaker_clones_core/build.rs @@ -12,7 +12,10 @@ use camino::Utf8PathBuf; mod build_support; use build_support::{ - exact_version, find_workspace_manifest, parser_dependency_requirement, read_workspace_manifest, + exact_version, + find_workspace_manifest, + parser_dependency_requirement, + read_workspace_manifest, }; const PARSER_VERSION_ENV: &str = "WHITAKER_RA_AP_SYNTAX_VERSION"; diff --git a/crates/whitaker_clones_core/src/ast/features.rs b/crates/whitaker_clones_core/src/ast/features.rs index a3a654246..8b48e3533 100644 --- a/crates/whitaker_clones_core/src/ast/features.rs +++ b/crates/whitaker_clones_core/src/ast/features.rs @@ -9,7 +9,10 @@ use super::{Depth, KindId, NormalizedNode, NormalizedTree}; /// # Examples /// /// ``` -/// use whitaker_clones_core::{KindCounts, ast::{Depth, KindId}}; +/// use whitaker_clones_core::{ +/// KindCounts, +/// ast::{Depth, KindId}, +/// }; /// /// let counts = KindCounts::default(); /// assert_eq!(counts.count(KindId::new(1), Depth::root()), 0); @@ -57,19 +60,13 @@ impl KindWeight { /// Returns a zero weight. #[must_use] - pub const fn zero() -> Self { - Self(0) - } + pub const fn zero() -> Self { Self(0) } /// Returns the fixed-point value. #[must_use] - pub const fn get(self) -> u128 { - self.0 - } + pub const fn get(self) -> u128 { self.0 } - const fn from_raw(value: u128) -> Self { - Self(value) - } + const fn from_raw(value: u128) -> Self { Self(value) } } /// Depth-weighted histogram keyed by syntax kind. @@ -87,9 +84,7 @@ pub struct KindHistogram(BTreeMap); impl KindHistogram { /// Returns the weight for `kind`, if present. #[must_use] - pub fn get(&self, kind: KindId) -> Option { - self.0.get(&kind).copied() - } + pub fn get(&self, kind: KindId) -> Option { self.0.get(&kind).copied() } /// Iterates over kind weights in deterministic key order. pub fn iter(&self) -> impl Iterator + '_ { @@ -163,7 +158,6 @@ impl ProductionMultiset { } /// Extracts exact kind counts from `tree`. -/// #[must_use] pub fn kind_counts(tree: &NormalizedTree) -> KindCounts { let mut counts = KindCounts::default(); @@ -216,9 +210,7 @@ fn count_node_kinds(node: &NormalizedNode, depth: Depth, counts: &mut KindCounts } } -const fn next_depth(depth: Depth) -> Depth { - Depth::new(depth.get().saturating_add(1)) -} +const fn next_depth(depth: Depth) -> Depth { Depth::new(depth.get().saturating_add(1)) } // Weights halve with depth (`2^-depth` in fixed point). Below the fixed-point // resolution the weight intentionally collapses to zero: depths 64..=127 shift diff --git a/crates/whitaker_clones_core/src/ast/hash.rs b/crates/whitaker_clones_core/src/ast/hash.rs index 5e7ac86e3..3a6945e84 100644 --- a/crates/whitaker_clones_core/src/ast/hash.rs +++ b/crates/whitaker_clones_core/src/ast/hash.rs @@ -4,7 +4,12 @@ use std::fmt; use super::{LeafClass, NormalizedNode, NormalizedTree}; use crate::hashing::{ - FNV_OFFSET_BASIS, PARSER_SCHEMA_VERSION, mix_byte, mix_bytes, mix_u16, mix_u64, + FNV_OFFSET_BASIS, + PARSER_SCHEMA_VERSION, + mix_byte, + mix_bytes, + mix_u16, + mix_u64, }; /// Opaque canonical AST subtree hash. @@ -12,8 +17,10 @@ use crate::hashing::{ /// # Examples /// /// ``` -/// use whitaker_clones_core::ast::{ByteSpan, KindId, NormalizedNode, NormalizedTree}; -/// use whitaker_clones_core::canonical_hash; +/// use whitaker_clones_core::{ +/// ast::{ByteSpan, KindId, NormalizedNode, NormalizedTree}, +/// canonical_hash, +/// }; /// /// let span = ByteSpan::new("fn f() {}", 0, 2)?; /// let tree = NormalizedTree::new(NormalizedNode::new(KindId::new(1), None, Vec::new()), span); @@ -26,9 +33,7 @@ pub struct AstHash(u64); impl AstHash { /// Renders the hash as a fixed-width lowercase hexadecimal string. #[must_use] - pub fn to_hex(&self) -> String { - format!("{:016x}", self.0) - } + pub fn to_hex(&self) -> String { format!("{:016x}", self.0) } } impl fmt::Display for AstHash { @@ -44,9 +49,7 @@ pub fn canonical_hash(tree: &NormalizedTree) -> AstHash { AstHash(hash_node(seed, tree.root())) } -fn seed_hash() -> u64 { - mix_bytes(FNV_OFFSET_BASIS, PARSER_SCHEMA_VERSION.as_bytes()) -} +fn seed_hash() -> u64 { mix_bytes(FNV_OFFSET_BASIS, PARSER_SCHEMA_VERSION.as_bytes()) } fn hash_node(seed: u64, node: &NormalizedNode) -> u64 { // Pop-based traversal keeps every stack access provably in bounds: each diff --git a/crates/whitaker_clones_core/src/ast/kani.rs b/crates/whitaker_clones_core/src/ast/kani.rs index bda926fb6..4c8c2538c 100644 --- a/crates/whitaker_clones_core/src/ast/kani.rs +++ b/crates/whitaker_clones_core/src/ast/kani.rs @@ -6,7 +6,13 @@ use std::ops::Range; use super::{ - ByteSpan, Depth, KindId, LeafClass, NormalizedNode, NormalizedTree, kind_counts, + ByteSpan, + Depth, + KindId, + LeafClass, + NormalizedNode, + NormalizedTree, + kind_counts, select_smallest_covering, }; @@ -16,9 +22,7 @@ const KANI_AST_UNWIND: usize = 5; const _: () = assert!(KANI_AST_MAX_CHILDREN == 2); const _: () = assert!(KANI_AST_UNWIND == KANI_AST_MAX_DEPTH + 2); -fn symbolic_kind() -> KindId { - KindId::new(kani::any()) -} +fn symbolic_kind() -> KindId { KindId::new(kani::any()) } fn ast_span() -> ByteSpan { match ByteSpan::new("abcd", 0, 1) { diff --git a/crates/whitaker_clones_core/src/ast/lowering.rs b/crates/whitaker_clones_core/src/ast/lowering.rs index b144ea5ed..d74b8b6d8 100644 --- a/crates/whitaker_clones_core/src/ast/lowering.rs +++ b/crates/whitaker_clones_core/src/ast/lowering.rs @@ -1,16 +1,21 @@ //! Adapter from parser syntax trees into the parser-agnostic AST domain. -use std::cell::Cell; -use std::ops::Range; +use std::{cell::Cell, ops::Range}; use ra_ap_syntax::{ - AstNode, Edition, NodeOrToken, SourceFile, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, + AstNode, + Edition, + NodeOrToken, + SourceFile, + SyntaxKind, + SyntaxNode, + SyntaxToken, + TextRange, TextSize, }; use tracing::{debug, error, warn}; use super::{AstError, AstResult, ByteSpan, KindId, LeafClass, NormalizedNode, NormalizedTree}; - pub use crate::hashing::PARSER_SCHEMA_VERSION; const MAX_AST_NODES: usize = 10_000; @@ -243,9 +248,7 @@ struct LoweringLimits { } impl LoweringLimits { - const fn new(span: ByteSpan) -> Self { - Self::with_depth_limit(MAX_AST_DEPTH, span) - } + const fn new(span: ByteSpan) -> Self { Self::with_depth_limit(MAX_AST_DEPTH, span) } const fn with_depth_limit(maximum_depth: usize, span: ByteSpan) -> Self { Self { @@ -340,17 +343,13 @@ fn is_identifier_like(kind: SyntaxKind) -> bool { kind == SyntaxKind::LIFETIME_IDENT || kind.is_any_identifier() } -fn kind_id(kind: SyntaxKind) -> KindId { - KindId::new(u16::from(kind)) -} +fn kind_id(kind: SyntaxKind) -> KindId { KindId::new(u16::from(kind)) } fn text_range(span: ByteSpan) -> TextRange { TextRange::new(TextSize::from(span.start()), TextSize::from(span.end())) } -fn range_to_u32(range: TextRange) -> Range { - u32::from(range.start())..u32::from(range.end()) -} +fn range_to_u32(range: TextRange) -> Range { u32::from(range.start())..u32::from(range.end()) } #[cfg(test)] #[path = "lowering_tests.rs"] diff --git a/crates/whitaker_clones_core/src/ast/lowering_tests.rs b/crates/whitaker_clones_core/src/ast/lowering_tests.rs index cc166147c..c779c1509 100644 --- a/crates/whitaker_clones_core/src/ast/lowering_tests.rs +++ b/crates/whitaker_clones_core/src/ast/lowering_tests.rs @@ -6,13 +6,23 @@ use rstest::rstest; use serde_json::json; use super::{ - LoweringLimits, MAX_AST_DEPTH, MAX_AST_NODES, kind_id, leaf_class, + LoweringLimits, + MAX_AST_DEPTH, + MAX_AST_NODES, + kind_id, + leaf_class, validate_covering_node_budget, }; use crate::{ - AstError, AstResult, ByteSpan, Production, + AstError, + AstResult, + ByteSpan, + Production, ast::{KindId, LeafClass, NormalizedNode, NormalizedTree, PARSER_SCHEMA_VERSION}, - canonical_hash, kind_counts, lower_span, production_multiset, + canonical_hash, + kind_counts, + lower_span, + production_multiset, }; fn kind_name(kind: KindId) -> String { diff --git a/crates/whitaker_clones_core/src/ast/mod.rs b/crates/whitaker_clones_core/src/ast/mod.rs index 1458a54b1..59f83891f 100644 --- a/crates/whitaker_clones_core/src/ast/mod.rs +++ b/crates/whitaker_clones_core/src/ast/mod.rs @@ -37,8 +37,15 @@ mod tree; pub use cover::select_smallest_covering; pub use error::{AstError, AstResult}; pub use features::{ - KindCounts, KindHistogram, KindWeight, Production, ProductionMultiset, kind_counts, - kind_histogram, production_multiset, weighted_histogram, + KindCounts, + KindHistogram, + KindWeight, + Production, + ProductionMultiset, + kind_counts, + kind_histogram, + production_multiset, + weighted_histogram, }; pub use hash::{AstHash, canonical_hash}; pub use lowering::{PARSER_SCHEMA_VERSION, lower_span}; diff --git a/crates/whitaker_clones_core/src/ast/tests.rs b/crates/whitaker_clones_core/src/ast/tests.rs index 12ebcbb2c..968c9d637 100644 --- a/crates/whitaker_clones_core/src/ast/tests.rs +++ b/crates/whitaker_clones_core/src/ast/tests.rs @@ -1,13 +1,26 @@ //! Tests for parser-independent AST feature extraction. -use super::{ - AstResult, ByteSpan, Depth, KindId, KindWeight, LeafClass, NormalizedNode, NormalizedTree, - Production, canonical_hash, kind_counts, kind_histogram, production_multiset, - select_smallest_covering, weighted_histogram, -}; use proptest::prelude::*; use rstest::rstest; +use super::{ + AstResult, + ByteSpan, + Depth, + KindId, + KindWeight, + LeafClass, + NormalizedNode, + NormalizedTree, + Production, + canonical_hash, + kind_counts, + kind_histogram, + production_multiset, + select_smallest_covering, + weighted_histogram, +}; + #[cfg(not(feature = "parser"))] #[rstest] fn parser_free_lowering_reports_parser_unavailable() { diff --git a/crates/whitaker_clones_core/src/ast/tree.rs b/crates/whitaker_clones_core/src/ast/tree.rs index 0ae7ea304..8275e3680 100644 --- a/crates/whitaker_clones_core/src/ast/tree.rs +++ b/crates/whitaker_clones_core/src/ast/tree.rs @@ -20,15 +20,11 @@ pub struct KindId(u16); impl KindId { /// Creates an opaque syntax-kind identifier. #[must_use] - pub const fn new(value: u16) -> Self { - Self(value) - } + pub const fn new(value: u16) -> Self { Self(value) } /// Returns the opaque numeric value. #[must_use] - pub const fn get(self) -> u16 { - self.0 - } + pub const fn get(self) -> u16 { self.0 } } /// Tree depth relative to the lowered subtree root. @@ -46,21 +42,15 @@ pub struct Depth(u16); impl Depth { /// Returns the root depth. #[must_use] - pub const fn root() -> Self { - Self(0) - } + pub const fn root() -> Self { Self(0) } /// Creates a depth value. #[must_use] - pub const fn new(value: u16) -> Self { - Self(value) - } + pub const fn new(value: u16) -> Self { Self(value) } /// Returns the underlying depth. #[must_use] - pub const fn get(self) -> u16 { - self.0 - } + pub const fn get(self) -> u16 { self.0 } } /// Normalized leaf token class for Type-2-style leaf erasure. @@ -125,21 +115,15 @@ impl NormalizedNode { /// Returns the node kind. #[must_use] - pub const fn kind(&self) -> KindId { - self.kind - } + pub const fn kind(&self) -> KindId { self.kind } /// Returns the optional leaf class. #[must_use] - pub const fn leaf(&self) -> Option { - self.leaf - } + pub const fn leaf(&self) -> Option { self.leaf } /// Returns the ordered child nodes. #[must_use] - pub fn children(&self) -> &[Self] { - &self.children - } + pub fn children(&self) -> &[Self] { &self.children } } /// Lowered candidate subtree plus its source span. @@ -163,21 +147,15 @@ pub struct NormalizedTree { impl NormalizedTree { /// Creates a lowered tree. #[must_use] - pub const fn new(root: NormalizedNode, span: ByteSpan) -> Self { - Self { root, span } - } + pub const fn new(root: NormalizedNode, span: ByteSpan) -> Self { Self { root, span } } /// Returns the lowered root node. #[must_use] - pub const fn root(&self) -> &NormalizedNode { - &self.root - } + pub const fn root(&self) -> &NormalizedNode { &self.root } /// Returns the source span represented by this tree. #[must_use] - pub const fn span(&self) -> ByteSpan { - self.span - } + pub const fn span(&self) -> ByteSpan { self.span } } /// Half-open byte span over source text. @@ -233,13 +211,9 @@ impl ByteSpan { /// Returns the start offset. #[must_use] - pub const fn start(self) -> u32 { - self.start - } + pub const fn start(self) -> u32 { self.start } /// Returns the exclusive end offset. #[must_use] - pub const fn end(self) -> u32 { - self.end - } + pub const fn end(self) -> u32 { self.end } } diff --git a/crates/whitaker_clones_core/src/index/fragment_id.rs b/crates/whitaker_clones_core/src/index/fragment_id.rs index b927ac75c..a16f045c6 100644 --- a/crates/whitaker_clones_core/src/index/fragment_id.rs +++ b/crates/whitaker_clones_core/src/index/fragment_id.rs @@ -18,15 +18,11 @@ impl FragmentId { /// assert_eq!(id.as_str(), "src/lib.rs:10..20"); /// ``` #[must_use] - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } + pub fn new(value: impl Into) -> Self { Self(value.into()) } /// Returns the fragment identifier as a string slice. #[must_use] - pub const fn as_str(&self) -> &str { - self.0.as_str() - } + pub const fn as_str(&self) -> &str { self.0.as_str() } /// Consumes the identifier and returns the owned string. /// @@ -39,27 +35,19 @@ impl FragmentId { /// assert_eq!(id.into_inner(), "fragment-a".to_owned()); /// ``` #[must_use] - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } } impl From<&str> for FragmentId { - fn from(value: &str) -> Self { - Self::new(value) - } + fn from(value: &str) -> Self { Self::new(value) } } impl From for FragmentId { - fn from(value: String) -> Self { - Self::new(value) - } + fn from(value: String) -> Self { Self::new(value) } } impl AsRef for FragmentId { - fn as_ref(&self) -> &str { - self.as_str() - } + fn as_ref(&self) -> &str { self.as_str() } } impl fmt::Display for FragmentId { diff --git a/crates/whitaker_clones_core/src/index/kani.rs b/crates/whitaker_clones_core/src/index/kani.rs index 4cbba70a2..a90194b8b 100644 --- a/crates/whitaker_clones_core/src/index/kani.rs +++ b/crates/whitaker_clones_core/src/index/kani.rs @@ -17,36 +17,39 @@ //! coverage: //! //! - `verify_lsh_config_new_smoke` checks one accepted concrete path. -//! - `verify_lsh_config_new_symbolic` exhausts the constructor across the -//! bounded `[0, 128]²` input space. -//! - `verify_lsh_config_new_overflow_product` drives the `checked_mul(None)` -//! branch with non-zero overflowing inputs. -//! - `verify_min_hasher_sketch_rejects_empty_input` checks the empty -//! retained-fingerprint boundary. -//! - `verify_min_hasher_sketch_is_deterministic` checks first, middle, and last -//! signature lanes for wide boundary-hash inputs. -//! - `verify_min_hasher_sketch_ignores_duplicate_hashes` compares a wide -//! boundary-hash set against the same set with repeated hashes. -//! - `verify_lsh_index_rejects_self_pairs` checks that repeated insertion of -//! one fragment cannot produce a self-pair. -//! - `verify_lsh_index_canonicalizes_pair_order` checks that reverse lexical -//! insertion still emits one canonical pair. -//! - `verify_lsh_index_deduplicates_repeated_band_collisions` checks that two -//! fragments colliding in two bands still emit one candidate pair. -//! - `verify_lsh_index_is_insertion_order_independent` checks that a bounded -//! three-fragment index produces the same candidates in forward and reverse -//! insertion order. - -use crate::token::Fingerprint; +//! - `verify_lsh_config_new_symbolic` exhausts the constructor across the bounded `[0, 128]²` input +//! space. +//! - `verify_lsh_config_new_overflow_product` drives the `checked_mul(None)` branch with non-zero +//! overflowing inputs. +//! - `verify_min_hasher_sketch_rejects_empty_input` checks the empty retained-fingerprint boundary. +//! - `verify_min_hasher_sketch_is_deterministic` checks first, middle, and last signature lanes for +//! wide boundary-hash inputs. +//! - `verify_min_hasher_sketch_ignores_duplicate_hashes` compares a wide boundary-hash set against +//! the same set with repeated hashes. +//! - `verify_lsh_index_rejects_self_pairs` checks that repeated insertion of one fragment cannot +//! produce a self-pair. +//! - `verify_lsh_index_canonicalizes_pair_order` checks that reverse lexical insertion still emits +//! one canonical pair. +//! - `verify_lsh_index_deduplicates_repeated_band_collisions` checks that two fragments colliding +//! in two bands still emit one candidate pair. +//! - `verify_lsh_index_is_insertion_order_independent` checks that a bounded three-fragment index +//! produces the same candidates in forward and reverse insertion order. use super::{ - CandidatePair, FragmentId, IndexError, LshConfig, LshIndex, MINHASH_SIZE, MinHashSignature, + CandidatePair, + FragmentId, + IndexError, + LshConfig, + LshIndex, + MINHASH_SIZE, + MinHashSignature, MinHasher, }; +use crate::token::Fingerprint; -const KANI_MINHASH_SEED: u64 = 0xA076_1D64_78BD_642F; -const KANI_MINHASH_MIDDLE_SEED: u64 = 0xE703_7ED1_A0B4_28DB; -const KANI_MINHASH_LAST_SEED: u64 = 0x8EBC_6AF0_9C88_C6E3; +const KANI_MINHASH_SEED: u64 = 0xa076_1d64_78bd_642f; +const KANI_MINHASH_MIDDLE_SEED: u64 = 0xe703_7ed1_a0b4_28db; +const KANI_MINHASH_LAST_SEED: u64 = 0x8ebc_6af0_9c88_c6e3; const KANI_LSH_UNWIND: usize = 7; const _: () = assert!(KANI_LSH_UNWIND == super::lsh::KANI_MAX_RECORDED_PAIRS + 1); @@ -62,9 +65,7 @@ fn checked_lane_hasher() -> MinHasher { ) } -fn fragment(id: &str) -> FragmentId { - FragmentId::from(id) -} +fn fragment(id: &str) -> FragmentId { FragmentId::from(id) } fn repeated_signature(value: u64) -> MinHashSignature { MinHashSignature::new(&[value; MINHASH_SIZE]) diff --git a/crates/whitaker_clones_core/src/index/lsh.rs b/crates/whitaker_clones_core/src/index/lsh.rs index ec0cc0075..b8272293e 100644 --- a/crates/whitaker_clones_core/src/index/lsh.rs +++ b/crates/whitaker_clones_core/src/index/lsh.rs @@ -49,7 +49,12 @@ impl LshIndex { /// /// ```rust /// use whitaker_clones_core::{ - /// Fingerprint, FragmentId, LshConfig, LshIndex, MinHasher, MINHASH_SIZE, + /// Fingerprint, + /// FragmentId, + /// LshConfig, + /// LshIndex, + /// MINHASH_SIZE, + /// MinHasher, /// }; /// /// let hasher = MinHasher::new(); @@ -247,9 +252,7 @@ impl InsertedFragmentsForKani { } } - const fn len(&self) -> usize { - self.len - } + const fn len(&self) -> usize { self.len } fn get(&self, index: usize) -> Option<&InsertedFragmentForKani> { self.items.get(index).and_then(Option::as_ref) diff --git a/crates/whitaker_clones_core/src/index/minhash.rs b/crates/whitaker_clones_core/src/index/minhash.rs index 70292ef57..4578f5257 100644 --- a/crates/whitaker_clones_core/src/index/minhash.rs +++ b/crates/whitaker_clones_core/src/index/minhash.rs @@ -25,13 +25,12 @@ use std::array; -use crate::token::Fingerprint; - use super::{IndexError, IndexResult, MINHASH_SIZE, MinHashSignature}; +use crate::token::Fingerprint; -const SEED_STREAM_START: u64 = 0x243F_6A88_85A3_08D3; -const SEED_STREAM_STEP: u64 = 0x9E37_79B9_7F4A_7C15; -const HASH_MIX: u64 = 0x94D0_49BB_1331_11EB; +const SEED_STREAM_START: u64 = 0x243f_6a88_85a3_08d3; +const SEED_STREAM_STEP: u64 = 0x9e37_79b9_7f4a_7c15; +const HASH_MIX: u64 = 0x94d0_49bb_1331_11eb; /// A typed `MinHash` seed value. /// @@ -47,9 +46,7 @@ pub struct MinHasher { } impl Default for MinHasher { - fn default() -> Self { - Self::new() - } + fn default() -> Self { Self::new() } } impl MinHasher { @@ -312,9 +309,7 @@ fn minimum_mixed_hash(seed: Seed, hashes: &[u64]) -> u64 { }) } -const fn mix_hash(seed: u64, hash: u64) -> u64 { - splitmix64(seed ^ hash.wrapping_mul(HASH_MIX)) -} +const fn mix_hash(seed: u64, hash: u64) -> u64 { splitmix64(seed ^ hash.wrapping_mul(HASH_MIX)) } /// Generates the next seed in the deterministic stream. /// @@ -332,7 +327,7 @@ const fn next_seed(state: &mut u64) -> Seed { /// `next_seed` to ensure deterministic, non-overlapping seed values. const fn splitmix64(value: u64) -> u64 { let mut mixed = value.wrapping_add(SEED_STREAM_STEP); - mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); - mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); mixed ^ (mixed >> 31) } diff --git a/crates/whitaker_clones_core/src/index/tests.rs b/crates/whitaker_clones_core/src/index/tests.rs index 36501cad2..b07205a8f 100644 --- a/crates/whitaker_clones_core/src/index/tests.rs +++ b/crates/whitaker_clones_core/src/index/tests.rs @@ -3,12 +3,18 @@ use rstest::{fixture, rstest}; use whitaker_test_macros::allow_fixture_expansion_lints; -use crate::token::Fingerprint; - use super::{ - CandidatePair, FragmentId, IndexError, LshConfig, LshIndex, MINHASH_SIZE, MinHashSignature, - MinHasher, minhash::unique_hashes, + CandidatePair, + FragmentId, + IndexError, + LshConfig, + LshIndex, + MINHASH_SIZE, + MinHashSignature, + MinHasher, + minhash::unique_hashes, }; +use crate::token::Fingerprint; fn fingerprints(values: &[u64]) -> Vec { values @@ -36,21 +42,15 @@ fn multi_band_config() -> LshConfig { #[allow_fixture_expansion_lints] #[fixture] -fn shared_signature() -> MinHashSignature { - sketch(&[1, 2, 3, 4]) -} +fn shared_signature() -> MinHashSignature { sketch(&[1, 2, 3, 4]) } #[allow_fixture_expansion_lints] #[fixture] -fn distinct_signature() -> MinHashSignature { - sketch(&[8, 9, 10, 11]) -} +fn distinct_signature() -> MinHashSignature { sketch(&[8, 9, 10, 11]) } #[allow_fixture_expansion_lints] #[fixture] -fn identical_signature() -> MinHashSignature { - sketch(&[5, 7, 11, 13]) -} +fn identical_signature() -> MinHashSignature { sketch(&[5, 7, 11, 13]) } struct FragmentIds { alpha: FragmentId, diff --git a/crates/whitaker_clones_core/src/index/types.rs b/crates/whitaker_clones_core/src/index/types.rs index 107ee5a61..715e0f7eb 100644 --- a/crates/whitaker_clones_core/src/index/types.rs +++ b/crates/whitaker_clones_core/src/index/types.rs @@ -27,7 +27,10 @@ impl CandidatePair { /// /// let pair = CandidatePair::new(FragmentId::from("beta"), FragmentId::from("alpha")); /// assert_eq!( - /// pair.map(|pair| (pair.left().as_str().to_owned(), pair.right().as_str().to_owned())), + /// pair.map(|pair| ( + /// pair.left().as_str().to_owned(), + /// pair.right().as_str().to_owned() + /// )), /// Some(("alpha".to_owned(), "beta".to_owned())) /// ); /// ``` @@ -47,15 +50,11 @@ impl CandidatePair { /// Returns the left fragment identifier. #[must_use] - pub const fn left(&self) -> &FragmentId { - &self.left - } + pub const fn left(&self) -> &FragmentId { &self.left } /// Returns the right fragment identifier. #[must_use] - pub const fn right(&self) -> &FragmentId { - &self.right - } + pub const fn right(&self) -> &FragmentId { &self.right } } /// Validated LSH settings for the fixed-width `MinHash` sketch. @@ -103,15 +102,11 @@ impl LshConfig { /// Returns the number of LSH bands. #[must_use] - pub const fn bands(self) -> usize { - self.bands.get() - } + pub const fn bands(self) -> usize { self.bands.get() } /// Returns the number of rows in each band. #[must_use] - pub const fn rows(self) -> usize { - self.rows.get() - } + pub const fn rows(self) -> usize { self.rows.get() } } const fn validate_product(bands: NonZeroUsize, rows: NonZeroUsize) -> IndexResult<()> { @@ -130,9 +125,7 @@ pub struct MinHashSignature([u64; MINHASH_SIZE]); impl MinHashSignature { #[must_use] - pub(crate) const fn new(values: &[u64; MINHASH_SIZE]) -> Self { - Self(*values) - } + pub(crate) const fn new(values: &[u64; MINHASH_SIZE]) -> Self { Self(*values) } /// Returns the sketch values in order. /// @@ -142,23 +135,18 @@ impl MinHashSignature { /// use whitaker_clones_core::{Fingerprint, MinHasher}; /// /// let hasher = MinHasher::new(); - /// let signature = hasher.sketch(&[ - /// Fingerprint::new(11, 0..1), - /// Fingerprint::new(22, 1..2), - /// ])?; + /// let signature = hasher.sketch(&[Fingerprint::new(11, 0..1), Fingerprint::new(22, 1..2)])?; /// assert_eq!(signature.values().len(), 128); /// # Ok::<(), whitaker_clones_core::IndexError>(()) /// ``` #[must_use] - pub const fn values(&self) -> &[u64; MINHASH_SIZE] { - &self.0 - } + pub const fn values(&self) -> &[u64; MINHASH_SIZE] { &self.0 } pub(crate) fn bands(&self, rows: usize) -> ChunksExact<'_, u64> { debug_assert!( self.0.len().is_multiple_of(rows), - "MinHashSignature length ({}) must divide evenly by rows ({}); \ - LshConfig ensures bands * rows == MINHASH_SIZE", + "MinHashSignature length ({}) must divide evenly by rows ({}); LshConfig ensures \ + bands * rows == MINHASH_SIZE", self.0.len(), rows ); diff --git a/crates/whitaker_clones_core/src/lib.rs b/crates/whitaker_clones_core/src/lib.rs index 1b76b2e20..507eebb57 100644 --- a/crates/whitaker_clones_core/src/lib.rs +++ b/crates/whitaker_clones_core/src/lib.rs @@ -18,20 +18,57 @@ pub mod run0; pub mod token; pub use ast::{ - AstError, AstHash, AstResult, ByteSpan, KindCounts, KindHistogram, KindWeight, NormalizedTree, - Production, ProductionMultiset, canonical_hash, kind_counts, kind_histogram, lower_span, - production_multiset, weighted_histogram, + AstError, + AstHash, + AstResult, + ByteSpan, + KindCounts, + KindHistogram, + KindWeight, + NormalizedTree, + Production, + ProductionMultiset, + canonical_hash, + kind_counts, + kind_histogram, + lower_span, + production_multiset, + weighted_histogram, }; pub use index::{ - CandidatePair, FragmentId, IndexError, IndexResult, LshConfig, LshIndex, MINHASH_SIZE, - MinHashSignature, MinHasher, + CandidatePair, + FragmentId, + IndexError, + IndexResult, + LshConfig, + LshIndex, + MINHASH_SIZE, + MinHashSignature, + MinHasher, }; pub use run0::{ - AcceptedPair, Run0Error, Run0Result, SimilarityRatio, SimilarityThreshold, TokenFragment, - TokenPassConfig, accept_candidate_pairs, emit_run0, + AcceptedPair, + Run0Error, + Run0Result, + SimilarityRatio, + SimilarityThreshold, + TokenFragment, + TokenPassConfig, + accept_candidate_pairs, + emit_run0, }; pub use token::{ - Fingerprint, IdentifierSymbol, LiteralSymbol, NormProfile, NormalizedToken, - NormalizedTokenKind, Result, ShingleSize, TokenPassError, WinnowWindow, hash_shingles, - normalize, winnow, + Fingerprint, + IdentifierSymbol, + LiteralSymbol, + NormProfile, + NormalizedToken, + NormalizedTokenKind, + Result, + ShingleSize, + TokenPassError, + WinnowWindow, + hash_shingles, + normalize, + winnow, }; diff --git a/crates/whitaker_clones_core/src/run0/emit.rs b/crates/whitaker_clones_core/src/run0/emit.rs index 681b94afb..f66defe5a 100644 --- a/crates/whitaker_clones_core/src/run0/emit.rs +++ b/crates/whitaker_clones_core/src/run0/emit.rs @@ -4,18 +4,27 @@ use std::collections::BTreeMap; use sha2::{Digest, Sha256}; use whitaker_sarif::{ - Level, LocationBuilder, RelatedLocation, ResultBuilder, Run, RunBuilder, WHITAKER_FRAGMENT_KEY, - WHK001_ID, WHK002_ID, WhitakerPropertiesBuilder, all_rules, deduplicate_results, + Level, + LocationBuilder, + RelatedLocation, + ResultBuilder, + Run, + RunBuilder, + WHITAKER_FRAGMENT_KEY, + WHK001_ID, + WHK002_ID, + WhitakerPropertiesBuilder, + all_rules, + deduplicate_results, }; -use crate::{CandidatePair, NormProfile}; - use super::{ error::{Run0Error, Run0Result}, score::{SimilarityRatio, jaccard_similarity, select_rule_profile}, span::region_for_range, types::{AcceptedPair, TokenFragment, TokenPassConfig}, }; +use crate::{CandidatePair, NormProfile}; /// Accepts canonical candidate pairs for later Run 0 emission. /// diff --git a/crates/whitaker_clones_core/src/run0/error.rs b/crates/whitaker_clones_core/src/run0/error.rs index 5f59fb70e..927753b56 100644 --- a/crates/whitaker_clones_core/src/run0/error.rs +++ b/crates/whitaker_clones_core/src/run0/error.rs @@ -24,7 +24,8 @@ pub enum Run0Error { /// The pair resolved to fragments emitted under different normalization profiles. #[error( - "candidate pair `{left_fragment}` and `{right_fragment}` must share the same normalization profile" + "candidate pair `{left_fragment}` and `{right_fragment}` must share the same \ + normalization profile" )] MixedProfiles { /// Left fragment identifier. @@ -42,7 +43,8 @@ pub enum Run0Error { /// A retained fingerprint byte range could not be mapped back to source text. #[error( - "fingerprint range {start}..{end} for `{fragment_id}` is invalid for source length {source_len}" + "fingerprint range {start}..{end} for `{fragment_id}` is invalid for source length \ + {source_len}" )] InvalidFingerprintRange { /// Fragment identifier. diff --git a/crates/whitaker_clones_core/src/run0/score.rs b/crates/whitaker_clones_core/src/run0/score.rs index 1bcf94584..8d7f7f836 100644 --- a/crates/whitaker_clones_core/src/run0/score.rs +++ b/crates/whitaker_clones_core/src/run0/score.rs @@ -2,9 +2,8 @@ use std::collections::BTreeSet; -use crate::{Fingerprint, NormProfile}; - use super::error::{Run0Error, Run0Result}; +use crate::{Fingerprint, NormProfile}; /// Integer-backed Jaccard similarity ratio. /// @@ -38,15 +37,11 @@ impl SimilarityRatio { /// Returns the numerator of the ratio. #[must_use] - pub const fn intersection(self) -> usize { - self.intersection - } + pub const fn intersection(self) -> usize { self.intersection } /// Returns the denominator of the ratio. #[must_use] - pub const fn union(self) -> usize { - self.union - } + pub const fn union(self) -> usize { self.union } /// Formats the ratio as a six-decimal string without floating-point arithmetic. #[must_use] @@ -77,8 +72,7 @@ impl SimilarityRatio { /// ``` /// use whitaker_clones_core::run0::SimilarityThreshold; /// -/// let threshold = SimilarityThreshold::new("custom", 4, 5) -/// .expect("valid threshold"); +/// let threshold = SimilarityThreshold::new("custom", 4, 5).expect("valid threshold"); /// assert_eq!(threshold.numerator(), 4); /// assert_eq!(threshold.denominator(), 5); /// ``` @@ -117,15 +111,11 @@ impl SimilarityThreshold { /// Returns the threshold numerator. #[must_use] - pub const fn numerator(self) -> usize { - self.numerator - } + pub const fn numerator(self) -> usize { self.numerator } /// Returns the threshold denominator. #[must_use] - pub const fn denominator(self) -> usize { - self.denominator - } + pub const fn denominator(self) -> usize { self.denominator } /// Returns `true` if the threshold represents a ratio within `(0, 1]`. const fn is_valid(self) -> bool { diff --git a/crates/whitaker_clones_core/src/run0/test_helpers.rs b/crates/whitaker_clones_core/src/run0/test_helpers.rs index 671fba632..19f28b8e8 100644 --- a/crates/whitaker_clones_core/src/run0/test_helpers.rs +++ b/crates/whitaker_clones_core/src/run0/test_helpers.rs @@ -1,8 +1,7 @@ //! Shared test helpers for Run 0 acceptance and emission tests. -use crate::{CandidatePair, Fingerprint, FragmentId, NormProfile}; - use super::{TokenFragment, TokenPassConfig}; +use crate::{CandidatePair, Fingerprint, FragmentId, NormProfile}; pub(super) struct FragmentInput<'a> { pub(super) id: &'a str, diff --git a/crates/whitaker_clones_core/src/run0/tests.rs b/crates/whitaker_clones_core/src/run0/tests.rs index ebe506c12..184943029 100644 --- a/crates/whitaker_clones_core/src/run0/tests.rs +++ b/crates/whitaker_clones_core/src/run0/tests.rs @@ -2,15 +2,18 @@ use whitaker_sarif::{Region, WHK001_ID, WHK002_ID}; -use crate::NormProfile; - use super::{ - AcceptedPair, Run0Error, SimilarityThreshold, TokenPassConfig, accept_candidate_pairs, + AcceptedPair, + Run0Error, + SimilarityThreshold, + TokenPassConfig, + accept_candidate_pairs, emit_run0, score::SimilarityRatio, span::region_for_range, test_helpers::{FragmentInput, config, fragment, pair}, }; +use crate::NormProfile; fn build_pair_and_accept( left: FragmentInput<'_>, diff --git a/crates/whitaker_clones_core/src/run0/tests_emit.rs b/crates/whitaker_clones_core/src/run0/tests_emit.rs index 0b8063c93..0c8ed9b5d 100644 --- a/crates/whitaker_clones_core/src/run0/tests_emit.rs +++ b/crates/whitaker_clones_core/src/run0/tests_emit.rs @@ -2,13 +2,14 @@ use whitaker_sarif::{WHITAKER_FRAGMENT_KEY, WHK002_ID, WhitakerProperties}; -use crate::{Fingerprint, NormProfile}; - use super::{ - AcceptedPair, SimilarityRatio, emit_run0, + AcceptedPair, + SimilarityRatio, + emit_run0, score::jaccard_similarity, test_helpers::{FragmentInput, config, fingerprint, fragment, pair}, }; +use crate::{Fingerprint, NormProfile}; #[test] fn duplicate_hashes_do_not_inflate_jaccard_score() { diff --git a/crates/whitaker_clones_core/src/run0/types.rs b/crates/whitaker_clones_core/src/run0/types.rs index 07fdca3b7..1776f802d 100644 --- a/crates/whitaker_clones_core/src/run0/types.rs +++ b/crates/whitaker_clones_core/src/run0/types.rs @@ -1,8 +1,7 @@ //! Public input and output types for token-pass Run 0 emission. -use crate::{CandidatePair, Fingerprint, FragmentId, NormProfile}; - use super::score::{SimilarityRatio, SimilarityThreshold}; +use crate::{CandidatePair, Fingerprint, FragmentId, NormProfile}; const DEFAULT_SHINGLE_SIZE: usize = 25; const DEFAULT_WINNOW_WINDOW: usize = 16; @@ -61,27 +60,19 @@ impl TokenFragment { /// Returns the stable fragment identifier. #[must_use] - pub const fn id(&self) -> &FragmentId { - &self.id - } + pub const fn id(&self) -> &FragmentId { &self.id } /// Returns the normalization profile used to produce this fragment. #[must_use] - pub const fn profile(&self) -> NormProfile { - self.profile - } + pub const fn profile(&self) -> NormProfile { self.profile } /// Returns the source artefact URI used in SARIF output. #[must_use] - pub const fn file_uri(&self) -> &str { - self.file_uri.as_str() - } + pub const fn file_uri(&self) -> &str { self.file_uri.as_str() } /// Returns the original source text used for byte-range mapping. #[must_use] - pub const fn source_text(&self) -> &str { - self.source_text.as_str() - } + pub const fn source_text(&self) -> &str { self.source_text.as_str() } /// Returns the retained token fingerprints for this fragment. #[must_use] @@ -160,39 +151,27 @@ impl TokenPassConfig { /// Returns the SARIF producer name. #[must_use] - pub const fn tool_name(&self) -> &str { - self.tool_name.as_str() - } + pub const fn tool_name(&self) -> &str { self.tool_name.as_str() } /// Returns the SARIF producer version. #[must_use] - pub const fn tool_version(&self) -> &str { - self.tool_version.as_str() - } + pub const fn tool_version(&self) -> &str { self.tool_version.as_str() } /// Returns the configured shingle size. #[must_use] - pub const fn shingle_size(&self) -> usize { - self.shingle_size - } + pub const fn shingle_size(&self) -> usize { self.shingle_size } /// Returns the configured winnow window. #[must_use] - pub const fn winnow_window(&self) -> usize { - self.winnow_window - } + pub const fn winnow_window(&self) -> usize { self.winnow_window } /// Returns the Type-1 acceptance threshold. #[must_use] - pub const fn type1_threshold(&self) -> SimilarityThreshold { - self.type1_threshold - } + pub const fn type1_threshold(&self) -> SimilarityThreshold { self.type1_threshold } /// Returns the Type-2 acceptance threshold. #[must_use] - pub const fn type2_threshold(&self) -> SimilarityThreshold { - self.type2_threshold - } + pub const fn type2_threshold(&self) -> SimilarityThreshold { self.type2_threshold } } /// An accepted token-pass candidate pair and its final rule classification. @@ -201,16 +180,16 @@ impl TokenPassConfig { /// /// ``` /// use whitaker_clones_core::{ -/// AcceptedPair, CandidatePair, FragmentId, NormProfile, SimilarityRatio, +/// AcceptedPair, +/// CandidatePair, +/// FragmentId, +/// NormProfile, +/// SimilarityRatio, /// }; /// /// let pair = CandidatePair::new(FragmentId::from("alpha"), FragmentId::from("beta")) /// .expect("distinct fragments"); -/// let accepted = AcceptedPair::new( -/// pair, -/// NormProfile::T1, -/// SimilarityRatio::new(4, 4), -/// ); +/// let accepted = AcceptedPair::new(pair, NormProfile::T1, SimilarityRatio::new(4, 4)); /// /// assert_eq!(accepted.profile(), NormProfile::T1); /// ``` @@ -234,19 +213,13 @@ impl AcceptedPair { /// Returns the canonical fragment pair. #[must_use] - pub const fn pair(&self) -> &CandidatePair { - &self.pair - } + pub const fn pair(&self) -> &CandidatePair { &self.pair } /// Returns the rule profile to emit for this pair. #[must_use] - pub const fn profile(&self) -> NormProfile { - self.profile - } + pub const fn profile(&self) -> NormProfile { self.profile } /// Returns the accepted Jaccard score. #[must_use] - pub const fn score(&self) -> SimilarityRatio { - self.score - } + pub const fn score(&self) -> SimilarityRatio { self.score } } diff --git a/crates/whitaker_clones_core/src/token/fingerprint.rs b/crates/whitaker_clones_core/src/token/fingerprint.rs index b20e1213b..3fe30715b 100644 --- a/crates/whitaker_clones_core/src/token/fingerprint.rs +++ b/crates/whitaker_clones_core/src/token/fingerprint.rs @@ -1,11 +1,19 @@ //! Shingling, Rabin-Karp rolling hashing, and winnowing helpers. use super::types::{ - Fingerprint, IdentifierSymbol, LiteralSymbol, NormalizedToken, NormalizedTokenKind, - ShingleSize, WinnowWindow, + Fingerprint, + IdentifierSymbol, + LiteralSymbol, + NormalizedToken, + NormalizedTokenKind, + ShingleSize, + WinnowWindow, }; use crate::hashing::{ - FNV_OFFSET_BASIS, RABIN_KARP_BASE, mix_byte as hash_byte, mix_bytes as hash_bytes, + FNV_OFFSET_BASIS, + RABIN_KARP_BASE, + mix_byte as hash_byte, + mix_bytes as hash_bytes, }; /// Builds Rabin-Karp fingerprints for all `k`-sized normalized token windows. diff --git a/crates/whitaker_clones_core/src/token/mod.rs b/crates/whitaker_clones_core/src/token/mod.rs index 0aeef83e1..95c771016 100644 --- a/crates/whitaker_clones_core/src/token/mod.rs +++ b/crates/whitaker_clones_core/src/token/mod.rs @@ -9,8 +9,14 @@ pub use error::{Result, TokenPassError}; pub use fingerprint::{hash_shingles, winnow}; pub use normalize::normalize; pub use types::{ - Fingerprint, IdentifierSymbol, LiteralSymbol, NormProfile, NormalizedToken, - NormalizedTokenKind, ShingleSize, WinnowWindow, + Fingerprint, + IdentifierSymbol, + LiteralSymbol, + NormProfile, + NormalizedToken, + NormalizedTokenKind, + ShingleSize, + WinnowWindow, }; #[cfg(test)] diff --git a/crates/whitaker_clones_core/src/token/normalize.rs b/crates/whitaker_clones_core/src/token/normalize.rs index 84680dc5a..bbc7725e1 100644 --- a/crates/whitaker_clones_core/src/token/normalize.rs +++ b/crates/whitaker_clones_core/src/token/normalize.rs @@ -182,7 +182,9 @@ fn process_token( /// /// assert_eq!( /// labels, -/// vec!["fn", "", "(", "", ":", "", ")", "{", "", "+", "", "}"] +/// vec![ +/// "fn", "", "(", "", ":", "", ")", "{", "", "+", "", "}" +/// ] /// ); /// # Ok::<(), whitaker_clones_core::TokenPassError>(()) /// ``` @@ -322,9 +324,7 @@ fn normalize_symbolic_text( wrap(symbol) } -fn raw_identifier_text(text: &str) -> &str { - text.strip_prefix("r#").unwrap_or(text) -} +fn raw_identifier_text(text: &str) -> &str { text.strip_prefix("r#").unwrap_or(text) } fn atom_label(kind: TokenKind) -> &'static str { match kind { diff --git a/crates/whitaker_clones_core/src/token/tests.rs b/crates/whitaker_clones_core/src/token/tests.rs index 3cfc71125..a8bb2b741 100644 --- a/crates/whitaker_clones_core/src/token/tests.rs +++ b/crates/whitaker_clones_core/src/token/tests.rs @@ -2,12 +2,20 @@ use rstest::rstest; -use crate::hashing::{FNV_OFFSET_BASIS, FNV_PRIME, RABIN_KARP_BASE}; - use super::{ - Fingerprint, IdentifierSymbol, LiteralSymbol, NormProfile, NormalizedTokenKind, ShingleSize, - TokenPassError, WinnowWindow, hash_shingles, normalize, winnow, + Fingerprint, + IdentifierSymbol, + LiteralSymbol, + NormProfile, + NormalizedTokenKind, + ShingleSize, + TokenPassError, + WinnowWindow, + hash_shingles, + normalize, + winnow, }; +use crate::hashing::{FNV_OFFSET_BASIS, FNV_PRIME, RABIN_KARP_BASE}; fn labels(source: &str, profile: NormProfile) -> Result, TokenPassError> { normalize(source, profile).map(|tokens| { diff --git a/crates/whitaker_clones_core/src/token/types.rs b/crates/whitaker_clones_core/src/token/types.rs index 08a446d01..f7d7c786e 100644 --- a/crates/whitaker_clones_core/src/token/types.rs +++ b/crates/whitaker_clones_core/src/token/types.rs @@ -99,9 +99,7 @@ pub struct ShingleSize(NonZeroUsize); impl ShingleSize { /// Returns the validated `k` as a plain `usize`. #[must_use] - pub const fn get(self) -> usize { - self.0.get() - } + pub const fn get(self) -> usize { self.0.get() } } impl TryFrom for ShingleSize { @@ -121,9 +119,7 @@ pub struct WinnowWindow(NonZeroUsize); impl WinnowWindow { /// Returns the validated window size as a plain `usize`. #[must_use] - pub const fn get(self) -> usize { - self.0.get() - } + pub const fn get(self) -> usize { self.0.get() } } impl TryFrom for WinnowWindow { @@ -148,7 +144,5 @@ pub struct Fingerprint { impl Fingerprint { /// Creates a fingerprint from a hash value and source range. #[must_use] - pub const fn new(hash: u64, range: Range) -> Self { - Self { hash, range } - } + pub const fn new(hash: u64, range: Range) -> Self { Self { hash, range } } } diff --git a/crates/whitaker_clones_core/tests/ast_boundary.rs b/crates/whitaker_clones_core/tests/ast_boundary.rs index acf740728..11275ff5b 100644 --- a/crates/whitaker_clones_core/tests/ast_boundary.rs +++ b/crates/whitaker_clones_core/tests/ast_boundary.rs @@ -207,9 +207,7 @@ fn contains_path(import: &[&str], path: &[&str]) -> bool { /// `[[crate, ::, ast, ::, tree, ::, ByteSpan],` /// ` [crate, ::, ast, ::, lowering, ::, lower_span]]`, so `contains_path` sees /// `ast :: lowering` regardless of the sibling ordering. -fn expand_use_tree<'a>(tokens: &[&'a str]) -> Vec> { - parse_use_tree(tokens, &[]).0 -} +fn expand_use_tree<'a>(tokens: &[&'a str]) -> Vec> { parse_use_tree(tokens, &[]).0 } /// Parses one use-tree item — a path prefix optionally followed by a `{ … }` /// group — returning the leaf paths it expands to and the unconsumed remainder diff --git a/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs b/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs index 0e96f77c4..88d4059fe 100644 --- a/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs +++ b/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs @@ -9,7 +9,12 @@ use ra_ap_syntax::SyntaxKind; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_clones_core::{ - AstError, AstHash, ByteSpan, NormalizedTree, canonical_hash, lower_span, + AstError, + AstHash, + ByteSpan, + NormalizedTree, + canonical_hash, + lower_span, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -84,9 +89,7 @@ struct AstFeatureWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> AstFeatureWorld { - AstFeatureWorld::default() -} +fn world() -> AstFeatureWorld { AstFeatureWorld::default() } fn whole_source_hash(source: &str) -> Result { let length = u32::try_from(source.len()) diff --git a/crates/whitaker_clones_core/tests/build_script_parsing.rs b/crates/whitaker_clones_core/tests/build_script_parsing.rs index c8a688111..862234a07 100644 --- a/crates/whitaker_clones_core/tests/build_script_parsing.rs +++ b/crates/whitaker_clones_core/tests/build_script_parsing.rs @@ -4,7 +4,10 @@ mod build_support; use build_support::{ - exact_version, find_workspace_manifest, is_workspace_manifest, parser_dependency_requirement, + exact_version, + find_workspace_manifest, + is_workspace_manifest, + parser_dependency_requirement, read_workspace_manifest, }; use camino::{Utf8Path, Utf8PathBuf}; diff --git a/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs b/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs index 08ed6baba..11d13f5e5 100644 --- a/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs +++ b/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs @@ -18,9 +18,7 @@ struct CandidatePairWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> CandidatePairWorld { - CandidatePairWorld::default() -} +fn world() -> CandidatePairWorld { CandidatePairWorld::default() } fn with_pair(world: &CandidatePairWorld, assert_fn: impl FnOnce(&CandidatePair)) { let borrowed_pair = world.pair.borrow(); @@ -73,21 +71,13 @@ fn then_no_candidate_pair_is_returned(world: &CandidatePairWorld) { } #[scenario(path = "tests/features/candidate_pair.feature", index = 0)] -fn scenario_ordered_distinct_ids(world: CandidatePairWorld) { - let _ = world; -} +fn scenario_ordered_distinct_ids(world: CandidatePairWorld) { let _ = world; } #[scenario(path = "tests/features/candidate_pair.feature", index = 1)] -fn scenario_reversed_distinct_ids(world: CandidatePairWorld) { - let _ = world; -} +fn scenario_reversed_distinct_ids(world: CandidatePairWorld) { let _ = world; } #[scenario(path = "tests/features/candidate_pair.feature", index = 2)] -fn scenario_identical_ids(world: CandidatePairWorld) { - let _ = world; -} +fn scenario_identical_ids(world: CandidatePairWorld) { let _ = world; } #[scenario(path = "tests/features/candidate_pair.feature", index = 3)] -fn scenario_lexical_order_edge_case(world: CandidatePairWorld) { - let _ = world; -} +fn scenario_lexical_order_edge_case(world: CandidatePairWorld) { let _ = world; } diff --git a/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs b/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs index 3a85db254..5c230c336 100644 --- a/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs +++ b/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs @@ -7,7 +7,13 @@ use std::{cell::RefCell, collections::BTreeMap}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_clones_core::{ - CandidatePair, Fingerprint, FragmentId, IndexError, LshConfig, LshIndex, MinHasher, + CandidatePair, + Fingerprint, + FragmentId, + IndexError, + LshConfig, + LshIndex, + MinHasher, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -22,9 +28,7 @@ struct MinHashLshWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> MinHashLshWorld { - MinHashLshWorld::default() -} +fn world() -> MinHashLshWorld { MinHashLshWorld::default() } fn with_candidates(world: &MinHashLshWorld, assert_fn: impl FnOnce(&[CandidatePair])) { let candidates = world.candidates.borrow(); @@ -187,41 +191,25 @@ fn then_candidate_generation_error_is(world: &MinHashLshWorld, name: String) -> } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 0)] -fn scenario_identical_fragments(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_identical_fragments(world: MinHashLshWorld) { let _ = world; } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 1)] -fn scenario_distinct_fragments(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_distinct_fragments(world: MinHashLshWorld) { let _ = world; } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 2)] -fn scenario_multiple_collisions_one_pair(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_multiple_collisions_one_pair(world: MinHashLshWorld) { let _ = world; } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 3)] -fn scenario_duplicate_hashes_use_set_semantics(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_duplicate_hashes_use_set_semantics(world: MinHashLshWorld) { let _ = world; } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 4)] -fn scenario_invalid_lsh_settings(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_invalid_lsh_settings(world: MinHashLshWorld) { let _ = world; } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 5)] -fn scenario_empty_fingerprints(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_empty_fingerprints(world: MinHashLshWorld) { let _ = world; } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 6)] -fn scenario_zero_rows(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_zero_rows(world: MinHashLshWorld) { let _ = world; } #[scenario(path = "tests/features/min_hash_lsh.feature", index = 7)] -fn scenario_invalid_non_zero_product(world: MinHashLshWorld) { - let _ = world; -} +fn scenario_invalid_non_zero_product(world: MinHashLshWorld) { let _ = world; } diff --git a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs index cb0ce7010..07a081395 100644 --- a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs +++ b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs @@ -7,7 +7,12 @@ use std::{cell::RefCell, collections::BTreeMap}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_clones_core::{ - CandidatePair, FragmentId, Run0Error, TokenFragment, TokenPassConfig, accept_candidate_pairs, + CandidatePair, + FragmentId, + Run0Error, + TokenFragment, + TokenPassConfig, + accept_candidate_pairs, emit_run0, }; use whitaker_sarif::{Run, SarifResult, WhitakerProperties}; @@ -36,9 +41,7 @@ impl Default for Run0World { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> Run0World { - Run0World::default() -} +fn world() -> Run0World { Run0World::default() } fn with_results(world: &Run0World, assert_fn: impl FnOnce(&[SarifResult])) { let borrowed_run = world.run.borrow(); @@ -321,31 +324,19 @@ fn then_primary_file_is(world: &Run0World, file_uri: String) { } #[scenario(path = "tests/features/run0_sarif.feature", index = 0)] -fn scenario_type1_pair(world: Run0World) { - let _ = world; -} +fn scenario_type1_pair(world: Run0World) { let _ = world; } #[scenario(path = "tests/features/run0_sarif.feature", index = 1)] -fn scenario_type2_pair(world: Run0World) { - let _ = world; -} +fn scenario_type2_pair(world: Run0World) { let _ = world; } #[scenario(path = "tests/features/run0_sarif.feature", index = 2)] -fn scenario_below_threshold(world: Run0World) { - let _ = world; -} +fn scenario_below_threshold(world: Run0World) { let _ = world; } #[scenario(path = "tests/features/run0_sarif.feature", index = 3)] -fn scenario_empty_fingerprints(world: Run0World) { - let _ = world; -} +fn scenario_empty_fingerprints(world: Run0World) { let _ = world; } #[scenario(path = "tests/features/run0_sarif.feature", index = 4)] -fn scenario_multiline_region(world: Run0World) { - let _ = world; -} +fn scenario_multiline_region(world: Run0World) { let _ = world; } #[scenario(path = "tests/features/run0_sarif.feature", index = 5)] -fn scenario_reversed_pair(world: Run0World) { - let _ = world; -} +fn scenario_reversed_pair(world: Run0World) { let _ = world; } diff --git a/crates/whitaker_clones_core/tests/token_pass_behaviour.rs b/crates/whitaker_clones_core/tests/token_pass_behaviour.rs index 3b23e79ec..b5c36395c 100644 --- a/crates/whitaker_clones_core/tests/token_pass_behaviour.rs +++ b/crates/whitaker_clones_core/tests/token_pass_behaviour.rs @@ -8,7 +8,13 @@ use std::cell::RefCell; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_clones_core::{ - Fingerprint, NormProfile, ShingleSize, TokenPassError, WinnowWindow, hash_shingles, normalize, + Fingerprint, + NormProfile, + ShingleSize, + TokenPassError, + WinnowWindow, + hash_shingles, + normalize, winnow, }; use whitaker_test_macros::allow_fixture_expansion_lints; @@ -31,9 +37,7 @@ struct TokenPassWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> TokenPassWorld { - TokenPassWorld::default() -} +fn world() -> TokenPassWorld { TokenPassWorld::default() } fn with_fingerprints(world: &TokenPassWorld, assert_fn: impl FnOnce(&[Fingerprint])) { let fingerprints = world.fingerprints.borrow(); @@ -287,31 +291,19 @@ fn then_error_is(world: &TokenPassWorld, message: String) { } #[scenario(path = "tests/features/token_pass.feature", index = 0)] -fn scenario_t1_trivia_removal(world: TokenPassWorld) { - let _ = world; -} +fn scenario_t1_trivia_removal(world: TokenPassWorld) { let _ = world; } #[scenario(path = "tests/features/token_pass.feature", index = 1)] -fn scenario_t2_renamed_functions_match(world: TokenPassWorld) { - let _ = world; -} +fn scenario_t2_renamed_functions_match(world: TokenPassWorld) { let _ = world; } #[scenario(path = "tests/features/token_pass.feature", index = 2)] -fn scenario_exact_k_fingerprint(world: TokenPassWorld) { - let _ = world; -} +fn scenario_exact_k_fingerprint(world: TokenPassWorld) { let _ = world; } #[scenario(path = "tests/features/token_pass.feature", index = 3)] -fn scenario_winnowing_rightmost_minimum(world: TokenPassWorld) { - let _ = world; -} +fn scenario_winnowing_rightmost_minimum(world: TokenPassWorld) { let _ = world; } #[scenario(path = "tests/features/token_pass.feature", index = 4)] -fn scenario_invalid_size(world: TokenPassWorld) { - let _ = world; -} +fn scenario_invalid_size(world: TokenPassWorld) { let _ = world; } #[scenario(path = "tests/features/token_pass.feature", index = 5)] -fn scenario_unterminated_literal(world: TokenPassWorld) { - let _ = world; -} +fn scenario_unterminated_literal(world: TokenPassWorld) { let _ = world; } diff --git a/crates/whitaker_sarif/src/builders/location_builder.rs b/crates/whitaker_sarif/src/builders/location_builder.rs index 0da961a2f..64e05ba1f 100644 --- a/crates/whitaker_sarif/src/builders/location_builder.rs +++ b/crates/whitaker_sarif/src/builders/location_builder.rs @@ -1,7 +1,9 @@ //! Builders for [`Location`] and [`Region`] objects. -use crate::error::SarifError; -use crate::model::location::{ArtefactLocation, Location, PhysicalLocation, Region}; +use crate::{ + error::SarifError, + model::location::{ArtefactLocation, Location, PhysicalLocation, Region}, +}; /// Fluent builder for constructing a [`Region`]. /// @@ -161,7 +163,12 @@ impl RegionBuilder { /// use whitaker_sarif::{LocationBuilder, RegionBuilder}; /// /// let loc = LocationBuilder::new("src/main.rs") -/// .with_region(RegionBuilder::new(10).with_end_line(15).build().expect("valid region")) +/// .with_region( +/// RegionBuilder::new(10) +/// .with_end_line(15) +/// .build() +/// .expect("valid region"), +/// ) /// .build(); /// assert_eq!(loc.physical_location.artefact_location.uri, "src/main.rs"); /// ``` @@ -214,9 +221,10 @@ impl LocationBuilder { #[cfg(test)] mod tests { + use rstest::rstest; + use super::*; use crate::error::SarifError; - use rstest::rstest; #[test] fn region_builder_minimal() { diff --git a/crates/whitaker_sarif/src/builders/log_builder.rs b/crates/whitaker_sarif/src/builders/log_builder.rs index 53a25b8c8..ccbd3cc0e 100644 --- a/crates/whitaker_sarif/src/builders/log_builder.rs +++ b/crates/whitaker_sarif/src/builders/log_builder.rs @@ -1,7 +1,9 @@ //! Builder for [`SarifLog`] objects. -use crate::model::log::{SARIF_SCHEMA, SARIF_VERSION, SarifLog}; -use crate::model::run::Run; +use crate::model::{ + log::{SARIF_SCHEMA, SARIF_VERSION, SarifLog}, + run::Run, +}; /// Fluent builder for constructing a [`SarifLog`]. /// @@ -47,7 +49,7 @@ impl SarifLogBuilder { /// # Examples /// /// ``` - /// use whitaker_sarif::{SarifLogBuilder, RunBuilder}; + /// use whitaker_sarif::{RunBuilder, SarifLogBuilder}; /// /// let run = RunBuilder::new("tool", "1.0").build(); /// let log = SarifLogBuilder::new().with_run(run).build(); @@ -81,9 +83,7 @@ impl SarifLogBuilder { } impl Default for SarifLogBuilder { - fn default() -> Self { - Self::new() - } + fn default() -> Self { Self::new() } } #[cfg(test)] diff --git a/crates/whitaker_sarif/src/builders/result_builder.rs b/crates/whitaker_sarif/src/builders/result_builder.rs index f791d4a15..f14856b64 100644 --- a/crates/whitaker_sarif/src/builders/result_builder.rs +++ b/crates/whitaker_sarif/src/builders/result_builder.rs @@ -4,9 +4,13 @@ use std::collections::HashMap; use serde_json::Value; -use crate::error::{Result, SarifError}; -use crate::model::location::{Location, RelatedLocation}; -use crate::model::result::{Level, Message, SarifResult}; +use crate::{ + error::{Result, SarifError}, + model::{ + location::{Location, RelatedLocation}, + result::{Level, Message, SarifResult}, + }, +}; /// Fluent builder for constructing a [`SarifResult`]. /// @@ -16,7 +20,7 @@ use crate::model::result::{Level, Message, SarifResult}; /// # Examples /// /// ``` -/// use whitaker_sarif::{ResultBuilder, Level}; +/// use whitaker_sarif::{Level, ResultBuilder}; /// /// let result = ResultBuilder::new("WHK001") /// .with_message("Type-1 clone detected") @@ -133,9 +137,10 @@ impl ResultBuilder { #[cfg(test)] mod tests { + use rstest::rstest; + use super::*; use crate::merge::WHITAKER_FRAGMENT_KEY; - use rstest::rstest; #[rstest] #[case("WHK001", "msg", Level::Warning)] diff --git a/crates/whitaker_sarif/src/builders/run_builder.rs b/crates/whitaker_sarif/src/builders/run_builder.rs index a436eb3b2..7fd618ac4 100644 --- a/crates/whitaker_sarif/src/builders/run_builder.rs +++ b/crates/whitaker_sarif/src/builders/run_builder.rs @@ -1,8 +1,10 @@ //! Builder for [`Run`] objects. -use crate::model::descriptor::ReportingDescriptor; -use crate::model::result::SarifResult; -use crate::model::run::{Artefact, Invocation, Run, Tool, ToolComponent}; +use crate::model::{ + descriptor::ReportingDescriptor, + result::SarifResult, + run::{Artefact, Invocation, Run, Tool, ToolComponent}, +}; /// Fluent builder for constructing a [`Run`]. /// @@ -50,7 +52,10 @@ impl RunBuilder { /// let run = RunBuilder::new("tool", "1.0") /// .with_information_uri("https://example.com") /// .build(); - /// assert_eq!(run.tool.driver.information_uri.as_deref(), Some("https://example.com")); + /// assert_eq!( + /// run.tool.driver.information_uri.as_deref(), + /// Some("https://example.com") + /// ); /// ``` #[must_use] pub fn with_information_uri(mut self, uri: impl Into) -> Self { @@ -81,7 +86,7 @@ impl RunBuilder { /// # Examples /// /// ``` - /// use whitaker_sarif::{RunBuilder, ResultBuilder}; + /// use whitaker_sarif::{ResultBuilder, RunBuilder}; /// /// let result = ResultBuilder::new("WHK001") /// .with_message("clone") @@ -101,7 +106,7 @@ impl RunBuilder { /// # Examples /// /// ``` - /// use whitaker_sarif::{RunBuilder, Invocation}; + /// use whitaker_sarif::{Invocation, RunBuilder}; /// /// let run = RunBuilder::new("tool", "1.0") /// .with_invocation(Invocation { @@ -122,7 +127,7 @@ impl RunBuilder { /// # Examples /// /// ``` - /// use whitaker_sarif::{RunBuilder, Artefact, ArtefactLocation}; + /// use whitaker_sarif::{Artefact, ArtefactLocation, RunBuilder}; /// /// let run = RunBuilder::new("tool", "1.0") /// .with_artefact(Artefact { @@ -175,14 +180,14 @@ impl RunBuilder { mod tests { //! Unit tests for [`RunBuilder`] construction and method chaining. + use rstest::{fixture, rstest}; + use super::*; use crate::rules::all_rules; - use rstest::{fixture, rstest}; + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] - fn builder() -> RunBuilder { - RunBuilder::new("tool", "1.0") - } + fn builder() -> RunBuilder { RunBuilder::new("tool", "1.0") } #[rstest] fn builds_run_with_tool(builder: RunBuilder) { diff --git a/crates/whitaker_sarif/src/lib.rs b/crates/whitaker_sarif/src/lib.rs index 63514b6ba..d4b07ccbf 100644 --- a/crates/whitaker_sarif/src/lib.rs +++ b/crates/whitaker_sarif/src/lib.rs @@ -4,8 +4,8 @@ //! Analysis Results Interchange Format (SARIF) 2.1.0 documents used by the //! Whitaker clone detection pipeline. It includes: //! -//! - **Model types** representing the SARIF 2.1.0 schema subset used by -//! Whitaker (logs, runs, results, locations, regions, rules). +//! - **Model types** representing the SARIF 2.1.0 schema subset used by Whitaker (logs, runs, +//! results, locations, regions, rules). //! - **Fluent builders** for ergonomic construction of SARIF objects. //! - **Rule definitions** for the three clone types (WHK001–WHK003). //! - **Whitaker properties** extension for attaching similarity metadata. @@ -23,31 +23,50 @@ pub mod test_support; pub mod whitaker_properties; // Error types +// Builders +pub use builders::{LocationBuilder, RegionBuilder, ResultBuilder, RunBuilder, SarifLogBuilder}; pub use error::{Result, SarifError}; - +// Merge logic +pub use merge::{WHITAKER_FRAGMENT_KEY, deduplicate_results, merge_runs}; // Model types pub use model::{ - Artefact, ArtefactLocation, Invocation, Level, Location, Message, MultiformatMessageString, - PhysicalLocation, Region, RelatedLocation, ReportingDescriptor, Run, SarifLog, SarifResult, - Tool, ToolComponent, + Artefact, + ArtefactLocation, + Invocation, + Level, + Location, + Message, + MultiformatMessageString, + PhysicalLocation, + Region, + RelatedLocation, + ReportingDescriptor, + Run, + SarifLog, + SarifResult, + Tool, + ToolComponent, +}; +// Path helpers +pub use paths::{ + AST_PASS_FILENAME, + REFINED_FILENAME, + TOKEN_PASS_FILENAME, + WHITAKER_DIR, + ast_pass_path, + refined_path, + token_pass_path, + whitaker_dir, }; - -// Builders -pub use builders::{LocationBuilder, RegionBuilder, ResultBuilder, RunBuilder, SarifLogBuilder}; - // Rules pub use rules::{ - WHK001_ID, WHK002_ID, WHK003_ID, all_rules, whk001_rule, whk002_rule, whk003_rule, + WHK001_ID, + WHK002_ID, + WHK003_ID, + all_rules, + whk001_rule, + whk002_rule, + whk003_rule, }; - // Whitaker properties extension pub use whitaker_properties::{WhitakerProperties, WhitakerPropertiesBuilder}; - -// Merge logic -pub use merge::{WHITAKER_FRAGMENT_KEY, deduplicate_results, merge_runs}; - -// Path helpers -pub use paths::{ - AST_PASS_FILENAME, REFINED_FILENAME, TOKEN_PASS_FILENAME, WHITAKER_DIR, ast_pass_path, - refined_path, token_pass_path, whitaker_dir, -}; diff --git a/crates/whitaker_sarif/src/merge.rs b/crates/whitaker_sarif/src/merge.rs index 083b73666..027af3f76 100644 --- a/crates/whitaker_sarif/src/merge.rs +++ b/crates/whitaker_sarif/src/merge.rs @@ -7,11 +7,10 @@ use std::collections::HashSet; -use crate::error::{Result, SarifError}; -use crate::model::descriptor::ReportingDescriptor; -use crate::model::location::Region; -use crate::model::result::SarifResult; -use crate::model::run::Run; +use crate::{ + error::{Result, SarifError}, + model::{descriptor::ReportingDescriptor, location::Region, result::SarifResult, run::Run}, +}; /// Fingerprint key used by the Whitaker clone detector for result deduplication. /// @@ -84,20 +83,20 @@ fn extract_key(result: &SarifResult) -> Option { /// # Examples /// /// ``` -/// use whitaker_sarif::{SarifResult, Level, Message, deduplicate_results}; +/// use whitaker_sarif::{Level, Message, SarifResult, deduplicate_results}; /// -/// let results = vec![ -/// SarifResult { -/// rule_id: "WHK001".into(), -/// level: Level::Warning, -/// message: Message { text: "clone".into() }, -/// locations: Vec::new(), -/// related_locations: Vec::new(), -/// partial_fingerprints: Default::default(), -/// properties: None, -/// baseline_state: None, +/// let results = vec![SarifResult { +/// rule_id: "WHK001".into(), +/// level: Level::Warning, +/// message: Message { +/// text: "clone".into(), /// }, -/// ]; +/// locations: Vec::new(), +/// related_locations: Vec::new(), +/// partial_fingerprints: Default::default(), +/// properties: None, +/// baseline_state: None, +/// }]; /// let deduped = deduplicate_results(&results); /// assert_eq!(deduped.len(), 1); /// ``` @@ -191,8 +190,10 @@ fn union_rules(runs: &[Run]) -> Vec { #[cfg(test)] mod tests { use super::*; - use crate::builders::{ResultBuilder, RunBuilder}; - use crate::test_support::make_keyed_result; + use crate::{ + builders::{ResultBuilder, RunBuilder}, + test_support::make_keyed_result, + }; fn merged_result_count(r1: SarifResult, r2: SarifResult) -> usize { let run_a = RunBuilder::new("tool", "1.0").with_result(r1).build(); diff --git a/crates/whitaker_sarif/src/model/descriptor.rs b/crates/whitaker_sarif/src/model/descriptor.rs index 7898ee845..948408ce4 100644 --- a/crates/whitaker_sarif/src/model/descriptor.rs +++ b/crates/whitaker_sarif/src/model/descriptor.rs @@ -48,7 +48,9 @@ pub struct ReportingDescriptor { /// ``` /// use whitaker_sarif::MultiformatMessageString; /// -/// let msg = MultiformatMessageString { text: "hello".into() }; +/// let msg = MultiformatMessageString { +/// text: "hello".into(), +/// }; /// assert_eq!(msg.text, "hello"); /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/crates/whitaker_sarif/src/model/location.rs b/crates/whitaker_sarif/src/model/location.rs index 04c8908b4..cc3501a45 100644 --- a/crates/whitaker_sarif/src/model/location.rs +++ b/crates/whitaker_sarif/src/model/location.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// # Examples /// /// ``` -/// use whitaker_sarif::{Location, PhysicalLocation, ArtefactLocation, Region}; +/// use whitaker_sarif::{ArtefactLocation, Location, PhysicalLocation, Region}; /// /// let loc = Location { /// physical_location: PhysicalLocation { diff --git a/crates/whitaker_sarif/src/model/log.rs b/crates/whitaker_sarif/src/model/log.rs index 382912816..dd09e31e9 100644 --- a/crates/whitaker_sarif/src/model/log.rs +++ b/crates/whitaker_sarif/src/model/log.rs @@ -70,9 +70,7 @@ mod tests { } #[test] - fn round_trip_empty_log() { - assert_json_round_trip(&SarifLog::default()); - } + fn round_trip_empty_log() { assert_json_round_trip(&SarifLog::default()); } #[test] fn schema_field_serializes_as_dollar_schema() { diff --git a/crates/whitaker_sarif/src/model/mod.rs b/crates/whitaker_sarif/src/model/mod.rs index 7869f6e4d..89c00df58 100644 --- a/crates/whitaker_sarif/src/model/mod.rs +++ b/crates/whitaker_sarif/src/model/mod.rs @@ -4,13 +4,11 @@ //! specification subset used by Whitaker. Types are organized by concept: //! //! - [`log`] — top-level [`SarifLog`] container. -//! - [`run`] — [`Run`], [`Tool`], [`ToolComponent`], [`Invocation`], and -//! [`Artefact`]. +//! - [`run`] — [`Run`], [`Tool`], [`ToolComponent`], [`Invocation`], and [`Artefact`]. //! - [`result`] — [`SarifResult`], [`Level`], and [`Message`]. -//! - [`location`] — [`Location`], [`PhysicalLocation`], -//! [`ArtefactLocation`], [`Region`], and [`RelatedLocation`]. -//! - [`descriptor`] — [`ReportingDescriptor`] and -//! [`MultiformatMessageString`]. +//! - [`location`] — [`Location`], [`PhysicalLocation`], [`ArtefactLocation`], [`Region`], and +//! [`RelatedLocation`]. +//! - [`descriptor`] — [`ReportingDescriptor`] and [`MultiformatMessageString`]. //! //! All types implement `Serialize` and `Deserialize` with `camelCase` field //! naming to match the SARIF JSON schema. diff --git a/crates/whitaker_sarif/src/model/result.rs b/crates/whitaker_sarif/src/model/result.rs index 1d7044e85..079f8d933 100644 --- a/crates/whitaker_sarif/src/model/result.rs +++ b/crates/whitaker_sarif/src/model/result.rs @@ -47,7 +47,9 @@ pub enum Level { /// ``` /// use whitaker_sarif::Message; /// -/// let msg = Message { text: "clone detected".into() }; +/// let msg = Message { +/// text: "clone detected".into(), +/// }; /// assert_eq!(msg.text, "clone detected"); /// ``` #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -64,12 +66,14 @@ pub struct Message { /// # Examples /// /// ``` -/// use whitaker_sarif::{SarifResult, Level, Message}; +/// use whitaker_sarif::{Level, Message, SarifResult}; /// /// let result = SarifResult { /// rule_id: "WHK001".into(), /// level: Level::Warning, -/// message: Message { text: "clone detected".into() }, +/// message: Message { +/// text: "clone detected".into(), +/// }, /// locations: Vec::new(), /// related_locations: Vec::new(), /// partial_fingerprints: Default::default(), diff --git a/crates/whitaker_sarif/src/model/run.rs b/crates/whitaker_sarif/src/model/run.rs index bba772b96..7c09d5f29 100644 --- a/crates/whitaker_sarif/src/model/run.rs +++ b/crates/whitaker_sarif/src/model/run.rs @@ -7,8 +7,7 @@ use serde::{Deserialize, Serialize}; -use super::descriptor::ReportingDescriptor; -use super::result::SarifResult; +use super::{descriptor::ReportingDescriptor, result::SarifResult}; /// A single analysis tool execution. /// @@ -150,8 +149,10 @@ pub struct Artefact { #[cfg(test)] mod tests { use super::*; - use crate::model::location::ArtefactLocation; - use crate::test_support::{assert_json_round_trip, assert_serialized_json}; + use crate::{ + model::location::ArtefactLocation, + test_support::{assert_json_round_trip, assert_serialized_json}, + }; #[test] fn run_round_trip() { diff --git a/crates/whitaker_sarif/src/rules.rs b/crates/whitaker_sarif/src/rules.rs index 12096a0e5..28cd17f20 100644 --- a/crates/whitaker_sarif/src/rules.rs +++ b/crates/whitaker_sarif/src/rules.rs @@ -117,9 +117,7 @@ pub fn whk003_rule() -> ReportingDescriptor { /// assert_eq!(rules.len(), 3); /// ``` #[must_use] -pub fn all_rules() -> Vec { - vec![whk001_rule(), whk002_rule(), whk003_rule()] -} +pub fn all_rules() -> Vec { vec![whk001_rule(), whk002_rule(), whk003_rule()] } #[cfg(test)] mod tests { diff --git a/crates/whitaker_sarif/src/test_support.rs b/crates/whitaker_sarif/src/test_support.rs index 7510b0bf2..2e6888163 100644 --- a/crates/whitaker_sarif/src/test_support.rs +++ b/crates/whitaker_sarif/src/test_support.rs @@ -6,12 +6,13 @@ use core::fmt::Debug; -use serde::Serialize; -use serde::de::DeserializeOwned; +use serde::{Serialize, de::DeserializeOwned}; -use crate::builders::{LocationBuilder, RegionBuilder, ResultBuilder}; -use crate::merge::WHITAKER_FRAGMENT_KEY; -use crate::model::result::{Level, SarifResult}; +use crate::{ + builders::{LocationBuilder, RegionBuilder, ResultBuilder}, + merge::WHITAKER_FRAGMENT_KEY, + model::result::{Level, SarifResult}, +}; /// Asserts that `value` survives a JSON serialize/deserialize round trip /// unchanged. diff --git a/crates/whitaker_sarif/src/whitaker_properties.rs b/crates/whitaker_sarif/src/whitaker_properties.rs index f4f1136f2..f9cc613f4 100644 --- a/crates/whitaker_sarif/src/whitaker_properties.rs +++ b/crates/whitaker_sarif/src/whitaker_properties.rs @@ -88,8 +88,8 @@ impl WhitakerProperties { /// # Examples /// /// ``` -/// use whitaker_sarif::{WhitakerProperties, WhitakerPropertiesBuilder}; /// use serde_json::Value; +/// use whitaker_sarif::{WhitakerProperties, WhitakerPropertiesBuilder}; /// /// let props = WhitakerPropertiesBuilder::new("T1") /// .with_k(25) diff --git a/crates/whitaker_sarif/tests/sarif_behaviour.rs b/crates/whitaker_sarif/tests/sarif_behaviour.rs index 8e7f3aedd..90cbf6ca4 100644 --- a/crates/whitaker_sarif/tests/sarif_behaviour.rs +++ b/crates/whitaker_sarif/tests/sarif_behaviour.rs @@ -8,8 +8,18 @@ use camino::Utf8PathBuf; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_sarif::{ - Level, LocationBuilder, RegionBuilder, ResultBuilder, RunBuilder, SarifLog, SarifLogBuilder, - SarifResult, WhitakerProperties, WhitakerPropertiesBuilder, all_rules, merge_runs, + Level, + LocationBuilder, + RegionBuilder, + ResultBuilder, + RunBuilder, + SarifLog, + SarifLogBuilder, + SarifResult, + WhitakerProperties, + WhitakerPropertiesBuilder, + all_rules, + merge_runs, model::{descriptor::ReportingDescriptor, run::Run}, token_pass_path, }; @@ -34,9 +44,7 @@ struct SarifWorld { #[allow_fixture_expansion_lints] #[fixture] -fn world() -> SarifWorld { - SarifWorld::default() -} +fn world() -> SarifWorld { SarifWorld::default() } // -- Helper functions (match-based to avoid expect/unwrap) -- @@ -243,9 +251,7 @@ fn when_json_deserialized(world: &SarifWorld) { } #[when("all Whitaker rules are retrieved")] -fn when_rules_retrieved(world: &SarifWorld) { - *world.rules.borrow_mut() = all_rules(); -} +fn when_rules_retrieved(world: &SarifWorld) { *world.rules.borrow_mut() = all_rules(); } #[when("the token pass path is requested")] fn when_token_path_requested(world: &SarifWorld) { @@ -378,41 +384,25 @@ fn then_path_ends_with(world: &SarifWorld, suffix: String) { // -- Scenario bindings (indices match feature file order) -- #[scenario(path = "tests/features/sarif.feature", index = 0)] -fn scenario_minimal_log(world: SarifWorld) { - let _ = world; -} +fn scenario_minimal_log(world: SarifWorld) { let _ = world; } #[scenario(path = "tests/features/sarif.feature", index = 1)] -fn scenario_result_with_rule(world: SarifWorld) { - let _ = world; -} +fn scenario_result_with_rule(world: SarifWorld) { let _ = world; } #[scenario(path = "tests/features/sarif.feature", index = 2)] -fn scenario_whitaker_properties(world: SarifWorld) { - let _ = world; -} +fn scenario_whitaker_properties(world: SarifWorld) { let _ = world; } #[scenario(path = "tests/features/sarif.feature", index = 3)] -fn scenario_merge_deduplicates(world: SarifWorld) { - let _ = world; -} +fn scenario_merge_deduplicates(world: SarifWorld) { let _ = world; } #[scenario(path = "tests/features/sarif.feature", index = 4)] -fn scenario_round_trip(world: SarifWorld) { - let _ = world; -} +fn scenario_round_trip(world: SarifWorld) { let _ = world; } #[scenario(path = "tests/features/sarif.feature", index = 5)] -fn scenario_empty_log(world: SarifWorld) { - let _ = world; -} +fn scenario_empty_log(world: SarifWorld) { let _ = world; } #[scenario(path = "tests/features/sarif.feature", index = 6)] -fn scenario_all_rules(world: SarifWorld) { - let _ = world; -} +fn scenario_all_rules(world: SarifWorld) { let _ = world; } #[scenario(path = "tests/features/sarif.feature", index = 7)] -fn scenario_path_helpers(world: SarifWorld) { - let _ = world; -} +fn scenario_path_helpers(world: SarifWorld) { let _ = world; } diff --git a/installer/src/artefact/download.rs b/installer/src/artefact/download.rs index e25b8781a..a412142b1 100644 --- a/installer/src/artefact/download.rs +++ b/installer/src/artefact/download.rs @@ -4,9 +4,7 @@ //! and manifests from the GitHub rolling release, enabling dependency //! injection for testing. -use std::path::Path; -use std::sync::OnceLock; -use std::time::Duration; +use std::{path::Path, sync::OnceLock, time::Duration}; /// The GitHub repository owner/name for URL construction. const GITHUB_REPO: &str = "leynos/whitaker"; @@ -157,9 +155,10 @@ fn map_ureq_error(url: &str, err: &ureq::Error) -> DownloadError { mod tests { //! Tests for artefact download URL and error mapping behaviour. - use super::*; use rstest::rstest; + use super::*; + #[test] fn asset_url_contains_repo_and_tag() { let url = HttpDownloader::asset_url("test.tar.zst"); diff --git a/installer/src/artefact/extraction.rs b/installer/src/artefact/extraction.rs index cb7333b62..e4a03e824 100644 --- a/installer/src/artefact/extraction.rs +++ b/installer/src/artefact/extraction.rs @@ -115,10 +115,12 @@ fn validate_entry_path(path: &Path) -> Result<(), ExtractionError> { mod tests { //! Tests for artefact archive extraction. - use super::*; - use rstest::rstest; use std::path::PathBuf; + use rstest::rstest; + + use super::*; + #[test] fn extract_real_archive() { // Create a temp archive with a single file, then extract it. diff --git a/installer/src/artefact/git_sha.rs b/installer/src/artefact/git_sha.rs index db105824b..ea81112ef 100644 --- a/installer/src/artefact/git_sha.rs +++ b/installer/src/artefact/git_sha.rs @@ -4,10 +4,12 @@ //! of 7–40 characters, matching the range of abbreviated to full git //! object names. -use super::error::{ArtefactError, Result}; -use serde::Serialize; use std::fmt; +use serde::Serialize; + +use super::error::{ArtefactError, Result}; + /// Minimum length of an abbreviated git SHA (7 hex characters). const MIN_LEN: usize = 7; @@ -40,9 +42,7 @@ impl GitSha { /// assert_eq!(sha.as_str(), "abc1234"); /// ``` #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } + pub fn as_str(&self) -> &str { &self.0 } /// Consume the wrapper and return the inner string. /// @@ -55,9 +55,7 @@ impl GitSha { /// assert_eq!(sha.into_inner(), "abc1234"); /// ``` #[must_use] - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } } impl TryFrom<&str> for GitSha { @@ -80,15 +78,11 @@ impl TryFrom for GitSha { } impl AsRef for GitSha { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for GitSha { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl<'de> serde::Deserialize<'de> for GitSha { @@ -148,9 +142,10 @@ fn validate_git_sha(value: &str) -> Result<()> { mod tests { //! Tests for git SHA parsing and validation. - use super::*; use rstest::rstest; + use super::*; + #[test] fn accepts_seven_char_abbreviated_sha() { let sha = GitSha::try_from("abc1234"); diff --git a/installer/src/artefact/manifest.rs b/installer/src/artefact/manifest.rs index c59e1c9cc..936e68113 100644 --- a/installer/src/artefact/manifest.rs +++ b/installer/src/artefact/manifest.rs @@ -4,14 +4,18 @@ //! archive ships a `manifest.json` capturing provenance, content listing, //! and the archive checksum. -use super::git_sha::GitSha; -use super::schema_version::SchemaVersion; -use super::sha256_digest::Sha256Digest; -use super::target::TargetTriple; -use super::toolchain_channel::ToolchainChannel; -use serde::{Deserialize, Serialize}; use std::fmt; +use serde::{Deserialize, Serialize}; + +use super::{ + git_sha::GitSha, + schema_version::SchemaVersion, + sha256_digest::Sha256Digest, + target::TargetTriple, + toolchain_channel::ToolchainChannel, +}; + /// Provenance fields that identify an artefact build. /// /// Groups the identity components (git SHA, schema version, toolchain, and @@ -62,28 +66,26 @@ pub struct ManifestContent { /// # Examples /// /// ``` -/// use whitaker_installer::artefact::manifest::{ -/// GeneratedAt, Manifest, ManifestContent, ManifestProvenance, +/// use whitaker_installer::artefact::{ +/// git_sha::GitSha, +/// manifest::{GeneratedAt, Manifest, ManifestContent, ManifestProvenance}, +/// schema_version::SchemaVersion, +/// sha256_digest::Sha256Digest, +/// target::TargetTriple, +/// toolchain_channel::ToolchainChannel, /// }; -/// use whitaker_installer::artefact::git_sha::GitSha; -/// use whitaker_installer::artefact::schema_version::SchemaVersion; -/// use whitaker_installer::artefact::sha256_digest::Sha256Digest; -/// use whitaker_installer::artefact::target::TargetTriple; -/// use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; /// /// let provenance = ManifestProvenance { /// git_sha: GitSha::try_from("abc1234").expect("valid git SHA"), /// schema_version: SchemaVersion::current(), /// toolchain: ToolchainChannel::try_from("nightly-2026-05-28") /// .expect("valid toolchain channel"), -/// target: TargetTriple::try_from("x86_64-unknown-linux-gnu") -/// .expect("valid target triple"), +/// target: TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target triple"), /// }; /// let content = ManifestContent { /// generated_at: GeneratedAt::new("2026-05-28T00:00:00Z"), /// files: vec!["libwhitaker_lints.so".to_owned()], -/// sha256: Sha256Digest::try_from("a".repeat(64).as_str()) -/// .expect("valid SHA-256 digest"), +/// sha256: Sha256Digest::try_from("a".repeat(64).as_str()).expect("valid SHA-256 digest"), /// }; /// let manifest = Manifest::new(provenance, content); /// assert_eq!(manifest.git_sha().as_str(), "abc1234"); @@ -117,9 +119,7 @@ impl GeneratedAt { /// assert_eq!(ts.as_str(), "2026-05-28T00:00:00Z"); /// ``` #[must_use] - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } + pub fn new(value: impl Into) -> Self { Self(value.into()) } /// Return the timestamp as a string slice. /// @@ -132,15 +132,11 @@ impl GeneratedAt { /// assert_eq!(ts.as_str(), "2026-05-28T00:00:00Z"); /// ``` #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } + pub fn as_str(&self) -> &str { &self.0 } } impl fmt::Display for GeneratedAt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } /// Helper macro for manifest doc examples — constructs a sample @@ -151,14 +147,14 @@ impl fmt::Display for GeneratedAt { #[macro_export] macro_rules! _manifest_doc_setup { ($manifest:ident) => { - use whitaker_installer::artefact::git_sha::GitSha; - use whitaker_installer::artefact::manifest::{ - GeneratedAt, Manifest, ManifestContent, ManifestProvenance, + use whitaker_installer::artefact::{ + git_sha::GitSha, + manifest::{GeneratedAt, Manifest, ManifestContent, ManifestProvenance}, + schema_version::SchemaVersion, + sha256_digest::Sha256Digest, + target::TargetTriple, + toolchain_channel::ToolchainChannel, }; - use whitaker_installer::artefact::schema_version::SchemaVersion; - use whitaker_installer::artefact::sha256_digest::Sha256Digest; - use whitaker_installer::artefact::target::TargetTriple; - use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; let provenance = ManifestProvenance { git_sha: GitSha::try_from("abc1234").expect("valid git SHA"), @@ -203,9 +199,7 @@ impl Manifest { /// assert_eq!(manifest.git_sha().as_str(), "abc1234"); /// ``` #[must_use] - pub const fn git_sha(&self) -> &GitSha { - &self.provenance.git_sha - } + pub const fn git_sha(&self) -> &GitSha { &self.provenance.git_sha } /// Return the schema version. /// @@ -216,9 +210,7 @@ impl Manifest { /// assert_eq!(u32::from(manifest.schema_version()), 1); /// ``` #[must_use] - pub const fn schema_version(&self) -> SchemaVersion { - self.provenance.schema_version - } + pub const fn schema_version(&self) -> SchemaVersion { self.provenance.schema_version } /// Return the toolchain channel. /// @@ -229,9 +221,7 @@ impl Manifest { /// assert_eq!(manifest.toolchain().as_str(), "nightly-2026-05-28"); /// ``` #[must_use] - pub const fn toolchain(&self) -> &ToolchainChannel { - &self.provenance.toolchain - } + pub const fn toolchain(&self) -> &ToolchainChannel { &self.provenance.toolchain } /// Return the target triple. /// @@ -242,9 +232,7 @@ impl Manifest { /// assert_eq!(manifest.target().as_str(), "x86_64-unknown-linux-gnu"); /// ``` #[must_use] - pub const fn target(&self) -> &TargetTriple { - &self.provenance.target - } + pub const fn target(&self) -> &TargetTriple { &self.provenance.target } /// Return the build timestamp. /// @@ -255,9 +243,7 @@ impl Manifest { /// assert_eq!(manifest.generated_at().as_str(), "2026-05-28T00:00:00Z"); /// ``` #[must_use] - pub const fn generated_at(&self) -> &GeneratedAt { - &self.content.generated_at - } + pub const fn generated_at(&self) -> &GeneratedAt { &self.content.generated_at } /// Return the list of files in the archive. /// @@ -268,9 +254,7 @@ impl Manifest { /// assert_eq!(manifest.files(), &["libwhitaker_lints.so"]); /// ``` #[must_use] - pub fn files(&self) -> &[String] { - &self.content.files - } + pub fn files(&self) -> &[String] { &self.content.files } /// Return the SHA-256 digest of the archive. /// @@ -281,9 +265,7 @@ impl Manifest { /// assert_eq!(manifest.sha256().as_str().len(), 64); /// ``` #[must_use] - pub const fn sha256(&self) -> &Sha256Digest { - &self.content.sha256 - } + pub const fn sha256(&self) -> &Sha256Digest { &self.content.sha256 } } #[cfg(test)] diff --git a/installer/src/artefact/manifest_parser.rs b/installer/src/artefact/manifest_parser.rs index 3cff9f7f3..9c2a9faf2 100644 --- a/installer/src/artefact/manifest_parser.rs +++ b/installer/src/artefact/manifest_parser.rs @@ -50,9 +50,10 @@ pub fn parse_manifest(json: &str) -> Result { mod tests { //! Tests for artefact manifest parsing. - use super::*; use rstest::rstest; + use super::*; + fn valid_manifest_json() -> String { concat!( r#"{"git_sha":"abc1234","schema_version":1,"#, diff --git a/installer/src/artefact/naming.rs b/installer/src/artefact/naming.rs index 703fb047d..f37bc7cda 100644 --- a/installer/src/artefact/naming.rs +++ b/installer/src/artefact/naming.rs @@ -3,11 +3,10 @@ //! Constructs deterministic archive names in the format specified by ADR-001: //! `whitaker-lints---.tar.zst`. -use super::git_sha::GitSha; -use super::target::TargetTriple; -use super::toolchain_channel::ToolchainChannel; use std::fmt; +use super::{git_sha::GitSha, target::TargetTriple, toolchain_channel::ToolchainChannel}; + /// The fixed prefix for all artefact archive names. const ARTEFACT_PREFIX: &str = "whitaker-lints"; @@ -22,10 +21,12 @@ const ARTEFACT_EXTENSION: &str = ".tar.zst"; /// # Examples /// /// ``` -/// use whitaker_installer::artefact::naming::ArtefactName; -/// use whitaker_installer::artefact::git_sha::GitSha; -/// use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; -/// use whitaker_installer::artefact::target::TargetTriple; +/// use whitaker_installer::artefact::{ +/// git_sha::GitSha, +/// naming::ArtefactName, +/// target::TargetTriple, +/// toolchain_channel::ToolchainChannel, +/// }; /// /// let sha: GitSha = "abc1234".try_into().expect("valid git SHA"); /// let toolchain: ToolchainChannel = "nightly-2026-05-28" @@ -61,27 +62,19 @@ impl ArtefactName { /// Return the git SHA component. #[must_use] - pub const fn git_sha(&self) -> &GitSha { - &self.git_sha - } + pub const fn git_sha(&self) -> &GitSha { &self.git_sha } /// Return the toolchain channel component. #[must_use] - pub const fn toolchain(&self) -> &ToolchainChannel { - &self.toolchain - } + pub const fn toolchain(&self) -> &ToolchainChannel { &self.toolchain } /// Return the target triple component. #[must_use] - pub const fn target(&self) -> &TargetTriple { - &self.target - } + pub const fn target(&self) -> &TargetTriple { &self.target } /// Return the filename as a string without consuming the value. #[must_use] - pub fn filename(&self) -> String { - self.to_string() - } + pub fn filename(&self) -> String { self.to_string() } } impl fmt::Display for ArtefactName { @@ -98,9 +91,10 @@ impl fmt::Display for ArtefactName { mod tests { //! Tests for artefact release-asset naming. + use rstest::{fixture, rstest}; + use super::*; use crate::artefact::error::ArtefactError; - use rstest::{fixture, rstest}; #[fixture] fn sample_name() -> Result { diff --git a/installer/src/artefact/packaging.rs b/installer/src/artefact/packaging.rs index 28c09649c..8dc96d7fb 100644 --- a/installer/src/artefact/packaging.rs +++ b/installer/src/artefact/packaging.rs @@ -5,8 +5,8 @@ //! //! # Preconditions //! -//! - All library file paths in [`PackageParams::library_files`] must -//! exist on disk and have a filename component. +//! - All library file paths in [`PackageParams::library_files`] must exist on disk and have a +//! filename component. //! - The `output_dir` must exist and be writable. //! //! # Outputs and side effects @@ -27,19 +27,25 @@ //! `SHA-256(downloaded_archive)` and comparing against the `sha256` //! field from the manifest obtained via the release API. -use super::git_sha::GitSha; -use super::manifest::{GeneratedAt, Manifest, ManifestContent, ManifestProvenance}; -use super::naming::ArtefactName; -use super::packaging_error::PackagingError; -use super::schema_version::SchemaVersion; -use super::sha256_digest::Sha256Digest; -use super::target::TargetTriple; -use super::toolchain_channel::ToolchainChannel; -use crate::hex::to_lower_hex; +use std::{ + fs, + io::Read, + path::{Path, PathBuf}, +}; + use sha2::{Digest, Sha256}; -use std::fs; -use std::io::Read; -use std::path::{Path, PathBuf}; + +use super::{ + git_sha::GitSha, + manifest::{GeneratedAt, Manifest, ManifestContent, ManifestProvenance}, + naming::ArtefactName, + packaging_error::PackagingError, + schema_version::SchemaVersion, + sha256_digest::Sha256Digest, + target::TargetTriple, + toolchain_channel::ToolchainChannel, +}; +use crate::hex::to_lower_hex; /// Input parameters for the [`package_artefact`] function. /// @@ -139,8 +145,7 @@ pub fn create_archive( /// use whitaker_installer::artefact::packaging::generate_manifest_json; /// /// let json = generate_manifest_json(&manifest).expect("serialization"); -/// let parsed: serde_json::Value = -/// serde_json::from_str(&json).expect("valid JSON"); +/// let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid JSON"); /// let obj = parsed.as_object().expect("top-level object"); /// assert!(obj.contains_key("git_sha")); /// assert!(obj.contains_key("sha256")); diff --git a/installer/src/artefact/packaging_tests.rs b/installer/src/artefact/packaging_tests.rs index 85f66c87b..75680d50d 100644 --- a/installer/src/artefact/packaging_tests.rs +++ b/installer/src/artefact/packaging_tests.rs @@ -8,15 +8,11 @@ use crate::artefact::error::ArtefactError; #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn temp_dir() -> std::io::Result { - TempDir::new() -} +fn temp_dir() -> std::io::Result { TempDir::new() } #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn sample_git_sha() -> Result { - GitSha::try_from("abc1234") -} +fn sample_git_sha() -> Result { GitSha::try_from("abc1234") } #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] diff --git a/installer/src/artefact/schema_version.rs b/installer/src/artefact/schema_version.rs index 4bfadf0d5..62676b537 100644 --- a/installer/src/artefact/schema_version.rs +++ b/installer/src/artefact/schema_version.rs @@ -3,10 +3,12 @@ //! Restricts the version to the range `1..=CURRENT_MAX`, matching the //! versioning policy defined in ADR-001. -use super::error::{ArtefactError, Result}; -use serde::Serialize; use std::fmt; +use serde::Serialize; + +use super::error::{ArtefactError, Result}; + /// The highest schema version this build can read. const CURRENT_MAX: u32 = 1; @@ -32,15 +34,11 @@ pub struct SchemaVersion(u32); impl SchemaVersion { /// Return the current (latest) schema version. #[must_use] - pub const fn current() -> Self { - Self(CURRENT_MAX) - } + pub const fn current() -> Self { Self(CURRENT_MAX) } /// Return the inner version number. #[must_use] - pub const fn as_u32(self) -> u32 { - self.0 - } + pub const fn as_u32(self) -> u32 { self.0 } } impl<'de> serde::Deserialize<'de> for SchemaVersion { @@ -68,15 +66,11 @@ impl TryFrom for SchemaVersion { } impl From for u32 { - fn from(v: SchemaVersion) -> Self { - v.0 - } + fn from(v: SchemaVersion) -> Self { v.0 } } impl fmt::Display for SchemaVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[cfg(test)] diff --git a/installer/src/artefact/sha256_digest.rs b/installer/src/artefact/sha256_digest.rs index 3e9dda26c..ad3ad1272 100644 --- a/installer/src/artefact/sha256_digest.rs +++ b/installer/src/artefact/sha256_digest.rs @@ -3,10 +3,12 @@ //! Validates that the value is a 64-character lowercase hexadecimal string //! representing a 256-bit hash digest. -use super::error::{ArtefactError, Result}; -use serde::Serialize; use std::fmt; +use serde::Serialize; + +use super::error::{ArtefactError, Result}; + /// Expected length of a hex-encoded SHA-256 digest. const DIGEST_HEX_LEN: usize = 64; @@ -38,9 +40,7 @@ impl Sha256Digest { /// assert_eq!(digest.as_str(), hex); /// ``` #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } + pub fn as_str(&self) -> &str { &self.0 } /// Consume the wrapper and return the inner string. /// @@ -54,9 +54,7 @@ impl Sha256Digest { /// assert_eq!(digest.into_inner(), hex); /// ``` #[must_use] - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } } impl<'de> serde::Deserialize<'de> for Sha256Digest { @@ -89,15 +87,11 @@ impl TryFrom for Sha256Digest { } impl AsRef for Sha256Digest { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for Sha256Digest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } /// Validate that `value` is a well-formed hex-encoded SHA-256 digest. @@ -128,14 +122,13 @@ fn validate_sha256(value: &str) -> Result<()> { mod tests { //! Tests for SHA-256 digest parsing and formatting. - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] - fn valid_digest() -> String { - "a".repeat(64) - } + fn valid_digest() -> String { "a".repeat(64) } #[rstest] fn accepts_valid_sixty_four_char_hex(valid_digest: String) { diff --git a/installer/src/artefact/target.rs b/installer/src/artefact/target.rs index c02462501..fb8b20c6d 100644 --- a/installer/src/artefact/target.rs +++ b/installer/src/artefact/target.rs @@ -3,10 +3,12 @@ //! Only the five triples listed in ADR-001 are accepted. Any other triple //! is rejected at construction time with a descriptive error. -use super::error::{ArtefactError, Result}; -use serde::Serialize; use std::fmt; +use serde::Serialize; + +use super::error::{ArtefactError, Result}; + /// The supported target triples for prebuilt artefact distribution. /// /// These correspond to the target matrix defined in ADR-001. @@ -39,21 +41,15 @@ pub struct TargetTriple(String); impl TargetTriple { /// Return the triple as a string slice. #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } + pub fn as_str(&self) -> &str { &self.0 } /// Consume the wrapper and return the inner string. #[must_use] - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } /// Return the full list of supported target triples. #[must_use] - pub const fn supported() -> &'static [&'static str] { - SUPPORTED_TARGETS - } + pub const fn supported() -> &'static [&'static str] { SUPPORTED_TARGETS } /// Return the shared library extension for this target triple. /// @@ -66,8 +62,7 @@ impl TargetTriple { /// ``` /// use whitaker_installer::artefact::target::TargetTriple; /// - /// let linux: TargetTriple = "x86_64-unknown-linux-gnu" - /// .try_into().expect("valid"); + /// let linux: TargetTriple = "x86_64-unknown-linux-gnu".try_into().expect("valid"); /// assert_eq!(linux.library_extension(), ".so"); /// ``` #[must_use] @@ -91,25 +86,18 @@ impl TargetTriple { /// ``` /// use whitaker_installer::artefact::target::TargetTriple; /// - /// let win: TargetTriple = "x86_64-pc-windows-msvc" - /// .try_into().expect("valid"); + /// let win: TargetTriple = "x86_64-pc-windows-msvc".try_into().expect("valid"); /// assert_eq!(win.library_prefix(), ""); /// ``` #[must_use] - pub fn library_prefix(&self) -> &'static str { - if self.is_windows() { "" } else { "lib" } - } + pub fn library_prefix(&self) -> &'static str { if self.is_windows() { "" } else { "lib" } } /// Whether this target is a Windows platform. #[must_use] - pub fn is_windows(&self) -> bool { - self.0.contains("windows") - } + pub fn is_windows(&self) -> bool { self.0.contains("windows") } /// Whether this target is a macOS (Darwin) platform. - fn is_darwin(&self) -> bool { - self.0.contains("darwin") - } + fn is_darwin(&self) -> bool { self.0.contains("darwin") } } impl<'de> serde::Deserialize<'de> for TargetTriple { @@ -140,30 +128,25 @@ impl TryFrom<&str> for TargetTriple { impl TryFrom for TargetTriple { type Error = ArtefactError; - fn try_from(value: String) -> Result { - Self::try_from(value.as_str()) - } + fn try_from(value: String) -> Result { Self::try_from(value.as_str()) } } impl AsRef for TargetTriple { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for TargetTriple { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[cfg(test)] mod tests { //! Tests for target-triple parsing and validation. - use super::*; use rstest::rstest; + use super::*; + #[test] fn accepts_all_supported_targets() { for target in SUPPORTED_TARGETS { diff --git a/installer/src/artefact/toolchain_channel.rs b/installer/src/artefact/toolchain_channel.rs index 5f4b802b7..5ff05ef78 100644 --- a/installer/src/artefact/toolchain_channel.rs +++ b/installer/src/artefact/toolchain_channel.rs @@ -5,10 +5,12 @@ //! characters permitted in Rust toolchain channel specifiers, including //! host-qualified names such as `nightly-2026-05-28-x86_64-unknown-linux-gnu`. -use super::error::{ArtefactError, Result}; -use serde::Serialize; use std::fmt; +use serde::Serialize; + +use super::error::{ArtefactError, Result}; + /// A validated Rust toolchain channel string (e.g. `nightly-2026-05-28`). /// /// # Examples @@ -40,15 +42,11 @@ impl ToolchainChannel { /// ``` /// use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; /// - /// let channel: ToolchainChannel = "stable" - /// .try_into() - /// .expect("valid toolchain channel"); + /// let channel: ToolchainChannel = "stable".try_into().expect("valid toolchain channel"); /// assert_eq!(channel.as_str(), "stable"); /// ``` #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } + pub fn as_str(&self) -> &str { &self.0 } /// Consume the wrapper and return the inner string. /// @@ -64,9 +62,7 @@ impl ToolchainChannel { /// assert_eq!(inner, "nightly-2026-05-28"); /// ``` #[must_use] - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } } impl TryFrom<&str> for ToolchainChannel { @@ -98,9 +94,7 @@ impl TryFrom for ToolchainChannel { } impl AsRef for ToolchainChannel { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl<'de> serde::Deserialize<'de> for ToolchainChannel { @@ -114,18 +108,17 @@ impl<'de> serde::Deserialize<'de> for ToolchainChannel { } impl fmt::Display for ToolchainChannel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[cfg(test)] mod tests { //! Tests for toolchain channel parsing and validation. - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::nightly_with_date("nightly-2026-05-28")] #[case::stable("stable")] diff --git a/installer/src/artefact/verification.rs b/installer/src/artefact/verification.rs index b1cfcc375..60179d22b 100644 --- a/installer/src/artefact/verification.rs +++ b/installer/src/artefact/verification.rs @@ -33,9 +33,7 @@ impl VerificationPolicy { /// digest of the downloaded archive and compare it against the digest /// recorded in the manifest before extracting any files. #[must_use] - pub const fn require_checksum(&self) -> bool { - self.require_checksum - } + pub const fn require_checksum(&self) -> bool { self.require_checksum } } impl Default for VerificationPolicy { diff --git a/installer/src/bin/package_dependency_binary.rs b/installer/src/bin/package_dependency_binary.rs index e39769076..13256c2b0 100644 --- a/installer/src/bin/package_dependency_binary.rs +++ b/installer/src/bin/package_dependency_binary.rs @@ -1,18 +1,23 @@ //! Package dependency binaries and shared provenance assets for release uploads. +use std::{ + io::Write, + path::{Path, PathBuf}, +}; + use clap::{Parser, Subcommand}; -use std::io::Write; -use std::path::{Path, PathBuf}; use thiserror::Error; -use whitaker_installer::dependency_binaries::{ - find_dependency_binary, required_dependency_binaries, -}; -use whitaker_installer::dependency_packaging::{ - DependencyPackageParams, DependencyPackagingError, package_dependency_binary, - write_provenance_markdown, +use whitaker_installer::{ + dependency_binaries::{find_dependency_binary, required_dependency_binaries}, + dependency_packaging::{ + DependencyPackageParams, + DependencyPackagingError, + package_dependency_binary, + write_provenance_markdown, + }, + installer_packaging::TargetTriple, + output::write_stderr_line, }; -use whitaker_installer::installer_packaging::TargetTriple; -use whitaker_installer::output::write_stderr_line; /// Package repository-hosted dependency binaries for release publication. #[derive(Parser, Debug)] @@ -134,10 +139,11 @@ fn run_provenance(output_dir: &Path) -> Result<(), CliError> { mod tests { //! Tests for the dependency-binary packaging command. - use super::*; use tempfile::tempdir; use whitaker_installer::dependency_binaries::provenance_filename; + use super::*; + #[test] fn run_package_command_rejects_invalid_target() { let temp_dir = tempdir().expect("temp dir"); diff --git a/installer/src/bin/package_installer_bin.rs b/installer/src/bin/package_installer_bin.rs index 169ed3369..9db1ef0c4 100644 --- a/installer/src/bin/package_installer_bin.rs +++ b/installer/src/bin/package_installer_bin.rs @@ -4,15 +4,15 @@ //! [`whitaker_installer::installer_packaging::package_installer`] invoked //! by the release CI workflow to create binstall-compatible archives. +use std::{io::Write, path::PathBuf}; + use clap::Parser; -use std::io::Write; -use std::path::PathBuf; use thiserror::Error; -use whitaker_installer::artefact::error::ArtefactError; -use whitaker_installer::installer_packaging::{ - InstallerPackagingError, TargetTriple, Version, package_installer, +use whitaker_installer::{ + artefact::error::ArtefactError, + installer_packaging::{InstallerPackagingError, TargetTriple, Version, package_installer}, + output::write_stderr_line, }; -use whitaker_installer::output::write_stderr_line; /// Package the `whitaker-installer` binary into a release archive. /// @@ -85,10 +85,11 @@ fn run(cli: Cli) -> Result<(), CliError> { mod tests { //! Tests for the installer packaging command. - use super::*; use clap::Parser; use rstest::rstest; + use super::*; + const BASE_ARGS: [&str; 9] = [ "whitaker-package-installer", "--crate-version", diff --git a/installer/src/bin/package_lints.rs b/installer/src/bin/package_lints.rs index b2cee0605..fd9dc35b0 100644 --- a/installer/src/bin/package_lints.rs +++ b/installer/src/bin/package_lints.rs @@ -4,22 +4,27 @@ //! invoked by both the Makefile `package-lints` target and the //! rolling-release CI workflow. +use std::{ + io::Write, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; + use clap::Parser; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; -use whitaker_installer::artefact::error::ArtefactError; -use whitaker_installer::artefact::git_sha::GitSha; -use whitaker_installer::artefact::manifest::GeneratedAt; -use whitaker_installer::artefact::packaging::{ - PackageParams, generate_manifest_json, package_artefact, +use whitaker_installer::{ + artefact::{ + error::ArtefactError, + git_sha::GitSha, + manifest::GeneratedAt, + packaging::{PackageParams, generate_manifest_json, package_artefact}, + packaging_error::PackagingError, + target::TargetTriple, + toolchain_channel::ToolchainChannel, + }, + output::write_stderr_line, + resolution::{LINT_CRATES, SUITE_CRATE}, }; -use whitaker_installer::artefact::packaging_error::PackagingError; -use whitaker_installer::artefact::target::TargetTriple; -use whitaker_installer::artefact::toolchain_channel::ToolchainChannel; -use whitaker_installer::output::write_stderr_line; -use whitaker_installer::resolution::{LINT_CRATES, SUITE_CRATE}; /// Package prebuilt lint libraries into `.tar.zst` archives following /// the ADR-001 naming convention and write a sidecar @@ -255,10 +260,12 @@ const fn civil_from_epoch(epoch_secs: u64) -> (i64, u64, u64) { mod tests { //! Tests for the lint-library packaging command. - use super::*; + use std::fs; + use clap::Parser; use rstest::{fixture, rstest}; - use std::fs; + + use super::*; /// Common CLI base arguments shared across parsing tests. const BASE_ARGS: [&str; 9] = [ diff --git a/installer/src/builder.rs b/installer/src/builder.rs index 739358dc7..38d109ea1 100644 --- a/installer/src/builder.rs +++ b/installer/src/builder.rs @@ -3,18 +3,28 @@ //! This module provides utilities to build Dylint lint crates in release mode //! with the required features enabled. -use crate::error::{InstallerError, Result}; -use crate::toolchain::Toolchain; -use camino::Utf8PathBuf; use std::process::Command; +use camino::Utf8PathBuf; + // Re-export from submodules for backwards compatibility pub use crate::crate_name::CrateName; -pub use crate::resolution::{ - CrateResolutionOptions, EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE, is_known_crate, - resolve_crates, validate_crate_names, +use crate::{ + error::{InstallerError, Result}, + toolchain::Toolchain, +}; +pub use crate::{ + resolution::{ + CrateResolutionOptions, + EXPERIMENTAL_LINT_CRATES, + LINT_CRATES, + SUITE_CRATE, + is_known_crate, + resolve_crates, + validate_crate_names, + }, + workspace::find_workspace_root, }; -pub use crate::workspace::find_workspace_root; /// Configuration for the build process. #[derive(Debug, Clone)] @@ -63,9 +73,7 @@ pub struct Builder { impl Builder { /// Create a new builder with the given configuration. #[must_use] - pub const fn new(config: BuildConfig) -> Self { - Self { config } - } + pub const fn new(config: BuildConfig) -> Self { Self { config } } /// Build a single lint crate. /// @@ -186,15 +194,11 @@ impl Builder { /// This method is primarily useful for testing to verify that the correct /// configuration was constructed. #[must_use] - pub const fn config(&self) -> &BuildConfig { - &self.config - } + pub const fn config(&self) -> &BuildConfig { &self.config } } impl CrateBuilder for Builder { - fn build_all(&self, crates: &[CrateName]) -> Result> { - self.build_all(crates) - } + fn build_all(&self, crates: &[CrateName]) -> Result> { self.build_all(crates) } } /// Return the platform-specific library file extension (including the dot). @@ -231,9 +235,10 @@ pub const fn library_prefix() -> &'static str { mod tests { //! Tests for lint-library build orchestration. - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[fixture] fn builder() -> Builder { Builder { diff --git a/installer/src/cli.rs b/installer/src/cli.rs index fcc281437..a2807936b 100644 --- a/installer/src/cli.rs +++ b/installer/src/cli.rs @@ -4,11 +4,11 @@ //! from the main entrypoint to keep the binary small and focused on //! orchestration. -use crate::crate_name::CrateName; -use crate::resolution::EXPERIMENTAL_LINT_CRATES; use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; +use crate::{crate_name::CrateName, resolution::EXPERIMENTAL_LINT_CRATES}; + /// Install Whitaker Dylint lint libraries. #[derive(Parser, Debug)] #[command(name = "whitaker-installer")] @@ -185,15 +185,13 @@ impl InstallArgs { /// /// Prebuilt artefacts are skipped when: /// - `--build-only` is set, or - /// - experimental lint behaviour is requested, either via - /// `--experimental` (suite build) or explicit experimental crates when - /// the experimental crate list is non-empty. + /// - experimental lint behaviour is requested, either via `--experimental` (suite build) or + /// explicit experimental crates when the experimental crate list is non-empty. /// /// # Examples /// /// ``` - /// use whitaker_installer::cli::InstallArgs; - /// use whitaker_installer::crate_name::CrateName; + /// use whitaker_installer::{cli::InstallArgs, crate_name::CrateName}; /// /// let requested = vec![CrateName::from("whitaker_suite")]; /// diff --git a/installer/src/crate_name.rs b/installer/src/crate_name.rs index c468fe13a..8d36993f9 100644 --- a/installer/src/crate_name.rs +++ b/installer/src/crate_name.rs @@ -17,54 +17,41 @@ pub struct CrateName(String); impl CrateName { /// Create a new crate name. #[must_use] - pub fn new(name: impl Into) -> Self { - Self(name.into()) - } + pub fn new(name: impl Into) -> Self { Self(name.into()) } /// Get the crate name as a string slice. #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } + pub fn as_str(&self) -> &str { &self.0 } /// Consume the wrapper and return the inner string. #[must_use] - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } } impl AsRef for CrateName { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl From<&str> for CrateName { - fn from(s: &str) -> Self { - Self(s.to_owned()) - } + fn from(s: &str) -> Self { Self(s.to_owned()) } } impl From for CrateName { - fn from(s: String) -> Self { - Self(s) - } + fn from(s: String) -> Self { Self(s) } } impl fmt::Display for CrateName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[cfg(test)] mod tests { //! Tests for crate-name normalization. - use super::*; use std::collections::HashSet; + use super::*; + #[test] fn new_creates_valid_instance() { let name = CrateName::new("test_crate"); diff --git a/installer/src/dependency_binaries/install/checksum.rs b/installer/src/dependency_binaries/install/checksum.rs index fb069bd69..741cdf493 100644 --- a/installer/src/dependency_binaries/install/checksum.rs +++ b/installer/src/dependency_binaries/install/checksum.rs @@ -7,14 +7,14 @@ //! owned here and imported by `downloader`, keeping the module dependency //! one-way. -use super::installer::DependencyBinaryInstallError; -use crate::hex::to_lower_hex; +use std::{io, io::Read, path::Path}; + use sha2::{Digest, Sha256}; -use std::io; -use std::io::Read; -use std::path::Path; use tracing::{debug, warn}; +use super::installer::DependencyBinaryInstallError; +use crate::hex::to_lower_hex; + /// Bounded `category` field for every checksum boundary event. Owned here so the /// module dependency stays one-way (`downloader` imports this; not vice versa). pub(super) const CATEGORY_CHECKSUM: &str = "checksum"; @@ -173,11 +173,13 @@ pub(super) fn verify_archive_checksum( mod tests { //! Tests for checksum parsing, streaming SHA-256, and verification. - use super::*; - use rstest::rstest; use std::io::Write; + + use rstest::rstest; use tempfile::NamedTempFile; + use super::*; + /// Write `contents` to a fresh temp file and return the handle. fn temp_file_with(contents: &[u8]) -> io::Result { let mut file = NamedTempFile::new()?; @@ -187,9 +189,7 @@ mod tests { } /// Reopen `file` as a fresh read handle for streaming into the hasher. - fn read_handle(file: &NamedTempFile) -> io::Result { - file.reopen() - } + fn read_handle(file: &NamedTempFile) -> io::Result { file.reopen() } #[rstest] #[case(404, true)] diff --git a/installer/src/dependency_binaries/install/downloader.rs b/installer/src/dependency_binaries/install/downloader.rs index f6ea8f206..8d853093a 100644 --- a/installer/src/dependency_binaries/install/downloader.rs +++ b/installer/src/dependency_binaries/install/downloader.rs @@ -1,18 +1,28 @@ //! Download support for repository-hosted dependency-binary archives. -use crate::artefact::download::HttpDownloader; - -use super::checksum::{ - CATEGORY_CHECKSUM, fetch_expected_checksum, map_ureq_error, verify_archive_checksum, +use std::{ + io::{self, Read, Write}, + path::Path, }; -use super::installer::DependencyBinaryInstallError; + use camino::{Utf8Path, Utf8PathBuf}; -use cap_std::ambient_authority; -use cap_std::fs_utf8::{Dir, File}; -use std::io::{self, Read, Write}; -use std::path::Path; +use cap_std::{ + ambient_authority, + fs_utf8::{Dir, File}, +}; use tracing::{debug, instrument, warn}; +use super::{ + checksum::{ + CATEGORY_CHECKSUM, + fetch_expected_checksum, + map_ureq_error, + verify_archive_checksum, + }, + installer::DependencyBinaryInstallError, +}; +use crate::artefact::download::HttpDownloader; + const DOWNLOAD_TIMEOUT_SECS: u64 = 30; /// Maximum archive size accepted, so a runaway response cannot fill the disk. @@ -194,9 +204,7 @@ impl DownloadDestination { } /// Reopen the written archive through the capability for verification. - fn open_archive(&self) -> io::Result { - self.dir.open(&self.archive_name) - } + fn open_archive(&self) -> io::Result { self.dir.open(&self.archive_name) } /// Remove a partial or unverified archive; a cleanup failure is only logged. fn remove_partial_archive(&self) { diff --git a/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs index 7b091cf5c..b99d0e0c0 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs @@ -2,8 +2,10 @@ use std::io; -use super::super::downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}; -use super::*; +use super::{ + super::downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}, + *, +}; #[test] fn download_from_urls_rejects_a_non_utf8_destination_before_any_request() { diff --git a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs index 493c38722..6affab33e 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs @@ -39,17 +39,11 @@ struct DownloadHarness { } impl DownloadHarness { - fn archive_url(&self) -> String { - self.server.url("/archive.tgz") - } + fn archive_url(&self) -> String { self.server.url("/archive.tgz") } - fn checksum_url(&self) -> String { - self.server.url("/archive.tgz.sha256") - } + fn checksum_url(&self) -> String { self.server.url("/archive.tgz.sha256") } - fn requested_paths(&self) -> Vec { - self.server.requested_paths() - } + fn requested_paths(&self) -> Vec { self.server.requested_paths() } /// Open the destination's parent directory as a capability, for asserting on /// the written archive. diff --git a/installer/src/dependency_binaries/install/downloader_tests.rs b/installer/src/dependency_binaries/install/downloader_tests.rs index e6c97caa0..cc2c6ab5d 100644 --- a/installer/src/dependency_binaries/install/downloader_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_tests.rs @@ -2,12 +2,14 @@ //! Tests for downloader error mapping and archive checksum verification. -use super::*; -use crate::hex::to_lower_hex; -use sha2::{Digest, Sha256}; use std::io::Write; + +use sha2::{Digest, Sha256}; use tempfile::TempDir; +use super::*; +use crate::hex::to_lower_hex; + // The under-cap success path is covered end to end by the local-server // boundary tests; this exercises the over-cap rejection they cannot. #[test] diff --git a/installer/src/dependency_binaries/install/extractor.rs b/installer/src/dependency_binaries/install/extractor.rs index 62036bf22..4eca82d0a 100644 --- a/installer/src/dependency_binaries/install/extractor.rs +++ b/installer/src/dependency_binaries/install/extractor.rs @@ -1,11 +1,15 @@ //! Archive extraction helpers for repository-hosted dependency binaries. -use super::installer::DependencyBinaryInstallError; -use std::fs::File; -use std::io::{self, Read, Write}; -use std::path::{Path, PathBuf}; +use std::{ + fs::File, + io::{self, Read, Write}, + path::{Path, PathBuf}, +}; + use tempfile::NamedTempFile; +use super::installer::DependencyBinaryInstallError; + /// Extracts a single executable from dependency archives. #[cfg_attr(test, mockall::automock)] pub trait DependencyArchiveExtractor { diff --git a/installer/src/dependency_binaries/install/http_test_server.rs b/installer/src/dependency_binaries/install/http_test_server.rs index fe6d665fd..62497a91c 100644 --- a/installer/src/dependency_binaries/install/http_test_server.rs +++ b/installer/src/dependency_binaries/install/http_test_server.rs @@ -5,14 +5,20 @@ //! Kept in its own module so the boundary-test file stays within its size //! budget. -use std::collections::HashMap; -use std::io; -use std::io::{BufRead, BufReader, Write}; -use std::net::{TcpListener, TcpStream}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, PoisonError}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; +use std::{ + collections::HashMap, + io, + io::{BufRead, BufReader, Write}, + net::{TcpListener, TcpStream}, + sync::{ + Arc, + Mutex, + PoisonError, + atomic::{AtomicBool, Ordering}, + }, + thread::{self, JoinHandle}, + time::Duration, +}; /// One canned HTTP/1.1 response body served for a matched path. `declared_len` /// is the advertised `Content-Length`, which normally matches `body`. @@ -73,9 +79,7 @@ impl LocalServer { }) } - pub(super) fn url(&self, path: &str) -> String { - format!("{}{path}", self.base_url) - } + pub(super) fn url(&self, path: &str) -> String { format!("{}{path}", self.base_url) } pub(super) fn requested_paths(&self) -> Vec { // A poisoned lock only means a test thread panicked while logging a diff --git a/installer/src/dependency_binaries/install/installer.rs b/installer/src/dependency_binaries/install/installer.rs index a2648365c..7c896d7b0 100644 --- a/installer/src/dependency_binaries/install/installer.rs +++ b/installer/src/dependency_binaries/install/installer.rs @@ -1,17 +1,20 @@ //! Installer orchestration for repository-hosted dependency binaries. -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; -use thiserror::Error; +use std::{ + fs, + io, + path::{Path, PathBuf}, +}; -use crate::artefact::target::TargetTriple; -use crate::dirs::BaseDirs; +use thiserror::Error; -use super::super::manifest::DependencyBinary; -use super::downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}; -use super::extractor::{DependencyArchiveExtractor, RepositoryArchiveExtractor}; -use super::metadata::{archive_filename, expected_member_path}; +use super::{ + super::manifest::DependencyBinary, + downloader::{DependencyArchiveDownloader, RepositoryArchiveDownloader}, + extractor::{DependencyArchiveExtractor, RepositoryArchiveExtractor}, + metadata::{archive_filename, expected_member_path}, +}; +use crate::{artefact::target::TargetTriple, dirs::BaseDirs}; /// Errors returned while installing repository-hosted dependency binaries. #[derive(Debug, Error)] @@ -80,9 +83,7 @@ pub enum DependencyBinaryInstallError { impl DependencyBinaryInstallError { /// Returns `true` when the failure is caused by a missing repository asset. #[must_use] - pub(crate) const fn is_not_found(&self) -> bool { - matches!(self, Self::NotFound { .. }) - } + pub(crate) const fn is_not_found(&self) -> bool { matches!(self, Self::NotFound { .. }) } } /// Installs dependency binaries from repository-hosted release assets. diff --git a/installer/src/dependency_binaries/install/metadata.rs b/installer/src/dependency_binaries/install/metadata.rs index 5f512b197..61a7853d7 100644 --- a/installer/src/dependency_binaries/install/metadata.rs +++ b/installer/src/dependency_binaries/install/metadata.rs @@ -1,8 +1,7 @@ //! Target and filename helpers for dependency-binary installation. -use crate::artefact::target::TargetTriple; - use super::super::manifest::DependencyBinary; +use crate::artefact::target::TargetTriple; const PROVENANCE_FILENAME: &str = "dependency-binaries-licences.md"; @@ -37,9 +36,7 @@ pub fn host_target() -> Option { /// Return the release-side provenance asset filename. #[must_use] -pub const fn provenance_filename() -> &'static str { - PROVENANCE_FILENAME -} +pub const fn provenance_filename() -> &'static str { PROVENANCE_FILENAME } /// Compute the platform-specific executable name for a dependency binary. #[must_use] diff --git a/installer/src/dependency_binaries/install/mod.rs b/installer/src/dependency_binaries/install/mod.rs index f884de72b..cd54d7ce5 100644 --- a/installer/src/dependency_binaries/install/mod.rs +++ b/installer/src/dependency_binaries/install/mod.rs @@ -22,6 +22,8 @@ pub use extractor::DependencyArchiveExtractor; #[cfg(test)] pub use installer::MockDependencyBinaryInstaller; pub use installer::{ - DependencyBinaryInstallError, DependencyBinaryInstaller, RepositoryDependencyBinaryInstaller, + DependencyBinaryInstallError, + DependencyBinaryInstaller, + RepositoryDependencyBinaryInstaller, }; pub use metadata::{archive_filename, binary_filename, host_target, provenance_filename}; diff --git a/installer/src/dependency_binaries/install/tests.rs b/installer/src/dependency_binaries/install/tests.rs index 1ea9e99d9..92b598248 100644 --- a/installer/src/dependency_binaries/install/tests.rs +++ b/installer/src/dependency_binaries/install/tests.rs @@ -1,16 +1,22 @@ //! Unit tests for repository-hosted dependency-binary installation helpers. -use super::downloader::MockDependencyArchiveDownloader; -use super::extractor::MockDependencyArchiveExtractor; -use super::installer::{InstallSupport, install_with}; -use super::metadata::expected_member_path; -use super::{archive_filename, *}; -use crate::dirs::MockBaseDirs; -use crate::installer_packaging::TargetTriple; +use std::{ + fs, + path::{Path, PathBuf}, +}; + use mockall::predicate::{always, eq}; use rstest::{fixture, rstest}; -use std::fs; -use std::path::{Path, PathBuf}; + +use super::{ + archive_filename, + downloader::MockDependencyArchiveDownloader, + extractor::MockDependencyArchiveExtractor, + installer::{InstallSupport, install_with}, + metadata::expected_member_path, + *, +}; +use crate::{dirs::MockBaseDirs, installer_packaging::TargetTriple}; /// Build a deterministic installation setup for success and missing-binary /// scenarios. diff --git a/installer/src/dependency_binaries/manifest.rs b/installer/src/dependency_binaries/manifest.rs index 4d422b3b2..f9c3a602f 100644 --- a/installer/src/dependency_binaries/manifest.rs +++ b/installer/src/dependency_binaries/manifest.rs @@ -3,8 +3,9 @@ //! The committed `installer/dependency-binaries.toml` file is the single source //! of truth for required dependency-tool versions, licences, and provenance. -use serde::Deserialize; use std::sync::OnceLock; + +use serde::Deserialize; use thiserror::Error; /// One repository-owned dependency binary requirement. @@ -13,12 +14,13 @@ use thiserror::Error; /// /// ``` /// use whitaker_installer::dependency_binaries::{ -/// parse_manifest, required_dependency_binaries, DependencyBinary +/// DependencyBinary, +/// parse_manifest, +/// required_dependency_binaries, /// }; /// /// // Parse the embedded manifest to obtain dependency binaries -/// let dependencies = required_dependency_binaries() -/// .expect("embedded manifest should be valid"); +/// let dependencies = required_dependency_binaries().expect("embedded manifest should be valid"); /// /// // Access fields on a dependency binary /// if let Some(tool) = dependencies.iter().find(|d| d.package() == "cargo-dylint") { @@ -45,9 +47,7 @@ impl DependencyBinary { /// /// See the [`DependencyBinary`] type documentation for a complete example. #[must_use] - pub fn package(&self) -> &str { - &self.package - } + pub fn package(&self) -> &str { &self.package } /// Return the executable basename without any platform suffix. /// @@ -55,9 +55,7 @@ impl DependencyBinary { /// /// See the [`DependencyBinary`] type documentation for a complete example. #[must_use] - pub fn binary(&self) -> &str { - &self.binary - } + pub fn binary(&self) -> &str { &self.binary } /// Return the required upstream version. /// @@ -65,9 +63,7 @@ impl DependencyBinary { /// /// See the [`DependencyBinary`] type documentation for a complete example. #[must_use] - pub fn version(&self) -> &str { - &self.version - } + pub fn version(&self) -> &str { &self.version } /// Return the upstream licence string recorded in the manifest. /// @@ -75,9 +71,7 @@ impl DependencyBinary { /// /// See the [`DependencyBinary`] type documentation for a complete example. #[must_use] - pub fn license(&self) -> &str { - &self.license - } + pub fn license(&self) -> &str { &self.license } /// Return the upstream repository URL. /// @@ -85,9 +79,7 @@ impl DependencyBinary { /// /// See the [`DependencyBinary`] type documentation for a complete example. #[must_use] - pub fn repository(&self) -> &str { - &self.repository - } + pub fn repository(&self) -> &str { &self.repository } } #[derive(Debug, Deserialize)] @@ -107,9 +99,7 @@ pub enum ManifestError { } impl From for ManifestError { - fn from(error: toml::de::Error) -> Self { - Self::ParseError(error.to_string()) - } + fn from(error: toml::de::Error) -> Self { Self::ParseError(error.to_string()) } } /// Return the embedded manifest contents. @@ -123,9 +113,7 @@ impl From for ManifestError { /// assert!(contents.contains("dependency_binaries")); /// ``` #[must_use] -pub const fn manifest_contents() -> &'static str { - include_str!("../../dependency-binaries.toml") -} +pub const fn manifest_contents() -> &'static str { include_str!("../../dependency-binaries.toml") } /// Parse manifest TOML into typed dependency entries. /// @@ -140,12 +128,10 @@ pub const fn manifest_contents() -> &'static str { /// Parse the embedded manifest: /// /// ``` -/// use whitaker_installer::dependency_binaries::{ -/// manifest_contents, parse_manifest -/// }; +/// use whitaker_installer::dependency_binaries::{manifest_contents, parse_manifest}; /// -/// let dependencies = parse_manifest(manifest_contents()) -/// .expect("embedded manifest should be valid"); +/// let dependencies = +/// parse_manifest(manifest_contents()).expect("embedded manifest should be valid"); /// /// assert!(!dependencies.is_empty()); /// ``` @@ -190,8 +176,8 @@ pub const fn manifest_contents() -> &'static str { /// repository = "https://github.com/trailofbits/dylint" /// "#; /// -/// let error = parse_manifest(manifest_with_duplicates) -/// .expect_err("should reject duplicate packages"); +/// let error = +/// parse_manifest(manifest_with_duplicates).expect_err("should reject duplicate packages"); /// assert!(error.to_string().contains("cargo-dylint")); /// ``` pub fn parse_manifest(contents: &str) -> Result, ManifestError> { @@ -221,8 +207,7 @@ pub fn parse_manifest(contents: &str) -> Result, ManifestE /// ``` /// use whitaker_installer::dependency_binaries::required_dependency_binaries; /// -/// let dependencies = required_dependency_binaries() -/// .expect("embedded manifest should be valid"); +/// let dependencies = required_dependency_binaries().expect("embedded manifest should be valid"); /// /// // Iterate over all required dependency binaries /// for tool in dependencies { @@ -264,8 +249,7 @@ pub fn required_dependency_binaries() -> Result<&'static [DependencyBinary], Man /// ``` /// use whitaker_installer::dependency_binaries::find_dependency_binary; /// -/// let result = find_dependency_binary("non-existent-package") -/// .expect("manifest should parse"); +/// let result = find_dependency_binary("non-existent-package").expect("manifest should parse"); /// /// assert!(result.is_none()); /// ``` @@ -295,9 +279,10 @@ pub fn find_dependency_binary( mod tests { //! Tests for the embedded dependency-binary manifest. - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[fixture] fn missing_field_manifest() -> &'static str { r#" diff --git a/installer/src/dependency_binaries/mod.rs b/installer/src/dependency_binaries/mod.rs index abae4ad42..6d322818e 100644 --- a/installer/src/dependency_binaries/mod.rs +++ b/installer/src/dependency_binaries/mod.rs @@ -10,11 +10,21 @@ mod manifest; #[cfg(test)] pub use install::MockDependencyBinaryInstaller; pub use install::{ - DependencyArchiveDownloader, DependencyArchiveExtractor, DependencyBinaryInstallError, - DependencyBinaryInstaller, RepositoryDependencyBinaryInstaller, archive_filename, - binary_filename, host_target, provenance_filename, + DependencyArchiveDownloader, + DependencyArchiveExtractor, + DependencyBinaryInstallError, + DependencyBinaryInstaller, + RepositoryDependencyBinaryInstaller, + archive_filename, + binary_filename, + host_target, + provenance_filename, }; pub use manifest::{ - DependencyBinary, ManifestError, find_dependency_binary, manifest_contents, parse_manifest, + DependencyBinary, + ManifestError, + find_dependency_binary, + manifest_contents, + parse_manifest, required_dependency_binaries, }; diff --git a/installer/src/dependency_packaging.rs b/installer/src/dependency_packaging.rs index 6944c59cc..ee6be6cee 100644 --- a/installer/src/dependency_packaging.rs +++ b/installer/src/dependency_packaging.rs @@ -4,12 +4,16 @@ //! `cargo-dylint` and `dylint-link` with deterministic names and inner //! directories for each supported target. +use std::{ + fs, + io, + path::{Path, PathBuf}, +}; + +use thiserror::Error; + use crate::dependency_binaries::{DependencyBinary, binary_filename, provenance_filename}; pub use crate::installer_packaging::{ArchiveFormat, TargetTriple}; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; -use thiserror::Error; /// Parameters for packaging a dependency binary archive. #[derive(Debug)] @@ -115,9 +119,8 @@ pub fn package_dependency_binary( pub fn render_provenance_markdown(dependencies: &[DependencyBinary]) -> String { let sections: String = dependencies.iter().map(render_dependency_section).collect(); format!( - "# Dependency binary licences and provenance\n\n\ - Whitaker publishes the following third-party dependency binaries \ - from repository releases.\n\n{sections}" + "# Dependency binary licences and provenance\n\nWhitaker publishes the following \ + third-party dependency binaries from repository releases.\n\n{sections}" ) } diff --git a/installer/src/dependency_packaging_tests.rs b/installer/src/dependency_packaging_tests.rs index 7965d2a2e..4cf0fe583 100644 --- a/installer/src/dependency_packaging_tests.rs +++ b/installer/src/dependency_packaging_tests.rs @@ -8,8 +8,13 @@ use crate::{ artefact::error::ArtefactError, dependency_binaries::find_dependency_binary, dependency_packaging::{ - ArchiveFormat, DependencyPackageParams, DependencyPackagingError, archive_format, - inner_dir_name, package_dependency_binary, render_provenance_markdown, + ArchiveFormat, + DependencyPackageParams, + DependencyPackagingError, + archive_format, + inner_dir_name, + package_dependency_binary, + render_provenance_markdown, write_provenance_markdown, }, installer_packaging::TargetTriple, @@ -30,9 +35,7 @@ fn linux_target() -> std::result::Result { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn temp_dir() -> std::io::Result { - tempfile::tempdir() -} +fn temp_dir() -> std::io::Result { tempfile::tempdir() } #[test] fn archive_format_matches_target_platform() { diff --git a/installer/src/deps/install.rs b/installer/src/deps/install.rs index 5aa5fcf3a..9b13f87a5 100644 --- a/installer/src/deps/install.rs +++ b/installer/src/deps/install.rs @@ -1,21 +1,30 @@ //! Install orchestration types and helpers for dependency tools. -use crate::dependency_binaries::{DependencyBinaryInstaller, find_dependency_binary}; -use crate::error::{InstallerError, Result}; -use crate::installer_packaging::TargetTriple; -use std::io::Write; -use std::process::Output; +use std::{io::Write, process::Output}; use super::{ - CARGO_DYLINT_TOOL, CommandExecutor, DEPENDENCY_TOOLS, DYLINT_LINK_TOOL, DependencyTool, - DylintToolStatus, is_binstall_available, is_tool_installed, + CARGO_DYLINT_TOOL, + CommandExecutor, + DEPENDENCY_TOOLS, + DYLINT_LINK_TOOL, + DependencyTool, + DylintToolStatus, + is_binstall_available, + is_tool_installed, +}; +use crate::{ + dependency_binaries::{DependencyBinaryInstaller, find_dependency_binary}, + error::{InstallerError, Result}, + installer_packaging::TargetTriple, }; #[path = "install_repository.rs"] mod repository; use repository::{ - RepositoryInstall, RepositoryInstallRequest, attempt_repository_install, + RepositoryInstall, + RepositoryInstallRequest, + attempt_repository_install, resolve_dependency_binary, }; diff --git a/installer/src/deps/install_repository.rs b/installer/src/deps/install_repository.rs index c64586ca8..960fa69bc 100644 --- a/installer/src/deps/install_repository.rs +++ b/installer/src/deps/install_repository.rs @@ -8,8 +8,16 @@ use std::io::Write; use super::{ - CommandExecutor, DYLINT_LINK_TOOL, DependencyTool, InstallContext, InstallerError, - RepositoryInstallContext, Result, find_dependency_binary, is_tool_installed, write_message, + CommandExecutor, + DYLINT_LINK_TOOL, + DependencyTool, + InstallContext, + InstallerError, + RepositoryInstallContext, + Result, + find_dependency_binary, + is_tool_installed, + write_message, }; use crate::dependency_binaries::DependencyBinary; diff --git a/installer/src/deps/install_tests.rs b/installer/src/deps/install_tests.rs index d6f9c7f44..5f2ac572f 100644 --- a/installer/src/deps/install_tests.rs +++ b/installer/src/deps/install_tests.rs @@ -4,7 +4,8 @@ use rstest::rstest; use super::*; use crate::test_utils::dependency_binary_helpers::{ - dylint_link_install_list_check, with_fake_binary_on_path, + dylint_link_install_list_check, + with_fake_binary_on_path, }; #[rstest] diff --git a/installer/src/deps/mod.rs b/installer/src/deps/mod.rs index 005e11923..8f5d80062 100644 --- a/installer/src/deps/mod.rs +++ b/installer/src/deps/mod.rs @@ -4,20 +4,31 @@ //! available, then installs any missing tools by preferring repository-hosted //! release archives before falling back to `cargo binstall` or `cargo install`. -use crate::dependency_binaries::{ - DependencyBinary, DependencyBinaryInstaller, RepositoryDependencyBinaryInstaller, - find_dependency_binary, host_target, +use std::{ + io, + io::Write, + path::Path, + process::{Command, Output}, +}; + +use crate::{ + dependency_binaries::{ + DependencyBinary, + DependencyBinaryInstaller, + RepositoryDependencyBinaryInstaller, + find_dependency_binary, + host_target, + }, + dirs::{BaseDirs, SystemBaseDirs}, + error::{InstallerError, Result}, }; -use crate::dirs::{BaseDirs, SystemBaseDirs}; -use crate::error::{InstallerError, Result}; -use std::io; -use std::io::Write; -use std::path::Path; -use std::process::{Command, Output}; mod install; use install::{ - InstallContext, cargo_fallback_mode, command_succeeds, install_missing_tools, + InstallContext, + cargo_fallback_mode, + command_succeeds, + install_missing_tools, repository_install_context, }; @@ -77,9 +88,7 @@ pub struct DylintToolStatus { impl DylintToolStatus { /// Returns `true` when both tools are installed. #[must_use] - pub const fn all_installed(&self) -> bool { - self.cargo_dylint && self.dylint_link - } + pub const fn all_installed(&self) -> bool { self.cargo_dylint && self.dylint_link } } /// Additional install options used by test-support hooks. @@ -274,9 +283,7 @@ fn is_binstall_available(executor: &dyn CommandExecutor) -> bool { } #[cfg(test)] -fn is_binary_on_path(binary_name: &str) -> bool { - find_binary_on_path(binary_name).is_some() -} +fn is_binary_on_path(binary_name: &str) -> bool { find_binary_on_path(binary_name).is_some() } fn find_binary_on_path(binary_name: &str) -> Option { let path_var = std::env::var_os("PATH")?; @@ -344,9 +351,7 @@ fn is_executable_file(path: &Path) -> bool { } #[cfg(not(unix))] -fn is_executable_file(path: &Path) -> bool { - path.is_file() -} +fn is_executable_file(path: &Path) -> bool { path.is_file() } #[cfg(test)] mod path_tests; diff --git a/installer/src/deps/path_tests.rs b/installer/src/deps/path_tests.rs index 92ec99cb1..6b5aa25d8 100644 --- a/installer/src/deps/path_tests.rs +++ b/installer/src/deps/path_tests.rs @@ -6,11 +6,17 @@ use super::*; use crate::{ test_support::env_test_guard, test_utils::{ - ExpectedCall, StubExecutor, + ExpectedCall, + StubExecutor, dependency_binary_helpers::{ - cargo_dylint_check, cargo_dylint_check_with_result, dylint_link_install_list_check, - dylint_link_install_list_check_with_version, with_fake_binary_on_path, with_fake_path, - write_fake_binary, write_fake_binary_with_status, + cargo_dylint_check, + cargo_dylint_check_with_result, + dylint_link_install_list_check, + dylint_link_install_list_check_with_version, + with_fake_binary_on_path, + with_fake_path, + write_fake_binary, + write_fake_binary_with_status, }, stdout_output, }, diff --git a/installer/src/deps/tests.rs b/installer/src/deps/tests.rs index 6a265d3ca..78f891ce0 100644 --- a/installer/src/deps/tests.rs +++ b/installer/src/deps/tests.rs @@ -1,15 +1,28 @@ //! Tests for Dylint tool dependency installation and fallback behaviour. +use std::path::PathBuf; + use super::*; -use crate::artefact::error::ArtefactError; -use crate::dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}; -use crate::installer_packaging::TargetTriple; -use crate::test_utils::dependency_binary_helpers::{ - binstall_install, binstall_version_check_with_result, cargo_dylint_check, - cargo_dylint_check_with_result, dylint_link_install_list_check, with_fake_binary_on_path, +use crate::{ + artefact::error::ArtefactError, + dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}, + installer_packaging::TargetTriple, + test_utils::{ + ExpectedCall, + StubDirs, + StubExecutor, + dependency_binary_helpers::{ + binstall_install, + binstall_version_check_with_result, + cargo_dylint_check, + cargo_dylint_check_with_result, + dylint_link_install_list_check, + with_fake_binary_on_path, + }, + failure_output, + success_output, + }, }; -use crate::test_utils::{ExpectedCall, StubDirs, StubExecutor, failure_output, success_output}; -use std::path::PathBuf; const OPTIONS_MSG: &str = "dependency install options should build"; diff --git a/installer/src/deps/tests_repository.rs b/installer/src/deps/tests_repository.rs index 6d3657217..1ffe88d5b 100644 --- a/installer/src/deps/tests_repository.rs +++ b/installer/src/deps/tests_repository.rs @@ -3,20 +3,29 @@ use std::path::PathBuf; use super::{ - DependencyBinaryInstaller, DependencyTool, DylintToolStatus, OPTIONS_MSG, + DependencyBinaryInstaller, + DependencyTool, + DylintToolStatus, + OPTIONS_MSG, install::{InstallContext, InstallMode, install_tool, repository_install_context}, - install_dylint_tools_with_options, install_options, + install_dylint_tools_with_options, + install_options, }; -use crate::dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}; -use crate::error::InstallerError; -use crate::installer_packaging::TargetTriple; -use crate::test_utils::{ - StubDirs, StubExecutor, - dependency_binary_helpers::{ - binstall_install, binstall_version_check_with_result, dylint_link_install_list_check, - with_fake_binary_on_path, +use crate::{ + dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}, + error::InstallerError, + installer_packaging::TargetTriple, + test_utils::{ + StubDirs, + StubExecutor, + dependency_binary_helpers::{ + binstall_install, + binstall_version_check_with_result, + dylint_link_install_list_check, + with_fake_binary_on_path, + }, + success_output, }, - success_output, }; /// Writes a fake `dylint-link` into a temporary directory and returns the diff --git a/installer/src/dirs.rs b/installer/src/dirs.rs index b63ae9194..79caf4bab 100644 --- a/installer/src/dirs.rs +++ b/installer/src/dirs.rs @@ -73,7 +73,8 @@ pub trait BaseDirs { /// use whitaker_installer::dirs::{BaseDirs, SystemBaseDirs}; /// /// let dirs = SystemBaseDirs::new().expect("failed to initialize directories"); -/// let data_dir = dirs.whitaker_data() +/// let data_dir = dirs +/// .whitaker_data() /// .expect("could not determine data directory"); /// println!("Whitaker data at: {}", data_dir.display()); /// ``` @@ -101,9 +102,7 @@ impl SystemBaseDirs { } impl BaseDirs for SystemBaseDirs { - fn home(&self) -> Option { - Some(self.user_dirs.home_dir().to_owned()) - } + fn home(&self) -> Option { Some(self.user_dirs.home_dir().to_owned()) } fn executables(&self) -> Option { #[cfg(unix)] @@ -113,9 +112,7 @@ impl BaseDirs for SystemBaseDirs { self.home().map(|h| h.join(".local").join("bin")) } - fn whitaker_data(&self) -> Option { - Some(self.project_dirs.data_dir().to_owned()) - } + fn whitaker_data(&self) -> Option { Some(self.project_dirs.data_dir().to_owned()) } } #[cfg(test)] diff --git a/installer/src/error.rs b/installer/src/error.rs index 122dda29c..c8b207a4d 100644 --- a/installer/src/error.rs +++ b/installer/src/error.rs @@ -4,10 +4,11 @@ //! to users when installation fails. Each error includes recovery hints where //! applicable. -use crate::crate_name::CrateName; use camino::Utf8PathBuf; use thiserror::Error; +use crate::crate_name::CrateName; + /// Errors that can occur during the installation process. #[derive(Debug, Error)] pub enum InstallerError { diff --git a/installer/src/git.rs b/installer/src/git.rs index 595810032..5dd8a9606 100644 --- a/installer/src/git.rs +++ b/installer/src/git.rs @@ -4,15 +4,21 @@ //! including initial cloning and subsequent updates. Operations have a //! configurable timeout to prevent hangs on network issues. -use crate::error::{InstallerError, Result}; -use crate::workspace::WHITAKER_REPO_URL; +use std::{ + io::Read, + process::{Child, Command, Output, Stdio}, + thread::JoinHandle, + time::Duration, +}; + use camino::Utf8Path; -use std::io::Read; -use std::process::{Child, Command, Output, Stdio}; -use std::thread::JoinHandle; -use std::time::Duration; use wait_timeout::ChildExt; +use crate::{ + error::{InstallerError, Result}, + workspace::WHITAKER_REPO_URL, +}; + /// Default timeout for git operations (5 minutes). const GIT_TIMEOUT: Duration = Duration::from_mins(5); diff --git a/installer/src/install_flow/mod.rs b/installer/src/install_flow/mod.rs index 1404a1594..595346dff 100644 --- a/installer/src/install_flow/mod.rs +++ b/installer/src/install_flow/mod.rs @@ -3,28 +3,24 @@ //! This module keeps prebuilt-download fallback and metrics recording logic //! separate from CLI orchestration in `main.rs`. -use camino::Utf8Path; -use camino::Utf8PathBuf; -use std::collections::HashSet; -use std::fs; -use std::io; -use std::io::Write; -use std::time::Duration; -use whitaker_installer::builder::{library_extension, library_prefix}; -use whitaker_installer::cli::InstallArgs; -use whitaker_installer::crate_name::CrateName; -use whitaker_installer::deps::{ - CommandExecutor, check_dylint_tools, install_dylint_tools_with_output, -}; +use std::{collections::HashSet, fs, io, io::Write, time::Duration}; + +use camino::{Utf8Path, Utf8PathBuf}; #[cfg(test)] use whitaker_installer::deps::{DependencyInstallOptions, install_dylint_tools_with_options}; -use whitaker_installer::dirs::BaseDirs; -use whitaker_installer::error::{InstallerError, Result}; -use whitaker_installer::install_metrics::{InstallMode, RecordOutcome, record_install}; -use whitaker_installer::output::write_stderr_line; -use whitaker_installer::prebuilt::{PrebuiltConfig, PrebuiltResult, attempt_prebuilt}; -use whitaker_installer::prebuilt_path::prebuilt_library_dir; -use whitaker_installer::resolution::{EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE}; +use whitaker_installer::{ + builder::{library_extension, library_prefix}, + cli::InstallArgs, + crate_name::CrateName, + deps::{CommandExecutor, check_dylint_tools, install_dylint_tools_with_output}, + dirs::BaseDirs, + error::{InstallerError, Result}, + install_metrics::{InstallMode, RecordOutcome, record_install}, + output::write_stderr_line, + prebuilt::{PrebuiltConfig, PrebuiltResult, attempt_prebuilt}, + prebuilt_path::prebuilt_library_dir, + resolution::{EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE}, +}; pub(crate) fn ensure_dylint_tools_core( quiet: bool, diff --git a/installer/src/install_flow/tests.rs b/installer/src/install_flow/tests.rs index 64eebea23..0b3c2b67b 100644 --- a/installer/src/install_flow/tests.rs +++ b/installer/src/install_flow/tests.rs @@ -1,10 +1,14 @@ //! Unit tests for install-flow prebuilt staging and fallback behaviour. -use super::*; +use std::{ + path::PathBuf, + sync::atomic::{AtomicBool, Ordering}, +}; + use camino::Utf8PathBuf; use rstest::{fixture, rstest}; -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; + +use super::*; struct StagingFixture { _temp_dir: tempfile::TempDir, @@ -17,15 +21,9 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home(&self) -> Option { - None - } - fn executables(&self) -> Option { - None - } - fn whitaker_data(&self) -> Option { - self.data_dir.clone() - } + fn home(&self) -> Option { None } + fn executables(&self) -> Option { None } + fn whitaker_data(&self) -> Option { self.data_dir.clone() } } static PRUNE_HOOK_CALLED: AtomicBool = AtomicBool::new(false); diff --git a/installer/src/install_metrics.rs b/installer/src/install_metrics.rs index a10f0a8d3..9d1be4146 100644 --- a/installer/src/install_metrics.rs +++ b/installer/src/install_metrics.rs @@ -4,13 +4,17 @@ //! Metrics are stored in Whitaker's data directory at: //! `/metrics/install_metrics.json`. -use crate::dirs::BaseDirs; +use std::{ + fs::{File, OpenOptions}, + io::{Read, Seek, SeekFrom, Write}, + path::{Path, PathBuf}, + time::Duration, +}; + use fs2::FileExt; use serde::{Deserialize, Serialize}; -use std::fs::{File, OpenOptions}; -use std::io::{Read, Seek, SeekFrom, Write}; -use std::path::{Path, PathBuf}; -use std::time::Duration; + +use crate::dirs::BaseDirs; const METRICS_DIRNAME: &str = "metrics"; const METRICS_FILENAME: &str = "install_metrics.json"; @@ -42,6 +46,7 @@ impl InstallMetrics { /// /// ``` /// use std::time::Duration; + /// /// use whitaker_installer::install_metrics::{InstallMetrics, InstallMode}; /// /// let mut metrics = InstallMetrics::default(); @@ -49,9 +54,7 @@ impl InstallMetrics { /// assert_eq!(metrics.total_installs(), 1); /// ``` #[must_use] - pub const fn total_installs(&self) -> u64 { - self.total_installs - } + pub const fn total_installs(&self) -> u64 { self.total_installs } /// Returns the number of successful prebuilt-download installs. /// @@ -59,6 +62,7 @@ impl InstallMetrics { /// /// ``` /// use std::time::Duration; + /// /// use whitaker_installer::install_metrics::{InstallMetrics, InstallMode}; /// /// let mut metrics = InstallMetrics::default(); @@ -66,9 +70,7 @@ impl InstallMetrics { /// assert_eq!(metrics.download_installs(), 1); /// ``` #[must_use] - pub const fn download_installs(&self) -> u64 { - self.download_installs - } + pub const fn download_installs(&self) -> u64 { self.download_installs } /// Returns the number of successful local-build installs. /// @@ -76,6 +78,7 @@ impl InstallMetrics { /// /// ``` /// use std::time::Duration; + /// /// use whitaker_installer::install_metrics::{InstallMetrics, InstallMode}; /// /// let mut metrics = InstallMetrics::default(); @@ -83,9 +86,7 @@ impl InstallMetrics { /// assert_eq!(metrics.build_installs(), 1); /// ``` #[must_use] - pub const fn build_installs(&self) -> u64 { - self.build_installs - } + pub const fn build_installs(&self) -> u64 { self.build_installs } /// Returns total cumulative install duration. /// @@ -93,6 +94,7 @@ impl InstallMetrics { /// /// ``` /// use std::time::Duration; + /// /// use whitaker_installer::install_metrics::InstallMetrics; /// /// assert_eq!( @@ -139,6 +141,7 @@ impl InstallMetrics { /// /// ``` /// use std::time::Duration; + /// /// use whitaker_installer::install_metrics::{InstallMetrics, InstallMode}; /// /// let mut metrics = InstallMetrics::default(); @@ -169,6 +172,7 @@ impl InstallMetrics { /// /// ``` /// use std::time::Duration; + /// /// use whitaker_installer::install_metrics::{InstallMetrics, InstallMode}; /// /// let mut metrics = InstallMetrics::default(); @@ -206,15 +210,11 @@ pub struct RecordOutcome { impl RecordOutcome { /// Returns the updated aggregate metrics. #[must_use] - pub const fn metrics(&self) -> &InstallMetrics { - &self.metrics - } + pub const fn metrics(&self) -> &InstallMetrics { &self.metrics } /// Returns true when a malformed metrics file was reset to defaults. #[must_use] - pub const fn recovered_from_corrupt_file(&self) -> bool { - self.recovered_from_corrupt_file - } + pub const fn recovered_from_corrupt_file(&self) -> bool { self.recovered_from_corrupt_file } } /// Records one successful install in Whitaker's metrics store. @@ -380,10 +380,6 @@ fn format_duration(duration: Duration) -> String { format!("{seconds}.{millis:03}s") } -const fn should_format_with_hours(hours: u64) -> bool { - hours > 0 -} +const fn should_format_with_hours(hours: u64) -> bool { hours > 0 } -const fn should_format_with_minutes(minutes: u64) -> bool { - minutes > 0 -} +const fn should_format_with_minutes(minutes: u64) -> bool { minutes > 0 } diff --git a/installer/src/install_metrics_tests.rs b/installer/src/install_metrics_tests.rs index 3dbce5d5e..484e1c7d7 100644 --- a/installer/src/install_metrics_tests.rs +++ b/installer/src/install_metrics_tests.rs @@ -5,7 +5,10 @@ use std::{io::ErrorKind, path::PathBuf, time::Duration}; use rstest::{fixture, rstest}; use crate::install_metrics::{ - InstallMetrics, InstallMetricsError, InstallMode, record_install_at_path, + InstallMetrics, + InstallMetricsError, + InstallMode, + record_install_at_path, }; struct MetricsPathFixture { diff --git a/installer/src/installer_packaging.rs b/installer/src/installer_packaging.rs index ed2a3dfcf..2f6253ca0 100644 --- a/installer/src/installer_packaging.rs +++ b/installer/src/installer_packaging.rs @@ -16,15 +16,17 @@ //! //! Windows archives use `.zip` format and the `.exe` suffix. -pub use crate::artefact::target::TargetTriple; -pub use crate::version::Version; +use std::{ + fs, + io, + path::{Path, PathBuf}, +}; -use crate::binstall_metadata::{DEFAULT_PKG_FMT, WINDOWS_PKG_FMT}; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; use thiserror::Error; +use crate::binstall_metadata::{DEFAULT_PKG_FMT, WINDOWS_PKG_FMT}; +pub use crate::{artefact::target::TargetTriple, version::Version}; + /// The crate name used in archive and directory names. const CRATE_NAME: &str = "whitaker-installer"; @@ -82,12 +84,15 @@ pub enum InstallerPackagingError { /// # Examples /// /// ``` -/// use whitaker_installer::installer_packaging::{archive_filename, Version, TargetTriple}; +/// use whitaker_installer::installer_packaging::{TargetTriple, Version, archive_filename}; /// /// let v = Version::new("0.2.1"); /// let t = TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"); /// let name = archive_filename(&v, &t); -/// assert_eq!(name, "whitaker-installer-x86_64-unknown-linux-gnu-v0.2.1.tgz"); +/// assert_eq!( +/// name, +/// "whitaker-installer-x86_64-unknown-linux-gnu-v0.2.1.tgz" +/// ); /// ``` #[must_use] pub fn archive_filename(version: &Version, target: &TargetTriple) -> String { @@ -111,7 +116,7 @@ pub fn archive_filename(version: &Version, target: &TargetTriple) -> String { /// # Examples /// /// ``` -/// use whitaker_installer::installer_packaging::{inner_dir_name, Version, TargetTriple}; +/// use whitaker_installer::installer_packaging::{TargetTriple, Version, inner_dir_name}; /// /// let v = Version::new("0.2.1"); /// let t = TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid target"); @@ -131,7 +136,7 @@ pub fn inner_dir_name(version: &Version, target: &TargetTriple) -> String { /// # Examples /// /// ``` -/// use whitaker_installer::installer_packaging::{binary_filename, TargetTriple}; +/// use whitaker_installer::installer_packaging::{TargetTriple, binary_filename}; /// /// assert_eq!( /// binary_filename(&TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid")), @@ -159,7 +164,7 @@ pub fn binary_filename(target: &TargetTriple) -> String { /// # Examples /// /// ``` -/// use whitaker_installer::installer_packaging::{archive_format, ArchiveFormat, TargetTriple}; +/// use whitaker_installer::installer_packaging::{ArchiveFormat, TargetTriple, archive_format}; /// /// assert_eq!( /// archive_format(&TargetTriple::try_from("x86_64-unknown-linux-gnu").expect("valid")), diff --git a/installer/src/lib.rs b/installer/src/lib.rs index 94e14c535..2e0c1afce 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -7,8 +7,7 @@ //! # Modules //! //! - [`artefact`] - Artefact naming, manifest schema, and verification policy -//! - [`binstall_metadata`] - Cargo-binstall metadata constants and template -//! expansion +//! - [`binstall_metadata`] - Cargo-binstall metadata constants and template expansion //! - [`builder`] - Cargo build orchestration for lint crates //! - [`cli`] - Command-line argument definitions //! - [`crate_name`] - Semantic wrapper for lint crate names @@ -17,8 +16,7 @@ //! - [`error`] - Semantic error types with recovery hints //! - [`git`] - Repository cloning and updating //! - [`install_metrics`] - Local installer metrics persistence and summaries -//! - [`installer_packaging`] - Installer binary archive packaging for release -//! distribution +//! - [`installer_packaging`] - Installer binary archive packaging for release distribution //! - [`list`] - List command implementation //! - [`list_output`] - Output formatting for lint listing //! - [`output`] - Shell snippet generation for environment configuration @@ -28,8 +26,8 @@ //! - [`resolution`] - Crate resolution and validation //! - [`scanner`] - Lint scanner for discovering installed libraries //! - [`stager`] - File staging with platform-specific naming conventions -//! - [`test_support`] - Hidden test-only hooks shared by installer behavioural -//! and integration tests +//! - [`test_support`] - Hidden test-only hooks shared by installer behavioural and integration +//! tests //! - [`toolchain`] - Rust toolchain detection and validation //! - [`version`] - Semantic crate version wrapper //! - [`workspace`] - Workspace detection and path resolution diff --git a/installer/src/list.rs b/installer/src/list.rs index d4b14b046..c148ab8fd 100644 --- a/installer/src/list.rs +++ b/installer/src/list.rs @@ -3,17 +3,20 @@ //! This module provides the `run_list` command handler and supporting functions //! for querying and displaying installed lint libraries. +use std::io::Write; + use camino::{Utf8Path, Utf8PathBuf}; use log::trace; -use std::io::Write; -use crate::cli::ListArgs; -use crate::dirs::{BaseDirs, SystemBaseDirs}; -use crate::error::{InstallerError, Result}; -use crate::list_output::{format_human, format_json}; -use crate::scanner::{InstalledLints, scan_installed}; -use crate::stager::default_target_dir; -use crate::toolchain::Toolchain; +use crate::{ + cli::ListArgs, + dirs::{BaseDirs, SystemBaseDirs}, + error::{InstallerError, Result}, + list_output::{format_human, format_json}, + scanner::{InstalledLints, scan_installed}, + stager::default_target_dir, + toolchain::Toolchain, +}; /// Lists installed lint libraries and their associated lints. /// diff --git a/installer/src/list_output.rs b/installer/src/list_output.rs index 752220524..2e244a848 100644 --- a/installer/src/list_output.rs +++ b/installer/src/list_output.rs @@ -12,8 +12,7 @@ use crate::scanner::{InstalledLibrary, InstalledLints, lints_for_library}; /// # Examples /// /// ``` -/// use whitaker_installer::list_output::format_human; -/// use whitaker_installer::scanner::InstalledLints; +/// use whitaker_installer::{list_output::format_human, scanner::InstalledLints}; /// /// let lints = InstalledLints::default(); /// let output = format_human(&lints, None); @@ -75,8 +74,7 @@ fn format_library_lines(library: &InstalledLibrary) -> String { /// # Examples /// /// ``` -/// use whitaker_installer::list_output::format_json; -/// use whitaker_installer::scanner::InstalledLints; +/// use whitaker_installer::{list_output::format_json, scanner::InstalledLints}; /// /// let lints = InstalledLints::default(); /// let json = format_json(&lints, None); @@ -153,12 +151,13 @@ pub struct LibraryEntry { mod tests { //! Tests for human-readable and JSON list formatting. - use super::*; - use crate::builder::CrateName; - use crate::scanner::InstalledLibrary; - use camino::Utf8PathBuf; use std::collections::BTreeMap; + use camino::Utf8PathBuf; + + use super::*; + use crate::{builder::CrateName, scanner::InstalledLibrary}; + fn sample_lints() -> InstalledLints { let mut by_toolchain = BTreeMap::new(); by_toolchain.insert( diff --git a/installer/src/list_tests.rs b/installer/src/list_tests.rs index de6a1bc41..d02ad12fd 100644 --- a/installer/src/list_tests.rs +++ b/installer/src/list_tests.rs @@ -1,10 +1,12 @@ //! Tests for the `list` command handler and its path resolution helpers. -use super::*; -use rstest::{fixture, rstest}; use std::fs; + +use rstest::{fixture, rstest}; use tempfile::TempDir; +use super::*; + // ------------------------------------------------------------------------- // Fixtures // ------------------------------------------------------------------------- diff --git a/installer/src/main.rs b/installer/src/main.rs index a95778398..c9688ed60 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -7,31 +7,36 @@ mod install_flow; mod staged_suite; +use std::{io::Write, time::Instant}; + +use camino::{Utf8Path, Utf8PathBuf}; +use clap::Parser; +use whitaker_installer::{ + cli::{Cli, Command, InstallArgs}, + crate_name::CrateName, + deps::SystemCommandExecutor, + dirs::{BaseDirs, SystemBaseDirs}, + error::{InstallerError, Result}, + install_metrics::InstallMode, + list::{determine_target_dir, run_list}, + output::{DryRunInfo, DryRunSkips, ShellSnippet, write_stderr_line}, + pipeline::{PipelineContext, perform_build, stage_libraries}, + prebuilt_path::prebuilt_library_dir, + resolution::{CrateResolutionOptions, resolve_crates, validate_crate_names}, + toolchain::Toolchain, + wrapper::{generate_wrapper_scripts, path_instructions}, +}; + #[cfg(test)] use crate::install_flow::ensure_dylint_tools_with_options; use crate::install_flow::{ - MetricsWriteContext, PrebuiltInstallationContext, detect_host_target, - ensure_dylint_tools_with_executor, try_prebuilt_installation, write_install_metrics, -}; -use camino::{Utf8Path, Utf8PathBuf}; -use clap::Parser; -use std::io::Write; -use std::time::Instant; -use whitaker_installer::cli::{Cli, Command, InstallArgs}; -use whitaker_installer::crate_name::CrateName; -use whitaker_installer::deps::SystemCommandExecutor; -use whitaker_installer::dirs::{BaseDirs, SystemBaseDirs}; -use whitaker_installer::error::{InstallerError, Result}; -use whitaker_installer::install_metrics::InstallMode; -use whitaker_installer::list::{determine_target_dir, run_list}; -use whitaker_installer::output::{DryRunInfo, DryRunSkips, ShellSnippet, write_stderr_line}; -use whitaker_installer::pipeline::{PipelineContext, perform_build, stage_libraries}; -use whitaker_installer::prebuilt_path::prebuilt_library_dir; -use whitaker_installer::resolution::{ - CrateResolutionOptions, resolve_crates, validate_crate_names, + MetricsWriteContext, + PrebuiltInstallationContext, + detect_host_target, + ensure_dylint_tools_with_executor, + try_prebuilt_installation, + write_install_metrics, }; -use whitaker_installer::toolchain::Toolchain; -use whitaker_installer::wrapper::{generate_wrapper_scripts, path_instructions}; fn main() { let cli = Cli::parse(); @@ -221,7 +226,10 @@ fn ensure_whitaker_workspace( stderr: &mut dyn Write, ) -> Result { use whitaker_installer::workspace::{ - WorkspaceAction, clone_directory, decide_workspace_action, ensure_workspace, + WorkspaceAction, + clone_directory, + decide_workspace_action, + ensure_workspace, }; if !args.quiet diff --git a/installer/src/output.rs b/installer/src/output.rs index 3dac5eb43..c14172ffb 100644 --- a/installer/src/output.rs +++ b/installer/src/output.rs @@ -4,10 +4,12 @@ //! that users can add to their shell profile to enable Dylint library discovery, //! as well as dry-run information formatting. -use crate::crate_name::CrateName; -use camino::Utf8Path; use std::io::Write; +use camino::Utf8Path; + +use crate::crate_name::CrateName; + /// Write a line to stderr, ignoring write failures. /// /// This is a best-effort logging helper that silently ignores write errors. @@ -39,9 +41,7 @@ impl ShellSnippet { /// use camino::Utf8PathBuf; /// use whitaker_installer::output::ShellSnippet; /// - /// let path = Utf8PathBuf::from( - /// "/home/user/.local/share/dylint/lib/nightly-2025-01-15/release" - /// ); + /// let path = Utf8PathBuf::from("/home/user/.local/share/dylint/lib/nightly-2025-01-15/release"); /// let snippet = ShellSnippet::new(&path); /// /// assert!(snippet.bash.contains("DYLINT_LIBRARY_PATH")); @@ -86,8 +86,7 @@ pub fn success_message(count: usize, target_dir: &Utf8Path) -> String { /// /// ``` /// use camino::Utf8PathBuf; -/// use whitaker_installer::crate_name::CrateName; -/// use whitaker_installer::output::DryRunInfo; +/// use whitaker_installer::{crate_name::CrateName, output::DryRunInfo}; /// /// let workspace = Utf8PathBuf::from("/home/user/whitaker"); /// let target = Utf8PathBuf::from("/home/user/.local/share/dylint/lib"); @@ -178,10 +177,11 @@ impl DryRunInfo<'_> { mod tests { //! Tests for installer output rendering. - use super::*; use camino::Utf8PathBuf; use rstest::{fixture, rstest}; + use super::*; + /// Shared fixture providing a test library path. #[fixture] fn test_path() -> Utf8PathBuf { @@ -191,9 +191,7 @@ mod tests { /// Shared fixture providing a shell snippet for the test path. #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] - fn test_snippet(test_path: Utf8PathBuf) -> ShellSnippet { - ShellSnippet::new(&test_path) - } + fn test_snippet(test_path: Utf8PathBuf) -> ShellSnippet { ShellSnippet::new(&test_path) } #[rstest] fn snippet_contains_path(test_snippet: ShellSnippet, test_path: Utf8PathBuf) { diff --git a/installer/src/pipeline.rs b/installer/src/pipeline.rs index 148ee8ca4..997b1f9ea 100644 --- a/installer/src/pipeline.rs +++ b/installer/src/pipeline.rs @@ -4,16 +4,20 @@ //! staging the resulting libraries. It coordinates between the builder, stager, //! and output modules to provide a complete build pipeline. -use crate::builder::{BuildConfig, BuildResult, Builder, CrateBuilder}; -use crate::crate_name::CrateName; -use crate::error::Result; -use crate::output::{success_message, write_stderr_line}; -use crate::scanner::lints_for_library_with_experimental; -use crate::stager::Stager; -use crate::toolchain::Toolchain; -use camino::{Utf8Path, Utf8PathBuf}; use std::io::Write; +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::{ + builder::{BuildConfig, BuildResult, Builder, CrateBuilder}, + crate_name::CrateName, + error::Result, + output::{success_message, write_stderr_line}, + scanner::lints_for_library_with_experimental, + stager::Stager, + toolchain::Toolchain, +}; + /// Creates a [`BuildConfig`] from the pipeline context. /// /// This extracts the build configuration parameters from the pipeline context @@ -24,9 +28,11 @@ use std::io::Write; /// # Example /// /// ``` -/// use whitaker_installer::pipeline::{build_config_from_context, PipelineContext}; -/// use whitaker_installer::toolchain::Toolchain; /// use camino::{Utf8Path, Utf8PathBuf}; +/// use whitaker_installer::{ +/// pipeline::{PipelineContext, build_config_from_context}, +/// toolchain::Toolchain, +/// }; /// /// let workspace = Utf8PathBuf::from("/workspace"); /// let target = Utf8PathBuf::from("/staging"); @@ -69,9 +75,8 @@ pub fn build_config_from_context(context: &PipelineContext<'_>) -> BuildConfig { /// # Example /// /// ``` -/// use whitaker_installer::pipeline::PipelineContext; -/// use whitaker_installer::toolchain::Toolchain; /// use camino::Utf8PathBuf; +/// use whitaker_installer::{pipeline::PipelineContext, toolchain::Toolchain}; /// /// let workspace = Utf8PathBuf::from("/workspace"); /// let target = Utf8PathBuf::from("/staging"); diff --git a/installer/src/pipeline_staging_tests.rs b/installer/src/pipeline_staging_tests.rs index 695576886..87d9c488e 100644 --- a/installer/src/pipeline_staging_tests.rs +++ b/installer/src/pipeline_staging_tests.rs @@ -5,7 +5,10 @@ use rstest::{fixture, rstest}; use tempfile::TempDir; use crate::{ - builder::BuildResult, crate_name::CrateName, pipeline::stage_libraries, toolchain::Toolchain, + builder::BuildResult, + crate_name::CrateName, + pipeline::stage_libraries, + toolchain::Toolchain, }; /// Fixture providing a temporary directory for staging tests. @@ -45,9 +48,7 @@ impl StagingTestContext { }) } - fn target_dir(&self) -> &Utf8Path { - &self.target_dir - } + fn target_dir(&self) -> &Utf8Path { &self.target_dir } fn with_quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; @@ -91,9 +92,7 @@ fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> std::io::Resu #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn staging_ctx() -> std::io::Result { - StagingTestContext::new() -} +fn staging_ctx() -> std::io::Result { StagingTestContext::new() } /// Asserts that staging output lists the stable `bumpy_road_function` lint. /// diff --git a/installer/src/pipeline_tests.rs b/installer/src/pipeline_tests.rs index 724d58575..75f1a1000 100644 --- a/installer/src/pipeline_tests.rs +++ b/installer/src/pipeline_tests.rs @@ -93,9 +93,7 @@ impl PipelineContextProvider for TestContext { /// pipeline unit tests. #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn test_ctx() -> TestContext { - TestContext::new() -} +fn test_ctx() -> TestContext { TestContext::new() } // ------------------------------------------------------------------------- // build_config_from_context tests diff --git a/installer/src/prebuilt.rs b/installer/src/prebuilt.rs index 3e4fab244..fc29777c8 100644 --- a/installer/src/prebuilt.rs +++ b/installer/src/prebuilt.rs @@ -7,20 +7,24 @@ //! directory. On any failure the caller receives [`PrebuiltResult::Fallback`] //! and should proceed with local compilation. +use std::{io::Write, path::Path}; + use camino::{Utf8Path, Utf8PathBuf}; -use std::io::Write; -use std::path::Path; - -use crate::artefact::download::{ArtefactDownloader, DownloadError, HttpDownloader}; -use crate::artefact::extraction::{ArtefactExtractor, ZstdExtractor}; -use crate::artefact::manifest::Manifest; -use crate::artefact::manifest_parser::{ManifestParseError, parse_manifest}; -use crate::artefact::naming::ArtefactName; -use crate::artefact::packaging::compute_sha256; -use crate::artefact::packaging_error::PackagingError; -use crate::artefact::verification::VerificationPolicy; -use crate::builder::{library_extension, library_prefix}; -use crate::output::write_stderr_line; + +use crate::{ + artefact::{ + download::{ArtefactDownloader, DownloadError, HttpDownloader}, + extraction::{ArtefactExtractor, ZstdExtractor}, + manifest::Manifest, + manifest_parser::{ManifestParseError, parse_manifest}, + naming::ArtefactName, + packaging::compute_sha256, + packaging_error::PackagingError, + verification::VerificationPolicy, + }, + builder::{library_extension, library_prefix}, + output::write_stderr_line, +}; /// The outcome of a prebuilt download attempt. /// diff --git a/installer/src/prebuilt_path.rs b/installer/src/prebuilt_path.rs index fbe7990d6..bcf10bf0c 100644 --- a/installer/src/prebuilt_path.rs +++ b/installer/src/prebuilt_path.rs @@ -7,8 +7,10 @@ use camino::Utf8PathBuf; -use crate::dirs::BaseDirs; -use crate::error::{InstallerError, Result}; +use crate::{ + dirs::BaseDirs, + error::{InstallerError, Result}, +}; /// Build the canonical prebuilt library destination directory. /// @@ -47,10 +49,12 @@ pub fn prebuilt_library_dir( mod tests { //! Tests for prebuilt artefact path resolution. + use std::path::PathBuf; + + use rstest::rstest; + use super::*; use crate::dirs::MockBaseDirs; - use rstest::rstest; - use std::path::PathBuf; #[test] fn prebuilt_library_dir_builds_expected_path() { @@ -90,8 +94,7 @@ mod tests { #[cfg(unix)] #[test] fn prebuilt_library_dir_rejects_non_utf8_data_dir() { - use std::ffi::OsString; - use std::os::unix::ffi::OsStringExt; + use std::{ffi::OsString, os::unix::ffi::OsStringExt}; let mut dirs = MockBaseDirs::new(); dirs.expect_whitaker_data().return_once(|| { diff --git a/installer/src/resolution.rs b/installer/src/resolution.rs index dd7970ab0..0116cff18 100644 --- a/installer/src/resolution.rs +++ b/installer/src/resolution.rs @@ -3,10 +3,13 @@ //! This module determines which lint crates to build based on CLI options and //! validates that requested crate names are known. -use crate::crate_name::CrateName; -use crate::error::{InstallerError, Result}; use log::debug; +use crate::{ + crate_name::CrateName, + error::{InstallerError, Result}, +}; + /// Static list of lint crates available for building. /// /// This list includes all individual lint crates. The aggregated suite is @@ -48,13 +51,12 @@ pub struct CrateResolutionOptions { /// cherry-pick particular lints. /// /// The `experimental` flag has different effects depending on the mode: -/// - In `individual_lints` mode, experimental lint crates from -/// `EXPERIMENTAL_LINT_CRATES` are included in the returned crate list. -/// - In suite mode (default), the `experimental` flag is used by `BuildConfig` -/// to enable experimental features when building the suite crate. -/// - When `specific_lints` are provided, the returned list is exactly the -/// requested crate list after validation. Experimental crates still require -/// the `experimental` flag during validation. +/// - In `individual_lints` mode, experimental lint crates from `EXPERIMENTAL_LINT_CRATES` are +/// included in the returned crate list. +/// - In suite mode (default), the `experimental` flag is used by `BuildConfig` to enable +/// experimental features when building the suite crate. +/// - When `specific_lints` are provided, the returned list is exactly the requested crate list +/// after validation. Experimental crates still require the `experimental` flag during validation. /// /// Note: This function assumes that `specific_lints` have been validated via /// `validate_crate_names()` prior to calling. Callers must validate inputs @@ -148,9 +150,10 @@ pub fn validate_crate_names(names: &[CrateName], options: &CrateResolutionOption mod tests { //! Tests for release resolution. - use super::*; use rstest::rstest; + use super::*; + /// Test configuration for `resolve_crates` variants. struct ResolveCratesCase { /// Resolution flags under test. diff --git a/installer/src/scanner.rs b/installer/src/scanner.rs index f62894362..c49f3da07 100644 --- a/installer/src/scanner.rs +++ b/installer/src/scanner.rs @@ -3,14 +3,15 @@ //! This module provides utilities to scan the staging directory and parse //! library filenames to extract lint metadata. -use std::collections::BTreeMap; -use std::io; +use std::{collections::BTreeMap, io}; use camino::{Utf8Path, Utf8PathBuf}; -use crate::builder::{library_extension, library_prefix}; -use crate::crate_name::CrateName; -use crate::resolution::{EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE}; +use crate::{ + builder::{library_extension, library_prefix}, + crate_name::CrateName, + resolution::{EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE}, +}; /// Metadata about an installed lint library. #[derive(Debug, Clone, PartialEq, Eq)] @@ -33,9 +34,7 @@ pub struct InstalledLints { impl InstalledLints { /// Returns true if no lints are installed. #[must_use] - pub fn is_empty(&self) -> bool { - self.by_toolchain.is_empty() - } + pub fn is_empty(&self) -> bool { self.by_toolchain.is_empty() } } /// Scan the staging directory for installed libraries. @@ -183,8 +182,7 @@ pub fn parse_library_filename(filename: &str) -> Option<(CrateName, String)> { /// # Examples /// /// ``` -/// use whitaker_installer::scanner::lints_for_library; -/// use whitaker_installer::crate_name::CrateName; +/// use whitaker_installer::{crate_name::CrateName, scanner::lints_for_library}; /// /// let suite_lints = lints_for_library(&CrateName::from("whitaker_suite")); /// assert!(suite_lints.len() > 1); @@ -206,8 +204,7 @@ pub fn lints_for_library(crate_name: &CrateName) -> Vec<&'static str> { /// # Examples /// /// ``` -/// use whitaker_installer::scanner::lints_for_library_with_experimental; -/// use whitaker_installer::crate_name::CrateName; +/// use whitaker_installer::{crate_name::CrateName, scanner::lints_for_library_with_experimental}; /// /// let lints = lints_for_library_with_experimental(&CrateName::from("whitaker_suite"), true); /// assert!(lints.contains(&"bumpy_road_function")); @@ -241,10 +238,11 @@ pub fn lints_for_library_with_experimental( mod tests { //! Tests for scanning installed lint libraries. - use super::*; use rstest::rstest; use tempfile::TempDir; + use super::*; + /// Skip test execution on non-Linux platforms where library extensions differ. macro_rules! skip_unless_linux { () => { diff --git a/installer/src/staged_suite.rs b/installer/src/staged_suite.rs index 2a2180ec9..ccc09a57b 100644 --- a/installer/src/staged_suite.rs +++ b/installer/src/staged_suite.rs @@ -4,14 +4,17 @@ //! helper exists only so debug-built test binaries can stage a cheap synthetic //! artefact instead of recursively rebuilding the workspace inside nextest. -use camino::{Utf8Path, Utf8PathBuf}; use std::fs; -use whitaker_installer::crate_name::CrateName; -use whitaker_installer::error::{InstallerError, Result}; -use whitaker_installer::resolution::SUITE_CRATE; -use whitaker_installer::stager::Stager; -use whitaker_installer::test_support::TEST_STAGE_SUITE_ENV; -use whitaker_installer::toolchain::Toolchain; + +use camino::{Utf8Path, Utf8PathBuf}; +use whitaker_installer::{ + crate_name::CrateName, + error::{InstallerError, Result}, + resolution::SUITE_CRATE, + stager::Stager, + test_support::TEST_STAGE_SUITE_ENV, + toolchain::Toolchain, +}; pub(crate) fn try_test_staged_suite_installation( requested_crates: &[CrateName], @@ -58,12 +61,13 @@ mod tests { //! temporary environment-variable helpers to exercise the debug-only staged //! suite shortcuts without leaking process-wide state between cases. - use super::*; use rstest::{fixture, rstest}; use temp_env::{with_var, with_var_unset}; use tempfile::TempDir; use whitaker_installer::test_support::env_test_guard; + use super::*; + struct StagedSuiteSetup { _guard: std::sync::MutexGuard<'static, ()>, _temp_dir: TempDir, @@ -72,9 +76,7 @@ mod tests { } impl StagedSuiteSetup { - fn requested_suite_crates() -> Vec { - vec![CrateName::from(SUITE_CRATE)] - } + fn requested_suite_crates() -> Vec { vec![CrateName::from(SUITE_CRATE)] } fn stager(&self) -> Stager { Stager::new(self.target_dir.clone(), self.toolchain.channel()) diff --git a/installer/src/stager.rs b/installer/src/stager.rs index 0d9b43dab..b554f11bb 100644 --- a/installer/src/stager.rs +++ b/installer/src/stager.rs @@ -3,12 +3,16 @@ //! This module handles copying built libraries to the target directory with //! the toolchain-specific naming convention required by Dylint. -use crate::builder::{BuildResult, library_extension, library_prefix}; -use crate::crate_name::CrateName; -use crate::error::{InstallerError, Result}; -use camino::{Utf8Path, Utf8PathBuf}; use std::fs; +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::{ + builder::{BuildResult, library_extension, library_prefix}, + crate_name::CrateName, + error::{InstallerError, Result}, +}; + /// Handles staging of built libraries to the target directory. pub struct Stager { target_dir: Utf8PathBuf, @@ -92,9 +96,7 @@ impl Stager { /// Return the target directory root. #[must_use] - pub fn target_dir(&self) -> &Utf8Path { - &self.target_dir - } + pub fn target_dir(&self) -> &Utf8Path { &self.target_dir } /// Compute the staged filename with toolchain suffix. /// diff --git a/installer/src/test_utils/dependency_binary_helpers.rs b/installer/src/test_utils/dependency_binary_helpers.rs index 9ce6d9813..8c1c98aee 100644 --- a/installer/src/test_utils/dependency_binary_helpers.rs +++ b/installer/src/test_utils/dependency_binary_helpers.rs @@ -1,16 +1,23 @@ //! Test helpers for dependency binary installation tests. -use crate::dependency_binaries::find_dependency_binary; -use crate::error::Result; -use crate::test_utils::{ExpectedCall, failure_output, stdout_output, success_output}; use std::process::Output; +use crate::{ + dependency_binaries::find_dependency_binary, + error::Result, + test_utils::{ExpectedCall, failure_output, stdout_output, success_output}, +}; + #[path = "dependency_binary_helpers_fakes.rs"] mod fakes; pub use fakes::{ - AlwaysNotFoundRepositoryInstaller, path_binary_location, with_fake_binary_on_path, - with_fake_path, write_fake_binary, write_fake_binary_with_status, + AlwaysNotFoundRepositoryInstaller, + path_binary_location, + with_fake_binary_on_path, + with_fake_path, + write_fake_binary, + write_fake_binary_with_status, }; /// Expected outcome of repository-install verification in a test scenario. diff --git a/installer/src/test_utils/dependency_binary_helpers_fakes.rs b/installer/src/test_utils/dependency_binary_helpers_fakes.rs index bacbba51a..cda8f7c8f 100644 --- a/installer/src/test_utils/dependency_binary_helpers_fakes.rs +++ b/installer/src/test_utils/dependency_binary_helpers_fakes.rs @@ -4,17 +4,23 @@ //! at them so dependency-install probes can be exercised without touching a //! real toolchain. -use std::fs; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; -use crate::dependency_binaries::{ - DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller, +use crate::{ + dependency_binaries::{ + DependencyBinary, + DependencyBinaryInstallError, + DependencyBinaryInstaller, + }, + dirs::BaseDirs, + installer_packaging::TargetTriple, + test_support::env_test_guard, }; -use crate::dirs::BaseDirs; -use crate::installer_packaging::TargetTriple; -use crate::test_support::env_test_guard; /// Repository installer test double that always reports a missing archive. pub struct AlwaysNotFoundRepositoryInstaller; diff --git a/installer/src/test_utils/dependency_binary_helpers_tests.rs b/installer/src/test_utils/dependency_binary_helpers_tests.rs index de37b4f24..10d8aa27b 100644 --- a/installer/src/test_utils/dependency_binary_helpers_tests.rs +++ b/installer/src/test_utils/dependency_binary_helpers_tests.rs @@ -1,7 +1,10 @@ //! Tests for dependency binary helper fixtures and expected call builders. use crate::test_utils::dependency_binary_helpers::{ - ExpectedCallConfig, RepositoryVerification, dependency_version, expected_calls, + ExpectedCallConfig, + RepositoryVerification, + dependency_version, + expected_calls, repository_verification_call, }; diff --git a/installer/src/test_utils/mod.rs b/installer/src/test_utils/mod.rs index 3d5c77adc..38ebc93e2 100644 --- a/installer/src/test_utils/mod.rs +++ b/installer/src/test_utils/mod.rs @@ -125,17 +125,11 @@ pub struct StubDirs { } impl BaseDirs for StubDirs { - fn home(&self) -> Option { - None - } + fn home(&self) -> Option { None } - fn executables(&self) -> Option { - self.bin_dir.clone() - } + fn executables(&self) -> Option { self.bin_dir.clone() } - fn whitaker_data(&self) -> Option { - None - } + fn whitaker_data(&self) -> Option { None } } /// Compute the SHA-256 hex digest of a byte slice for test fixtures. diff --git a/installer/src/tests/mod.rs b/installer/src/tests/mod.rs index 72181f870..217766485 100644 --- a/installer/src/tests/mod.rs +++ b/installer/src/tests/mod.rs @@ -13,7 +13,9 @@ use whitaker_installer::{ installer_packaging::TargetTriple, test_utils::{ dependency_binary_helpers::{ - AlwaysNotFoundRepositoryInstaller, cargo_dylint_check, dylint_link_install_list_check, + AlwaysNotFoundRepositoryInstaller, + cargo_dylint_check, + dylint_link_install_list_check, with_fake_binary_on_path, }, *, @@ -252,17 +254,11 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home(&self) -> Option { - self.home.clone() - } + fn home(&self) -> Option { self.home.clone() } - fn executables(&self) -> Option { - self.bin.clone() - } + fn executables(&self) -> Option { self.bin.clone() } - fn whitaker_data(&self) -> Option { - self.data.clone() - } + fn whitaker_data(&self) -> Option { self.data.clone() } } #[test] diff --git a/installer/src/toolchain/mod.rs b/installer/src/toolchain/mod.rs index 5865ce98f..4ed64922a 100644 --- a/installer/src/toolchain/mod.rs +++ b/installer/src/toolchain/mod.rs @@ -3,10 +3,12 @@ //! This module provides utilities to detect the pinned Rust toolchain from //! `rust-toolchain.toml` and verify that it is installed via rustup. -use crate::error::{InstallerError, Result}; -use camino::{Utf8Path, Utf8PathBuf}; use std::process::{Command, Output}; +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::error::{InstallerError, Result}; + /// Components required for building dylint lints. /// /// Only these components are installed by the installer, regardless of what @@ -30,9 +32,7 @@ pub struct ToolchainInstallStatus { impl ToolchainInstallStatus { /// Returns true if the toolchain was installed during this run. #[must_use] - pub const fn installed_toolchain(&self) -> bool { - self.installed_toolchain - } + pub const fn installed_toolchain(&self) -> bool { self.installed_toolchain } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -161,15 +161,11 @@ impl Toolchain { /// Return the channel string for `cargo +` invocations. #[must_use] - pub fn channel(&self) -> &str { - &self.channel - } + pub fn channel(&self) -> &str { &self.channel } /// Return the workspace root path. #[must_use] - pub fn workspace_root(&self) -> &Utf8Path { - &self.workspace_root - } + pub fn workspace_root(&self) -> &Utf8Path { &self.workspace_root } fn is_installed_with(&self, runner: &dyn CommandRunner) -> Result { let output = run_rustup(runner, &["run", &self.channel, "rustc", "--version"])?; diff --git a/installer/src/toolchain/tests/failure_mocks/mod.rs b/installer/src/toolchain/tests/failure_mocks/mod.rs index e426d186b..db6b56f6b 100644 --- a/installer/src/toolchain/tests/failure_mocks/mod.rs +++ b/installer/src/toolchain/tests/failure_mocks/mod.rs @@ -2,8 +2,12 @@ use super::*; use crate::toolchain::tests::test_helpers::{ - ToolchainInstallExpectation, expect_rustc_version, expect_toolchain_install, - matches_multi_component_add, output_with_status, output_with_stderr, + ToolchainInstallExpectation, + expect_rustc_version, + expect_toolchain_install, + matches_multi_component_add, + output_with_status, + output_with_stderr, }; /// Describes the type of installation failure being tested. @@ -35,9 +39,7 @@ pub(super) struct ToolchainChannel<'a>(pub(super) &'a str); impl<'a> ToolchainChannel<'a> { /// Returns the inner channel string slice (e.g. `"nightly-2026-05-28"`). - pub(super) fn as_str(self) -> &'a str { - self.0 - } + pub(super) fn as_str(self) -> &'a str { self.0 } } /// The exact stderr string emitted by the mock when a toolchain installation diff --git a/installer/src/toolchain/tests/mod.rs b/installer/src/toolchain/tests/mod.rs index 1806b0f5b..322e8ada2 100644 --- a/installer/src/toolchain/tests/mod.rs +++ b/installer/src/toolchain/tests/mod.rs @@ -4,13 +4,22 @@ mod failure_mocks; mod test_helpers; use failure_mocks::{ - COMPONENT_INSTALL_FAILURE_MESSAGE, FailureSetup, InstallFailure, ToolchainChannel, - assert_failure_error, setup_failure_mocks, + COMPONENT_INSTALL_FAILURE_MESSAGE, + FailureSetup, + InstallFailure, + ToolchainChannel, + assert_failure_error, + setup_failure_mocks, }; use rstest::rstest; use test_helpers::{ - CapturingCommandRunner, ToolchainInstallExpectation, expect_rustc_version, - expect_toolchain_install, matches_multi_component_add, output_with_status, output_with_stderr, + CapturingCommandRunner, + ToolchainInstallExpectation, + expect_rustc_version, + expect_toolchain_install, + matches_multi_component_add, + output_with_status, + output_with_stderr, test_toolchain, }; diff --git a/installer/src/version.rs b/installer/src/version.rs index 9face4b47..787ac7e37 100644 --- a/installer/src/version.rs +++ b/installer/src/version.rs @@ -15,45 +15,31 @@ pub struct Version(String); impl Version { /// Create a new [`Version`] from any string-like value. #[must_use] - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } + pub fn new(value: impl Into) -> Self { Self(value.into()) } /// Borrow the underlying version string. #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } + pub fn as_str(&self) -> &str { &self.0 } /// Consume the wrapper and return the inner string. #[must_use] - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } } impl AsRef for Version { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { &self.0 } } impl From<&str> for Version { - fn from(value: &str) -> Self { - Self(value.to_owned()) - } + fn from(value: &str) -> Self { Self(value.to_owned()) } } impl From for Version { - fn from(value: String) -> Self { - Self(value) - } + fn from(value: String) -> Self { Self(value) } } impl fmt::Display for Version { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } #[cfg(test)] diff --git a/installer/src/workspace.rs b/installer/src/workspace.rs index 1a6bbac99..1b16bb7ab 100644 --- a/installer/src/workspace.rs +++ b/installer/src/workspace.rs @@ -3,10 +3,13 @@ //! This module provides utilities for detecting whether the current directory //! is a Whitaker workspace and for resolving platform-specific clone locations. -use crate::dirs::BaseDirs; -use crate::error::{InstallerError, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use crate::{ + dirs::BaseDirs, + error::{InstallerError, Result}, +}; + /// Repository URL for cloning Whitaker. pub const WHITAKER_REPO_URL: &str = "https://github.com/leynos/whitaker"; diff --git a/installer/src/workspace_tests.rs b/installer/src/workspace_tests.rs index fa51facdf..33cdafc06 100644 --- a/installer/src/workspace_tests.rs +++ b/installer/src/workspace_tests.rs @@ -1,12 +1,13 @@ //! Tests for workspace discovery and layout. -use super::*; -use crate::dirs::{MockBaseDirs, SystemBaseDirs}; +use std::{fs, path::PathBuf}; + use rstest::{fixture, rstest}; -use std::fs; -use std::path::PathBuf; use tempfile::TempDir; +use super::*; +use crate::dirs::{MockBaseDirs, SystemBaseDirs}; + /// A temporary directory converted to a UTF-8 path for workspace tests. struct TempWorkspace { _temp: TempDir, diff --git a/installer/src/wrapper.rs b/installer/src/wrapper.rs index 5f279d9a0..cc074a116 100644 --- a/installer/src/wrapper.rs +++ b/installer/src/wrapper.rs @@ -4,12 +4,16 @@ //! that set the `DYLINT_LIBRARY_PATH` environment variable and invoke //! `cargo dylint`. -use crate::dirs::BaseDirs; -use crate::error::{InstallerError, Result}; -use crate::resolution::SUITE_CRATE; -use camino::Utf8Path; use std::path::Path; +use camino::Utf8Path; + +use crate::{ + dirs::BaseDirs, + error::{InstallerError, Result}, + resolution::SUITE_CRATE, +}; + /// Result of wrapper script generation. #[derive(Debug)] pub struct WrapperResult { @@ -44,8 +48,10 @@ pub struct WrapperResult { /// /// ```no_run /// use camino::Utf8Path; -/// use whitaker_installer::dirs::{BaseDirs, SystemBaseDirs}; -/// use whitaker_installer::wrapper::generate_wrapper_scripts; +/// use whitaker_installer::{ +/// dirs::{BaseDirs, SystemBaseDirs}, +/// wrapper::generate_wrapper_scripts, +/// }; /// /// let dirs = SystemBaseDirs::new().expect("failed to initialize directories"); /// let library_path = Utf8Path::new("/home/user/.local/share/dylint/lib"); @@ -212,9 +218,10 @@ pub fn path_instructions(bin_dir: &Path) -> String { mod tests { //! Tests for the generated cargo-whitaker wrapper script. - use super::*; use tempfile::TempDir; + use super::*; + #[test] fn is_directory_in_path_returns_false_for_random_dir() { let temp = TempDir::new().expect("failed to create temp dir"); diff --git a/installer/tests/behaviour_artefact.rs b/installer/tests/behaviour_artefact.rs index a05d37bae..885acf51c 100644 --- a/installer/tests/behaviour_artefact.rs +++ b/installer/tests/behaviour_artefact.rs @@ -40,9 +40,7 @@ struct ArtefactWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> ArtefactWorld { - ArtefactWorld::default() -} +fn world() -> ArtefactWorld { ArtefactWorld::default() } /// Check that an error option contains a specific `ArtefactError` variant. fn ensure_error_matches( @@ -274,62 +272,46 @@ fn then_action_is_fallback(world: &mut ArtefactWorld) -> Result<(), String> { path = "tests/features/artefact_policy.feature", name = "Construct artefact name from valid components" )] -fn scenario_construct_artefact_name(world: ArtefactWorld) { - let _ = world; -} +fn scenario_construct_artefact_name(world: ArtefactWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_policy.feature", name = "Reject unsupported target triple" )] -fn scenario_reject_unsupported_target(world: ArtefactWorld) { - let _ = world; -} +fn scenario_reject_unsupported_target(world: ArtefactWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_policy.feature", name = "Accept all five supported target triples" )] -fn scenario_accept_all_supported_targets(world: ArtefactWorld) { - let _ = world; -} +fn scenario_accept_all_supported_targets(world: ArtefactWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_policy.feature", name = "Reject invalid git SHA" )] -fn scenario_reject_invalid_git_sha(world: ArtefactWorld) { - let _ = world; -} +fn scenario_reject_invalid_git_sha(world: ArtefactWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_policy.feature", name = "Reject empty toolchain channel" )] -fn scenario_reject_empty_channel(world: ArtefactWorld) { - let _ = world; -} +fn scenario_reject_empty_channel(world: ArtefactWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_policy.feature", name = "Construct manifest with all fields" )] -fn scenario_construct_manifest(world: ArtefactWorld) { - let _ = world; -} +fn scenario_construct_manifest(world: ArtefactWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_policy.feature", name = "Default verification policy requires checksum" )] -fn scenario_default_verification_policy(world: ArtefactWorld) { - let _ = world; -} +fn scenario_default_verification_policy(world: ArtefactWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_policy.feature", name = "Verification failure triggers fallback" )] -fn scenario_verification_failure_fallback(world: ArtefactWorld) { - let _ = world; -} +fn scenario_verification_failure_fallback(world: ArtefactWorld) { let _ = world; } diff --git a/installer/tests/behaviour_artefact_packaging.rs b/installer/tests/behaviour_artefact_packaging.rs index 7cc0d5afc..1b3e30e00 100644 --- a/installer/tests/behaviour_artefact_packaging.rs +++ b/installer/tests/behaviour_artefact_packaging.rs @@ -14,7 +14,11 @@ use whitaker_installer::artefact::{ manifest::GeneratedAt, naming::ArtefactName, packaging::{ - PackageOutput, PackageParams, compute_sha256, generate_manifest_json, package_artefact, + PackageOutput, + PackageParams, + compute_sha256, + generate_manifest_json, + package_artefact, }, packaging_error::PackagingError, target::TargetTriple, @@ -40,9 +44,7 @@ struct PackagingWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> PackagingWorld { - PackagingWorld::default() -} +fn world() -> PackagingWorld { PackagingWorld::default() } /// Return the temp directory path, creating the directory if needed. fn temp_path(world: &mut PackagingWorld) -> Result { @@ -146,9 +148,7 @@ fn given_target(world: &mut PackagingWorld, triple: String) -> Result<(), String } #[when("the artefact is packaged")] -fn when_packaged(world: &mut PackagingWorld) -> Result<(), String> { - run_packaging(world) -} +fn when_packaged(world: &mut PackagingWorld) -> Result<(), String> { run_packaging(world) } #[then("the archive exists with the expected ADR-001 filename")] fn then_archive_exists(world: &mut PackagingWorld) -> Result<(), String> { @@ -404,62 +404,46 @@ fn list_archive_entries(world: &PackagingWorld) -> Result, String> { path = "tests/features/artefact_packaging.feature", name = "Package a single library file into a tar.zst archive" )] -fn scenario_package_single_library(world: PackagingWorld) { - let _ = world; -} +fn scenario_package_single_library(world: PackagingWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_packaging.feature", name = "Manifest JSON contains all required fields" )] -fn scenario_manifest_fields(world: PackagingWorld) { - let _ = world; -} +fn scenario_manifest_fields(world: PackagingWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_packaging.feature", name = "Manifest sha256 matches the archive digest" )] -fn scenario_manifest_digest_self_consistency(world: PackagingWorld) { - let _ = world; -} +fn scenario_manifest_digest_self_consistency(world: PackagingWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_packaging.feature", name = "Archive SHA-256 is a valid digest" )] -fn scenario_archive_sha256(world: PackagingWorld) { - let _ = world; -} +fn scenario_archive_sha256(world: PackagingWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_packaging.feature", name = "Packaging rejects an empty file list" )] -fn scenario_reject_empty_files(world: PackagingWorld) { - let _ = world; -} +fn scenario_reject_empty_files(world: PackagingWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_packaging.feature", name = "Archive filename matches ArtefactName convention" )] -fn scenario_filename_matches(world: PackagingWorld) { - let _ = world; -} +fn scenario_filename_matches(world: PackagingWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_packaging.feature", name = "Archive contains multiple library files" )] -fn scenario_multi_library(world: PackagingWorld) { - let _ = world; -} +fn scenario_multi_library(world: PackagingWorld) { let _ = world; } #[scenario( path = "tests/features/artefact_packaging.feature", name = "Manifest files field lists all library basenames" )] -fn scenario_manifest_files_field(world: PackagingWorld) { - let _ = world; -} +fn scenario_manifest_files_field(world: PackagingWorld) { let _ = world; } diff --git a/installer/tests/behaviour_binstall.rs b/installer/tests/behaviour_binstall.rs index 12b79cbc6..bb8a7a39d 100644 --- a/installer/tests/behaviour_binstall.rs +++ b/installer/tests/behaviour_binstall.rs @@ -8,8 +8,13 @@ use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use toml::Table; use whitaker_installer::binstall_metadata::{ - BIN_DIR_TEMPLATE, PKG_URL_TEMPLATE, WINDOWS_OVERRIDE_TARGET, expand_bin_dir, expand_pkg_url, - extract_binstall_table, load_cargo_toml, + BIN_DIR_TEMPLATE, + PKG_URL_TEMPLATE, + WINDOWS_OVERRIDE_TARGET, + expand_bin_dir, + expand_pkg_url, + extract_binstall_table, + load_cargo_toml, }; // --------------------------------------------------------------------------- @@ -32,9 +37,7 @@ struct BinstallWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> BinstallWorld { - BinstallWorld::default() -} +fn world() -> BinstallWorld { BinstallWorld::default() } /// Fetch the binstall table, failing if it has not been loaded. fn binstall_table(world: &BinstallWorld) -> Result<&Table, String> { @@ -217,54 +220,40 @@ fn then_no_invalid_placeholder( path = "tests/features/binstall_metadata.feature", name = "Binstall metadata section exists in Cargo.toml" )] -fn scenario_binstall_metadata_exists(world: BinstallWorld) { - let _ = world; -} +fn scenario_binstall_metadata_exists(world: BinstallWorld) { let _ = world; } #[scenario( path = "tests/features/binstall_metadata.feature", name = "Windows override uses zip format" )] -fn scenario_windows_override(world: BinstallWorld) { - let _ = world; -} +fn scenario_windows_override(world: BinstallWorld) { let _ = world; } #[scenario( path = "tests/features/binstall_metadata.feature", name = "URL template expands correctly for Linux" )] -fn scenario_url_linux(world: BinstallWorld) { - let _ = world; -} +fn scenario_url_linux(world: BinstallWorld) { let _ = world; } #[scenario( path = "tests/features/binstall_metadata.feature", name = "URL template expands correctly for Windows" )] -fn scenario_url_windows(world: BinstallWorld) { - let _ = world; -} +fn scenario_url_windows(world: BinstallWorld) { let _ = world; } #[scenario( path = "tests/features/binstall_metadata.feature", name = "Binary directory expands correctly for Unix" )] -fn scenario_bin_dir_unix(world: BinstallWorld) { - let _ = world; -} +fn scenario_bin_dir_unix(world: BinstallWorld) { let _ = world; } #[scenario( path = "tests/features/binstall_metadata.feature", name = "Binary directory expands correctly for Windows" )] -fn scenario_bin_dir_windows(world: BinstallWorld) { - let _ = world; -} +fn scenario_bin_dir_windows(world: BinstallWorld) { let _ = world; } #[scenario( path = "tests/features/binstall_metadata.feature", name = "No invalid placeholders in templates" )] -fn scenario_no_invalid_placeholders(world: BinstallWorld) { - let _ = world; -} +fn scenario_no_invalid_placeholders(world: BinstallWorld) { let _ = world; } diff --git a/installer/tests/behaviour_cli.rs b/installer/tests/behaviour_cli.rs index 9dc75446c..a15475e3e 100644 --- a/installer/tests/behaviour_cli.rs +++ b/installer/tests/behaviour_cli.rs @@ -14,13 +14,23 @@ use std::process::Command; use rstest_bdd_macros::{given, then, when}; pub(crate) use support::{CliWorld, cli_world}; use support::{ - assert_cli_exits_successfully, assert_cli_exits_with_error, assert_dry_run_output_is_shown, + assert_cli_exits_successfully, + assert_cli_exits_with_error, + assert_dry_run_output_is_shown, assert_experimental_lint_dry_run_output_is_shown, - assert_experimental_lint_opt_in_message_is_shown, assert_installation_succeeds_or_is_skipped, - assert_suite_library_is_staged, assert_unknown_lint_message_is_shown, - configure_dry_run_experimental_lint, configure_dry_run_experimental_lint_with_opt_in, - configure_dry_run_unknown_lint, configure_dry_run_with_target_dir, configure_suite_install, - is_toolchain_installed, pinned_toolchain_channel, run_installer_cli, workspace_root, + assert_experimental_lint_opt_in_message_is_shown, + assert_installation_succeeds_or_is_skipped, + assert_suite_library_is_staged, + assert_unknown_lint_message_is_shown, + configure_dry_run_experimental_lint, + configure_dry_run_experimental_lint_with_opt_in, + configure_dry_run_unknown_lint, + configure_dry_run_with_target_dir, + configure_suite_install, + is_toolchain_installed, + pinned_toolchain_channel, + run_installer_cli, + workspace_root, }; #[given("the installer is invoked with dry-run and a target directory")] @@ -29,9 +39,7 @@ fn given_dry_run_with_target_dir(cli_world: &CliWorld) { } #[given("the installer is invoked with dry-run and an unknown lint")] -fn given_dry_run_unknown_lint(cli_world: &CliWorld) { - configure_dry_run_unknown_lint(cli_world); -} +fn given_dry_run_unknown_lint(cli_world: &CliWorld) { configure_dry_run_unknown_lint(cli_world); } #[given("the installer is invoked with dry-run and an experimental lint")] fn given_dry_run_experimental_lint(cli_world: &CliWorld) { @@ -46,29 +54,19 @@ fn given_dry_run_experimental_lint_with_opt_in(cli_world: &CliWorld) { } #[given("the installer is invoked to a temporary directory")] -fn given_suite_install(cli_world: &CliWorld) { - configure_suite_install(cli_world); -} +fn given_suite_install(cli_world: &CliWorld) { configure_suite_install(cli_world); } #[when("the installer CLI is run")] -fn when_installer_cli_run(cli_world: &CliWorld) { - run_installer_cli(cli_world); -} +fn when_installer_cli_run(cli_world: &CliWorld) { run_installer_cli(cli_world); } #[then("the CLI exits successfully")] -fn then_cli_exits_successfully(cli_world: &CliWorld) { - assert_cli_exits_successfully(cli_world); -} +fn then_cli_exits_successfully(cli_world: &CliWorld) { assert_cli_exits_successfully(cli_world); } #[then("dry-run output is shown")] -fn then_dry_run_output_is_shown(cli_world: &CliWorld) { - assert_dry_run_output_is_shown(cli_world); -} +fn then_dry_run_output_is_shown(cli_world: &CliWorld) { assert_dry_run_output_is_shown(cli_world); } #[then("the CLI exits with an error")] -fn then_cli_exits_with_error(cli_world: &CliWorld) { - assert_cli_exits_with_error(cli_world); -} +fn then_cli_exits_with_error(cli_world: &CliWorld) { assert_cli_exits_with_error(cli_world); } #[then("an unknown lint message is shown")] fn then_unknown_lint_message_is_shown(cli_world: &CliWorld) { @@ -91,9 +89,7 @@ fn then_installation_succeeds_or_is_skipped(cli_world: &CliWorld) { } #[then("the suite library is staged")] -fn then_suite_library_is_staged(cli_world: &CliWorld) { - assert_suite_library_is_staged(cli_world); -} +fn then_suite_library_is_staged(cli_world: &CliWorld) { assert_suite_library_is_staged(cli_world); } #[test] fn dry_run_reports_verbosity_levels() { diff --git a/installer/tests/behaviour_cli/scenarios.rs b/installer/tests/behaviour_cli/scenarios.rs index 924b4b019..15d371ed1 100644 --- a/installer/tests/behaviour_cli/scenarios.rs +++ b/installer/tests/behaviour_cli/scenarios.rs @@ -7,19 +7,13 @@ use super::{CliWorld, cli_world}; // Do not reorder scenarios in tests/features/installer.feature — bindings are // index-based. #[scenario(path = "tests/features/installer.feature", index = 12)] -fn scenario_dry_run_outputs_configuration(cli_world: CliWorld) { - let _ = cli_world; -} +fn scenario_dry_run_outputs_configuration(cli_world: CliWorld) { let _ = cli_world; } #[scenario(path = "tests/features/installer.feature", index = 13)] -fn scenario_dry_run_rejects_unknown_lint(cli_world: CliWorld) { - let _ = cli_world; -} +fn scenario_dry_run_rejects_unknown_lint(cli_world: CliWorld) { let _ = cli_world; } #[scenario(path = "tests/features/installer.feature", index = 14)] -fn scenario_install_suite_to_temp_dir(cli_world: CliWorld) { - let _ = cli_world; -} +fn scenario_install_suite_to_temp_dir(cli_world: CliWorld) { let _ = cli_world; } #[scenario(path = "tests/features/installer.feature", index = 21)] fn scenario_dry_run_rejects_experimental_lint_without_opt_in(cli_world: CliWorld) { diff --git a/installer/tests/behaviour_cli/support.rs b/installer/tests/behaviour_cli/support.rs index d38b8efb1..5dc056a38 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -9,7 +9,9 @@ use std::{ use rstest::fixture; use tempfile::TempDir; use whitaker_installer::{ - dirs::SystemBaseDirs, prebuilt_path::prebuilt_library_dir, test_support::TEST_STAGE_SUITE_ENV, + dirs::SystemBaseDirs, + prebuilt_path::prebuilt_library_dir, + test_support::TEST_STAGE_SUITE_ENV, toolchain::parse_toolchain_channel, }; @@ -27,9 +29,7 @@ pub(super) struct CliWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -pub(super) fn cli_world() -> CliWorld { - CliWorld::default() -} +pub(super) fn cli_world() -> CliWorld { CliWorld::default() } pub(super) fn workspace_root() -> PathBuf { let manifest_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")); @@ -220,8 +220,12 @@ pub(super) fn run_installer_cli(cli_world: &CliWorld) { mod assertions; pub(super) use assertions::{ - assert_cli_exits_successfully, assert_cli_exits_with_error, assert_dry_run_output_is_shown, + assert_cli_exits_successfully, + assert_cli_exits_with_error, + assert_dry_run_output_is_shown, assert_experimental_lint_dry_run_output_is_shown, - assert_experimental_lint_opt_in_message_is_shown, assert_installation_succeeds_or_is_skipped, - assert_suite_library_is_staged, assert_unknown_lint_message_is_shown, + assert_experimental_lint_opt_in_message_is_shown, + assert_installation_succeeds_or_is_skipped, + assert_suite_library_is_staged, + assert_unknown_lint_message_is_shown, }; diff --git a/installer/tests/behaviour_core.rs b/installer/tests/behaviour_core.rs index d1feef1c3..61e6be471 100644 --- a/installer/tests/behaviour_core.rs +++ b/installer/tests/behaviour_core.rs @@ -12,7 +12,11 @@ use whitaker_installer::{ crate_name::CrateName, output::ShellSnippet, resolution::{ - CrateResolutionOptions, EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE, resolve_crates, + CrateResolutionOptions, + EXPERIMENTAL_LINT_CRATES, + LINT_CRATES, + SUITE_CRATE, + resolve_crates, validate_crate_names, }, toolchain::parse_toolchain_channel, @@ -32,9 +36,7 @@ struct CrateResolutionWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn crate_world() -> CrateResolutionWorld { - CrateResolutionWorld::default() -} +fn crate_world() -> CrateResolutionWorld { CrateResolutionWorld::default() } #[given("no specific lints are requested")] fn given_no_specific_lints(crate_world: &CrateResolutionWorld) { @@ -140,9 +142,7 @@ struct ValidationWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn validation_world() -> ValidationWorld { - ValidationWorld::default() -} +fn validation_world() -> ValidationWorld { ValidationWorld::default() } #[given("a list of valid crate names")] fn given_valid_names(validation_world: &ValidationWorld) { @@ -227,9 +227,7 @@ struct ToolchainWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn toolchain_world() -> ToolchainWorld { - ToolchainWorld::default() -} +fn toolchain_world() -> ToolchainWorld { ToolchainWorld::default() } #[given("a rust-toolchain.toml with standard format")] fn given_standard_toolchain(toolchain_world: &ToolchainWorld) { @@ -296,9 +294,7 @@ struct SnippetWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn snippet_world() -> SnippetWorld { - SnippetWorld::default() -} +fn snippet_world() -> SnippetWorld { SnippetWorld::default() } #[given("a target library path")] fn given_library_path(snippet_world: &SnippetWorld) { @@ -360,9 +356,7 @@ fn scenario_resolve_suite_only_by_default(crate_world: CrateResolutionWorld) { } #[scenario(path = "tests/features/installer.feature", index = 1)] -fn scenario_resolve_individual_lints(crate_world: CrateResolutionWorld) { - let _ = crate_world; -} +fn scenario_resolve_individual_lints(crate_world: CrateResolutionWorld) { let _ = crate_world; } #[scenario(path = "tests/features/installer.feature", index = 2)] fn scenario_resolve_individual_lints_with_experimental(crate_world: CrateResolutionWorld) { @@ -370,39 +364,25 @@ fn scenario_resolve_individual_lints_with_experimental(crate_world: CrateResolut } #[scenario(path = "tests/features/installer.feature", index = 3)] -fn scenario_resolve_specific_lints(crate_world: CrateResolutionWorld) { - let _ = crate_world; -} +fn scenario_resolve_specific_lints(crate_world: CrateResolutionWorld) { let _ = crate_world; } #[scenario(path = "tests/features/installer.feature", index = 4)] -fn scenario_validate_known_names(validation_world: ValidationWorld) { - let _ = validation_world; -} +fn scenario_validate_known_names(validation_world: ValidationWorld) { let _ = validation_world; } #[scenario(path = "tests/features/installer.feature", index = 5)] -fn scenario_reject_unknown_names(validation_world: ValidationWorld) { - let _ = validation_world; -} +fn scenario_reject_unknown_names(validation_world: ValidationWorld) { let _ = validation_world; } #[scenario(path = "tests/features/installer.feature", index = 6)] -fn scenario_parse_standard_toolchain(toolchain_world: ToolchainWorld) { - let _ = toolchain_world; -} +fn scenario_parse_standard_toolchain(toolchain_world: ToolchainWorld) { let _ = toolchain_world; } #[scenario(path = "tests/features/installer.feature", index = 7)] -fn scenario_parse_top_level_channel(toolchain_world: ToolchainWorld) { - let _ = toolchain_world; -} +fn scenario_parse_top_level_channel(toolchain_world: ToolchainWorld) { let _ = toolchain_world; } #[scenario(path = "tests/features/installer.feature", index = 8)] -fn scenario_reject_missing_channel(toolchain_world: ToolchainWorld) { - let _ = toolchain_world; -} +fn scenario_reject_missing_channel(toolchain_world: ToolchainWorld) { let _ = toolchain_world; } #[scenario(path = "tests/features/installer.feature", index = 9)] -fn scenario_generate_shell_snippets(snippet_world: SnippetWorld) { - let _ = snippet_world; -} +fn scenario_generate_shell_snippets(snippet_world: SnippetWorld) { let _ = snippet_world; } #[scenario(path = "tests/features/installer.feature", index = 19)] fn scenario_validate_experimental_names_with_opt_in(validation_world: ValidationWorld) { diff --git a/installer/tests/behaviour_dependency_binaries.rs b/installer/tests/behaviour_dependency_binaries.rs index babdb2f26..2a93ceeb8 100644 --- a/installer/tests/behaviour_dependency_binaries.rs +++ b/installer/tests/behaviour_dependency_binaries.rs @@ -7,7 +7,9 @@ use rstest_bdd_macros::{given, scenario, then, when}; use temp_env::with_var; use whitaker_installer::{ dependency_binaries::{ - DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller, + DependencyBinary, + DependencyBinaryInstallError, + DependencyBinaryInstaller, required_dependency_binaries, }, dependency_packaging::render_provenance_markdown, @@ -16,9 +18,13 @@ use whitaker_installer::{ installer_packaging::TargetTriple, test_support::env_test_guard, test_utils::{ - StubDirs, StubExecutor, + StubDirs, + StubExecutor, dependency_binary_helpers::{ - ExpectedCallConfig, RepositoryVerification, expected_calls, path_binary_location, + ExpectedCallConfig, + RepositoryVerification, + expected_calls, + path_binary_location, write_fake_binary, }, }, @@ -107,9 +113,7 @@ struct DependencyBinaryWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> DependencyBinaryWorld { - DependencyBinaryWorld::default() -} +fn world() -> DependencyBinaryWorld { DependencyBinaryWorld::default() } #[given("the missing tool is \"{tool}\"")] fn given_missing_tool(world: &mut DependencyBinaryWorld, tool: String) { @@ -365,19 +369,13 @@ fn then_provenance_contains( } #[scenario(path = "tests/features/dependency_binaries.feature", index = 0)] -fn scenario_install_cargo_dylint_from_repository(world: DependencyBinaryWorld) { - let _ = world; -} +fn scenario_install_cargo_dylint_from_repository(world: DependencyBinaryWorld) { let _ = world; } #[scenario(path = "tests/features/dependency_binaries.feature", index = 1)] -fn scenario_install_dylint_link_from_repository(world: DependencyBinaryWorld) { - let _ = world; -} +fn scenario_install_dylint_link_from_repository(world: DependencyBinaryWorld) { let _ = world; } #[scenario(path = "tests/features/dependency_binaries.feature", index = 2)] -fn scenario_repository_falls_back_to_binstall(world: DependencyBinaryWorld) { - let _ = world; -} +fn scenario_repository_falls_back_to_binstall(world: DependencyBinaryWorld) { let _ = world; } #[scenario(path = "tests/features/dependency_binaries.feature", index = 3)] fn scenario_repository_and_binstall_and_cargo_all_fail(world: DependencyBinaryWorld) { @@ -402,21 +400,13 @@ fn scenario_repository_verification_failure_uses_binstall(world: DependencyBinar } #[scenario(path = "tests/features/dependency_binaries.feature", index = 7)] -fn scenario_unsupported_target_uses_binstall(world: DependencyBinaryWorld) { - let _ = world; -} +fn scenario_unsupported_target_uses_binstall(world: DependencyBinaryWorld) { let _ = world; } #[scenario(path = "tests/features/dependency_binaries.feature", index = 8)] -fn scenario_repository_success_without_binstall(world: DependencyBinaryWorld) { - let _ = world; -} +fn scenario_repository_success_without_binstall(world: DependencyBinaryWorld) { let _ = world; } #[scenario(path = "tests/features/dependency_binaries.feature", index = 9)] -fn scenario_provenance_lists_both_dependencies(world: DependencyBinaryWorld) { - let _ = world; -} +fn scenario_provenance_lists_both_dependencies(world: DependencyBinaryWorld) { let _ = world; } #[scenario(path = "tests/features/dependency_binaries.feature", index = 10)] -fn scenario_dylint_link_missing_after_install_fails(world: DependencyBinaryWorld) { - let _ = world; -} +fn scenario_dylint_link_missing_after_install_fails(world: DependencyBinaryWorld) { let _ = world; } diff --git a/installer/tests/behaviour_docs.rs b/installer/tests/behaviour_docs.rs index 838dee9ea..edac722ac 100644 --- a/installer/tests/behaviour_docs.rs +++ b/installer/tests/behaviour_docs.rs @@ -27,9 +27,7 @@ struct TomlWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn toml_world() -> TomlWorld { - TomlWorld::default() -} +fn toml_world() -> TomlWorld { TomlWorld::default() } /// Helper function to set TOML content in the world fixture. fn set_toml_content(toml_world: &TomlWorld, content: &str) { @@ -314,46 +312,34 @@ fn then_locale_configuration_present(toml_world: &TomlWorld) -> Result<(), Strin path = "tests/features/consumer_guidance.feature", name = "Suite-only workspace metadata is valid TOML" )] -fn scenario_suite_only_metadata(toml_world: TomlWorld) { - let _ = toml_world; -} +fn scenario_suite_only_metadata(toml_world: TomlWorld) { let _ = toml_world; } #[scenario( path = "tests/features/consumer_guidance.feature", name = "Individual crates workspace metadata is valid TOML" )] -fn scenario_individual_crates_metadata(toml_world: TomlWorld) { - let _ = toml_world; -} +fn scenario_individual_crates_metadata(toml_world: TomlWorld) { let _ = toml_world; } #[scenario( path = "tests/features/consumer_guidance.feature", name = "Version-pinned workspace metadata with tag is valid TOML" )] -fn scenario_tag_pinning_metadata(toml_world: TomlWorld) { - let _ = toml_world; -} +fn scenario_tag_pinning_metadata(toml_world: TomlWorld) { let _ = toml_world; } #[scenario( path = "tests/features/consumer_guidance.feature", name = "Version-pinned workspace metadata with revision is valid TOML" )] -fn scenario_revision_pinning_metadata(toml_world: TomlWorld) { - let _ = toml_world; -} +fn scenario_revision_pinning_metadata(toml_world: TomlWorld) { let _ = toml_world; } #[scenario( path = "tests/features/consumer_guidance.feature", name = "Pre-built library path workspace metadata is valid TOML" )] -fn scenario_prebuilt_path_metadata(toml_world: TomlWorld) { - let _ = toml_world; -} +fn scenario_prebuilt_path_metadata(toml_world: TomlWorld) { let _ = toml_world; } #[scenario( path = "tests/features/consumer_guidance.feature", name = "dylint.toml lint configuration is valid TOML" )] -fn scenario_dylint_toml_config(toml_world: TomlWorld) { - let _ = toml_world; -} +fn scenario_dylint_toml_config(toml_world: TomlWorld) { let _ = toml_world; } diff --git a/installer/tests/behaviour_install_metrics.rs b/installer/tests/behaviour_install_metrics.rs index 26357d725..10bc84d28 100644 --- a/installer/tests/behaviour_install_metrics.rs +++ b/installer/tests/behaviour_install_metrics.rs @@ -6,7 +6,10 @@ use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use tempfile::TempDir; use whitaker_installer::install_metrics::{ - InstallMetrics, InstallMode, RecordOutcome, record_install_at_path, + InstallMetrics, + InstallMode, + RecordOutcome, + record_install_at_path, }; #[derive(Default)] @@ -22,9 +25,7 @@ struct InstallMetricsWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> InstallMetricsWorld { - InstallMetricsWorld::default() -} +fn world() -> InstallMetricsWorld { InstallMetricsWorld::default() } /// Compare two values for equality, reporting a mismatch as an error. fn ensure_eq(actual: &T, expected: &U, context: &str) -> Result<(), String> @@ -131,9 +132,7 @@ fn when_build_recorded(world: &mut InstallMetricsWorld, millis: u64) -> Result<( } #[when("download and build rates are calculated")] -fn when_rates_calculated(world: &mut InstallMetricsWorld) { - let _ = world; -} +fn when_rates_calculated(world: &mut InstallMetricsWorld) { let _ = world; } #[then("total installs is {expected:u64}")] fn then_total_installs(world: &mut InstallMetricsWorld, expected: u64) -> Result<(), String> { @@ -259,46 +258,34 @@ fn then_warning_text_contains( path = "tests/features/install_metrics.feature", name = "Record a successful prebuilt-download install" )] -fn scenario_download_install(world: InstallMetricsWorld) { - let _ = world; -} +fn scenario_download_install(world: InstallMetricsWorld) { let _ = world; } #[scenario( path = "tests/features/install_metrics.feature", name = "Record a successful build-only install" )] -fn scenario_build_only_install(world: InstallMetricsWorld) { - let _ = world; -} +fn scenario_build_only_install(world: InstallMetricsWorld) { let _ = world; } #[scenario( path = "tests/features/install_metrics.feature", name = "Record download and build installs" )] -fn scenario_download_and_build_installs(world: InstallMetricsWorld) { - let _ = world; -} +fn scenario_download_and_build_installs(world: InstallMetricsWorld) { let _ = world; } #[scenario( path = "tests/features/install_metrics.feature", name = "Recover from a corrupt metrics file" )] -fn scenario_recover_from_corrupt_file(world: InstallMetricsWorld) { - let _ = world; -} +fn scenario_recover_from_corrupt_file(world: InstallMetricsWorld) { let _ = world; } #[scenario( path = "tests/features/install_metrics.feature", name = "Report write failures as warning text" )] -fn scenario_report_write_failures(world: InstallMetricsWorld) { - let _ = world; -} +fn scenario_report_write_failures(world: InstallMetricsWorld) { let _ = world; } #[scenario( path = "tests/features/install_metrics.feature", name = "Zero-state rates are zero" )] -fn scenario_zero_state_rates(world: InstallMetricsWorld) { - let _ = world; -} +fn scenario_zero_state_rates(world: InstallMetricsWorld) { let _ = world; } diff --git a/installer/tests/behaviour_installer_release.rs b/installer/tests/behaviour_installer_release.rs index 1ec28e587..7572d5114 100644 --- a/installer/tests/behaviour_installer_release.rs +++ b/installer/tests/behaviour_installer_release.rs @@ -11,8 +11,13 @@ use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_installer::{ binstall_metadata, installer_packaging::{ - self, ArchiveFormat, InstallerPackageOutput, InstallerPackageParams, - InstallerPackagingError, TargetTriple, Version, + self, + ArchiveFormat, + InstallerPackageOutput, + InstallerPackageParams, + InstallerPackagingError, + TargetTriple, + Version, }, }; @@ -34,9 +39,7 @@ struct InstallerReleaseWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> InstallerReleaseWorld { - InstallerReleaseWorld::default() -} +fn world() -> InstallerReleaseWorld { InstallerReleaseWorld::default() } // --------------------------------------------------------------------------- // Step definitions @@ -230,46 +233,34 @@ fn read_zip_entries(path: &std::path::Path) -> Result, String> { path = "tests/features/installer_release.feature", name = "Archive filename uses tgz for Linux target" )] -fn scenario_archive_filename_tgz(world: InstallerReleaseWorld) { - let _ = world; -} +fn scenario_archive_filename_tgz(world: InstallerReleaseWorld) { let _ = world; } #[scenario( path = "tests/features/installer_release.feature", name = "Archive filename uses zip for Windows target" )] -fn scenario_archive_filename_zip(world: InstallerReleaseWorld) { - let _ = world; -} +fn scenario_archive_filename_zip(world: InstallerReleaseWorld) { let _ = world; } #[scenario( path = "tests/features/installer_release.feature", name = "Archive contains correct directory structure for Unix" )] -fn scenario_archive_structure_unix(world: InstallerReleaseWorld) { - let _ = world; -} +fn scenario_archive_structure_unix(world: InstallerReleaseWorld) { let _ = world; } #[scenario( path = "tests/features/installer_release.feature", name = "Windows archive contains exe binary" )] -fn scenario_archive_structure_windows(world: InstallerReleaseWorld) { - let _ = world; -} +fn scenario_archive_structure_windows(world: InstallerReleaseWorld) { let _ = world; } #[scenario( path = "tests/features/installer_release.feature", name = "Archive filename matches binstall pkg-url template" )] -fn scenario_binstall_url_match(world: InstallerReleaseWorld) { - let _ = world; -} +fn scenario_binstall_url_match(world: InstallerReleaseWorld) { let _ = world; } #[scenario( path = "tests/features/installer_release.feature", name = "Packaging rejects missing binary" )] -fn scenario_packaging_rejects_missing(world: InstallerReleaseWorld) { - let _ = world; -} +fn scenario_packaging_rejects_missing(world: InstallerReleaseWorld) { let _ = world; } diff --git a/installer/tests/behaviour_prebuilt.rs b/installer/tests/behaviour_prebuilt.rs index 5ca3a6466..6176c5537 100644 --- a/installer/tests/behaviour_prebuilt.rs +++ b/installer/tests/behaviour_prebuilt.rs @@ -377,54 +377,40 @@ fn then_destination_is_not_created(world: &mut PrebuiltWorld) -> Result<(), Stri path = "tests/features/prebuilt_download.feature", name = "Successful prebuilt download and verification" )] -fn scenario_successful_download(world: PrebuiltWorld) { - let _ = world; -} +fn scenario_successful_download(world: PrebuiltWorld) { let _ = world; } #[scenario( path = "tests/features/prebuilt_download.feature", name = "Checksum mismatch triggers fallback" )] -fn scenario_checksum_mismatch(world: PrebuiltWorld) { - let _ = world; -} +fn scenario_checksum_mismatch(world: PrebuiltWorld) { let _ = world; } #[scenario( path = "tests/features/prebuilt_download.feature", name = "Network failure triggers fallback" )] -fn scenario_network_failure(world: PrebuiltWorld) { - let _ = world; -} +fn scenario_network_failure(world: PrebuiltWorld) { let _ = world; } #[scenario( path = "tests/features/prebuilt_download.feature", name = "Missing artefact triggers fallback" )] -fn scenario_not_found(world: PrebuiltWorld) { - let _ = world; -} +fn scenario_not_found(world: PrebuiltWorld) { let _ = world; } #[scenario( path = "tests/features/prebuilt_download.feature", name = "Destination path creation failure triggers fallback" )] -fn scenario_destination_creation_failure(world: PrebuiltWorld) { - let _ = world; -} +fn scenario_destination_creation_failure(world: PrebuiltWorld) { let _ = world; } #[scenario( path = "tests/features/prebuilt_download.feature", name = "Toolchain mismatch triggers fallback" )] -fn scenario_toolchain_mismatch(world: PrebuiltWorld) { - let _ = world; -} +fn scenario_toolchain_mismatch(world: PrebuiltWorld) { let _ = world; } #[scenario( path = "tests/features/prebuilt_download.feature", name = "Build-only flag skips prebuilt" )] -fn scenario_build_only(world: PrebuiltWorld) { - let _ = world; -} +fn scenario_build_only(world: PrebuiltWorld) { let _ = world; } diff --git a/installer/tests/behaviour_staging.rs b/installer/tests/behaviour_staging.rs index 8009d2e04..69c6d0397 100644 --- a/installer/tests/behaviour_staging.rs +++ b/installer/tests/behaviour_staging.rs @@ -26,9 +26,7 @@ struct StagingWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn staging_world() -> StagingWorld { - StagingWorld::default() -} +fn staging_world() -> StagingWorld { StagingWorld::default() } #[given("a built library")] fn given_built_library(staging_world: &StagingWorld) { @@ -111,9 +109,7 @@ mod staging_failure { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] - pub fn staging_failure_world() -> StagingFailureWorld { - StagingFailureWorld::default() - } + pub fn staging_failure_world() -> StagingFailureWorld { StagingFailureWorld::default() } #[given("a non-writable staging directory")] pub fn given_non_writable_dir( @@ -212,9 +208,7 @@ mod staging_failure { // --------------------------------------------------------------------------- #[scenario(path = "tests/features/installer.feature", index = 10)] -fn scenario_stage_with_toolchain_suffix(staging_world: StagingWorld) { - let _ = staging_world; -} +fn scenario_stage_with_toolchain_suffix(staging_world: StagingWorld) { let _ = staging_world; } #[cfg(unix)] #[scenario(path = "tests/features/installer.feature", index = 11)] diff --git a/installer/tests/behaviour_toolchain.rs b/installer/tests/behaviour_toolchain.rs index ff344cec2..39fa32403 100644 --- a/installer/tests/behaviour_toolchain.rs +++ b/installer/tests/behaviour_toolchain.rs @@ -5,8 +5,8 @@ //! //! The tests include: //! - Dry-run scenarios that test toolchain detection (skipped if toolchain missing) -//! - Install scenarios that exercise auto-install using an isolated rustup -//! environment (`RUSTUP_HOME/CARGO_HOME` set to temp directories) +//! - Install scenarios that exercise auto-install using an isolated rustup environment +//! (`RUSTUP_HOME/CARGO_HOME` set to temp directories) //! - Failure scenarios that test error handling with a non-existent toolchain mod prebuilt_markers; @@ -23,14 +23,25 @@ use toolchain_steps::{ToolchainWorld, world}; reason = "step definitions are consumed by rstest-bdd macro expansion, not by direct calls" )] use toolchain_steps::{ - given_auto_detect_toolchain, given_auto_detect_toolchain_install, - given_auto_detect_toolchain_quiet, given_isolated_rustup_auto_install, - given_isolated_rustup_quiet, given_nonexistent_toolchain, given_nonexistent_toolchain_quiet, - then_cli_exits_successfully, then_cli_exits_with_error, then_dry_run_shows_toolchain, - then_error_includes_toolchain_name, then_error_mentions_install_failure, - then_error_output_is_minimal, then_install_message_shown, - then_installation_succeeds_or_is_skipped, then_no_install_message, - then_suite_library_is_staged, then_toolchain_installed_in_isolated_env, when_installer_cli_run, + given_auto_detect_toolchain, + given_auto_detect_toolchain_install, + given_auto_detect_toolchain_quiet, + given_isolated_rustup_auto_install, + given_isolated_rustup_quiet, + given_nonexistent_toolchain, + given_nonexistent_toolchain_quiet, + then_cli_exits_successfully, + then_cli_exits_with_error, + then_dry_run_shows_toolchain, + then_error_includes_toolchain_name, + then_error_mentions_install_failure, + then_error_output_is_minimal, + then_install_message_shown, + then_installation_succeeds_or_is_skipped, + then_no_install_message, + then_suite_library_is_staged, + then_toolchain_installed_in_isolated_env, + when_installer_cli_run, }; // --------------------------------------------------------------------------- @@ -38,36 +49,22 @@ use toolchain_steps::{ // --------------------------------------------------------------------------- #[scenario(path = "tests/features/toolchain.feature", index = 0)] -fn scenario_auto_detect_toolchain_dry_run(world: ToolchainWorld) { - let _ = world; -} +fn scenario_auto_detect_toolchain_dry_run(world: ToolchainWorld) { let _ = world; } #[scenario(path = "tests/features/toolchain.feature", index = 1)] -fn scenario_auto_detect_toolchain_quiet_mode(world: ToolchainWorld) { - let _ = world; -} +fn scenario_auto_detect_toolchain_quiet_mode(world: ToolchainWorld) { let _ = world; } #[scenario(path = "tests/features/toolchain.feature", index = 2)] -fn scenario_auto_detect_toolchain_install(world: ToolchainWorld) { - let _ = world; -} +fn scenario_auto_detect_toolchain_install(world: ToolchainWorld) { let _ = world; } #[scenario(path = "tests/features/toolchain.feature", index = 3)] -fn scenario_auto_install_success_emits_message(world: ToolchainWorld) { - let _ = world; -} +fn scenario_auto_install_success_emits_message(world: ToolchainWorld) { let _ = world; } #[scenario(path = "tests/features/toolchain.feature", index = 4)] -fn scenario_auto_install_success_quiet_mode(world: ToolchainWorld) { - let _ = world; -} +fn scenario_auto_install_success_quiet_mode(world: ToolchainWorld) { let _ = world; } #[scenario(path = "tests/features/toolchain.feature", index = 5)] -fn scenario_auto_install_failure_reports_error(world: ToolchainWorld) { - let _ = world; -} +fn scenario_auto_install_failure_reports_error(world: ToolchainWorld) { let _ = world; } #[scenario(path = "tests/features/toolchain.feature", index = 6)] -fn scenario_auto_install_failure_quiet_mode(world: ToolchainWorld) { - let _ = world; -} +fn scenario_auto_install_failure_quiet_mode(world: ToolchainWorld) { let _ = world; } diff --git a/installer/tests/behaviour_workflows.rs b/installer/tests/behaviour_workflows.rs index 93296b2f5..9e34c7689 100644 --- a/installer/tests/behaviour_workflows.rs +++ b/installer/tests/behaviour_workflows.rs @@ -28,9 +28,7 @@ struct WorkflowWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> WorkflowWorld { - WorkflowWorld::default() -} +fn world() -> WorkflowWorld { WorkflowWorld::default() } fn workspace_root() -> Result { PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) @@ -270,21 +268,13 @@ fn then_output_includes_library_path_instructions(world: &WorkflowWorld) -> Resu // --------------------------------------------------------------------------- #[scenario(path = "tests/features/installer.feature", index = 15)] -fn scenario_dry_run_skip_deps(world: WorkflowWorld) { - let _ = world; -} +fn scenario_dry_run_skip_deps(world: WorkflowWorld) { let _ = world; } #[scenario(path = "tests/features/installer.feature", index = 16)] -fn scenario_dry_run_no_update(world: WorkflowWorld) { - let _ = world; -} +fn scenario_dry_run_no_update(world: WorkflowWorld) { let _ = world; } #[scenario(path = "tests/features/installer.feature", index = 17)] -fn scenario_dry_run_skip_wrapper(world: WorkflowWorld) { - let _ = world; -} +fn scenario_dry_run_skip_wrapper(world: WorkflowWorld) { let _ = world; } #[scenario(path = "tests/features/installer.feature", index = 18)] -fn scenario_skip_wrapper_outputs_shell_snippet(world: WorkflowWorld) { - let _ = world; -} +fn scenario_skip_wrapper_outputs_shell_snippet(world: WorkflowWorld) { let _ = world; } diff --git a/installer/tests/toolchain_steps/mod.rs b/installer/tests/toolchain_steps/mod.rs index 9808b3856..eb2eaf646 100644 --- a/installer/tests/toolchain_steps/mod.rs +++ b/installer/tests/toolchain_steps/mod.rs @@ -11,8 +11,13 @@ use rstest::fixture; use rstest_bdd_macros::{given, then, when}; pub use scenario_setup::{FAKE_TOOLCHAIN, ToolchainWorld, setup_install_scenario}; use scenario_setup::{ - ensure_toolchain_installed_in_isolated_env, get_combined_output_string, get_output, - get_stderr_string, setup_auto_install_scenario, setup_dry_run_scenario, setup_failure_scenario, + ensure_toolchain_installed_in_isolated_env, + get_combined_output_string, + get_output, + get_stderr_string, + setup_auto_install_scenario, + setup_dry_run_scenario, + setup_failure_scenario, }; use super::{prebuilt_markers::PREBUILT_INSTALL_MARKER, support::workspace_root}; @@ -86,9 +91,7 @@ fn assert_stderr_contains( #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -pub fn world() -> ToolchainWorld { - ToolchainWorld::default() -} +pub fn world() -> ToolchainWorld { ToolchainWorld::default() } // --------------------------------------------------------------------------- // Given steps diff --git a/installer/tests/toolchain_steps/scenario_setup.rs b/installer/tests/toolchain_steps/scenario_setup.rs index 872d2e526..ccba940ce 100644 --- a/installer/tests/toolchain_steps/scenario_setup.rs +++ b/installer/tests/toolchain_steps/scenario_setup.rs @@ -12,7 +12,9 @@ use std::{ use tempfile::TempDir; use crate::support::{ - is_toolchain_installed, is_toolchain_installed_in_env, pinned_toolchain_channel, + is_toolchain_installed, + is_toolchain_installed_in_env, + pinned_toolchain_channel, setup_isolated_rustup, }; diff --git a/src/config.rs b/src/config.rs index d223dfdcd..0314448f1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -56,7 +56,8 @@ impl SharedConfig { #[cfg(not(feature = "dylint-driver"))] { panic!( - "`SharedConfig::load` uses the Dylint loader; use `SharedConfig::load_with` to inject a stub when testing" + "`SharedConfig::load` uses the Dylint loader; use `SharedConfig::load_with` to \ + inject a stub when testing" ); } } @@ -91,9 +92,7 @@ impl SharedConfig { /// Whitespace-only values are treated as absent to avoid surprising /// behaviour when `dylint.toml` is templated or patched. #[must_use] - pub fn locale(&self) -> Option<&str> { - normalize_locale(self.locale.as_deref()) - } + pub fn locale(&self) -> Option<&str> { normalize_locale(self.locale.as_deref()) } } /// Settings that influence the forthcoming `module_max_lines` lint. @@ -106,9 +105,7 @@ pub struct ModuleMaxLinesConfig { } impl ModuleMaxLinesConfig { - const fn default_max_lines() -> usize { - 400 - } + const fn default_max_lines() -> usize { 400 } } impl Default for ModuleMaxLinesConfig { @@ -121,9 +118,10 @@ impl Default for ModuleMaxLinesConfig { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] fn defaults_match_the_suite_baseline() { let config = SharedConfig::default(); diff --git a/src/hir/mod.rs b/src/hir/mod.rs index 0d2de1ec0..908be583c 100644 --- a/src/hir/mod.rs +++ b/src/hir/mod.rs @@ -1,7 +1,6 @@ //! Helpers for working with HIR constructs shared across Whitaker lints. -use std::collections::HashSet; -use std::sync::LazyLock; +use std::{collections::HashSet, sync::LazyLock}; use rustc_ast::AttrStyle; use rustc_hir as hir; @@ -188,7 +187,7 @@ pub fn collect_harness_test_functions(cx: &LateContext<'_>) -> HashSet( /// /// Two independent kinds of evidence qualify a module: /// -/// 1. An explicit `RSTEST_HARNESS_DESCRIPTOR` const. This marker is unambiguous -/// rstest-synthesis evidence — no hand-authored test module emits it — so it -/// qualifies regardless of the module's expansion provenance. Manual -/// regression fixtures that cannot run the real proc-macro rely on it. -/// 2. The inner `fn`/`const` harness-descriptor pair that `rustc --test` -/// synthesizes for a `#[test]` function. This pair, on its own, is *not* -/// rstest-specific: the `--test` harness emits the same-named, same-span -/// `const` descriptor for **any** `#[test]` function, so a hand-authored -/// `mod foo { #[test] fn bar() {} }` sitting next to an ordinary `fn foo` -/// has an identical HIR shape and would otherwise wrongly exempt `fn foo`. +/// 1. An explicit `RSTEST_HARNESS_DESCRIPTOR` const. This marker is unambiguous rstest-synthesis +/// evidence — no hand-authored test module emits it — so it qualifies regardless of the module's +/// expansion provenance. Manual regression fixtures that cannot run the real proc-macro rely on +/// it. +/// 2. The inner `fn`/`const` harness-descriptor pair that `rustc --test` synthesizes for a +/// `#[test]` function. This pair, on its own, is *not* rstest-specific: the `--test` harness +/// emits the same-named, same-span `const` descriptor for **any** `#[test]` function, so a +/// hand-authored `mod foo { #[test] fn bar() {} }` sitting next to an ordinary `fn foo` has an +/// identical HIR shape and would otherwise wrongly exempt `fn foo`. /// /// The distinguishing invariant is expansion provenance: rstest generates the /// companion module through its attribute proc-macro, so `module_item.span` diff --git a/src/hir/tests.rs b/src/hir/tests.rs index 8456daf18..971514785 100644 --- a/src/hir/tests.rs +++ b/src/hir/tests.rs @@ -12,22 +12,31 @@ //! regressions, which exercise this detection path end-to-end with real rstest //! expansion output: //! - `crates/no_expect_outside_tests/examples/pass_expect_in_rstest_harness.rs` -//! - `crates/no_expect_outside_tests/src/lib_ui_tests.rs` -//! (`example_compiles_under_test_harness`) +//! - `crates/no_expect_outside_tests/src/lib_ui_tests.rs` (`example_compiles_under_test_harness`) -use super::{recover_user_editable_hir_span, span_recovery_frames}; use rstest::{fixture, rstest}; use rustc_data_structures::stable_hash::{ - RawDefId, RawDefPathHash, RawSpan, StableHashControls, StableHashCtxt, StableHasher, + RawDefId, + RawDefPathHash, + RawSpan, + StableHashControls, + StableHashCtxt, + StableHasher, +}; +use rustc_span::{ + BytePos, + DUMMY_SP, + Span, + SyntaxContext, + edition::Edition, + hygiene::{ExpnData, ExpnKind, LocalExpnId, MacroKind, Transparency}, + sym, }; -use rustc_span::edition::Edition; -use rustc_span::hygiene::{ExpnData, ExpnKind, LocalExpnId, MacroKind, Transparency}; -use rustc_span::{BytePos, DUMMY_SP, Span, SyntaxContext, sym}; use whitaker_common::SpanRecoveryFrame; -fn test_span(lo: u32, hi: u32) -> Span { - Span::with_root_ctxt(BytePos(lo), BytePos(hi)) -} +use super::{recover_user_editable_hir_span, span_recovery_frames}; + +fn test_span(lo: u32, hi: u32) -> Span { Span::with_root_ctxt(BytePos(lo), BytePos(hi)) } #[derive(Clone, Copy)] struct TestHashStableContext; @@ -35,9 +44,7 @@ struct TestHashStableContext; impl StableHashCtxt for TestHashStableContext { fn stable_hash_span(&mut self, _span: RawSpan, _hasher: &mut StableHasher) {} - fn def_path_hash(&self, _def_id: RawDefId) -> RawDefPathHash { - RawDefPathHash([0; 16]) - } + fn def_path_hash(&self, _def_id: RawDefId) -> RawDefPathHash { RawDefPathHash([0; 16]) } fn stable_hash_controls(&self) -> StableHashControls { StableHashControls { hash_spans: false } diff --git a/src/lib.rs b/src/lib.rs index 73d71456a..0e8eedd07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,12 +18,13 @@ pub mod testing; pub use config::{ModuleMaxLinesConfig, SharedConfig}; #[cfg(feature = "dylint-driver")] pub use hir::{ - module_body_span, module_header_span, recover_user_editable_hir_span, span_recovery_frames, + module_body_span, + module_header_span, + recover_user_editable_hir_span, + span_recovery_frames, }; pub use lints::{LintCrateTemplate, TemplateError, TemplateFiles}; /// Returns a greeting for the library. #[must_use] -pub const fn greet() -> &'static str { - "Hello from Whitaker!" -} +pub const fn greet() -> &'static str { "Hello from Whitaker!" } diff --git a/src/lints/template/mod.rs b/src/lints/template/mod.rs index fb7bd9d6f..4cf6db5ea 100644 --- a/src/lints/template/mod.rs +++ b/src/lints/template/mod.rs @@ -34,9 +34,7 @@ impl TemplateFiles { /// assert!(files.manifest().contains("name = \"demo_lint\"")); /// ``` #[must_use] - pub fn manifest(&self) -> &str { - &self.manifest - } + pub fn manifest(&self) -> &str { &self.manifest } /// Returns the generated `src/lib.rs` source. /// @@ -52,9 +50,7 @@ impl TemplateFiles { /// assert!(files.lib_rs().contains("pub struct DemoLint")); /// ``` #[must_use] - pub fn lib_rs(&self) -> &str { - &self.lib_rs - } + pub fn lib_rs(&self) -> &str { &self.lib_rs } /// Parses the manifest into a TOML [`toml::Value`] for inspection. /// @@ -74,10 +70,7 @@ impl TemplateFiles { /// .manifest_document() /// .expect("generated manifest parses"); /// - /// assert_eq!( - /// manifest["package"]["name"].as_str(), - /// Some("demo_lint") - /// ); + /// assert_eq!(manifest["package"]["name"].as_str(), Some("demo_lint")); /// ``` pub fn manifest_document(&self) -> Result { toml::from_str(self.manifest()) @@ -98,7 +91,8 @@ pub enum TemplateError { }, /// Crate names may contain only ASCII lowercase letters, digits, `-`, or `_`. #[error( - "crate name may only contain lowercase ASCII letters, digits, '-' or '_' (invalid `{character}`)" + "crate name may only contain lowercase ASCII letters, digits, '-' or '_' (invalid \ + `{character}`)" )] InvalidCrateNameCharacter { /// Character that violated the allowed set. @@ -171,27 +165,19 @@ impl LintCrateTemplate { /// Returns the crate name used by the template. #[must_use] - pub fn crate_name(&self) -> &str { - &self.crate_name - } + pub fn crate_name(&self) -> &str { &self.crate_name } /// Returns the lint constant derived from the crate name. #[must_use] - pub fn lint_constant(&self) -> &str { - &self.lint_constant - } + pub fn lint_constant(&self) -> &str { &self.lint_constant } /// Returns the name of the lint pass struct. #[must_use] - pub fn pass_struct(&self) -> &str { - &self.pass_struct - } + pub fn pass_struct(&self) -> &str { &self.pass_struct } /// Returns the UI tests directory used by the template. #[must_use] - pub fn ui_tests_directory(&self) -> &str { - &self.ui_tests_directory - } + pub fn ui_tests_directory(&self) -> &str { &self.ui_tests_directory } /// Render the template into manifest and source files. #[must_use] @@ -210,9 +196,10 @@ impl LintCrateTemplate { #[cfg(test)] mod tests { - use super::*; use toml::Value; + use super::*; + #[test] fn template_rejects_invalid_crate_name() { let Err(error) = LintCrateTemplate::new("1invalid") else { diff --git a/src/lints/template/validation.rs b/src/lints/template/validation.rs index 66abaf442..1a2b1badd 100644 --- a/src/lints/template/validation.rs +++ b/src/lints/template/validation.rs @@ -142,10 +142,10 @@ pub(crate) fn normalize_ui_directory(input: &str) -> Result(callback: impl FnOnce() -> R) -> R { } #[cfg(not(windows))] -fn with_vcpkg_root(callback: impl FnOnce() -> R) -> R { - callback() -} +fn with_vcpkg_root(callback: impl FnOnce() -> R) -> R { callback() } fn directory_is_rooted(path: &Utf8Path) -> bool { #[cfg(windows)] diff --git a/src/testing/ui/tests.rs b/src/testing/ui/tests.rs index 2ef12c3ac..956e45091 100644 --- a/src/testing/ui/tests.rs +++ b/src/testing/ui/tests.rs @@ -1,12 +1,16 @@ //! Tests that verify the UI harness runner validates inputs and propagates //! errors from custom runners. -use super::{HarnessError, run_with_runner}; +use std::{ + env, + sync::{Mutex, MutexGuard, OnceLock}, +}; + use camino::{Utf8Path, Utf8PathBuf}; use rstest::rstest; -use std::env; -use std::sync::{Mutex, MutexGuard, OnceLock}; use whitaker_common::test_support::with_env_var; +use super::{HarnessError, run_with_runner}; + #[rstest] #[case( " ", diff --git a/src/testing/ui/toolchain.rs b/src/testing/ui/toolchain.rs index 4d7ed8d80..c502b21e8 100644 --- a/src/testing/ui/toolchain.rs +++ b/src/testing/ui/toolchain.rs @@ -6,7 +6,9 @@ //! to refresh that copy. use std::{ - env, fmt, fs, + env, + fmt, + fs, io::Cursor, path::PathBuf, process::{Command, Output}, @@ -27,17 +29,11 @@ impl CrateName { clippy::missing_const_for_fn, reason = "String allocations require runtime heap access" )] - fn new_unchecked(name: String) -> Self { - Self(name) - } + fn new_unchecked(name: String) -> Self { Self(name) } - pub const fn as_str(&self) -> &str { - self.0.as_str() - } + pub const fn as_str(&self) -> &str { self.0.as_str() } - pub fn into_inner(self) -> String { - self.0 - } + pub fn into_inner(self) -> String { self.0 } } impl TryFrom<&str> for CrateName { @@ -55,15 +51,11 @@ impl TryFrom<&str> for CrateName { impl TryFrom for CrateName { type Error = CrateNameError; - fn try_from(value: String) -> Result { - Self::try_from(value.as_str()) - } + fn try_from(value: String) -> Result { Self::try_from(value.as_str()) } } impl AsRef for CrateName { - fn as_ref(&self) -> &str { - self.as_str() - } + fn as_ref(&self) -> &str { self.as_str() } } impl fmt::Display for CrateName { diff --git a/suite/src/driver.rs b/suite/src/driver.rs index 7ee02fd48..1eedcdcb6 100644 --- a/suite/src/driver.rs +++ b/suite/src/driver.rs @@ -1,13 +1,9 @@ //! Combined lint wiring for the suite cdylib. -use crate::lints::SUITE_LINT_DECLS; -use dylint_linting::dylint_library; -use rustc_lint::{Lint, LintStore, LintVec, declare_combined_late_lint_pass}; -use rustc_session::Session; - // Import constituent lint pass types required by `late_lint_methods!`. use bumpy_road_function::BumpyRoadFunction; use conditional_max_n_branches::ConditionalMaxNBranches; +use dylint_linting::dylint_library; use function_attrs_follow_docs::FunctionAttrsFollowDocs; use module_max_lines::ModuleMaxLines; use module_must_have_inner_docs::ModuleMustHaveInnerDocs; @@ -16,8 +12,12 @@ use no_std_fs_operations::NoStdFsOperations; use no_unwrap_or_else_panic::NoUnwrapOrElsePanic; #[cfg(feature = "experimental-rstest-helper-should-be-fixture")] use rstest_helper_should_be_fixture::RstestHelperShouldBeFixture; +use rustc_lint::{Lint, LintStore, LintVec, declare_combined_late_lint_pass}; +use rustc_session::Session; use test_must_not_have_example::TestMustNotHaveExample; +use crate::lints::SUITE_LINT_DECLS; + dylint_library!(); macro_rules! define_suite_pass { @@ -80,9 +80,7 @@ pub fn register_suite_lints(store: &mut LintStore) { /// assert!(names.contains(&"no_unwrap_or_else_panic".to_string())); /// ``` #[must_use] -pub const fn suite_lint_decls() -> &'static [&'static Lint] { - SUITE_LINT_DECLS -} +pub const fn suite_lint_decls() -> &'static [&'static Lint] { SUITE_LINT_DECLS } /// Initializes Dylint configuration then registers the suite lints. fn register_suite_entry(sess: &Session, store: &mut LintStore) { diff --git a/suite/tests/registration.rs b/suite/tests/registration.rs index 2c1f78865..8611a350e 100644 --- a/suite/tests/registration.rs +++ b/suite/tests/registration.rs @@ -33,14 +33,10 @@ impl RegistrationWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> RegistrationWorld { - RegistrationWorld::new() -} +fn world() -> RegistrationWorld { RegistrationWorld::new() } #[given("an empty lint store")] -fn given_empty_store(world: &RegistrationWorld) { - world.reset(); -} +fn given_empty_store(world: &RegistrationWorld) { world.reset(); } #[given("the suite lints are already registered")] fn given_already_registered(world: &RegistrationWorld) { @@ -115,11 +111,7 @@ fn then_registration_succeeds(world: &RegistrationWorld) { } #[scenario(path = "tests/features/suite_registration.feature", index = 0)] -fn scenario_registers_cleanly(world: RegistrationWorld) { - let _ = world; -} +fn scenario_registers_cleanly(world: RegistrationWorld) { let _ = world; } #[scenario(path = "tests/features/suite_registration.feature", index = 1)] -fn scenario_double_registration(world: RegistrationWorld) { - let _ = world; -} +fn scenario_double_registration(world: RegistrationWorld) { let _ = world; } diff --git a/tests/build_config.rs b/tests/build_config.rs index d1aa5df9c..bd7724145 100644 --- a/tests/build_config.rs +++ b/tests/build_config.rs @@ -1,7 +1,6 @@ //! Build configuration guards for dynamic linking expectations. -use std::fs; -use std::path::Path; +use std::{fs, path::Path}; use toml::Value; diff --git a/tests/config_loading.rs b/tests/config_loading.rs index 76dd862bd..ba315a072 100644 --- a/tests/config_loading.rs +++ b/tests/config_loading.rs @@ -1,10 +1,12 @@ //! Behaviour-driven tests for shared configuration loading. -use std::any::Any; -use std::cell::RefCell; -use std::convert::Infallible; -use std::panic::{AssertUnwindSafe, catch_unwind}; -use std::str::FromStr; +use std::{ + any::Any, + cell::RefCell, + convert::Infallible, + panic::{AssertUnwindSafe, catch_unwind}, + str::FromStr, +}; mod support; @@ -14,15 +16,13 @@ use support::locale::StepLocale; use whitaker::SharedConfig; use whitaker_common::i18n::normalize_locale; +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn config_source() -> RefCell> { - RefCell::new(None) -} +fn config_source() -> RefCell> { RefCell::new(None) } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn load_result() -> RefCell>> { - RefCell::new(None) -} +fn load_result() -> RefCell>> { RefCell::new(None) } fn panic_message(payload: Box) -> String { match payload.downcast::() { @@ -50,15 +50,11 @@ impl FromStr for ErrorSnippet { } impl AsRef for ErrorSnippet { - fn as_ref(&self) -> &str { - self.0.as_str() - } + fn as_ref(&self) -> &str { self.0.as_str() } } impl ErrorSnippet { - fn into_inner(self) -> String { - self.0 - } + fn into_inner(self) -> String { self.0 } } #[given("no configuration state has been prepared")] @@ -71,9 +67,7 @@ fn reset_state( } #[given("no workspace configuration overrides are provided")] -fn no_overrides(config_source: &RefCell>) { - config_source.borrow_mut().take(); -} +fn no_overrides(config_source: &RefCell>) { config_source.borrow_mut().take(); } #[given("the workspace config sets the module max line limit to {value}")] fn override_max_lines(config_source: &RefCell>, value: usize) { diff --git a/tests/lint_template.rs b/tests/lint_template.rs index 57437adaa..935183ec6 100644 --- a/tests/lint_template.rs +++ b/tests/lint_template.rs @@ -16,17 +16,11 @@ struct TemplateWorld { } impl TemplateWorld { - fn set_crate_name(&self, value: String) { - *self.crate_name.borrow_mut() = value; - } + fn set_crate_name(&self, value: String) { *self.crate_name.borrow_mut() = value; } - fn crate_name(&self) -> String { - self.crate_name.borrow().clone() - } + fn crate_name(&self) -> String { self.crate_name.borrow().clone() } - fn set_ui_directory(&self, value: String) { - *self.ui_directory.borrow_mut() = value; - } + fn set_ui_directory(&self, value: String) { *self.ui_directory.borrow_mut() = value; } fn render(&self) { let crate_name = self.crate_name.borrow().clone(); @@ -62,29 +56,21 @@ impl TemplateWorld { struct StepString(String); impl StepString { - fn into_inner(self) -> String { - self.0 - } + fn into_inner(self) -> String { self.0 } } impl From for StepString { - fn from(value: String) -> Self { - Self(value) - } + fn from(value: String) -> Self { Self(value) } } impl From for String { - fn from(value: StepString) -> Self { - value.0 - } + fn from(value: StepString) -> Self { value.0 } } impl std::str::FromStr for StepString { type Err = std::convert::Infallible; - fn from_str(input: &str) -> Result { - Ok(Self(input.to_owned())) - } + fn from_str(input: &str) -> Result { Ok(Self(input.to_owned())) } } #[fixture] @@ -96,9 +82,7 @@ fn world() -> TemplateWorld { } #[given("the lint crate name is blank")] -fn given_blank_name(world: &TemplateWorld) { - world.set_crate_name(String::new()); -} +fn given_blank_name(world: &TemplateWorld) { world.set_crate_name(String::new()); } #[given("the lint crate name is {name}")] fn given_crate_name(world: &TemplateWorld, name: StepString) { @@ -111,14 +95,10 @@ fn given_ui_directory(world: &TemplateWorld, directory: StepString) { } #[given("the UI tests directory is blank")] -fn given_blank_ui_directory(world: &TemplateWorld) { - world.set_ui_directory(String::new()); -} +fn given_blank_ui_directory(world: &TemplateWorld) { world.set_ui_directory(String::new()); } #[when("I render the lint crate template")] -fn when_render(world: &TemplateWorld) { - world.render(); -} +fn when_render(world: &TemplateWorld) { world.render(); } #[then("the manifest declares a cdylib crate type")] fn then_manifest_declares_cdylib(world: &TemplateWorld) { @@ -294,71 +274,43 @@ fn then_parent_directory_error(world: &TemplateWorld) { } #[scenario(path = "tests/features/lint_template.feature", index = 0)] -fn scenario_renders_manifest_and_source(world: TemplateWorld) { - let _ = world; -} +fn scenario_renders_manifest_and_source(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 1)] -fn scenario_renders_nested_ui_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_renders_nested_ui_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 2)] -fn scenario_renders_windows_ui_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_renders_windows_ui_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 3)] -fn scenario_rejects_blank_name(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_blank_name(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 4)] -fn scenario_rejects_non_letter_start(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_non_letter_start(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 5)] -fn scenario_rejects_trailing_separator(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_trailing_separator(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 6)] -fn scenario_rejects_absolute_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_absolute_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 7)] -fn scenario_rejects_absolute_windows_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_absolute_windows_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 8)] -fn scenario_rejects_unc_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_unc_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 9)] -fn scenario_rejects_drive_relative_windows_ui_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_drive_relative_windows_ui_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 10)] -fn scenario_rejects_parent_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_parent_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 11)] -fn scenario_rejects_invalid_character(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_invalid_character(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 12)] -fn scenario_rejects_blank_ui_directory(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_blank_ui_directory(world: TemplateWorld) { let _ = world; } #[scenario(path = "tests/features/lint_template.feature", index = 13)] -fn scenario_rejects_trailing_underscore(world: TemplateWorld) { - let _ = world; -} +fn scenario_rejects_trailing_underscore(world: TemplateWorld) { let _ = world; } diff --git a/tests/locale_resolution.rs b/tests/locale_resolution.rs index b06c12cfc..aacafb62b 100644 --- a/tests/locale_resolution.rs +++ b/tests/locale_resolution.rs @@ -1,7 +1,6 @@ //! Behaviour-driven tests covering locale resolution semantics. -use std::cell::RefCell; -use std::str::FromStr; +use std::{cell::RefCell, str::FromStr}; mod support; @@ -20,9 +19,7 @@ struct LocaleWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn world() -> LocaleWorld { - LocaleWorld::default() -} +fn world() -> LocaleWorld { LocaleWorld::default() } #[derive(Debug)] struct StepSource(LocaleSource); @@ -42,9 +39,7 @@ impl FromStr for StepSource { } impl StepSource { - const fn into_inner(self) -> LocaleSource { - self.0 - } + const fn into_inner(self) -> LocaleSource { self.0 } } fn resolved(world: &LocaleWorld) -> LocaleSelection { @@ -56,9 +51,7 @@ fn resolved(world: &LocaleWorld) -> LocaleSelection { } #[given("no explicit locale override is provided")] -fn no_explicit(world: &LocaleWorld) { - world.explicit.borrow_mut().take(); -} +fn no_explicit(world: &LocaleWorld) { world.explicit.borrow_mut().take(); } #[given("the explicit locale override is {value}")] fn set_explicit(world: &LocaleWorld, value: StepLocale) { @@ -66,9 +59,7 @@ fn set_explicit(world: &LocaleWorld, value: StepLocale) { } #[given("DYLINT_LOCALE is not set")] -fn clear_environment(world: &LocaleWorld) { - world.environment.borrow_mut().take(); -} +fn clear_environment(world: &LocaleWorld) { world.environment.borrow_mut().take(); } #[given("DYLINT_LOCALE is {value}")] fn set_environment(world: &LocaleWorld, value: StepLocale) { @@ -76,9 +67,7 @@ fn set_environment(world: &LocaleWorld, value: StepLocale) { } #[given("no configuration locale is provided")] -fn clear_configuration(world: &LocaleWorld) { - world.configuration.borrow_mut().take(); -} +fn clear_configuration(world: &LocaleWorld) { world.configuration.borrow_mut().take(); } #[given("the configuration locale is {value}")] fn set_configuration(world: &LocaleWorld, value: StepLocale) { @@ -127,26 +116,16 @@ fn assert_fallback_not_used(world: &LocaleWorld) { } #[scenario("tests/features/locale_resolution.feature", index = 0)] -fn scenario_fallback(world: LocaleWorld) { - let _ = world; -} +fn scenario_fallback(world: LocaleWorld) { let _ = world; } #[scenario("tests/features/locale_resolution.feature", index = 1)] -fn scenario_environment(world: LocaleWorld) { - let _ = world; -} +fn scenario_environment(world: LocaleWorld) { let _ = world; } #[scenario("tests/features/locale_resolution.feature", index = 2)] -fn scenario_configuration(world: LocaleWorld) { - let _ = world; -} +fn scenario_configuration(world: LocaleWorld) { let _ = world; } #[scenario("tests/features/locale_resolution.feature", index = 3)] -fn scenario_explicit(world: LocaleWorld) { - let _ = world; -} +fn scenario_explicit(world: LocaleWorld) { let _ = world; } #[scenario("tests/features/locale_resolution.feature", index = 4)] -fn scenario_whitespace(world: LocaleWorld) { - let _ = world; -} +fn scenario_whitespace(world: LocaleWorld) { let _ = world; } diff --git a/tests/nextest_ui_filter.rs b/tests/nextest_ui_filter.rs index b837e5125..0f748690b 100644 --- a/tests/nextest_ui_filter.rs +++ b/tests/nextest_ui_filter.rs @@ -12,8 +12,7 @@ //! `ui`, **not** `ui::ui`) and asserts that the nextest filter contains the //! clause needed to capture that pattern. -use std::fs; -use std::path::Path; +use std::{fs, path::Path}; use rstest::{fixture, rstest}; use toml::Value; @@ -112,9 +111,9 @@ fn serial_dylint_ui_filter_captures_integration_ui_binaries(serial_dylint_ui_ove // `test(ui::ui)` missed it because the test name is plain `ui`. assert!( filter.contains("(binary(ui) & test(=ui))"), - "the serial-dylint-ui filter must contain `(binary(ui) & test(=ui))` \ - to capture integration test binaries named `ui` with a top-level \ - `fn ui()` (e.g. {crates:?}); found filter: {filter}" + "the serial-dylint-ui filter must contain `(binary(ui) & test(=ui))` to capture \ + integration test binaries named `ui` with a top-level `fn ui()` (e.g. {crates:?}); found \ + filter: {filter}" ); } diff --git a/tests/support/locale.rs b/tests/support/locale.rs index 77e71fa38..631edeb5c 100644 --- a/tests/support/locale.rs +++ b/tests/support/locale.rs @@ -4,8 +4,7 @@ //! stripping whitespace and quotation marks to normalize values before they are //! passed to locale resolution and configuration helpers. -use std::convert::Infallible; -use std::str::FromStr; +use std::{convert::Infallible, str::FromStr}; /// Wrapper for locale values supplied via behaviour-driven test steps. #[derive(Clone, Debug)] @@ -28,7 +27,5 @@ impl FromStr for StepLocale { impl StepLocale { /// Consumes the step value, yielding the parsed string. - pub fn into_inner(self) -> String { - self.raw - } + pub fn into_inner(self) -> String { self.raw } } diff --git a/tests/ui_harness.rs b/tests/ui_harness.rs index 408120c29..ae8b5ab0d 100644 --- a/tests/ui_harness.rs +++ b/tests/ui_harness.rs @@ -11,29 +11,21 @@ use whitaker::testing::ui::{HarnessError, run_with_runner}; struct StepString(String); impl StepString { - fn into_inner(self) -> String { - self.0 - } + fn into_inner(self) -> String { self.0 } } impl From for StepString { - fn from(value: std::string::String) -> Self { - Self(value) - } + fn from(value: std::string::String) -> Self { Self(value) } } impl From for String { - fn from(value: StepString) -> Self { - value.0 - } + fn from(value: StepString) -> Self { value.0 } } impl std::str::FromStr for StepString { type Err = Infallible; - fn from_str(input: &str) -> Result { - Ok(Self(input.to_owned())) - } + fn from_str(input: &str) -> Result { Ok(Self(input.to_owned())) } } #[derive(Debug)] @@ -59,14 +51,10 @@ impl Default for HarnessWorld { #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] -fn harness_world() -> HarnessWorld { - HarnessWorld::default() -} +fn harness_world() -> HarnessWorld { HarnessWorld::default() } #[given("the harness has no crate name")] -fn clear_crate(harness_world: &HarnessWorld) { - harness_world.crate_name.borrow_mut().clear(); -} +fn clear_crate(harness_world: &HarnessWorld) { harness_world.crate_name.borrow_mut().clear(); } #[given("the harness is prepared for crate {name}")] fn prepare_crate(harness_world: &HarnessWorld, name: String) { @@ -170,33 +158,21 @@ fn assert_runner_failure(harness_world: &HarnessWorld, snippet: StepString) { } #[scenario(path = "tests/features/ui_harness.feature", index = 0)] -fn scenario_runs_successfully(harness_world: HarnessWorld) { - let _ = harness_world; -} +fn scenario_runs_successfully(harness_world: HarnessWorld) { let _ = harness_world; } #[scenario(path = "tests/features/ui_harness.feature", index = 1)] -fn scenario_rejects_empty_crate(harness_world: HarnessWorld) { - let _ = harness_world; -} +fn scenario_rejects_empty_crate(harness_world: HarnessWorld) { let _ = harness_world; } #[scenario(path = "tests/features/ui_harness.feature", index = 2)] -fn scenario_rejects_absolute_directory(harness_world: HarnessWorld) { - let _ = harness_world; -} +fn scenario_rejects_absolute_directory(harness_world: HarnessWorld) { let _ = harness_world; } #[scenario(path = "tests/features/ui_harness.feature", index = 3)] -fn scenario_propagates_runner_failure(harness_world: HarnessWorld) { - let _ = harness_world; -} +fn scenario_propagates_runner_failure(harness_world: HarnessWorld) { let _ = harness_world; } #[cfg(windows)] #[scenario(path = "tests/features/ui_harness.feature", index = 4)] -fn scenario_rejects_unc_directory(harness_world: HarnessWorld) { - let _ = harness_world; -} +fn scenario_rejects_unc_directory(harness_world: HarnessWorld) { let _ = harness_world; } #[cfg(windows)] #[scenario(path = "tests/features/ui_harness.feature", index = 5)] -fn scenario_rejects_drive_relative_directory(harness_world: HarnessWorld) { - let _ = harness_world; -} +fn scenario_rejects_drive_relative_directory(harness_world: HarnessWorld) { let _ = harness_world; }