From ef67b2c3f9094840367ee63d55015540bc699327 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 17 Aug 2026 02:44:07 +0200 Subject: [PATCH 01/21] Harden workspace lint configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote the root package's clippy policy to `[workspace.lints]` so every member crate — including the rustc proxy shims and support crates — is held to the same standard, and extend it: - add `disallowed_methods` and `missing_assert_message` to the clippy set; - forbid `unsafe_code` workspace-wide; - add `[lints] workspace = true` to every member crate, removing the now-redundant local `expect_used`/`unwrap_used` and `unexpected_cfgs` tables; - introduce `.rustfmt.toml` with the estate's unstable nightly formatting options; - run workspace doctests (`cargo test --workspace --doc --all-features`) from the `test` target with warnings denied; - request the `rust-analyzer` component in `rust-toolchain.toml`. The tree is brought into compliance by the preparatory stack beneath this branch; this commit records the policy itself. --- .rustfmt.toml | 14 ++++++++++++++ Cargo.toml | 8 +++++++- Makefile | 1 + common/Cargo.toml | 8 ++------ crates/bumpy_road_function/Cargo.toml | 3 +++ crates/clippy_utils/Cargo.toml | 3 +++ crates/conditional_max_n_branches/Cargo.toml | 3 +++ crates/function_attrs_follow_docs/Cargo.toml | 3 +++ crates/module_max_lines/Cargo.toml | 3 +++ crates/module_must_have_inner_docs/Cargo.toml | 3 +++ crates/no_expect_outside_tests/Cargo.toml | 3 +++ crates/no_std_fs_operations/Cargo.toml | 3 +++ crates/no_unwrap_or_else_panic/Cargo.toml | 5 ++--- crates/rstest_helper_should_be_fixture/Cargo.toml | 3 +++ crates/rustc_ast/Cargo.toml | 3 +++ crates/rustc_attr_data_structures/Cargo.toml | 3 +++ crates/rustc_hir/Cargo.toml | 3 +++ crates/rustc_lint/Cargo.toml | 3 +++ crates/rustc_middle/Cargo.toml | 3 +++ crates/rustc_session/Cargo.toml | 3 +++ crates/rustc_span/Cargo.toml | 3 +++ crates/test_must_not_have_example/Cargo.toml | 3 +++ crates/whitaker_sarif/Cargo.toml | 5 ++--- rust-toolchain.toml | 2 +- suite/Cargo.toml | 3 +++ 25 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..f7ad026ba --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,14 @@ +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 diff --git a/Cargo.toml b/Cargo.toml index 7e79a2660..93e60e41e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,10 @@ rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } dylint_testing = { workspace = true } -[lints.clippy] +[lints] +workspace = true + +[workspace.lints.clippy] pedantic = { level = "warn", priority = -1 } # 1. hygiene @@ -109,8 +112,10 @@ allow_attributes = "deny" allow_attributes_without_reason = "deny" blanket_clippy_restriction_lints = "deny" cognitive_complexity = "deny" +disallowed_methods = "deny" needless_pass_by_value = "deny" implicit_hasher = "deny" +missing_assert_message = "deny" # 2. debugging leftovers dbg_macro = "deny" @@ -166,6 +171,7 @@ result_large_err = "deny" [workspace.lints.rust] unknown_lints = "deny" renamed_and_removed_lints = "deny" +unsafe_code = "forbid" missing_docs = "deny" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } diff --git a/Makefile b/Makefile index b5cf88850..dfef1468c 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,7 @@ test: ## Run tests with warnings treated as errors WHITAKER_BACKUP=""; \ fi; \ RUSTFLAGS="-C prefer-dynamic -Z force-unstable-if-unmarked $(RUST_FLAGS)" $(CARGO) $(TEST_RUNNER) $(CARGO_LOCKED) $(TEST_CARGO_FLAGS) $(BUILD_JOBS) $(if $(NEXTEST_PROFILE),--profile $(NEXTEST_PROFILE)); \ + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) test --workspace --doc --all-features $(BUILD_JOBS); \ if [ "$${ACT_WORKFLOW_TESTS:-0}" = "1" ]; then \ $(MAKE) workflow-test; \ fi diff --git a/common/Cargo.toml b/common/Cargo.toml index 76b52eb51..6cea5d915 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -35,9 +35,5 @@ regex = "1.10.4" logtest = "2.0.0" -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } - -[lints.clippy] -expect_used = "deny" -unwrap_used = "deny" +[lints] +workspace = true diff --git a/crates/bumpy_road_function/Cargo.toml b/crates/bumpy_road_function/Cargo.toml index 1b1e1e3d6..01ba78caa 100644 --- a/crates/bumpy_road_function/Cargo.toml +++ b/crates/bumpy_road_function/Cargo.toml @@ -51,3 +51,6 @@ whitaker = { workspace = true } whitaker-common = { workspace = true } camino = { workspace = true } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/clippy_utils/Cargo.toml b/crates/clippy_utils/Cargo.toml index 8c2f68b86..01243cdd6 100644 --- a/crates/clippy_utils/Cargo.toml +++ b/crates/clippy_utils/Cargo.toml @@ -10,3 +10,6 @@ publish = false rustc_hir = { workspace = true } rustc_lint = { workspace = true } rustc_span = { workspace = true } + +[lints] +workspace = true diff --git a/crates/conditional_max_n_branches/Cargo.toml b/crates/conditional_max_n_branches/Cargo.toml index bcbc70f97..1017d300b 100644 --- a/crates/conditional_max_n_branches/Cargo.toml +++ b/crates/conditional_max_n_branches/Cargo.toml @@ -52,3 +52,6 @@ rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } dylint_testing = { workspace = true } serial_test = "4.0.1" + +[lints] +workspace = true diff --git a/crates/function_attrs_follow_docs/Cargo.toml b/crates/function_attrs_follow_docs/Cargo.toml index bb4d06c81..7f776a42b 100644 --- a/crates/function_attrs_follow_docs/Cargo.toml +++ b/crates/function_attrs_follow_docs/Cargo.toml @@ -46,3 +46,6 @@ rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } dylint_testing = { workspace = true } serial_test = "4.0.1" + +[lints] +workspace = true diff --git a/crates/module_max_lines/Cargo.toml b/crates/module_max_lines/Cargo.toml index 7001aca96..a181deac2 100644 --- a/crates/module_max_lines/Cargo.toml +++ b/crates/module_max_lines/Cargo.toml @@ -41,3 +41,6 @@ rstest-bdd-macros = { workspace = true } dylint_testing = { workspace = true } tempfile = "3.14.0" glob = "0.3" + +[lints] +workspace = true diff --git a/crates/module_must_have_inner_docs/Cargo.toml b/crates/module_must_have_inner_docs/Cargo.toml index 284e79b42..b97955b4a 100644 --- a/crates/module_must_have_inner_docs/Cargo.toml +++ b/crates/module_must_have_inner_docs/Cargo.toml @@ -39,3 +39,6 @@ rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } dylint_testing = { workspace = true } serial_test = "4.0.1" + +[lints] +workspace = true diff --git a/crates/no_expect_outside_tests/Cargo.toml b/crates/no_expect_outside_tests/Cargo.toml index 8e32153f3..76e1a3e8d 100644 --- a/crates/no_expect_outside_tests/Cargo.toml +++ b/crates/no_expect_outside_tests/Cargo.toml @@ -44,3 +44,6 @@ dylint_testing = { workspace = true } camino = { workspace = true } tokio = { version = "1.50.0", features = ["macros", "rt", "rt-multi-thread"] } temp-env = { workspace = true } + +[lints] +workspace = true diff --git a/crates/no_std_fs_operations/Cargo.toml b/crates/no_std_fs_operations/Cargo.toml index 08f46a848..ca97638a1 100644 --- a/crates/no_std_fs_operations/Cargo.toml +++ b/crates/no_std_fs_operations/Cargo.toml @@ -48,3 +48,6 @@ serial_test = "4.0.1" serde_json = { workspace = true } tempfile = { workspace = true } toml = { workspace = true } + +[lints] +workspace = true diff --git a/crates/no_unwrap_or_else_panic/Cargo.toml b/crates/no_unwrap_or_else_panic/Cargo.toml index 2b30b4ec3..2286b44f1 100644 --- a/crates/no_unwrap_or_else_panic/Cargo.toml +++ b/crates/no_unwrap_or_else_panic/Cargo.toml @@ -46,6 +46,5 @@ rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } temp-env = { workspace = true } -[lints.clippy] -expect_used = "deny" -unwrap_used = "deny" +[lints] +workspace = true diff --git a/crates/rstest_helper_should_be_fixture/Cargo.toml b/crates/rstest_helper_should_be_fixture/Cargo.toml index 611763185..e44bb18df 100644 --- a/crates/rstest_helper_should_be_fixture/Cargo.toml +++ b/crates/rstest_helper_should_be_fixture/Cargo.toml @@ -53,3 +53,6 @@ proptest = { workspace = true } rstest = { workspace = true } toml = { workspace = true } trybuild = { workspace = true } + +[lints] +workspace = true diff --git a/crates/rustc_ast/Cargo.toml b/crates/rustc_ast/Cargo.toml index 765672166..c09414252 100644 --- a/crates/rustc_ast/Cargo.toml +++ b/crates/rustc_ast/Cargo.toml @@ -6,3 +6,6 @@ publish = false [lib] test = false + +[lints] +workspace = true diff --git a/crates/rustc_attr_data_structures/Cargo.toml b/crates/rustc_attr_data_structures/Cargo.toml index e89341cdd..91f9baf1c 100644 --- a/crates/rustc_attr_data_structures/Cargo.toml +++ b/crates/rustc_attr_data_structures/Cargo.toml @@ -6,3 +6,6 @@ publish = false [lib] test = false + +[lints] +workspace = true diff --git a/crates/rustc_hir/Cargo.toml b/crates/rustc_hir/Cargo.toml index dabd8d654..e3bf64bfd 100644 --- a/crates/rustc_hir/Cargo.toml +++ b/crates/rustc_hir/Cargo.toml @@ -6,3 +6,6 @@ publish = false [lib] test = false + +[lints] +workspace = true diff --git a/crates/rustc_lint/Cargo.toml b/crates/rustc_lint/Cargo.toml index a08bfa0fe..72ba107b2 100644 --- a/crates/rustc_lint/Cargo.toml +++ b/crates/rustc_lint/Cargo.toml @@ -6,3 +6,6 @@ publish = false [lib] test = false + +[lints] +workspace = true diff --git a/crates/rustc_middle/Cargo.toml b/crates/rustc_middle/Cargo.toml index 236450792..da219a41a 100644 --- a/crates/rustc_middle/Cargo.toml +++ b/crates/rustc_middle/Cargo.toml @@ -6,3 +6,6 @@ publish = false [lib] test = false + +[lints] +workspace = true diff --git a/crates/rustc_session/Cargo.toml b/crates/rustc_session/Cargo.toml index bc54bb631..adba42fd1 100644 --- a/crates/rustc_session/Cargo.toml +++ b/crates/rustc_session/Cargo.toml @@ -6,3 +6,6 @@ publish = false [lib] test = false + +[lints] +workspace = true diff --git a/crates/rustc_span/Cargo.toml b/crates/rustc_span/Cargo.toml index 36dfd7cda..ba8fe723e 100644 --- a/crates/rustc_span/Cargo.toml +++ b/crates/rustc_span/Cargo.toml @@ -6,3 +6,6 @@ publish = false [lib] test = false + +[lints] +workspace = true diff --git a/crates/test_must_not_have_example/Cargo.toml b/crates/test_must_not_have_example/Cargo.toml index 18605b91a..58283da3a 100644 --- a/crates/test_must_not_have_example/Cargo.toml +++ b/crates/test_must_not_have_example/Cargo.toml @@ -39,3 +39,6 @@ rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } dylint_testing = { workspace = true } camino = { workspace = true } + +[lints] +workspace = true diff --git a/crates/whitaker_sarif/Cargo.toml b/crates/whitaker_sarif/Cargo.toml index 0e44258b3..515d90cf2 100644 --- a/crates/whitaker_sarif/Cargo.toml +++ b/crates/whitaker_sarif/Cargo.toml @@ -24,6 +24,5 @@ rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } tempfile = { workspace = true } -[lints.clippy] -expect_used = "deny" -unwrap_used = "deny" +[lints] +workspace = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 39266bc69..ee7543464 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "nightly-2026-05-28" -components = ["rustfmt", "clippy", "rustc-dev", "llvm-tools-preview", "rust-src"] +components = ["rustfmt", "clippy", "rustc-dev", "llvm-tools-preview", "rust-src", "rust-analyzer"] diff --git a/suite/Cargo.toml b/suite/Cargo.toml index 780108db7..1bf70a37e 100644 --- a/suite/Cargo.toml +++ b/suite/Cargo.toml @@ -52,3 +52,6 @@ rstest_helper_should_be_fixture = { path = "../crates/rstest_helper_should_be_fi rstest = { workspace = true } rstest-bdd = { workspace = true } rstest-bdd-macros = { workspace = true } + +[lints] +workspace = true From fa00702692d0af3bfd04903e33bd9eb0458bed42 Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 18 Aug 2026 05:13:44 +0200 Subject: [PATCH 02/21] Fix workspace-lint violations across support crates Bring the tree toward compliance with the hardened `[workspace.lints]` policy, fixing findings at source rather than suppressing them: - apply the machine-applicable clippy fixes across the workspace (`const fn`, owned-string clones, `use_self`, import granularity, format-args interpolation); - restructure panic-prone indexing in the decomposition-advice, LCOM4, clone-detection, and signal modules with iterator and `get`-based rewrites that preserve behaviour; - replace the unsafe `EnvVarGuard`/`LocaleOverride` environment guards with scoped `temp_env`-backed helpers (`with_env_var`, `with_env_var_removed`, `with_locale`) and rework the UI runner and all call sites onto them, satisfying `unsafe_code = "forbid"`; - declare the suite's Dylint entry point through a new `declare_dylint_register_entry!` macro in whitaker-common so the `#[unsafe(no_mangle)]` attribute expands from an external macro; - replace the installer's `libc::geteuid()` root probe with the safe `rustix::process::geteuid()` and drop the now-unused libc dependency; - document struct fields, `# Errors`, and `# Panics` contracts; rename shadowed bindings; add assertion messages; make non-test helpers fallible instead of panicking; - scope the one sanctioned `#[expect(clippy::float_arithmetic)]` to the bumpy-road signal functions whose weights are user-configured floats; - split `lint` into `lint-clippy` and `lint-whitaker` so the Whitaker suite lints the support crates in the gate and CI installs the pinned whitaker-installer; - refresh the spelling exemptions for moved SARIF identifier lines and correct two non-oxendict spellings. --- .github/workflows/ci.yml | 21 ++ AGENTS.md | 18 +- Cargo.lock | 4 +- Cargo.toml | 1 - Makefile | 17 +- common/Cargo.toml | 1 + common/src/attributes/attribute.rs | 4 +- common/src/attributes/tests.rs | 4 +- common/src/brain_trait_metrics/diagnostic.rs | 14 +- .../brain_trait_metrics/diagnostic_tests.rs | 25 +- common/src/brain_trait_metrics/evaluation.rs | 24 +- .../brain_trait_metrics/evaluation_tests.rs | 23 +- 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 +- .../cognitive_complexity_tests.rs | 29 +-- common/src/brain_type_metrics/diagnostic.rs | 67 +++-- .../brain_type_metrics/diagnostic_tests.rs | 13 +- common/src/brain_type_metrics/evaluation.rs | 30 +-- .../brain_type_metrics/evaluation_tests.rs | 21 +- .../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 | 87 +++++-- common/src/context.rs | 2 +- common/src/decomposition_advice/community.rs | 114 +++++---- common/src/decomposition_advice/note.rs | 18 +- common/src/decomposition_advice/note_tests.rs | 10 +- common/src/decomposition_advice/profile.rs | 10 +- common/src/decomposition_advice/suggestion.rs | 47 ++-- .../decomposition_advice/tests/adjacency.rs | 21 +- .../{tests.rs => tests/mod.rs} | 53 ++-- .../decomposition_advice/tests/propagation.rs | 10 +- .../tests/test_fixtures.rs | 7 +- 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 | 3 +- common/src/i18n/loader.rs | 78 ++++-- common/src/i18n/locales.rs | 14 +- common/src/i18n/mod.rs | 4 + common/src/i18n/selection.rs | 13 +- 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/rstest/tests/fingerprint_props.rs | 5 +- common/src/rstest/tests/mod.rs | 14 +- common/src/span.rs | 13 +- common/src/test_support/decomposition.rs | 6 + .../test_support/decomposition_adjacency.rs | 8 +- .../decomposition_label_propagation.rs | 4 +- .../decomposition_vector_algebra.rs | 8 +- common/src/test_support/fixtures.rs | 16 +- common/src/test_support/mod.rs | 231 +++++------------- common/src/test_support/ui.rs | 74 ++++-- .../tests/brain_trait_evaluation_behaviour.rs | 26 +- common/tests/brain_trait_metrics_behaviour.rs | 34 ++- .../tests/brain_type_evaluation_behaviour.rs | 8 +- common/tests/brain_type_metrics_behaviour.rs | 6 +- .../tests/cognitive_complexity_behaviour.rs | 2 +- common/tests/complexity_signal_behaviour.rs | 55 ++++- common/tests/cosine_threshold_behaviour.rs | 2 +- .../decomposition_adjacency_behaviour.rs | 6 +- .../tests/decomposition_advice_behaviour.rs | 52 ++-- ...ecomposition_diagnostic_notes_behaviour.rs | 52 ++-- .../decomposition_vector_algebra_behaviour.rs | 2 +- common/tests/i18n_behaviour.rs | 47 ++-- common/tests/lcom4_behaviour.rs | 2 +- common/tests/localizer_helpers.rs | 66 +++-- common/tests/rstest_detection_behaviour.rs | 8 +- common/tests/support/i18n_helpers.rs | 10 +- crates/bumpy_road_function/src/analysis.rs | 4 +- crates/bumpy_road_function/src/driver.rs | 10 +- .../bumpy_road_function/src/driver/config.rs | 2 +- .../src/driver/diagnostic.rs | 8 +- .../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 +- .../src/tests/ui.rs | 7 +- crates/module_max_lines/src/driver.rs | 11 +- .../module_must_have_inner_docs/src/driver.rs | 10 +- .../src/driver/parser.rs | 10 +- .../src/tests/ui.rs | 7 +- .../fail_expect_in_rstest_non_test_module.rs | 4 +- crates/no_expect_outside_tests/src/context.rs | 32 +-- .../src/context/tests.rs | 2 +- .../src/dependency_rlib_tests.rs | 4 +- .../src/diagnostics.rs | 30 ++- .../no_expect_outside_tests/src/driver/mod.rs | 4 +- .../src/driver/tests.rs | 2 +- .../src/lib_ui_tests.rs | 6 +- .../src/tests/localization.rs | 6 +- crates/no_std_fs_operations/src/behaviour.rs | 4 +- .../no_std_fs_operations/src/config_tests.rs | 14 +- .../no_std_fs_operations/src/diagnostics.rs | 18 +- 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/tests/ui.rs | 7 +- crates/no_std_fs_operations/src/usage.rs | 6 +- .../no_std_fs_operations/src/usage/tests.rs | 2 +- .../tests/test_support/mod.rs | 2 +- crates/no_unwrap_or_else_panic/src/context.rs | 25 +- .../src/context/tests.rs | 2 +- .../src/diagnostics.rs | 6 +- crates/no_unwrap_or_else_panic/src/driver.rs | 12 +- .../src/lib_ui_tests.rs | 4 +- .../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/collector_tests.rs | 10 +- .../src/driver.rs | 6 +- .../src/driver_tests.rs | 20 +- .../src/visitor.rs | 6 +- .../tests/ui.rs | 21 +- .../tests/ui/harness_lock.rs | 6 +- .../tests/ui/lock_model.rs | 8 +- .../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 +- .../src/ast/lowering_tests.rs | 60 +++-- crates/whitaker_clones_core/src/ast/tests.rs | 67 ++--- 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 | 9 +- .../whitaker_clones_core/src/index/types.rs | 25 +- crates/whitaker_clones_core/src/lib.rs | 2 +- crates/whitaker_clones_core/src/run0/emit.rs | 24 +- 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 | 63 ++--- .../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 +- .../tests/ast_boundary.rs | 4 +- .../tests/ast_feature_extraction_behaviour.rs | 4 +- .../tests/build_script_parsing.rs | 2 +- .../tests/min_hash_lsh_behaviour.rs | 2 +- .../tests/token_pass_behaviour.rs | 2 +- .../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 | 43 ++-- .../whitaker_sarif/tests/sarif_behaviour.rs | 87 ++++--- docs/developers-guide.md | 47 ++-- installer/Cargo.toml | 2 +- installer/src/artefact/git_sha.rs | 6 +- installer/src/artefact/manifest.rs | 14 +- installer/src/artefact/naming.rs | 8 +- installer/src/artefact/packaging.rs | 17 +- installer/src/artefact/packaging_tests.rs | 12 +- installer/src/artefact/schema_version.rs | 4 +- installer/src/artefact/sha256_digest.rs | 6 +- installer/src/artefact/target.rs | 2 +- installer/src/artefact/toolchain_channel.rs | 2 +- installer/src/artefact/verification.rs | 2 +- .../src/bin/package_dependency_binary.rs | 2 +- installer/src/bin/package_installer_bin.rs | 2 +- installer/src/bin/package_lints.rs | 6 +- installer/src/binstall_metadata.rs | 26 +- installer/src/binstall_metadata_tests.rs | 24 +- installer/src/builder.rs | 4 +- installer/src/cli.rs | 2 +- .../dependency_binaries/install/checksum.rs | 9 +- .../dependency_binaries/install/downloader.rs | 21 +- .../dependency_binaries/install/extractor.rs | 10 +- .../dependency_binaries/install/installer.rs | 2 +- .../dependency_binaries/install/metadata.rs | 2 +- installer/src/dependency_binaries/manifest.rs | 8 +- installer/src/dependency_packaging.rs | 62 +++-- installer/src/dependency_packaging_tests.rs | 2 +- installer/src/deps.rs | 40 +-- installer/src/deps/install.rs | 79 +++--- installer/src/deps/install_tests.rs | 7 +- installer/src/deps/path_tests.rs | 36 +-- installer/src/deps/tests.rs | 27 +- installer/src/error.rs | 10 +- installer/src/git.rs | 79 +++--- installer/src/hex.rs | 16 +- installer/src/install_flow.rs | 2 +- installer/src/install_metrics.rs | 77 +++--- installer/src/install_metrics_tests.rs | 8 +- installer/src/installer_packaging.rs | 17 +- installer/src/installer_packaging_tests.rs | 8 +- installer/src/list.rs | 1 + installer/src/list_output.rs | 47 ++-- installer/src/main.rs | 12 +- installer/src/output.rs | 33 ++- installer/src/prebuilt_path.rs | 4 +- installer/src/resolution.rs | 3 +- installer/src/scanner.rs | 20 +- installer/src/stager.rs | 6 +- installer/src/test_utils.rs | 21 +- .../test_utils/dependency_binary_helpers.rs | 109 ++++++--- .../dependency_binary_helpers_tests.rs | 4 +- installer/src/tests.rs | 18 +- installer/src/tests/fast_path.rs | 2 +- installer/src/toolchain.rs | 2 +- installer/src/workspace.rs | 7 + installer/src/wrapper.rs | 14 +- installer/tests/behaviour_artefact.rs | 8 +- .../tests/behaviour_artefact_packaging.rs | 2 +- installer/tests/behaviour_binstall.rs | 4 +- installer/tests/behaviour_cli/support.rs | 5 +- .../tests/behaviour_dependency_binaries.rs | 8 +- installer/tests/behaviour_docs.rs | 4 +- installer/tests/behaviour_install_metrics.rs | 23 +- .../tests/behaviour_installer_release.rs | 2 +- installer/tests/behaviour_prebuilt.rs | 2 +- installer/tests/behaviour_staging.rs | 22 +- installer/tests/behaviour_toolchain.rs | 4 +- installer/tests/behaviour_workflows.rs | 6 +- installer/tests/doc_extraction/extraction.rs | 4 +- .../tests/features/install_metrics.feature | 16 +- installer/tests/support/mod.rs | 23 +- src/testing/ui/mod.rs | 117 +++------ src/testing/ui/tests.rs | 43 ++-- suite/Cargo.toml | 1 + suite/src/driver.rs | 15 +- suite/tests/registration.rs | 9 +- typos.local.toml | 3 + typos.toml | 3 + 249 files changed, 2454 insertions(+), 1972 deletions(-) rename common/src/decomposition_advice/{tests.rs => tests/mod.rs} (90%) create mode 100644 common/src/dylint_entry.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a50a23dce..0dd68da32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: BUILD_PROFILE: debug CARGO_INCREMENTAL: 0 CARGO_TERM_COLOR: always + WHITAKER_INSTALLER_VERSION: '0.2.7' RUSTFLAGS: -D warnings RUSTDOCFLAGS: -D warnings SCCACHE_GHA_ENABLED: "true" @@ -129,6 +130,26 @@ jobs: !**/.uv-cache/** !**/.uv-tools/** + - name: Cache Whitaker installer + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cargo/bin/whitaker-installer + ~/.cache/cargo-binstall + key: whitaker-installer-${{ runner.os }}-${{ runner.arch }}-${{ env.WHITAKER_INSTALLER_VERSION }} + + - name: Install Whitaker + run: | + if ! command -v whitaker-installer >/dev/null 2>&1; then + if cargo binstall --version >/dev/null 2>&1; then + cargo binstall --no-confirm --locked "whitaker-installer@${WHITAKER_INSTALLER_VERSION}" + else + echo "cargo-binstall unavailable; building whitaker-installer from crates.io" + cargo install --locked whitaker-installer --version "${WHITAKER_INSTALLER_VERSION}" + fi + fi + whitaker-installer + - name: Lint run: make lint diff --git a/AGENTS.md b/AGENTS.md index ad0d6d482..649329cb1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,18 +146,26 @@ project: - `make lint` executes: ```sh + RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc --workspace --no-deps cargo clippy --workspace --all-targets --all-features -- -D warnings + whitaker --all -- -p whitaker-common -p whitaker-installer \ + -p whitaker_clones_core -p whitaker_sarif --all-targets --all-features ``` - linting every target with all features enabled and denying all Clippy - warnings. - - `make test` executes: + building rustdoc with warnings denied, linting every target with all + features enabled and denying all Clippy warnings, and running the + Whitaker Dylint suite over the support crates (install via + `cargo install whitaker-installer && whitaker-installer`). `make + lint-clippy` runs the rustdoc and Clippy subset; `make lint-whitaker` + runs the Whitaker subset. + - `make test` executes `cargo nextest run` over the CI crate subset and + then the workspace doctests: ```sh - cargo test --workspace + RUSTFLAGS="-D warnings" cargo test --workspace --doc --all-features ``` - running the full workspace test suite. Use `make fmt` + running the full workspace test suite including doctests. Use `make fmt` (`cargo fmt --workspace`) to apply formatting fixes reported by the formatter check. - Clippy warnings MUST be disallowed. diff --git a/Cargo.lock b/Cargo.lock index 8700d93d1..524629a5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3649,6 +3649,7 @@ dependencies = [ "rstest", "rstest-bdd", "rstest-bdd-macros", + "temp-env", "tempfile", "thiserror 2.0.20", "unic-langid", @@ -3664,12 +3665,12 @@ dependencies = [ "directories-next", "flate2", "fs2", - "libc", "log", "mockall", "rstest", "rstest-bdd", "rstest-bdd-macros", + "rustix", "serde", "serde_json", "sha2", @@ -3746,6 +3747,7 @@ dependencies = [ "rustc_session", "rustc_span", "test_must_not_have_example", + "whitaker-common", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 93e60e41e..1908c5852 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/Makefile b/Makefile index dfef1468c..18a69a627 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help all clean test coverage build release lint fmt check-fmt markdownlint nixie publish-check typecheck install-smoke release-installer-dry-run package-lints workflow-test workflow-test-deps test-workflow-contracts verus kani verus-clone-detector kani-clone-detector spelling spelling-config spelling-config-write spelling-phrase-check spelling-helper-test +.PHONY: help all clean test coverage build release lint lint-clippy lint-whitaker fmt check-fmt markdownlint nixie publish-check typecheck install-smoke release-installer-dry-run package-lints workflow-test workflow-test-deps test-workflow-contracts verus kani verus-clone-detector kani-clone-detector spelling spelling-config spelling-config-write spelling-phrase-check spelling-helper-test # Appended only on targets that invoke binaries commonly installed under these # prefixes (cargo/bun/user-local), so the default recipe environment stays @@ -53,6 +53,13 @@ DYLINT_LINK_VERSION ?= 6.0.1 # needs a newer rustc than the repository's pinned nightly provides. DYLINT_TOOLS_TOOLCHAIN ?= stable WHITAKER_SCRIPT ?= $(HOME)/.local/bin/whitaker +WHITAKER ?= whitaker +# Crates linted by the Whitaker suite. The rustc_* proxy shims, the lint +# crates, the aggregated suite, and the whitaker root crate all require +# rustc_private plumbing (dylint-driver feature, prefer-dynamic RUSTFLAGS) +# that `cargo dylint`'s plain check build cannot provide, so the suite runs +# over the support crates that build as ordinary libraries. +WHITAKER_PACKAGES ?= -p whitaker-common -p whitaker-installer -p whitaker_clones_core -p whitaker_sarif build: target/debug/$(APP) ## Build debug binary release: target/release/$(APP) ## Build release binary @@ -148,10 +155,16 @@ target/%/$(APP): ## Build binary in debug or release mode manifest=$$(grep -l whitaker-installer */Cargo.toml crates/*/Cargo.toml); \ $(CARGO) build $(CARGO_LOCKED) $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(APP) --manifest-path "$$manifest" -lint: ## Run Clippy with warnings denied +lint: lint-clippy lint-whitaker ## Run rustdoc, Clippy, and the Whitaker Dylint suite + +lint-clippy: ## Run rustdoc and Clippy with warnings denied RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) doc $(CARGO_LOCKED) --workspace --no-deps $(CARGO) clippy $(CARGO_LOCKED) $(CARGO_FLAGS) -- $(RUST_FLAGS) +lint-whitaker: ## Run the Whitaker Dylint suite with warnings denied + @export PATH="$$PATH:$(TOOL_PATH_SUFFIX)"; \ + RUSTFLAGS="$(RUST_FLAGS)" $(WHITAKER) --all -- $(WHITAKER_PACKAGES) --all-targets --all-features + fmt: ## Format Rust and Markdown sources $(CARGO) fmt --all export PATH="$$PATH:$(TOOL_PATH_SUFFIX)"; mdformat-all diff --git a/common/Cargo.toml b/common/Cargo.toml index 6cea5d915..a89370b9a 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -18,6 +18,7 @@ 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 } 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/diagnostic_tests.rs b/common/src/brain_trait_metrics/diagnostic_tests.rs index e555fbd31..5a4a7afc6 100644 --- a/common/src/brain_trait_metrics/diagnostic_tests.rs +++ b/common/src/brain_trait_metrics/diagnostic_tests.rs @@ -20,7 +20,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 +39,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 +55,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 +69,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 +90,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 +118,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 +133,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 +152,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 +194,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 +239,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.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/evaluation_tests.rs b/common/src/brain_trait_metrics/evaluation_tests.rs index 8cf779f4f..030e6fb65 100644 --- a/common/src/brain_trait_metrics/evaluation_tests.rs +++ b/common/src/brain_trait_metrics/evaluation_tests.rs @@ -116,7 +116,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 +125,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 +150,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 +159,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 +173,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 +182,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 +211,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_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 e5e03328c..607eb4427 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/cognitive_complexity_tests.rs b/common/src/brain_type_metrics/cognitive_complexity_tests.rs index b9eb59c5e..134d398f8 100644 --- a/common/src/brain_type_metrics/cognitive_complexity_tests.rs +++ b/common/src/brain_type_metrics/cognitive_complexity_tests.rs @@ -151,10 +151,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 +165,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 +183,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 +197,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 +220,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 +238,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 +285,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.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/diagnostic_tests.rs b/common/src/brain_type_metrics/diagnostic_tests.rs index 12cfeae3e..7b310faa6 100644 --- a/common/src/brain_type_metrics/diagnostic_tests.rs +++ b/common/src/brain_type_metrics/diagnostic_tests.rs @@ -33,10 +33,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 +332,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.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/evaluation_tests.rs b/common/src/brain_type_metrics/evaluation_tests.rs index b94404713..57d34578c 100644 --- a/common/src/brain_type_metrics/evaluation_tests.rs +++ b/common/src/brain_type_metrics/evaluation_tests.rs @@ -114,7 +114,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 +123,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 +137,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 +146,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 +162,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 +171,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 +230,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 +243,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/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 d71e903c6..04593b077 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; @@ -225,6 +262,13 @@ pub fn rasterize_signal( Ok(accumulate_signal_from_diff(diff.as_slice(), len)) } +/// Deprecated alias for [`rasterize_signal`]. +/// +/// # Errors +/// +/// Propagates the same [`SignalBuildError`] conditions as [`rasterize_signal`]: +/// a zero (non-one-based) line in the range, an inverted range, or a segment +/// outside the function range. #[deprecated(note = "Use rasterize_signal instead.")] #[must_use = "Inspect the signal build result to handle invalid ranges"] pub fn rasterise_signal( @@ -239,11 +283,17 @@ pub fn rasterise_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. @@ -266,16 +316,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 }); } @@ -283,11 +333,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); } @@ -296,8 +346,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..05597a126 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,18 @@ 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/note_tests.rs b/common/src/decomposition_advice/note_tests.rs index 7697d051a..2f2b7dddb 100644 --- a/common/src/decomposition_advice/note_tests.rs +++ b/common/src/decomposition_advice/note_tests.rs @@ -14,8 +14,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 +43,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 +110,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,7 +173,7 @@ 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" 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/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 90% rename from common/src/decomposition_advice/tests.rs rename to common/src/decomposition_advice/tests/mod.rs index 93fc40af0..1b4ae6d5c 100644 --- a/common/src/decomposition_advice/tests.rs +++ b/common/src/decomposition_advice/tests/mod.rs @@ -119,8 +119,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 +132,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 +160,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 +199,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 +213,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 +221,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 +308,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 +348,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..4de628695 100644 --- a/common/src/decomposition_advice/tests/propagation.rs +++ b/common/src/decomposition_advice/tests/propagation.rs @@ -122,7 +122,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 +158,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 +168,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 +179,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..7a04cbcc6 100644 --- a/common/src/decomposition_advice/tests/test_fixtures.rs +++ b/common/src/decomposition_advice/tests/test_fixtures.rs @@ -6,6 +6,9 @@ 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 +24,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/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..9c158e03e 100644 --- a/common/src/i18n/helpers.rs +++ b/common/src/i18n/helpers.rs @@ -54,8 +54,7 @@ pub fn get_localizer_for_lint(lint_name: &str, configuration_locale: Option<&str pub fn branch_phrase(locale: &str, branches: usize) -> String { match locale .split_once('-') - .map(|(lang, _)| lang) - .unwrap_or(locale) + .map_or(locale, |(lang, _)| lang) { "cy" => welsh_branch_phrase(branches), "gd" => gaelic_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..e7171f8f2 100644 --- a/common/src/i18n/locales.rs +++ b/common/src/i18n/locales.rs @@ -4,14 +4,13 @@ //! 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 +24,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 383a426c2..9945211b6 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 ce7b02434..76e85e11e 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,14 @@ 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 bb7214a0e..6a84edc7e 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/rstest/tests/fingerprint_props.rs b/common/src/rstest/tests/fingerprint_props.rs index 6c1c2e70a..1ca668454 100644 --- a/common/src/rstest/tests/fingerprint_props.rs +++ b/common/src/rstest/tests/fingerprint_props.rs @@ -15,7 +15,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 +38,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..c8d4181c0 100644 --- a/common/src/rstest/tests/mod.rs +++ b/common/src/rstest/tests/mod.rs @@ -20,7 +20,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 +74,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 +125,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,7 +197,7 @@ 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()]) ); } @@ -211,13 +211,13 @@ fn source_span(line: usize, start: usize, end: usize) -> SourceSpan { 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] @@ -247,5 +247,5 @@ 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/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/decomposition.rs b/common/src/test_support/decomposition.rs index b4f534bdb..ae88e06f0 100644 --- a/common/src/test_support/decomposition.rs +++ b/common/src/test_support/decomposition.rs @@ -38,11 +38,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..517e0e9fb 100644 --- a/common/src/test_support/decomposition_adjacency.rs +++ b/common/src/test_support/decomposition_adjacency.rs @@ -89,7 +89,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 } @@ -166,7 +166,11 @@ impl AdjacencyReport { pub fn is_sorted(&self) -> bool { self.neighbours .iter() - .all(|bucket| bucket.windows(2).all(|pair| pair[0].0 <= pair[1].0)) + .all(|bucket| { + bucket + .windows(2) + .all(|pair| matches!(pair, [(left, _), (right, _)] if left <= right)) + }) } } diff --git a/common/src/test_support/decomposition_label_propagation.rs b/common/src/test_support/decomposition_label_propagation.rs index 774b3da00..71552cd1e 100644 --- a/common/src/test_support/decomposition_label_propagation.rs +++ b/common/src/test_support/decomposition_label_propagation.rs @@ -54,13 +54,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..e7fbb9fb3 100644 --- a/common/src/test_support/decomposition_vector_algebra.rs +++ b/common/src/test_support/decomposition_vector_algebra.rs @@ -64,7 +64,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 } @@ -94,7 +94,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 } @@ -124,7 +124,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 } @@ -154,7 +154,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..0d5a90d8e 100644 --- a/common/src/test_support/fixtures.rs +++ b/common/src/test_support/fixtures.rs @@ -34,6 +34,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() @@ -84,6 +90,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 +114,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(); diff --git a/common/src/test_support/mod.rs b/common/src/test_support/mod.rs index 37676df8f..6e13335f2 100644 --- a/common/src/test_support/mod.rs +++ b/common/src/test_support/mod.rs @@ -14,8 +14,9 @@ //! 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. +//! - [`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; @@ -27,7 +28,7 @@ pub use ui::{ read_fixture_config, resolve_fixture_config, run_fixtures_with, run_test_runner, }; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsStr; use std::sync::{Mutex, MutexGuard, OnceLock}; /// Serializes tests that mutate process-wide environment variables. @@ -35,6 +36,12 @@ use std::sync::{Mutex, MutexGuard, OnceLock}; /// 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. +/// +/// # Panics +/// +/// Panics when the lock is poisoned, which means an earlier test panicked +/// while mutating the environment; the process state can no longer be +/// trusted, so failing fast is the only safe response. pub fn env_test_guard() -> MutexGuard<'static, ()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())) @@ -42,182 +49,68 @@ pub fn env_test_guard() -> MutexGuard<'static, ()> { .unwrap_or_else(|error| panic!("expected environment test lock: {error}")) } -/// Guard that sets one environment variable and restores its prior state. +/// Runs `callback` with one environment variable temporarily set. /// -/// 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 } - } +/// 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 +/// +/// ```rust +/// use whitaker_common::test_support::with_env_var; +/// +/// 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 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 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); - } - } - } - } -} -/// Guard that overrides `DYLINT_LOCALE` for the lifetime of the instance. +/// Runs `callback` with one environment variable temporarily removed. /// -/// 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 prior value (if any) is restored after the callback completes or +/// panics. /// /// # Examples /// -/// ```ignore -/// use whitaker_common::test_support::LocaleOverride; -/// use serial_test::serial; +/// ```rust +/// use whitaker_common::test_support::with_env_var_removed; /// -/// #[test] -/// #[serial] -/// fn ui_runs_in_welsh_locale() { -/// let _guard = LocaleOverride::set("cy"); -/// // Execute lint UI harness here. -/// } +/// with_env_var_removed("WHITAKER_REMOVED_TEST_ENV_VAR", || { +/// assert!(std::env::var_os("WHITAKER_REMOVED_TEST_ENV_VAR").is_none()); +/// }); /// ``` -pub struct LocaleOverride { - previous: Option, -} - -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 } - } +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..635b0de7a 100644 --- a/common/src/test_support/ui.rs +++ b/common/src/test_support/ui.rs @@ -21,17 +21,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 +54,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 +77,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 +94,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 +151,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() { @@ -150,14 +187,14 @@ mod tests { 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] @@ -171,7 +208,8 @@ mod tests { 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] diff --git a/common/tests/brain_trait_evaluation_behaviour.rs b/common/tests/brain_trait_evaluation_behaviour.rs index 9ebbaa48d..f7525ceec 100644 --- a/common/tests/brain_trait_evaluation_behaviour.rs +++ b/common/tests/brain_trait_evaluation_behaviour.rs @@ -46,8 +46,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); @@ -179,15 +185,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..023f57f84 100644 --- a/common/tests/brain_trait_metrics_behaviour.rs +++ b/common/tests/brain_trait_metrics_behaviour.rs @@ -30,8 +30,8 @@ fn world() -> TraitMetricsWorld { } 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 +124,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..bb7aa245f 100644 --- a/common/tests/brain_type_evaluation_behaviour.rs +++ b/common/tests/brain_type_evaluation_behaviour.rs @@ -184,13 +184,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..1fb4a52cf 100644 --- a/common/tests/brain_type_metrics_behaviour.rs +++ b/common/tests/brain_type_metrics_behaviour.rs @@ -66,7 +66,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..f0f7d6444 100644 --- a/common/tests/cognitive_complexity_behaviour.rs +++ b/common/tests/cognitive_complexity_behaviour.rs @@ -80,7 +80,7 @@ fn given_push_nesting_expanded(world: &CcWorld) { #[given("nesting is popped")] fn given_pop_nesting(world: &CcWorld) { - with_builder(world, |b| b.pop_nesting()); + with_builder(world, whitaker_common::CognitiveComplexityBuilder::pop_nesting); } // --- When steps --- diff --git a/common/tests/complexity_signal_behaviour.rs b/common/tests/complexity_signal_behaviour.rs index ad05d2520..380aea7bd 100644 --- a/common/tests/complexity_signal_behaviour.rs +++ b/common/tests/complexity_signal_behaviour.rs @@ -119,10 +119,36 @@ fn parse_f64_list(values: &str) -> Vec { .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 +158,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:?})", ); } } @@ -178,13 +205,16 @@ fn then_built_signal(world: &SignalWorld, expected: 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); + let expected_values = parse_f64_list(&expected); + assert_vec_approx_eq(&actual, &expected_values); } #[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}")] @@ -192,13 +222,16 @@ fn then_smoothed_signal(world: &SignalWorld, expected: 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); + let expected_values = parse_f64_list(&expected); + assert_vec_approx_eq(&actual, &expected_values); } #[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/cosine_threshold_behaviour.rs b/common/tests/cosine_threshold_behaviour.rs index e30f1f5c8..c37078c11 100644 --- a/common/tests/cosine_threshold_behaviour.rs +++ b/common/tests/cosine_threshold_behaviour.rs @@ -15,7 +15,7 @@ enum MethodSide { } impl MethodSide { - fn key(self) -> &'static str { + const fn key(self) -> &'static str { match self { Self::Left => "left", Self::Right => "right", diff --git a/common/tests/decomposition_adjacency_behaviour.rs b/common/tests/decomposition_adjacency_behaviour.rs index c0f8cd790..4aa148e03 100644 --- a/common/tests/decomposition_adjacency_behaviour.rs +++ b/common/tests/decomposition_adjacency_behaviour.rs @@ -46,10 +46,10 @@ 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}")), } @@ -111,7 +111,7 @@ fn then_neighbours_of_node_are_sorted(world: &AdjacencyWorld, node: usize) -> Re 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); + 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..86ddc215e 100644 --- a/common/tests/decomposition_advice_behaviour.rs +++ b/common/tests/decomposition_advice_behaviour.rs @@ -28,7 +28,7 @@ impl std::str::FromStr for CsvList { .filter(|v| !v.is_empty()) .map(ToOwned::to_owned) .collect(); - Ok(CsvList(items)) + Ok(Self(items)) } } @@ -46,7 +46,7 @@ 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 +61,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 +79,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 +93,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 +145,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 +262,7 @@ 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..222934f16 100644 --- a/common/tests/decomposition_diagnostic_notes_behaviour.rs +++ b/common/tests/decomposition_diagnostic_notes_behaviour.rs @@ -67,7 +67,7 @@ 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,6 +82,17 @@ 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( @@ -89,24 +100,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); - 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")) - } - }; + 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 @@ -228,24 +223,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 +252,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_vector_algebra_behaviour.rs b/common/tests/decomposition_vector_algebra_behaviour.rs index fafbc1c98..7a6194943 100644 --- a/common/tests/decomposition_vector_algebra_behaviour.rs +++ b/common/tests/decomposition_vector_algebra_behaviour.rs @@ -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", diff --git a/common/tests/i18n_behaviour.rs b/common/tests/i18n_behaviour.rs index 3ea2c62f0..d0a19d7f7 100644 --- a/common/tests/i18n_behaviour.rs +++ b/common/tests/i18n_behaviour.rs @@ -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,14 +51,14 @@ 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)) } #[fixture] @@ -93,7 +92,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 +106,7 @@ 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 +120,41 @@ 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) { let message = fixture .result() + .unwrap_or_else(|| panic!("lookup should have been performed")) .unwrap_or_else(|error| panic!("message should resolve: {error}")); - let message = strip_isolation_marks(&message); - let snippet = strip_isolation_marks(&snippet); + let cleaned_message = strip_isolation_marks(&message); + let cleaned_snippet = strip_isolation_marks(&snippet); assert!( - message.contains(snippet.as_ref()), - "expected `{message}` to contain `{snippet}`", + cleaned_message.contains(cleaned_snippet.as_ref()), + "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:?}"), } } @@ -224,12 +231,12 @@ mod tests { use rstest::rstest; #[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/lcom4_behaviour.rs b/common/tests/lcom4_behaviour.rs index 367d7b803..ef125e5f5 100644 --- a/common/tests/lcom4_behaviour.rs +++ b/common/tests/lcom4_behaviour.rs @@ -22,7 +22,7 @@ impl LcomWorld { self.result.set(Some(cohesion_components(&methods))); } - fn result(&self) -> Option { + const fn result(&self) -> Option { self.result.get() } } diff --git a/common/tests/localizer_helpers.rs b/common/tests/localizer_helpers.rs index 5fc322406..1357ce428 100644 --- a/common/tests/localizer_helpers.rs +++ b/common/tests/localizer_helpers.rs @@ -14,7 +14,7 @@ use whitaker_common::i18n::{ Arguments, DiagnosticMessageSet, FluentValue, Localizer, MessageKey, MessageResolution, get_localizer_for_lint, noop_reporter, safe_resolve_message_set, }; -use whitaker_common::test_support::LocaleOverride; +use whitaker_common::test_support::with_locale; static ENVIRONMENT_LOCK: Mutex<()> = Mutex::new(()); @@ -33,7 +33,7 @@ struct HelperWorld { fallback: RefCell>, result: RefCell>, emitter: RecordingEmitter, - environment_override: RefCell>, + environment_locale: RefCell>, _guard: MutexGuard<'static, ()>, } @@ -51,19 +51,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,7 +66,10 @@ 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); } @@ -86,7 +83,11 @@ impl HelperWorld { fn assert_locale(&self, expected: &str) { let localizer = self.ensure_localizer(); - assert_eq!(localizer.locale(), expected); + assert_eq!( + localizer.locale(), + expected, + "expected resolved locale `{expected}`" + ); } fn set_message_key(&self, key: String) { @@ -128,7 +129,7 @@ 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; } @@ -185,7 +186,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 +196,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}")] @@ -216,12 +217,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")] @@ -242,23 +243,37 @@ fn when_resolve_messages(world: &HelperWorld) { #[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)); + let expected_snippet = unquote(&snippet); + assert!( + messages.primary().contains(expected_snippet), + "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)); + let expected_snippet = unquote(&snippet); + assert!( + messages.primary().contains(expected_snippet), + "expected resolved primary message to contain `{expected_snippet}`" + ); } #[then("no delayed bug is recorded")] @@ -303,8 +318,7 @@ fn invalid_locale_warns_and_falls_back() { let mut warned = false; while let Some(record) = logger.pop() { if record - .args() - .to_string() + .args().to_owned() .contains("unsupported DYLINT_LOCALE `xx-XX`") { warned = true; diff --git a/common/tests/rstest_detection_behaviour.rs b/common/tests/rstest_detection_behaviour.rs index 0ec2d9557..891543f92 100644 --- a/common/tests/rstest_detection_behaviour.rs +++ b/common/tests/rstest_detection_behaviour.rs @@ -123,7 +123,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 +155,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, @@ -213,7 +213,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 +238,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/support/i18n_helpers.rs b/common/tests/support/i18n_helpers.rs index e307c7293..787627d6e 100644 --- a/common/tests/support/i18n_helpers.rs +++ b/common/tests/support/i18n_helpers.rs @@ -14,7 +14,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 +67,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,7 +96,7 @@ pub fn extract_identifier(line: &str) -> Option { if id.is_empty() { return None; } - Some(id.to_string()) + Some(id.to_owned()) } #[cfg(test)] @@ -115,7 +115,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 +130,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/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index 72aa96d0c..a6ae2743c 100644 --- a/crates/bumpy_road_function/src/analysis.rs +++ b/crates/bumpy_road_function/src/analysis.rs @@ -97,7 +97,7 @@ pub fn normalise_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.rs b/crates/bumpy_road_function/src/driver.rs index 41ab1c795..f320d5492 100644 --- a/crates/bumpy_road_function/src/driver.rs +++ b/crates/bumpy_road_function/src/driver.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,7 @@ 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 +160,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/config.rs b/crates/bumpy_road_function/src/driver/config.rs index 4381ab7a9..2bb1101a7 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 467e43263..45bc0889e 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 { @@ -71,7 +71,7 @@ pub(super) fn emit_diagnostic( lint.span_label(span, label); } - lint.help(messages.help().to_string()); + lint.help(messages.help().to_owned()); }), ); } @@ -125,7 +125,7 @@ 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/segment_builder.rs b/crates/bumpy_road_function/src/driver/segment_builder.rs index 27992e200..d07eb72e0 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 e14199084..2a74138d5 100644 --- a/crates/conditional_max_n_branches/src/driver.rs +++ b/crates/conditional_max_n_branches/src/driver.rs @@ -261,7 +261,7 @@ fn normalise_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 1235e3d4b..93c46a224 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 localised_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/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/module_max_lines/src/driver.rs b/crates/module_max_lines/src/driver.rs index c67dd8522..b37b4a2fe 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.rs b/crates/module_must_have_inner_docs/src/driver.rs index 2bba08980..520ff0863 100644 --- a/crates/module_must_have_inner_docs/src/driver.rs +++ b/crates/module_must_have_inner_docs/src/driver.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/module_must_have_inner_docs/src/tests/ui.rs b/crates/module_must_have_inner_docs/src/tests/ui.rs index 88673b054..33e522bf4 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. @@ -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/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.rs b/crates/no_expect_outside_tests/src/context.rs index 59b8fc2dd..d892f1ad3 100644 --- a/crates/no_expect_outside_tests/src/context.rs +++ b/crates/no_expect_outside_tests/src/context.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 summarise_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,8 +260,7 @@ 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 { @@ -269,8 +268,7 @@ pub(crate) fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { } attr.meta_item_list() - .map(check_cfg_attr_for_test) - .unwrap_or(false) + .is_some_and(check_cfg_attr_for_test) } fn meta_item_inner_contains_test(item: MetaItemInner) -> bool { @@ -292,31 +290,28 @@ 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| { + .is_some_and(|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| { + .is_some_and(|items| { items .iter() .cloned() .any(|item| meta_item_inner_contains_test_with_polarity(item, is_positive)) }) - .unwrap_or(false) } 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 +319,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/dependency_rlib_tests.rs b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs index 0200c8a76..7357a2093 100644 --- a/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs +++ b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs @@ -153,9 +153,7 @@ fn set_modified_time(path: &Path, seconds_since_epoch: u64) { .metadata() .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()) + let times = existing_accessed.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/diagnostics.rs b/crates/no_expect_outside_tests/src/diagnostics.rs index 48d582e5a..9222c54ed 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 localised_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,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) @@ -233,9 +233,7 @@ fn fallback_messages( 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()); + .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 25d1485d4..a76b39798 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_expect_outside_tests/src/lib_ui_tests.rs b/crates/no_expect_outside_tests/src/lib_ui_tests.rs index 64dc2e9fe..42c9b0999 100644 --- a/crates/no_expect_outside_tests/src/lib_ui_tests.rs +++ b/crates/no_expect_outside_tests/src/lib_ui_tests.rs @@ -25,7 +25,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 +35,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, @@ -252,7 +252,7 @@ fn dependency_rlib_matches(deps_dir: &Path, prefix: &str) -> Result &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 +65,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) { diff --git a/crates/no_std_fs_operations/src/behaviour.rs b/crates/no_std_fs_operations/src/behaviour.rs index 4e3cb6174..4b3cca1ee 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/config_tests.rs b/crates/no_std_fs_operations/src/config_tests.rs index fd5e77f8e..f03432377 100644 --- a/crates/no_std_fs_operations/src/config_tests.rs +++ b/crates/no_std_fs_operations/src/config_tests.rs @@ -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/diagnostics.rs b/crates/no_std_fs_operations/src/diagnostics.rs index 6ba5a22df..cd68c8710 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())); }), ); } @@ -58,13 +58,11 @@ fn fallback_messages(operation: &str) -> StdFsMessages { let note = concat!( "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 +80,7 @@ pub(crate) fn localised_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 e77e9ab8d..247ec4971 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/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/src/usage.rs b/crates/no_std_fs_operations/src/usage.rs index f27902607..356b976d1 100644 --- a/crates/no_std_fs_operations/src/usage.rs +++ b/crates/no_std_fs_operations/src/usage.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 d7c9b7947..4b4183b4b 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_std_fs_operations/tests/test_support/mod.rs b/crates/no_std_fs_operations/tests/test_support/mod.rs index 55778e5bc..4e0580abc 100644 --- a/crates/no_std_fs_operations/tests/test_support/mod.rs +++ b/crates/no_std_fs_operations/tests/test_support/mod.rs @@ -35,7 +35,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", diff --git a/crates/no_unwrap_or_else_panic/src/context.rs b/crates/no_unwrap_or_else_panic/src/context.rs index ffc03bd60..5b3d20548 100644 --- a/crates/no_unwrap_or_else_panic/src/context.rs +++ b/crates/no_unwrap_or_else_panic/src/context.rs @@ -31,8 +31,8 @@ use rustc_span::sym; /// Summarize the context for a given HIR node. #[cfg(feature = "dylint-driver")] -pub(crate) fn summarise_context<'tcx>( - cx: &LateContext<'tcx>, +pub(crate) fn summarise_context( + cx: &LateContext<'_>, hir_id: hir::HirId, ) -> ContextSummary { let mut entries = Vec::new(); @@ -69,7 +69,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 +90,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,8 +159,7 @@ 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 { @@ -168,8 +167,7 @@ fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { } attr.meta_item_list() - .map(check_cfg_attr_for_test) - .unwrap_or(false) + .is_some_and(check_cfg_attr_for_test) } #[cfg(feature = "dylint-driver")] @@ -214,25 +212,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| { + .is_some_and(|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| 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 2260f891a..6dd9bbc2a 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, @@ -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 summarise_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 summarise_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/lib_ui_tests.rs b/crates/no_unwrap_or_else_panic/src/lib_ui_tests.rs index dd67f9a4a..fefe4e244 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 @@ -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, 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 dcdaef7bf..f22741da7 100644 --- a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs +++ b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs @@ -17,7 +17,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/collector_tests.rs b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs index af287e963..39704c9d1 100644 --- a/crates/rstest_helper_should_be_fixture/src/collector_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs @@ -34,7 +34,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 +111,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 +162,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 +170,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! { @@ -236,7 +236,7 @@ fn collect_spans(spans: &[(u32, u32, u32)]) -> Vec<(String, Vec<(u32, u32)>)> { .iter() .map(|record| (record.span.lo().0, record.span.hi().0)) .collect(); - (callee.to_string(), spans) + (callee.to_owned(), spans) }) .collect() } 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/driver_tests.rs b/crates/rstest_helper_should_be_fixture/src/driver_tests.rs index ecc59bbec..1a26693f8 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,16 +69,16 @@ 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: Vec = expected.into_iter().map(str::to_owned).collect(); assert_eq!(normalized, expected); } @@ -86,7 +86,7 @@ fn normalizes_provider_attributes(#[case] input: Vec, #[case] expected: #[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/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/rstest_helper_should_be_fixture/tests/ui.rs b/crates/rstest_helper_should_be_fixture/tests/ui.rs index 33f66f2d0..bb8fa6e12 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui.rs @@ -15,7 +15,7 @@ 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 whitaker_common::test_support::{run_test_runner, with_env_var}; use harness_lock::ExampleHarnessLock; @@ -102,17 +102,18 @@ 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 = std::fs::read_to_string(&summary_path) + .expect("collection summary should be written"); + let _ = 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..3b3914b01 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 @@ -19,11 +19,11 @@ 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"; @@ -237,7 +237,7 @@ fn make_example_harness_lock_stale(path: &Path) { } #[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(); 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..ac3fcac2a 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; } diff --git a/crates/test_must_not_have_example/src/behaviour.rs b/crates/test_must_not_have_example/src/behaviour.rs index 522268c86..95128fe07 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/lowering_tests.rs b/crates/whitaker_clones_core/src/ast/lowering_tests.rs index 28f6878ae..262c1e8fa 100644 --- a/crates/whitaker_clones_core/src/ast/lowering_tests.rs +++ b/crates/whitaker_clones_core/src/ast/lowering_tests.rs @@ -21,6 +21,10 @@ fn kind_name(kind: KindId) -> String { format!("{parser_kind:?}") } +fn offset_u32(offset: usize) -> u32 { + u32::try_from(offset).expect("test source offsets should fit in u32") +} + #[rstest] fn pinned_parser_snapshot_parses_current_edition_source() { let tree = lower_span_for("fn f() {}", "fn f").expect("source should lower"); @@ -49,35 +53,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 = ByteSpan::new(source, 0, offset_u32(source.len())).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 = ByteSpan::new(&source, 0, offset_u32(source.len())).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 = ByteSpan::new(&source, 0, offset_u32(source.len())).expect("span should validate"); assert_eq!( lower_span(&source, span), @@ -85,30 +87,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 = ByteSpan::new(source, 0, offset_u32(source.len())).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 +121,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 +137,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 +163,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 +182,19 @@ 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 span = ByteSpan::new(source, 0, offset_u32(source.len())).expect("span should validate"); assert_eq!( lower_span(source, span), Err(AstError::UnparsableSpan { start: 0, - end: source.len() as u32 + end: offset_u32(source.len()) }) ); - Ok(()) } fn lower_span_for(source: &str, needle: &str) -> Result { @@ -207,7 +202,10 @@ fn lower_span_for(source: &str, needle: &str) -> Result bool { @@ -236,7 +234,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 = ByteSpan::new(source, 0, offset_u32(source.len()))?; let tree = lower_span(source, span)?; let counts = kind_counts(&tree) .iter() diff --git a/crates/whitaker_clones_core/src/ast/tests.rs b/crates/whitaker_clones_core/src/ast/tests.rs index 5560ed1fc..c8b24da23 100644 --- a/crates/whitaker_clones_core/src/ast/tests.rs +++ b/crates/whitaker_clones_core/src/ast/tests.rs @@ -57,18 +57,17 @@ 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("static test span should be valid"); 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(&expect_feature_tree()); let expected = [ (KindId::new(1), Depth::root(), 1), @@ -79,13 +78,11 @@ 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(&expect_feature_tree()); assert_eq!( histogram.get(KindId::new(1)).map(KindWeight::get), @@ -99,29 +96,27 @@ 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("static test span should be valid"); 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(&expect_feature_tree()); assert_eq!( productions.count(Production::Bigram(KindId::new(1), KindId::new(2))), @@ -147,38 +142,30 @@ 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(&expect_feature_tree()), + canonical_hash(&expect_feature_tree()) ); - - Ok(()) } #[rstest] -fn canonical_hash_is_sensitive_to_child_order() -> AstResult<()> { - assert_ne!( - canonical_hash(&feature_tree()?), - canonical_hash(&reordered_tree()?) - ); - - Ok(()) +fn canonical_hash_is_sensitive_to_child_order() { + let reordered = reordered_tree().expect("static test span should be valid"); + assert_ne!(canonical_hash(&expect_feature_tree()), canonical_hash(&reordered)); } #[rstest] -fn canonical_hash_is_sensitive_to_leaf_class() -> AstResult<()> { - assert_ne!( - canonical_hash(&feature_tree()?), - canonical_hash(&different_leaf_tree()?) - ); +fn canonical_hash_is_sensitive_to_leaf_class() { + let different = different_leaf_tree().expect("static test span should be valid"); + assert_ne!(canonical_hash(&expect_feature_tree()), canonical_hash(&different)); +} - Ok(()) +fn expect_feature_tree() -> NormalizedTree { + feature_tree().expect("static test span should be valid") } fn feature_tree() -> AstResult { @@ -233,13 +220,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 = expect_feature_tree(); let distinct = tree_with_root(NormalizedNode::new( KindId::new(9), None, vec![literal(KindId::new(8))], - ))?; + )) + .expect("static test span should be valid"); assert_ne!(kind_counts(&expected), kind_counts(&distinct)); assert_ne!(kind_histogram(&expected), kind_histogram(&distinct)); @@ -248,7 +236,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/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 2f9871c5d..3fc4839a4 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}; @@ -104,8 +104,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; @@ -234,7 +234,8 @@ 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 9c9ece949..fbef6598c 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,25 @@ 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 f0c8d3dff..2411479c5 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,14 +27,17 @@ 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); } -fn assert_region(id: &str, source: &str, range: std::ops::Range, expected: Region) { +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); + assert_eq!(®ion, expected); } #[test] @@ -119,7 +122,7 @@ fn single_line_region_uses_one_based_columns() { "alpha", "fn a() {}\n", 0..8, - Region { + &Region { start_line: 1, start_column: Some(1), end_line: Some(1), @@ -136,7 +139,7 @@ fn multi_line_region_tracks_trailing_newline() { "alpha", "fn alpha() {\n value();\n}\n", 13..27, - Region { + &Region { start_line: 2, start_column: Some(1), end_line: Some(3), @@ -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", @@ -180,44 +183,43 @@ fn emit_run0_uses_primary_and_related_locations() { assert_eq!(result.rule_id, WHK001_ID); assert_eq!(result.locations.len(), 1); assert_eq!(result.related_locations.len(), 1); - assert_eq!( - result.locations[0].physical_location.artifact_location.uri, - "src/a.rs" - ); - assert_eq!( - result.related_locations[0] - .physical_location - .artifact_location - .uri, - "src/b.rs" - ); + let location = result + .locations + .first() + .expect("primary location should be present"); + assert_eq!(location.physical_location.artifact_location.uri, "src/a.rs"); + let related = result + .related_locations + .first() + .expect("related location should be present"); + assert_eq!(related.physical_location.artifact_location.uri, "src/b.rs"); } #[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 +249,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 +297,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 +321,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 e11af4c5e..24b656745 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 artifact 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_clones_core/tests/ast_boundary.rs b/crates/whitaker_clones_core/tests/ast_boundary.rs index 689179ffd..189d955f0 100644 --- a/crates/whitaker_clones_core/tests/ast_boundary.rs +++ b/crates/whitaker_clones_core/tests/ast_boundary.rs @@ -142,7 +142,7 @@ fn non_comment_lexemes(contents: &str) -> Vec<&str> { .collect() } -fn is_non_comment_lexeme(kind: TokenKind) -> bool { +const fn is_non_comment_lexeme(kind: TokenKind) -> bool { !matches!( kind, TokenKind::Whitespace @@ -190,7 +190,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(&"::" | &"{" | &",") ) }) } 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..d1ebd71a7 100644 --- a/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs +++ b/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs @@ -33,7 +33,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 +61,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, diff --git a/crates/whitaker_clones_core/tests/build_script_parsing.rs b/crates/whitaker_clones_core/tests/build_script_parsing.rs index 24e7b6f80..741814fa4 100644 --- a/crates/whitaker_clones_core/tests/build_script_parsing.rs +++ b/crates/whitaker_clones_core/tests/build_script_parsing.rs @@ -182,7 +182,7 @@ fn read_of_absent_manifest_reports_not_found() -> Result<(), Box()) + .map(str::parse::) .collect::, _>>() .expect("expected hash list should be valid"); diff --git a/crates/whitaker_sarif/src/builders/location_builder.rs b/crates/whitaker_sarif/src/builders/location_builder.rs index e3fc7febd..b9a75975b 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 artifact. #[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 artifact. #[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 3e76a0116..5af374968 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 311d631f9..1e94ac17f 100644 --- a/crates/whitaker_sarif/src/test_support.rs +++ b/crates/whitaker_sarif/src/test_support.rs @@ -10,7 +10,13 @@ use crate::model::result::{Level, SarifResult}; /// 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 90260386f..24d799c57 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}"), @@ -287,11 +287,12 @@ mod tests { .with_cosine(0.90) .build(); match props { - Ok(props) => { - let json = serde_json::to_string(&props); + Ok(built) => { + let json = serde_json::to_string(&built); match json { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(props, parsed), + Ok(serialized) => match serde_json::from_str::(&serialized) + { + Ok(parsed) => assert_eq!(built, parsed), Err(e) => panic!("deserialization failed: {e}"), }, Err(e) => panic!("serialization failed: {e}"), diff --git a/crates/whitaker_sarif/tests/sarif_behaviour.rs b/crates/whitaker_sarif/tests/sarif_behaviour.rs index ec1355269..093a7091d 100644 --- a/crates/whitaker_sarif/tests/sarif_behaviour.rs +++ b/crates/whitaker_sarif/tests/sarif_behaviour.rs @@ -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,51 @@ 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 +312,16 @@ 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 +329,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 +339,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/docs/developers-guide.md b/docs/developers-guide.md index 92b1938d3..4f4ed35d4 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/installer/Cargo.toml b/installer/Cargo.toml index 6d514e9e9..9e654f0aa 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -78,7 +78,7 @@ zip = { workspace = true } zstd = { workspace = true } [dev-dependencies] -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/git_sha.rs b/installer/src/artefact/git_sha.rs index 4bc775b5d..5ce246d0f 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(), 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/naming.rs b/installer/src/artefact/naming.rs index af6d94c2c..abc19f45b 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 } 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 PackageOutput { generated_at: GeneratedAt::new("2026-02-11T12:00:00Z"), }; - package_artefact(params).expect("packaging") + package_artefact(¶ms).expect("packaging") } #[rstest] @@ -278,7 +278,7 @@ fn packaging_produces_deterministic_digest(temp_dir: TempDir) { output_dir, generated_at: GeneratedAt::new("2026-02-12T10:00:00Z"), }; - let output = package_artefact(params).expect("packaging"); + let output = package_artefact(¶ms).expect("packaging"); digests.push(output.manifest.sha256().as_str().to_owned()); } assert_eq!( diff --git a/installer/src/artefact/schema_version.rs b/installer/src/artefact/schema_version.rs index 2f0b101a8..3e67f848d 100644 --- a/installer/src/artefact/schema_version.rs +++ b/installer/src/artefact/schema_version.rs @@ -32,13 +32,13 @@ pub struct SchemaVersion(u32); impl SchemaVersion { /// Return the current (latest) schema version. #[must_use] - pub fn current() -> 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 } } diff --git a/installer/src/artefact/sha256_digest.rs b/installer/src/artefact/sha256_digest.rs index c64cfc143..f6ed3e33e 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 }); } diff --git a/installer/src/artefact/target.rs b/installer/src/artefact/target.rs index f05aeb41b..7e408cfce 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 } diff --git a/installer/src/artefact/toolchain_channel.rs b/installer/src/artefact/toolchain_channel.rs index 03e0b0dcb..74851ce3d 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 == '_' } diff --git a/installer/src/artefact/verification.rs b/installer/src/artefact/verification.rs index e558fdc49..dbe99da6f 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 } } diff --git a/installer/src/bin/package_dependency_binary.rs b/installer/src/bin/package_dependency_binary.rs index bcec4f0c1..b08fbff2a 100644 --- a/installer/src/bin/package_dependency_binary.rs +++ b/installer/src/bin/package_dependency_binary.rs @@ -91,7 +91,7 @@ fn run(cli: Cli) -> Result<(), CliError> { .cloned() .ok_or(CliError::UnknownPackage(package))?; let target = TargetTriple::try_from(target.as_str())?; - let output = package_dependency_binary(DependencyPackageParams { + let output = package_dependency_binary(&DependencyPackageParams { dependency, target, binary_path, diff --git a/installer/src/bin/package_installer_bin.rs b/installer/src/bin/package_installer_bin.rs index 944d74e04..02a1582ee 100644 --- a/installer/src/bin/package_installer_bin.rs +++ b/installer/src/bin/package_installer_bin.rs @@ -66,7 +66,7 @@ fn run(cli: Cli) -> Result<(), CliError> { output_dir: cli.output_dir, }; - let output = package_installer(params)?; + let output = package_installer(¶ms)?; println!("Created {}", output.archive_path.display()); Ok(()) } diff --git a/installer/src/bin/package_lints.rs b/installer/src/bin/package_lints.rs index 1c6cdcd1f..5c6317893 100644 --- a/installer/src/bin/package_lints.rs +++ b/installer/src/bin/package_lints.rs @@ -128,7 +128,7 @@ 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()); @@ -187,7 +187,7 @@ fn validate_iso8601(ts: &str) -> Result<(), PackageCliError> { Ok(()) } -fn has_valid_length(b: &[u8]) -> bool { +const fn has_valid_length(b: &[u8]) -> bool { b.len() == 20 } @@ -226,7 +226,7 @@ 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) { +const fn civil_from_epoch(epoch_secs: u64) -> (u32, u32, u32) { let z = (epoch_secs / 86_400) as i64 + 719_468; let era = z.div_euclid(146_097); let doe = z.rem_euclid(146_097) as u64; // day of era [0, 146_096] 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 dab46c363..2a7bc2a25 100644 --- a/installer/src/binstall_metadata_tests.rs +++ b/installer/src/binstall_metadata_tests.rs @@ -9,8 +9,8 @@ use rstest::rstest; #[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()) @@ -20,8 +20,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()) @@ -31,8 +31,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()) @@ -42,8 +42,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()) @@ -61,8 +61,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()) @@ -81,8 +81,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..fd08c4e19 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 } } diff --git a/installer/src/cli.rs b/installer/src/cli.rs index 0ef5fbc51..b9227257d 100644 --- a/installer/src/cli.rs +++ b/installer/src/cli.rs @@ -256,7 +256,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/dependency_binaries/install/checksum.rs b/installer/src/dependency_binaries/install/checksum.rs index 066adb126..e14ef7ed8 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, }); } diff --git a/installer/src/dependency_binaries/install/downloader.rs b/installer/src/dependency_binaries/install/downloader.rs index 2b5d4d709..7d01ae5ea 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/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/installer.rs b/installer/src/dependency_binaries/install/installer.rs index 1130bdc28..dd8afcd53 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 { .. }) } } 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/manifest.rs b/installer/src/dependency_binaries/manifest.rs index 51bfdc95f..ce675353f 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())); } } 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 bb7fa5bfd..0451b682e 100644 --- a/installer/src/dependency_packaging_tests.rs +++ b/installer/src/dependency_packaging_tests.rs @@ -80,7 +80,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.rs b/installer/src/deps.rs index 242c05c2b..005e11923 100644 --- a/installer/src/deps.rs +++ b/installer/src/deps.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/install.rs b/installer/src/deps/install.rs index acd5a801d..1c4b03792 100644 --- a/installer/src/deps/install.rs +++ b/installer/src/deps/install.rs @@ -25,14 +25,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 +49,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 +68,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 { @@ -107,7 +109,7 @@ pub(super) fn install_tool( write_message( stderr, context.quiet, - format!("Installed {} from repository release.", tool.package), + &format!("Installed {} from repository release.", tool.package), ); return Ok(InstallOutcome::RepositoryRelease); } @@ -115,7 +117,7 @@ pub(super) fn install_tool( write_message( stderr, context.quiet, - format!( + &format!( "Repository install for {} failed verification; falling back to Cargo.", tool.package ), @@ -126,7 +128,7 @@ pub(super) fn install_tool( write_message( stderr, context.quiet, - format!( + &format!( "Repository install for {} unavailable: {error}. Falling back to Cargo.", tool.package ), @@ -136,7 +138,7 @@ pub(super) fn install_tool( write_message( stderr, context.quiet, - format!( + &format!( "Repository install for {} unavailable: {error}. Falling back to Cargo.", tool.package ), @@ -169,7 +171,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 +179,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 +208,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 +242,7 @@ fn try_binstall( write_message( stderr, quiet, - format!( + &format!( "Installed {} with cargo binstall.", cargo_install_plan.tool.package ), @@ -255,18 +257,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,7 +293,7 @@ fn run_cargo_install( }); } - write_message(stderr, quiet, success_message); + write_message(stderr, quiet, &success_message); Ok(InstallOutcome::CargoInstall) } @@ -328,11 +333,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 +348,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 +361,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_tests.rs b/installer/src/deps/install_tests.rs index dc41e6eee..8630d4a8e 100644 --- a/installer/src/deps/install_tests.rs +++ b/installer/src/deps/install_tests.rs @@ -26,7 +26,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] @@ -65,7 +66,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] @@ -89,5 +90,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/path_tests.rs b/installer/src/deps/path_tests.rs index efc7506a3..bd754f878 100644 --- a/installer/src/deps/path_tests.rs +++ b/installer/src/deps/path_tests.rs @@ -26,7 +26,8 @@ fn check_dylint_tools_reports_installed_tools() { } ); executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[rstest::rstest] @@ -35,7 +36,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), @@ -52,7 +53,8 @@ fn check_dylint_tools_rejects_unusable_cargo_dylint_output(#[case] version_stdou ); executor.assert_finished(); }, - ); + ) + .expect("prepare fake PATH"); } #[rstest::rstest] @@ -76,7 +78,8 @@ fn check_dylint_tools_rejects_unpinned_dylint_link(#[case] install_list_check: E } ); executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[test] @@ -88,7 +91,7 @@ fn check_dylint_tools_rejects_non_invocable_dylint_link_on_path() { #[cfg(not(windows))] let binary_path = directories[0].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()]); @@ -104,7 +107,8 @@ fn check_dylint_tools_rejects_non_invocable_dylint_link_on_path() { ); executor.assert_finished(); }, - ); + ) + .expect("prepare fake PATH"); } #[test] @@ -126,11 +130,12 @@ 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] @@ -142,12 +147,13 @@ fn is_binary_on_path_checks_multiple_directories() { #[cfg(not(windows))] let binary_path = directories[1].join("dylint-link"); - write_fake_binary(&binary_path, true); + write_fake_binary(&binary_path, true) }, || { assert!(is_binary_on_path("dylint-link")); }, - ); + ) + .expect("prepare fake PATH"); } #[cfg(unix)] @@ -155,7 +161,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)); } @@ -165,7 +171,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)); } @@ -184,7 +190,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)] @@ -211,5 +218,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 6e69ce5fa..537f26017 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 super::install::{InstallMode, install_tool}; use crate::dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}; use crate::installer_packaging::TargetTriple; use crate::test_utils::dependency_binary_helpers::{ @@ -57,7 +58,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("repository install should succeed"); @@ -89,7 +90,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("cargo binstall fallback should succeed"); @@ -123,7 +124,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("cargo install fallback should succeed"); @@ -153,7 +154,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("fallback after verification failure should succeed"); @@ -186,7 +187,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_err("install should fail after all fallbacks"); @@ -226,7 +227,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("source build should succeed"); @@ -270,10 +271,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("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.")); @@ -293,7 +295,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)) } @@ -323,7 +325,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("repository install should satisfy dylint-link"); @@ -365,10 +367,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("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/error.rs b/installer/src/error.rs index 93e45cdc4..e1f1273d5 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(), diff --git a/installer/src/git.rs b/installer/src/git.rs index f59a00e1c..b069213ee 100644 --- a/installer/src/git.rs +++ b/installer/src/git.rs @@ -12,7 +12,7 @@ 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. /// @@ -98,53 +98,52 @@ fn run_git_with_timeout( stdout_pipe .map(std::io::read_to_string) .transpose() - .map(|opt| opt.unwrap_or_default()) + .map(Option::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()) + .map(Option::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 { + if let Some(status) = child.wait_timeout(GIT_TIMEOUT)? { + // Command completed within timeout - collect output from threads + let stdout = stdout_thread + .join() + .map_err(|_| InstallerError::Git { operation, - message: format!( - "operation timed out after {} seconds", - GIT_TIMEOUT.as_secs() - ), - }) - } + 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(), + }) + } else { + // Timeout - kill the process and wait for the reader threads to + // finish. Each result is discarded deliberately: the operation has + // already failed, so cleanup is best effort. + drop(child.kill()); + drop(child.wait()); + drop(stdout_thread.join()); + drop(stderr_thread.join()); + Err(InstallerError::Git { + operation, + message: format!( + "operation timed out after {} seconds", + GIT_TIMEOUT.as_secs() + ), + }) } } 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.rs b/installer/src/install_flow.rs index 54bf69a13..85e9aa087 100644 --- a/installer/src/install_flow.rs +++ b/installer/src/install_flow.rs @@ -65,7 +65,7 @@ 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| { diff --git a/installer/src/install_metrics.rs b/installer/src/install_metrics.rs index c05fcb811..725503406 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, @@ -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 bf9760d8b..ed3b24b89 100644 --- a/installer/src/install_metrics_tests.rs +++ b/installer/src/install_metrics_tests.rs @@ -26,8 +26,8 @@ fn metrics_path_fixture() -> MetricsPathFixture { #[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_eq!(metrics.download_rate_permille(), 0, "zero-state download rate"); + assert_eq!(metrics.build_rate_permille(), 0, "zero-state build rate"); } #[test] @@ -40,8 +40,8 @@ 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_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 f3f0118d7..fb1660123 100644 --- a/installer/src/installer_packaging_tests.rs +++ b/installer/src/installer_packaging_tests.rs @@ -188,7 +188,7 @@ fn package_installer_creates_archive( ) { let fixture = packaging_fixture(target, content); let params = params_from_fixture(&fixture, "0.2.1", target); - 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); @@ -213,7 +213,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:?}" @@ -238,7 +238,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:?}" @@ -274,7 +274,7 @@ fn tgz_archive_preserves_binary_content() { let fixture = packaging_fixture("aarch64-unknown-linux-gnu", content); let params = params_from_fixture(&fixture, "0.2.1", "aarch64-unknown-linux-gnu"); - 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 0ba73eb98..7eb9c315b 100644 --- a/installer/src/list.rs +++ b/installer/src/list.rs @@ -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..7fef04f67 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,40 @@ 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() + .map(|lint| format!(" - {lint}\n")) + .collect(); + format!(" {}\n{lint_lines}", library.crate_name) +} + /// Format installed lints as JSON. /// /// # Examples diff --git a/installer/src/main.rs b/installer/src/main.rs index 1e9c2665a..ae32918ff 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,7 +54,7 @@ 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] { +const fn resolve_additional_components(args: &InstallArgs) -> &'static [&'static str] { if args.cranelift { &["rustc-codegen-cranelift"] } else { @@ -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_deps, + wrapper: args.skip_wrapper, + update: args.no_update, + }, jobs: args.jobs, crates: &requested_crates, }; diff --git a/installer/src/output.rs b/installer/src/output.rs index 53bc593f9..caa341a8a 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 { diff --git a/installer/src/prebuilt_path.rs b/installer/src/prebuilt_path.rs index 31a88e6b2..564c13c0f 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 + let data_dir = dirs .whitaker_data_dir() .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() diff --git a/installer/src/resolution.rs b/installer/src/resolution.rs index 371443856..e526ea79a 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(); } diff --git a/installer/src/scanner.rs b/installer/src/scanner.rs index 4f26b660b..e8faf2c21 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; diff --git a/installer/src/stager.rs b/installer/src/stager.rs index fd6772777..5ae2f74a2 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 { diff --git a/installer/src/test_utils.rs b/installer/src/test_utils.rs index 24f5745a9..4d8cfb05f 100644 --- a/installer/src/test_utils.rs +++ b/installer/src/test_utils.rs @@ -25,6 +25,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 +46,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 +65,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 +107,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 +138,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 +151,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 +163,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, ) } @@ -237,6 +241,7 @@ impl StubExecutor { /// }, /// ]); /// ``` + #[must_use] pub fn new(expected: Vec) -> Self { Self { expected: RefCell::new(expected.into()), diff --git a/installer/src/test_utils/dependency_binary_helpers.rs b/installer/src/test_utils/dependency_binary_helpers.rs index ddf5fdff7..b13f57501 100644 --- a/installer/src/test_utils/dependency_binary_helpers.rs +++ b/installer/src/test_utils/dependency_binary_helpers.rs @@ -45,26 +45,39 @@ impl DependencyBinaryInstaller for AlwaysNotFoundRepositoryInstaller { } /// Writes a fake binary at `path` that exits successfully. +/// +/// # Errors +/// +/// Returns any I/O error raised while writing the binary or setting its +/// permissions. #[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); +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. +/// +/// # Errors +/// +/// Returns any I/O error raised while writing the binary or setting its +/// permissions. #[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"); +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(()) } #[cfg(any(test, feature = "test-support"))] @@ -80,28 +93,46 @@ fn fake_binary_contents(exit_code: i32) -> Vec { } /// Runs a closure with `PATH` pointing at one or more fake directories. +/// +/// # Errors +/// +/// Returns an I/O error when the fake directories cannot be created, the +/// `setup` closure fails, or the fake `PATH` cannot be joined. #[cfg(any(test, feature = "test-support"))] -pub fn with_fake_path(setup: impl FnOnce(&[PathBuf]), run: impl FnOnce() -> T) -> T { +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. +/// +/// # Errors +/// +/// Returns an I/O error when the fake `PATH` cannot be prepared or the fake +/// binary cannot be written. #[cfg(any(test, feature = "test-support"))] -pub fn with_fake_binary_on_path(binary_name: &str, run: impl FnOnce() -> T) -> T { +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, ) } @@ -109,6 +140,7 @@ pub fn with_fake_binary_on_path(binary_name: &str, run: impl FnOnce() -> T) - /// 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"))] +#[must_use] pub fn path_binary_location(directory: &Path, binary_name: &str) -> PathBuf { #[cfg(windows)] { @@ -139,6 +171,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 +185,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 +195,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 +206,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 +232,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 +252,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 +271,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 +355,13 @@ 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.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)), - ); - vec![cargo_call] - } else { - vec![] - } + )] + }) } fn source_install_fallback_calls( @@ -383,10 +422,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,7 +439,8 @@ 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 { @@ -416,11 +453,12 @@ pub fn expected_calls(tool: &str, config: ExpectedCallConfig<'_>) -> Vec ExpectedCall { ExpectedCall { cmd: "cargo", @@ -430,6 +468,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..50e417aa9 100644 --- a/installer/src/test_utils/dependency_binary_helpers_tests.rs +++ b/installer/src/test_utils/dependency_binary_helpers_tests.rs @@ -35,7 +35,7 @@ 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, @@ -55,7 +55,7 @@ fn expected_calls_include_repository_probe_for_cargo_dylint() { 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, diff --git a/installer/src/tests.rs b/installer/src/tests.rs index 524be606e..af1b98f2d 100644 --- a/installer/src/tests.rs +++ b/installer/src/tests.rs @@ -118,12 +118,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); - 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] @@ -163,7 +164,7 @@ fn ensure_dylint_tools_installs_missing_tools( let mut stderr = Vec::new(); let options = dependency_install_options(&test_base_dirs, &repository_installer, quiet); - 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"); @@ -183,7 +184,8 @@ fn ensure_dylint_tools_installs_missing_tools( ); } executor.assert_finished(); - }); + }) + .expect("prepare fake PATH"); } #[rstest] @@ -211,7 +213,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); - 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!( @@ -221,7 +223,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)] @@ -272,8 +275,7 @@ 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:?}" ); } diff --git a/installer/src/tests/fast_path.rs b/installer/src/tests/fast_path.rs index 4dd9aa084..988b21fbb 100644 --- a/installer/src/tests/fast_path.rs +++ b/installer/src/tests/fast_path.rs @@ -59,7 +59,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/toolchain.rs b/installer/src/toolchain.rs index 9615cd5bf..5865ce98f 100644 --- a/installer/src/toolchain.rs +++ b/installer/src/toolchain.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/workspace.rs b/installer/src/workspace.rs index 3d06bbf5f..d6325d5df 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() { @@ -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/wrapper.rs b/installer/src/wrapper.rs index 8e9a190aa..078effce9 100644 --- a/installer/src/wrapper.rs +++ b/installer/src/wrapper.rs @@ -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) @@ -175,11 +173,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) + .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)] { diff --git a/installer/tests/behaviour_artefact.rs b/installer/tests/behaviour_artefact.rs index 492664494..221825876 100644 --- a/installer/tests/behaviour_artefact.rs +++ b/installer/tests/behaviour_artefact.rs @@ -43,20 +43,18 @@ fn world() -> ArtefactWorld { ArtefactWorld::default() } -/// Helper to assert that an error option contains a specific ArtefactError variant. +/// Helper to assert that an error option contains a specific `ArtefactError` variant. fn assert_error_matches(error: &Option, field_name: &str, predicate: F) where F: FnOnce(&ArtefactError) -> bool, { assert!( error.is_some(), - "expected {} validation to fail", - field_name + "expected {field_name} validation to fail" ); assert!( predicate(error.as_ref().expect("checked above")), - "error variant mismatch for {}", - field_name + "error variant mismatch for {field_name}" ); } diff --git a/installer/tests/behaviour_artefact_packaging.rs b/installer/tests/behaviour_artefact_packaging.rs index 861a7c645..fc905ea44 100644 --- a/installer/tests/behaviour_artefact_packaging.rs +++ b/installer/tests/behaviour_artefact_packaging.rs @@ -68,7 +68,7 @@ fn run_packaging(world: &mut PackagingWorld) { 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 f58c0dabf..620a164df 100644 --- a/installer/tests/behaviour_binstall.rs +++ b/installer/tests/behaviour_binstall.rs @@ -41,8 +41,8 @@ fn world() -> BinstallWorld { #[given("the installer Cargo.toml is loaded")] fn given_cargo_toml_loaded(world: &mut BinstallWorld) { - let table = load_cargo_toml(); - world.binstall_table = Some(extract_binstall_table(&table)); + let table = load_cargo_toml().expect("load installer Cargo.toml"); + world.binstall_table = Some(extract_binstall_table(&table).expect("extract binstall table")); } #[given("target \"{target}\" and version \"{version}\"")] diff --git a/installer/tests/behaviour_cli/support.rs b/installer/tests/behaviour_cli/support.rs index 1bae4eacd..41df63546 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -55,8 +55,7 @@ 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) { @@ -111,7 +110,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 { diff --git a/installer/tests/behaviour_dependency_binaries.rs b/installer/tests/behaviour_dependency_binaries.rs index e9281be5d..e486883c4 100644 --- a/installer/tests/behaviour_dependency_binaries.rs +++ b/installer/tests/behaviour_dependency_binaries.rs @@ -51,7 +51,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) }, ), @@ -167,7 +167,7 @@ fn build_stub_executor(world: &DependencyBinaryWorld, tool: &str) -> StubExecuto ) && !world.is_unsupported_target; 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, @@ -190,7 +190,7 @@ fn run_install_with_dylint_link_on_path( 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); + write_fake_binary(&dylint_link_path, true).expect("write fake dylint-link"); } with_var("PATH", Some(bin_dir), run_install) } @@ -227,7 +227,7 @@ fn when_dependency_installation_runs(world: &mut DependencyBinaryWorld) { &executor, &status, &mut world.stderr, - DependencyInstallOptions { + &DependencyInstallOptions { dirs: &dirs, repository_installer: &repository_installer, target, diff --git a/installer/tests/behaviour_docs.rs b/installer/tests/behaviour_docs.rs index d5e86c313..a80c66b7e 100644 --- a/installer/tests/behaviour_docs.rs +++ b/installer/tests/behaviour_docs.rs @@ -136,7 +136,7 @@ fn get_config_integer(table: &Table, section: &str, key: &str) -> i64 { table .get(section) .and_then(|s| s.get(key)) - .and_then(|v| v.as_integer()) + .and_then(toml::Value::as_integer) .unwrap_or_else(|| panic!("expected {section}.{key} to be an integer")) } @@ -266,7 +266,7 @@ fn then_no_unwrap_or_else_panic_allow_in_main_present(toml_world: &TomlWorld) { 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()) + .and_then(toml::Value::as_bool) .expect("expected no_unwrap_or_else_panic.allow_in_main boolean"); assert!( diff --git a/installer/tests/behaviour_install_metrics.rs b/installer/tests/behaviour_install_metrics.rs index 940dbd016..1513a6bce 100644 --- a/installer/tests/behaviour_install_metrics.rs +++ b/installer/tests/behaviour_install_metrics.rs @@ -9,7 +9,6 @@ use whitaker_installer::install_metrics::{ InstallMetrics, InstallMode, RecordOutcome, record_install_at_path, }; -const FLOAT_RATE_TOLERANCE: f64 = 1e-6; #[derive(Default)] struct InstallMetricsWorld { @@ -115,25 +114,23 @@ fn then_build_installs(world: &mut InstallMetricsWorld, expected: u64) { assert_eq!(metrics.build_installs(), expected); } -#[then("download rate is {expected:f64}")] -fn then_download_rate(world: &mut InstallMetricsWorld, expected: f64) { +#[then("download rate is {expected:u64} permille")] +fn then_download_rate(world: &mut InstallMetricsWorld, expected: u64) { let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert!( - (metrics.download_rate() - expected).abs() < FLOAT_RATE_TOLERANCE, - "expected {}, got {}", + assert_eq!( + metrics.download_rate_permille(), expected, - metrics.download_rate() + "download rate permille mismatch" ); } -#[then("build rate is {expected:f64}")] -fn then_build_rate(world: &mut InstallMetricsWorld, expected: f64) { +#[then("build rate is {expected:u64} permille")] +fn then_build_rate(world: &mut InstallMetricsWorld, expected: u64) { let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert!( - (metrics.build_rate() - expected).abs() < FLOAT_RATE_TOLERANCE, - "expected {}, got {}", + assert_eq!( + metrics.build_rate_permille(), expected, - metrics.build_rate() + "build rate permille mismatch" ); } diff --git a/installer/tests/behaviour_installer_release.rs b/installer/tests/behaviour_installer_release.rs index 83af635f5..f1bc92e56 100644 --- a/installer/tests/behaviour_installer_release.rs +++ b/installer/tests/behaviour_installer_release.rs @@ -83,7 +83,7 @@ fn attempt_packaging(world: &mut InstallerReleaseWorld) { 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 b51185f81..fa845dfaf 100644 --- a/installer/tests/behaviour_prebuilt.rs +++ b/installer/tests/behaviour_prebuilt.rs @@ -44,7 +44,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)), diff --git a/installer/tests/behaviour_staging.rs b/installer/tests/behaviour_staging.rs index 4736bb947..814ee5127 100644 --- a/installer/tests/behaviour_staging.rs +++ b/installer/tests/behaviour_staging.rs @@ -141,19 +141,15 @@ mod staging_failure { // 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. - } + .open(&probe_path) { + drop(file); + let _ = std::fs::remove_file(&probe_path); + staging_failure_world.skip_assertions.set(true); + } else { + // Expected: directory is not writable, continue. } let result = stager.prepare(); @@ -168,9 +164,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; } diff --git a/installer/tests/behaviour_toolchain.rs b/installer/tests/behaviour_toolchain.rs index 02da80e65..5754d9307 100644 --- a/installer/tests/behaviour_toolchain.rs +++ b/installer/tests/behaviour_toolchain.rs @@ -6,7 +6,7 @@ //! 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) +//! environment (`RUSTUP_HOME/CARGO_HOME` set to temp directories) //! - Failure scenarios that test error handling with a non-existent toolchain mod prebuilt_markers; @@ -19,7 +19,7 @@ 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)] 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..75aabf440 100644 --- a/installer/tests/behaviour_workflows.rs +++ b/installer/tests/behaviour_workflows.rs @@ -54,15 +54,13 @@ 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()) } fn skip_scenario_when_toolchain_missing(world: &WorkflowWorld, channel: &str) { if !is_toolchain_installed(channel) { eprintln!( - "Skipping scenario because rustup toolchain '{}' is not installed.", - channel + "Skipping scenario because rustup toolchain '{channel}' is not installed." ); world.skip_assertions.set(true); rstest_bdd::skip!( diff --git a/installer/tests/doc_extraction/extraction.rs b/installer/tests/doc_extraction/extraction.rs index 575c65bba..5ca7eb1ba 100644 --- a/installer/tests/doc_extraction/extraction.rs +++ b/installer/tests/doc_extraction/extraction.rs @@ -52,8 +52,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 { 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/installer/tests/support/mod.rs b/installer/tests/support/mod.rs index fbd8d8747..b5b6ba2cc 100644 --- a/installer/tests/support/mod.rs +++ b/installer/tests/support/mod.rs @@ -27,16 +27,15 @@ pub fn pinned_toolchain_channel() -> String { /// 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 +54,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,10 +66,10 @@ 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) { @@ -114,8 +112,7 @@ fn parse_rustup_location_output(output: &std::process::Output) -> String { .lines() .next() .expect("rustup not found in PATH") - .trim() - .to_string() + .trim().to_owned() } /// Locates the system rustup binary path. @@ -137,7 +134,7 @@ fn find_system_rustup() -> String { parse_rustup_location_output(&output) } -/// Installs rustup into the isolated cargo_bin directory. +/// Installs rustup into the isolated `cargo_bin` directory. #[cfg(unix)] fn install_rustup_to_cargo_bin(rustup_path: &str, cargo_bin: &Path) { std::os::unix::fs::symlink(rustup_path, cargo_bin.join("rustup")) @@ -150,7 +147,7 @@ fn install_rustup_to_cargo_bin(rustup_path: &str, cargo_bin: &Path) { .expect("failed to copy rustup to CARGO_HOME/bin"); } -/// 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 diff --git a/src/testing/ui/mod.rs b/src/testing/ui/mod.rs index b35348933..2ff9dec2a 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,87 +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, -} - -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); - } - } - } +/// 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)) } -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)] +fn with_vcpkg_root(callback: impl FnOnce() -> R) -> R { + use whitaker_common::test_support::with_env_var; - #[cfg(windows)] - if !vcpkg_applicable && !has_rustc_wrapper { - return None; - } - #[cfg(not(windows))] - if !has_rustc_wrapper { - return None; - } - - // 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 { #[cfg(windows)] { 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 1bf70a37e..535b61dc5 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); diff --git a/suite/tests/registration.rs b/suite/tests/registration.rs index d137100a3..10f1d10e2 100644 --- a/suite/tests/registration.rs +++ b/suite/tests/registration.rs @@ -49,14 +49,13 @@ 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() + (*message).to_owned() } else if let Some(message) = panic.downcast_ref::() { message.clone() } else { - "registration panicked with a non-string payload".to_string() + "registration panicked with a non-string payload".to_owned() } }); @@ -83,7 +82,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 +93,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); } diff --git a/typos.local.toml b/typos.local.toml index 94f5978bc..36ac75fbc 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -39,6 +39,8 @@ ignore = [ "(?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!\\(location\\.physical_location\\.artifact_location\\.uri, \"src/a\\.rs\"\\);$", + "(?m)^ assert_eq!\\(related\\.physical_location\\.artifact_location\\.uri, \"src/b\\.rs\"\\);$", "(?m)^ assert_eq!\\(normalise_locale\\(input\\), expected\\);$", "(?m)^ capitalised\\.push\\(character\\.to_ascii_lowercase\\(\\)\\);$", "(?m)^ depth: normalise_weight\\(settings\\.weights\\.depth, defaults\\.weights\\.depth\\),$", @@ -212,6 +214,7 @@ ignore = [ "(?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)^fn capitalise_segment\\(segment: &str\\) \\-> Option \\{$", "(?m)^fn localised_help_attributes_are_complete\\(\\) \\{$", diff --git a/typos.toml b/typos.toml index fdcc50807..5e9f3dd85 100644 --- a/typos.toml +++ b/typos.toml @@ -131,7 +131,9 @@ extend-ignore-re = [ "(?m)^ artefact: &cargo_metadata::Artifact,$", "(?m)^ artifact_dir = tmp_path / \"act\\-artifacts\"$", "(?m)^ artifacts: Vec,$", + "(?m)^ assert_eq!\\(location\\.physical_location\\.artifact_location\\.uri, \"src/a\\.rs\"\\);$", "(?m)^ assert_eq!\\(normalised, expected\\);$", + "(?m)^ assert_eq!\\(related\\.physical_location\\.artifact_location\\.uri, \"src/b\\.rs\"\\);$", "(?m)^ builder\\.add_default_method\\(\"normalise\", 7, false\\);$", "(?m)^ capitalised\\.push\\(first\\.to_ascii_uppercase\\(\\)\\);$", "(?m)^ context_label, fallback_messages, localised_messages,$", @@ -233,6 +235,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)^fn capitalise_segment\\(segment: &str\\) \\-> Option \\{$", From 16662b2b876b855df5ca63b71ed4f920493c02b2 Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 18 Aug 2026 05:13:47 +0200 Subject: [PATCH 03/21] Apply mechanical nightly rustfmt reformat Run `cargo fmt --all` under the new `.rustfmt.toml` (imports granularity, single-line functions, wrapped comments, formatted strings and doc examples). No behavioural changes. --- 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 +++++-------- .../brain_trait_metrics/diagnostic_tests.rs | 12 +- common/src/brain_trait_metrics/evaluation.rs | 32 ++--- .../brain_trait_metrics/evaluation_tests.rs | 3 +- 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 ++--- .../cognitive_complexity_tests.rs | 3 +- common/src/brain_type_metrics/diagnostic.rs | 73 +++++------ .../brain_type_metrics/diagnostic_tests.rs | 12 +- common/src/brain_type_metrics/evaluation.rs | 36 ++---- .../brain_type_metrics/evaluation_tests.rs | 3 +- .../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 | 33 +++-- common/src/context.rs | 104 ++++++++------- common/src/decomposition_advice/community.rs | 26 ++-- .../community_kani/propagate_labels.rs | 4 +- common/src/decomposition_advice/note.rs | 5 +- common/src/decomposition_advice/note_tests.rs | 16 ++- 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 | 21 ++-- .../decomposition_advice/tests/propagation.rs | 24 ++-- .../tests/test_fixtures.rs | 12 +- .../tests/vector_algebra.rs | 3 +- 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 | 34 +++-- common/src/i18n/loader.rs | 31 ++--- common/src/i18n/locales.rs | 10 +- common/src/i18n/mod.rs | 15 ++- common/src/i18n/selection.rs | 44 +++---- 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 | 13 +- common/src/rstest/tests/fingerprint_props.rs | 3 +- common/src/rstest/tests/mod.rs | 28 +++-- common/src/span.rs | 36 +++--- common/src/test_support/decomposition.rs | 17 ++- .../test_support/decomposition_adjacency.rs | 90 ++++++++----- .../decomposition_label_propagation.rs | 26 ++-- .../decomposition_vector_algebra.rs | 46 ++++--- common/src/test_support/fixtures.rs | 25 ++-- common/src/test_support/mod.rs | 39 +++--- common/src/test_support/ui.rs | 26 ++-- .../tests/brain_trait_evaluation_behaviour.rs | 54 ++++---- common/tests/brain_trait_metrics_behaviour.rs | 31 ++--- .../tests/brain_type_evaluation_behaviour.rs | 63 ++++------ common/tests/brain_type_metrics_behaviour.rs | 69 ++++------ .../tests/cognitive_complexity_behaviour.rs | 48 +++---- common/tests/complexity_signal_behaviour.rs | 61 ++++----- common/tests/context_behaviour.rs | 33 ++--- common/tests/cosine_threshold_behaviour.rs | 31 ++--- .../decomposition_adjacency_behaviour.rs | 28 ++--- .../tests/decomposition_advice_behaviour.rs | 47 +++---- ...ecomposition_diagnostic_notes_behaviour.rs | 59 ++++----- ...composition_label_propagation_behaviour.rs | 33 ++--- .../decomposition_vector_algebra_behaviour.rs | 27 ++-- common/tests/i18n_behaviour.rs | 81 ++++-------- common/tests/i18n_packaging.rs | 10 +- common/tests/lcom4_behaviour.rs | 58 +++------ common/tests/localizer_helpers.rs | 99 ++++++--------- common/tests/method_extraction_behaviour.rs | 47 ++----- common/tests/rstest_detection_behaviour.rs | 53 ++++---- common/tests/rstest_fingerprint_behaviour.rs | 61 ++++----- .../tests/rstest_span_recovery_behaviour.rs | 45 +++---- common/tests/support/i18n_helpers.rs | 1 + crates/bumpy_road_function/src/analysis.rs | 30 ++--- crates/bumpy_road_function/src/driver.rs | 30 +++-- .../bumpy_road_function/src/driver/config.rs | 2 +- .../src/driver/diagnostic.rs | 20 ++- .../src/driver/segment_builder.rs | 18 ++- .../tests/analysis_behaviour.rs | 37 +++--- crates/bumpy_road_function/tests/ui.rs | 6 +- .../conditional_max_n_branches/src/driver.rs | 19 +-- .../src/lib_ui_tests.rs | 6 +- .../src/tests/behaviour.rs | 46 +++---- .../function_attrs_follow_docs/src/driver.rs | 34 ++--- .../src/tests/localization.rs | 57 ++++----- .../src/tests/order_detection.rs | 48 +++---- .../src/tests/ui.rs | 8 +- crates/module_max_lines/src/driver.rs | 74 +++++------ crates/module_max_lines/src/lib_ui_tests.rs | 6 +- .../module_must_have_inner_docs/src/driver.rs | 19 +-- .../src/driver/inner_attr.rs | 3 +- .../src/driver/parser.rs | 23 ++-- .../src/tests/behaviour.rs | 62 +++------ .../src/tests/classifier.rs | 6 +- .../src/tests/span_to_snippet.rs | 14 ++- .../src/tests/ui.rs | 4 +- .../fail_expect_in_tokio_crate_non_test_fn.rs | 4 +- .../no_expect_outside_tests/src/behaviour.rs | 49 +++----- crates/no_expect_outside_tests/src/context.rs | 59 ++++----- .../src/context/tests.rs | 21 ++-- .../src/dependency_rlib_tests.rs | 30 ++--- .../src/diagnostics.rs | 53 ++++---- .../no_expect_outside_tests/src/driver/mod.rs | 13 +- .../src/driver/tests.rs | 44 ++++--- .../src/lib_ui_tests.rs | 21 ++-- crates/no_expect_outside_tests/src/tests.rs | 10 +- .../src/tests/localization.rs | 70 ++++------- .../src/tests/receiver_type_edge_cases.rs | 10 +- crates/no_std_fs_operations/src/behaviour.rs | 45 +++---- crates/no_std_fs_operations/src/config.rs | 6 +- .../no_std_fs_operations/src/config_tests.rs | 22 ++-- .../no_std_fs_operations/src/diagnostics.rs | 26 ++-- 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.rs | 15 +-- .../no_std_fs_operations/src/usage/tests.rs | 3 +- .../tests/integration_exclusion.rs | 18 +-- .../tests/test_support/mod.rs | 10 +- ...rap_in_aliased_test_crate_non_companion.rs | 11 +- ...l_unwrap_in_hand_written_test_companion.rs | 3 +- .../examples/pass_unwrap_in_rstest_harness.rs | 4 +- crates/no_unwrap_or_else_panic/src/context.rs | 22 ++-- .../src/context/tests.rs | 5 +- .../src/diagnostics.rs | 16 ++- crates/no_unwrap_or_else_panic/src/driver.rs | 20 +-- .../src/lib_ui_tests.rs | 10 +- .../src/panic_detector.rs | 3 +- crates/no_unwrap_or_else_panic/src/policy.rs | 10 +- .../src/tests/behaviour.rs | 60 +++------ .../examples/collection_zero_diagnostic.rs | 8 +- .../src/collector.rs | 18 ++- .../src/collector_tests.rs | 26 ++-- .../src/driver.rs | 30 +++-- .../src/visitor.rs | 35 ++++-- .../tests/ui.rs | 16 ++- .../tests/ui/harness_lock.rs | 13 +- .../src/behaviour.rs | 46 +++---- .../test_must_not_have_example/src/driver.rs | 26 ++-- .../src/heuristics.rs | 3 +- .../src/lib_ui_tests.rs | 7 +- 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 | 18 ++- crates/whitaker_clones_core/src/ast/mod.rs | 11 +- crates/whitaker_clones_core/src/ast/tests.rs | 33 +++-- 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 | 29 +++-- .../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 | 7 +- .../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 | 26 ++-- 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/mod.rs | 10 +- crates/whitaker_sarif/src/model/result.rs | 10 +- crates/whitaker_sarif/src/model/run.rs | 3 +- crates/whitaker_sarif/src/rules.rs | 4 +- crates/whitaker_sarif/src/test_support.rs | 8 +- .../whitaker_sarif/src/whitaker_properties.rs | 2 +- .../whitaker_sarif/tests/sarif_behaviour.rs | 70 +++++------ 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/manifest_tests.rs | 3 +- installer/src/artefact/naming.rs | 34 +++-- installer/src/artefact/packaging.rs | 37 +++--- installer/src/artefact/packaging_tests.rs | 11 +- 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 | 22 ++-- installer/src/bin/package_installer_bin.rs | 12 +- installer/src/bin/package_lints.rs | 37 +++--- installer/src/binstall_metadata_tests.rs | 3 +- installer/src/builder.rs | 39 +++--- installer/src/cli.rs | 12 +- installer/src/cli_tests.rs | 3 +- installer/src/crate_name.rs | 31 ++--- .../dependency_binaries/install/checksum.rs | 20 +-- .../dependency_binaries/install/downloader.rs | 40 +++--- .../install/downloader_boundary_tests.rs | 46 +++---- .../dependency_binaries/install/extractor.rs | 12 +- .../install/http_test_server.rs | 25 ++-- .../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 | 29 +++-- installer/src/deps.rs | 43 ++++--- installer/src/deps/install.rs | 21 ++-- installer/src/deps/install_tests.rs | 6 +- installer/src/deps/path_tests.rs | 26 ++-- installer/src/deps/tests.rs | 33 +++-- installer/src/dirs.rs | 11 +- installer/src/error.rs | 3 +- installer/src/git.rs | 14 ++- installer/src/install_flow.rs | 36 +++--- installer/src/install_flow/tests.rs | 26 ++-- installer/src/install_metrics.rs | 48 ++++--- installer/src/install_metrics_tests.rs | 25 ++-- installer/src/installer_packaging.rs | 27 ++-- installer/src/installer_packaging_tests.rs | 7 +- installer/src/lib.rs | 10 +- installer/src/list.rs | 25 ++-- installer/src/list_output.rs | 15 ++- installer/src/main.rs | 52 ++++---- installer/src/output.rs | 20 ++- installer/src/pipeline.rs | 29 +++-- installer/src/pipeline_staging_tests.rs | 22 ++-- installer/src/pipeline_tests.rs | 15 +-- installer/src/prebuilt.rs | 30 +++-- installer/src/prebuilt_path.rs | 15 ++- installer/src/prebuilt_tests.rs | 10 +- installer/src/resolution.rs | 23 ++-- installer/src/scanner.rs | 24 ++-- installer/src/staged_suite.rs | 24 ++-- installer/src/stager.rs | 16 +-- installer/src/test_utils.rs | 78 ++++++------ .../test_utils/dependency_binary_helpers.rs | 43 ++++--- .../dependency_binary_helpers_tests.rs | 5 +- installer/src/tests.rs | 43 +++---- installer/src/tests/fast_path.rs | 11 +- installer/src/toolchain.rs | 18 ++- .../src/toolchain/tests/failure_mocks.rs | 18 +-- installer/src/toolchain/tests/mod.rs | 20 ++- installer/src/toolchain/tests/test_helpers.rs | 4 +- installer/src/version.rs | 28 ++--- installer/src/workspace.rs | 16 ++- installer/src/wrapper.rs | 27 ++-- installer/tests/behaviour_artefact.rs | 61 +++------ .../tests/behaviour_artefact_packaging.rs | 70 ++++------- installer/tests/behaviour_binstall.rs | 41 +++--- installer/tests/behaviour_cli.rs | 53 ++++---- installer/tests/behaviour_cli/scenarios.rs | 15 +-- installer/tests/behaviour_cli/support.rs | 56 +++++---- installer/tests/behaviour_core.rs | 69 ++++------ .../tests/behaviour_dependency_binaries.rs | 72 +++++------ installer/tests/behaviour_docs.rs | 31 ++--- installer/tests/behaviour_install_metrics.rs | 42 +++---- .../tests/behaviour_installer_release.rs | 54 ++++---- installer/tests/behaviour_prebuilt.rs | 48 +++---- installer/tests/behaviour_staging.rs | 32 +++-- installer/tests/behaviour_toolchain.rs | 60 +++++---- installer/tests/behaviour_workflows.rs | 44 +++---- installer/tests/support/mod.rs | 10 +- installer/tests/toolchain_steps/mod.rs | 31 +++-- installer/tests/ui.rs | 9 +- 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 | 7 +- src/testing/ui/tests.rs | 10 +- src/testing/ui/toolchain.rs | 24 ++-- suite/src/driver.rs | 14 +-- suite/tests/registration.rs | 23 ++-- tests/build_config.rs | 3 +- tests/config_loading.rs | 32 ++--- 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 ++----- 322 files changed, 4048 insertions(+), 4612 deletions(-) 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/diagnostic_tests.rs b/common/src/brain_trait_metrics/diagnostic_tests.rs index 5a4a7afc6..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 // --------------------------------------------------------------------------- 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/evaluation_tests.rs b/common/src/brain_trait_metrics/evaluation_tests.rs index 030e6fb65..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 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 607eb4427..f6b638938 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/cognitive_complexity_tests.rs b/common/src/brain_type_metrics/cognitive_complexity_tests.rs index 134d398f8..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 // --------------------------------------------------------------------------- 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/diagnostic_tests.rs b/common/src/brain_type_metrics/diagnostic_tests.rs index 7b310faa6..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) // --------------------------------------------------------------------------- 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/evaluation_tests.rs b/common/src/brain_type_metrics/evaluation_tests.rs index 57d34578c..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 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 04593b077..2f375778f 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 /// @@ -346,8 +340,8 @@ 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.get(end + 1).copied().unwrap_or(0.0) - - prefix.get(start).copied().unwrap_or(0.0); + 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); } @@ -357,9 +351,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 05597a126..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, @@ -326,7 +324,9 @@ fn should_replace_best( let candidate_name = vectors .get(candidate_label) .map(MethodFeatureVector::method_name); - let best_name = vectors.get(best_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) 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 2f2b7dddb..3e7cdbe1d 100644 --- a/common/src/decomposition_advice/note_tests.rs +++ b/common/src/decomposition_advice/note_tests.rs @@ -1,10 +1,15 @@ //! 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() -> ( @@ -176,6 +181,7 @@ fn format_diagnostic_note_caps_methods_per_suggestion() { 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/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 1b4ae6d5c..8c791f04e 100644 --- a/common/src/decomposition_advice/tests/mod.rs +++ b/common/src/decomposition_advice/tests/mod.rs @@ -6,15 +6,22 @@ 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, + ExpectedSuggestion, + MethodInput, + assert_suggestion, + assert_type_decomposition_is_empty, + parser_serde_fs_fixture, + profile, +}; +use super::{ + community::{build_similarity_edges, detect_communities}, + profile::{DecompositionContext, SubjectKind}, + suggestion::{SuggestedExtractionKind, suggest_decomposition}, + vector::{build_feature_vector, dot_product, identifier_keywords}, }; -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; #[test] fn identifier_keywords_split_camel_case_and_remove_stop_words() { diff --git a/common/src/decomposition_advice/tests/propagation.rs b/common/src/decomposition_advice/tests/propagation.rs index 4de628695..190c7188c 100644 --- a/common/src/decomposition_advice/tests/propagation.rs +++ b/common/src/decomposition_advice/tests/propagation.rs @@ -2,11 +2,11 @@ use rstest::{fixture, rstest}; -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 @@ -20,9 +20,7 @@ fn edge(left: usize, right: usize, weight: u64) -> SimilarityEdge { } #[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> { @@ -40,19 +38,13 @@ fn linear_quartet_adjacency() -> Vec> { } #[fixture] -fn isolated_tail_vectors() -> Vec { - vectors(&["alpha", "beta", "gamma"]) -} +fn isolated_tail_vectors() -> Vec { vectors(&["alpha", "beta", "gamma"]) } #[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)]) } #[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 7a04cbcc6..d58767b20 100644 --- a/common/src/decomposition_advice/tests/test_fixtures.rs +++ b/common/src/decomposition_advice/tests/test_fixtures.rs @@ -1,9 +1,15 @@ //! 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, + 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/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/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 9c158e03e..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. @@ -52,10 +57,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_or(locale, |(lang, _)| lang) - { + match locale.split_once('-').map_or(locale, |(lang, _)| lang) { "cy" => welsh_branch_phrase(branches), "gd" => gaelic_branch_phrase(branches), _ => english_branch_phrase(branches), @@ -98,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")); @@ -129,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 e7171f8f2..f7f8362ef 100644 --- a/common/src/i18n/locales.rs +++ b/common/src/i18n/locales.rs @@ -4,22 +4,22 @@ //! bundles and provides utilities for checking whether a given locale tag is //! supported. - use fluent_templates::{Loader, loader::LanguageIdentifier}; use super::LOADER; static ALL_LOCALES: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - let mut locales: Vec = LOADER.locales().map(std::string::ToString::to_string).collect(); + let mut locales: Vec = LOADER + .locales() + .map(std::string::ToString::to_string) + .collect(); locales.sort_unstable(); locales }); /// 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 9945211b6..0b1ec30b9 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 76e85e11e..b6da8749b 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) { @@ -132,8 +119,14 @@ pub fn resolve_localizer( ) -> LocaleSelection { let candidates = [ (LocaleSource::ExplicitArgument, explicit.map(Cow::Borrowed)), - (LocaleSource::EnvironmentVariable, environment.map(Cow::Owned)), - (LocaleSource::Configuration, configuration.map(Cow::Borrowed)), + ( + LocaleSource::EnvironmentVariable, + environment.map(Cow::Owned), + ), + ( + LocaleSource::Configuration, + configuration.map(Cow::Borrowed), + ), ]; candidates @@ -150,9 +143,10 @@ pub fn normalise_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>, @@ -164,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 6a84edc7e..775d82225 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, - normalise_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, + normalise_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 563c39006..fdbf96d1d 100644 --- a/common/src/rstest/tests/fingerprint.rs +++ b/common/src/rstest/tests/fingerprint.rs @@ -1,10 +1,17 @@ //! Unit tests for shared `rstest` fingerprint data models. +use rstest::rstest; + use crate::rstest::{ - ArgAtom, ArgFingerprint, CalleeShape, ExprShape, LocalSlot, ParagraphFingerprint, - ParagraphNormalizer, StmtShape, + 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 1ca668454..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. diff --git a/common/src/rstest/tests/mod.rs b/common/src/rstest/tests/mod.rs index c8d4181c0..c5640685c 100644 --- a/common/src/rstest/tests/mod.rs +++ b/common/src/rstest/tests/mod.rs @@ -3,16 +3,30 @@ 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, + 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) 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 ae88e06f0..b3987900c 100644 --- a/common/src/test_support/decomposition.rs +++ b/common/src/test_support/decomposition.rs @@ -11,16 +11,21 @@ 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, + 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 diff --git a/common/src/test_support/decomposition_adjacency.rs b/common/src/test_support/decomposition_adjacency.rs index 517e0e9fb..1b7c2a947 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,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. /// @@ -100,9 +99,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 +121,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 +147,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,21 +173,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| matches!(pair, [(left, _), (right, _)] if left <= right)) - }) + self.neighbours.iter().all(|bucket| { + bucket + .windows(2) + .all(|pair| matches!(pair, [(left, _), (right, _)] if left <= right)) + }) } } @@ -186,7 +213,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() @@ -205,9 +233,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); /// ``` /// diff --git a/common/src/test_support/decomposition_label_propagation.rs b/common/src/test_support/decomposition_label_propagation.rs index 71552cd1e..f57596719 100644 --- a/common/src/test_support/decomposition_label_propagation.rs +++ b/common/src/test_support/decomposition_label_propagation.rs @@ -1,23 +1,21 @@ //! Observable label-propagation seams for decomposition advice tests. +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, }, }; -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"], @@ -41,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 e7fbb9fb3..3aa9f5bf1 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,16 +70,18 @@ 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`]. /// /// 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,16 +102,18 @@ 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`]. /// /// 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,16 +134,18 @@ 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`]. /// /// 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,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 0d5a90d8e..06e44e1bf 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()?; @@ -77,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()?; @@ -170,12 +169,16 @@ 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}; + 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"); diff --git a/common/src/test_support/mod.rs b/common/src/test_support/mod.rs index 6e13335f2..57dd188a8 100644 --- a/common/src/test_support/mod.rs +++ b/common/src/test_support/mod.rs @@ -6,31 +6,38 @@ //! //! ## 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. -//! - [`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. +//! - [`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. +//! - [`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, + sync::{Mutex, MutexGuard, OnceLock}, +}; + 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, }; -use std::ffi::OsStr; -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 diff --git a/common/src/test_support/ui.rs b/common/src/test_support/ui.rs index 635b0de7a..edd7d9ac6 100644 --- a/common/src/test_support/ui.rs +++ b/common/src/test_support/ui.rs @@ -4,14 +4,18 @@ //! 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, @@ -22,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. @@ -167,10 +167,12 @@ pub fn read_directory_config(directory: &Utf8Path) -> io::Result> #[cfg(test)] mod tests { - use super::*; - use camino::Utf8PathBuf; use std::fs; + use camino::Utf8PathBuf; + + use super::*; + fn utf8_path(buf: &Path) -> Utf8PathBuf { Utf8PathBuf::from_path_buf(buf.to_path_buf()).expect("utf8 path") } diff --git a/common/tests/brain_trait_evaluation_behaviour.rs b/common/tests/brain_trait_evaluation_behaviour.rs index f7525ceec..1972b5216 100644 --- a/common/tests/brain_trait_evaluation_behaviour.rs +++ b/common/tests/brain_trait_evaluation_behaviour.rs @@ -1,13 +1,21 @@ //! 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}; #[derive(Debug)] struct EvaluationWorld { @@ -96,9 +104,7 @@ impl EvaluationWorld { } #[fixture] -fn world() -> EvaluationWorld { - EvaluationWorld::default() -} +fn world() -> EvaluationWorld { EvaluationWorld::default() } // --- Given steps --- @@ -204,41 +210,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 023f57f84..3172353aa 100644 --- a/common/tests/brain_trait_metrics_behaviour.rs +++ b/common/tests/brain_trait_metrics_behaviour.rs @@ -1,8 +1,9 @@ //! 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}; #[derive(Clone, Debug)] @@ -25,9 +26,7 @@ struct TraitMetricsWorld { } #[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(); @@ -180,31 +179,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 bb7aa245f..3131e6807 100644 --- a/common/tests/brain_type_evaluation_behaviour.rs +++ b/common/tests/brain_type_evaluation_behaviour.rs @@ -1,13 +1,22 @@ //! 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}; #[derive(Debug)] struct EvaluationWorld { @@ -86,9 +95,7 @@ impl EvaluationWorld { } #[fixture] -fn world() -> EvaluationWorld { - EvaluationWorld::default() -} +fn world() -> EvaluationWorld { EvaluationWorld::default() } // --- Given steps --- @@ -100,9 +107,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) { @@ -200,46 +205,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 1fb4a52cf..3deb6312f 100644 --- a/common/tests/brain_type_metrics_behaviour.rs +++ b/common/tests/brain_type_metrics_behaviour.rs @@ -1,10 +1,15 @@ //! 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, + ForeignReferenceSet, + MethodMetrics, + TypeMetricsBuilder, + brain_methods, + foreign_reach_count, weighted_methods_count, }; @@ -48,9 +53,7 @@ impl Default for MetricsWorld { } #[fixture] -fn world() -> MetricsWorld { - MetricsWorld::default() -} +fn world() -> MetricsWorld { MetricsWorld::default() } // --- Helpers --- @@ -84,9 +87,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) { @@ -94,9 +95,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) { @@ -206,14 +205,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) { @@ -236,51 +231,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 f0f7d6444..7652c240d 100644 --- a/common/tests/cognitive_complexity_behaviour.rs +++ b/common/tests/cognitive_complexity_behaviour.rs @@ -1,8 +1,9 @@ //! 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; #[derive(Debug)] @@ -21,9 +22,7 @@ impl Default for CcWorld { } #[fixture] -fn world() -> CcWorld { - CcWorld::default() -} +fn world() -> CcWorld { CcWorld::default() } // --- Helpers --- @@ -74,13 +73,14 @@ fn given_push_nesting_not_expanded(world: &CcWorld) { } #[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) { with_builder(world, |b| b.push_nesting(true)); } #[given("nesting is popped")] fn given_pop_nesting(world: &CcWorld) { - with_builder(world, whitaker_common::CognitiveComplexityBuilder::pop_nesting); + with_builder( + world, + whitaker_common::CognitiveComplexityBuilder::pop_nesting, + ); } // --- When steps --- @@ -108,41 +108,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 380aea7bd..05aa8423c 100644 --- a/common/tests/complexity_signal_behaviour.rs +++ b/common/tests/complexity_signal_behaviour.rs @@ -1,10 +1,15 @@ //! 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, + LineSegment, + SignalBuildError, + SmoothingError, + rasterize_signal, + smooth_moving_average, }; #[derive(Debug, Default)] @@ -24,17 +29,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, @@ -98,9 +97,7 @@ impl SignalWorld { } #[fixture] -fn world() -> SignalWorld { - SignalWorld::default() -} +fn world() -> SignalWorld { SignalWorld::default() } /// Parses a comma-separated list of floating-point values. /// @@ -162,8 +159,8 @@ fn assert_vec_approx_eq(actual: &[f64], expected: &[f64]) { let distance = ulp_distance(*actual_value, *expected_value); assert!( distance.is_some_and(|ulps| ulps <= MAX_ULP_DISTANCE), - "expected element {idx} to be {expected_value}, got {actual_value} \ - (ULP distance {distance:?})", + "expected element {idx} to be {expected_value}, got {actual_value} (ULP distance \ + {distance:?})", ); } } @@ -186,19 +183,13 @@ fn given_raw_signal(world: &SignalWorld, values: 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) { @@ -235,26 +226,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 27e8f64d7..e0294115f 100644 --- a/common/tests/context_behaviour.rs +++ b/common/tests/context_behaviour.rs @@ -3,11 +3,14 @@ //! 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}, +}; #[derive(Clone, Debug, Default)] struct FunctionFixture { @@ -40,17 +43,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)); @@ -58,9 +55,7 @@ impl FunctionFixture { } #[fixture] -fn function() -> FunctionFixture { - FunctionFixture::new() -} +fn function() -> FunctionFixture { FunctionFixture::new() } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] struct Evaluation { is_test: bool, @@ -68,9 +63,7 @@ struct Evaluation { } #[fixture] -fn evaluation() -> Evaluation { - Evaluation::default() -} +fn evaluation() -> Evaluation { Evaluation::default() } #[given("a function annotated with rstest")] fn given_rstest(function: &FunctionFixture) { @@ -85,9 +78,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 recognises {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 c37078c11..505e9d9fa 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, +}; #[derive(Clone, Copy, Debug)] enum MethodSide { @@ -39,9 +40,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 { @@ -65,9 +64,7 @@ struct CosineThresholdWorld { } #[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( @@ -189,16 +186,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 4aa148e03..ed5c93b69 100644 --- a/common/tests/decomposition_adjacency_behaviour.rs +++ b/common/tests/decomposition_adjacency_behaviour.rs @@ -1,10 +1,14 @@ //! 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, + AdjacencyError, + AdjacencyReport, + EdgeInput, + adjacency_report, }; #[derive(Debug, Default)] @@ -15,9 +19,7 @@ struct AdjacencyWorld { } #[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) { @@ -124,21 +126,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 86ddc215e..9e92788f1 100644 --- a/common/tests/decomposition_advice_behaviour.rs +++ b/common/tests/decomposition_advice_behaviour.rs @@ -1,21 +1,23 @@ //! 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, + DecompositionContext, + DecompositionSuggestion, + MethodProfileBuilder, + SubjectKind, + SuggestedExtractionKind, + suggest_decomposition, }; #[derive(Debug, Clone)] 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 { @@ -42,9 +44,7 @@ struct DecompositionWorld { } #[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(); @@ -150,9 +150,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) { @@ -262,7 +260,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 {expected_methods:?}; actual suggestions: {actual:?}" + "missing {kind} suggestion labelled {label} containing methods \ + {expected_methods:?}; actual suggestions: {actual:?}" )) } }) @@ -298,26 +297,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 222934f16..52a708e0c 100644 --- a/common/tests/decomposition_diagnostic_notes_behaviour.rs +++ b/common/tests/decomposition_diagnostic_notes_behaviour.rs @@ -1,33 +1,32 @@ //! 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}, }; #[derive(Debug, Clone)] 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 { @@ -47,9 +46,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)] @@ -63,9 +60,7 @@ struct DiagnosticNoteWorld { } #[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(); @@ -161,9 +156,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) { @@ -273,38 +266,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 6a1ee8a3e..7fc8293f6 100644 --- a/common/tests/decomposition_label_propagation_behaviour.rs +++ b/common/tests/decomposition_label_propagation_behaviour.rs @@ -1,20 +1,21 @@ //! 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, + AdjacencyError, + EdgeInput, + LabelPropagationReport, + label_propagation_report, }; #[derive(Clone, Debug)] struct CsvList(Vec); impl CsvList { - fn into_vec(self) -> Vec { - self.0 - } + fn into_vec(self) -> Vec { self.0 } } impl FromStr for CsvList { @@ -36,9 +37,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 { @@ -68,9 +67,7 @@ struct LabelPropagationWorld { } #[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) { @@ -199,9 +196,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", @@ -215,14 +210,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 7a6194943..e6abfc4f1 100644 --- a/common/tests/decomposition_vector_algebra_behaviour.rs +++ b/common/tests/decomposition_vector_algebra_behaviour.rs @@ -1,13 +1,12 @@ //! 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}, }; #[derive(Clone, Copy, Debug)] @@ -41,9 +40,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 +64,7 @@ struct VectorAlgebraWorld { } #[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( @@ -222,17 +217,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 d0a19d7f7..027526a25 100644 --- a/common/tests/i18n_behaviour.rs +++ b/common/tests/i18n_behaviour.rs @@ -4,11 +4,10 @@ //! 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}; #[path = "support/i18n_helpers.rs"] @@ -23,9 +22,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(); @@ -62,19 +59,13 @@ fn branch_count_from_key(key: &str) -> Option<(String, u32)> { } #[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) { @@ -106,7 +97,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(i64::from(lint_count))); + 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; @@ -159,77 +153,52 @@ 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 { //! 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_owned(), 42)))] #[case("foo with lint 42", None)] diff --git a/common/tests/i18n_packaging.rs b/common/tests/i18n_packaging.rs index 3456d534b..a3532bead 100644 --- a/common/tests/i18n_packaging.rs +++ b/common/tests/i18n_packaging.rs @@ -6,12 +6,14 @@ #[cfg(unix)] mod unix { - use std::fs; - use std::path::{Path, PathBuf}; - use std::process::Command; - use whitaker_common::i18n::packaged_fallback_locale_path; + use std::{ + fs, + path::{Path, PathBuf}, + process::Command, + }; use tempfile::{Builder, TempDir}; + use whitaker_common::i18n::packaged_fallback_locale_path; #[test] fn fluent_bundles_are_included_in_the_package_tarball() { diff --git a/common/tests/lcom4_behaviour.rs b/common/tests/lcom4_behaviour.rs index ef125e5f5..40fc7d993 100644 --- a/common/tests/lcom4_behaviour.rs +++ b/common/tests/lcom4_behaviour.rs @@ -1,9 +1,12 @@ //! 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}; #[derive(Debug, Default)] @@ -13,24 +16,18 @@ 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() } } #[fixture] -fn world() -> LcomWorld { - LcomWorld::default() -} +fn world() -> LcomWorld { LcomWorld::default() } /// Parses a comma-separated list of field names into a `BTreeSet`. /// @@ -68,9 +65,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) { @@ -82,52 +77,37 @@ 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 --- #[cfg(test)] mod parse_field_set_tests { - use super::parse_field_set; 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 1357ce428..17c5358e9 100644 --- a/common/tests/localizer_helpers.rs +++ b/common/tests/localizer_helpers.rs @@ -3,18 +3,30 @@ //! Scenarios validate locale resolution and fallback handling so lints can //! depend on deterministic localization outcomes. +use std::{ + borrow::Cow, + cell::RefCell, + sync::{Mutex, MutexGuard}, +}; + 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::with_locale; static ENVIRONMENT_LOCK: Mutex<()> = Mutex::new(()); @@ -90,9 +102,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( @@ -116,13 +126,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(); @@ -169,20 +175,14 @@ impl HelperWorld { .unwrap_or_else(|| panic!("diagnostic messages should be resolved")) } - fn recorded_messages(&self) -> Vec { - self.emitter.recorded_messages() - } + fn recorded_messages(&self) -> Vec { self.emitter.recorded_messages() } } #[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) { @@ -190,9 +190,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) { @@ -206,14 +204,10 @@ 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) { world.assert_locale(unquote(&locale)); } #[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) { @@ -226,19 +220,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) { - world.resolve_messages(); -} +fn when_resolve_messages(world: &HelperWorld) { world.resolve_messages(); } #[then("the fallback primary message contains {snippet}")] fn then_fallback_primary(world: &HelperWorld, snippet: String) { @@ -282,29 +270,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() { @@ -318,7 +296,8 @@ fn invalid_locale_warns_and_falls_back() { let mut warned = false; while let Some(record) = logger.pop() { if record - .args().to_owned() + .args() + .to_owned() .contains("unsupported DYLINT_LOCALE `xx-XX`") { warned = true; diff --git a/common/tests/method_extraction_behaviour.rs b/common/tests/method_extraction_behaviour.rs index a84e86d98..841cc4067 100644 --- a/common/tests/method_extraction_behaviour.rs +++ b/common/tests/method_extraction_behaviour.rs @@ -1,8 +1,9 @@ //! 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}; #[derive(Debug, Default)] @@ -47,19 +48,13 @@ 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()) } } #[fixture] -fn world() -> ExtractionWorld { - ExtractionWorld::default() -} +fn world() -> ExtractionWorld { ExtractionWorld::default() } // --- Given steps --- @@ -91,9 +86,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 --- @@ -145,36 +138,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 891543f92..9b0347724 100644 --- a/common/tests/rstest_detection_behaviour.rs +++ b/common/tests/rstest_detection_behaviour.rs @@ -1,13 +1,22 @@ //! 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, + }, }; #[derive(Clone, Debug, Default)] @@ -101,19 +110,13 @@ impl DetectionWorld { } #[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) { @@ -137,14 +140,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) { @@ -174,14 +173,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> { @@ -189,9 +184,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 recognised 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 058fa87b2..4100d0238 100644 --- a/common/tests/rstest_fingerprint_behaviour.rs +++ b/common/tests/rstest_fingerprint_behaviour.rs @@ -1,11 +1,18 @@ //! 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, + ArgAtom, + ArgFingerprint, + CalleeShape, + ExprShape, + LocalSlot, + ParagraphFingerprint, + ParagraphNormalizer, + StmtShape, }; #[derive(Default)] @@ -49,19 +56,13 @@ impl FingerprintWorld { } #[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) { @@ -92,24 +93,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) { @@ -172,26 +165,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 4733ef5ee..99d60b88e 100644 --- a/common/tests/rstest_span_recovery_behaviour.rs +++ b/common/tests/rstest_span_recovery_behaviour.rs @@ -1,10 +1,13 @@ //! 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}, +}; #[derive(Default)] struct SpanRecoveryWorld { @@ -28,29 +31,19 @@ impl SpanRecoveryWorld { } #[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)) { @@ -85,21 +78,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/common/tests/support/i18n_helpers.rs b/common/tests/support/i18n_helpers.rs index 787627d6e..a243e6f8b 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}']; diff --git a/crates/bumpy_road_function/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index a6ae2743c..404c7aaa8 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::{Settings, Weights, normalise_settings}; /// /// let settings = Settings { /// threshold: -1.0, @@ -97,9 +97,7 @@ pub fn normalise_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.rs b/crates/bumpy_road_function/src/driver.rs index f320d5492..f74c9fd9b 100644 --- a/crates/bumpy_road_function/src/driver.rs +++ b/crates/bumpy_road_function/src/driver.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, normalise_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, normalise_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_linting::impl_late_lint! { pub BUMPY_ROAD_FUNCTION, @@ -152,7 +156,11 @@ fn extract_expr_target(expr: &hir::Expr<'_>) -> Option { )) } -const 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, diff --git a/crates/bumpy_road_function/src/driver/config.rs b/crates/bumpy_road_function/src/driver/config.rs index 2bb1101a7..33c86245a 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::normalise_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 45bc0889e..5f34150e2 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> { @@ -125,7 +128,12 @@ struct LineSpanMapper { } impl LineSpanMapper { - const 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/segment_builder.rs b/crates/bumpy_road_function/src/driver/segment_builder.rs index d07eb72e0..bceec6876 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, @@ -160,7 +160,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(), @@ -186,13 +188,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 909b8fe43..3ca92b7ac 100644 --- a/crates/bumpy_road_function/tests/analysis_behaviour.rs +++ b/crates/bumpy_road_function/tests/analysis_behaviour.rs @@ -7,13 +7,18 @@ #[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, + 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( @@ -106,9 +111,7 @@ struct World { } #[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) { @@ -118,19 +121,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) { @@ -152,14 +149,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/bumpy_road_function/tests/ui.rs b/crates/bumpy_road_function/tests/ui.rs index 7693b624b..ee9904e1a 100644 --- a/crates/bumpy_road_function/tests/ui.rs +++ b/crates/bumpy_road_function/tests/ui.rs @@ -7,9 +7,10 @@ #[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] @@ -21,7 +22,8 @@ fn ui() { }) .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}\", message: {error} }}" ) }); } diff --git a/crates/conditional_max_n_branches/src/driver.rs b/crates/conditional_max_n_branches/src/driver.rs index 2a74138d5..659641dfc 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 { @@ -287,9 +291,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/lib_ui_tests.rs b/crates/conditional_max_n_branches/src/lib_ui_tests.rs index 5afc924cb..5322b1316 100644 --- a/crates/conditional_max_n_branches/src/lib_ui_tests.rs +++ b/crates/conditional_max_n_branches/src/lib_ui_tests.rs @@ -2,9 +2,10 @@ //! `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] @@ -16,7 +17,8 @@ fn ui() { }) .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}\", message: {error} }}" ) }); } diff --git a/crates/conditional_max_n_branches/src/tests/behaviour.rs b/crates/conditional_max_n_branches/src/tests/behaviour.rs index 48dddf3ed..a6748ebdb 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 { @@ -13,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()); @@ -34,24 +32,16 @@ impl PredicateWorld { } #[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) { @@ -64,21 +54,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 93c46a224..52ae4fcc5 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}; @@ -195,23 +203,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 da1dcda97..3f8ead786 100644 --- a/crates/function_attrs_follow_docs/src/tests/localization.rs +++ b/crates/function_attrs_follow_docs/src/tests/localization.rs @@ -3,15 +3,20 @@ //! 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, + 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 { @@ -54,14 +59,10 @@ impl LocalizationWorld { } #[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) { @@ -79,14 +80,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 localise the diagnostic")] fn when_localize(world: &LocalizationWorld) { @@ -147,30 +144,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 891ee2a6e..1e7684642 100644 --- a/crates/function_attrs_follow_docs/src/tests/order_detection.rs +++ b/crates/function_attrs_follow_docs/src/tests/order_detection.rs @@ -3,30 +3,28 @@ //! 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_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() - } + 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)] @@ -47,14 +45,10 @@ impl AttributeWorld { } #[fixture] -fn world() -> AttributeWorld { - AttributeWorld::default() -} +fn world() -> AttributeWorld { AttributeWorld::default() } #[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) { @@ -82,9 +76,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)>) { @@ -96,9 +88,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 b37b4a2fe..548592159 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"; @@ -146,9 +150,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)); @@ -188,9 +193,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)] @@ -208,10 +214,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 { @@ -222,17 +230,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(); @@ -250,29 +252,19 @@ mod behaviour { } #[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) { @@ -290,24 +282,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_max_lines/src/lib_ui_tests.rs b/crates/module_max_lines/src/lib_ui_tests.rs index 3ac392068..7f84775f1 100644 --- a/crates/module_max_lines/src/lib_ui_tests.rs +++ b/crates/module_max_lines/src/lib_ui_tests.rs @@ -2,9 +2,10 @@ //! `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] @@ -16,7 +17,8 @@ fn ui() { }) .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}\", message: {error} }}" ) }); } diff --git a/crates/module_must_have_inner_docs/src/driver.rs b/crates/module_must_have_inner_docs/src/driver.rs index 520ff0863..9a6d377ec 100644 --- a/crates/module_must_have_inner_docs/src/driver.rs +++ b/crates/module_must_have_inner_docs/src/driver.rs @@ -14,13 +14,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; @@ -44,9 +49,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/inner_attr.rs b/crates/module_must_have_inner_docs/src/driver/inner_attr.rs index d73b62d6f..7e502bf83 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/parser.rs b/crates/module_must_have_inner_docs/src/driver/parser.rs index e5292ad2b..3bda7875e 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. @@ -87,9 +87,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. @@ -104,7 +102,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)"); /// @@ -155,9 +155,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 (_, tail) = skip_leading_whitespace(tail); @@ -228,17 +226,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 0e245e9c1..e4f2461df 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 { @@ -15,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(); @@ -35,29 +35,19 @@ impl ModuleWorld { } #[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) { @@ -65,14 +55,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) { @@ -93,31 +79,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/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..c73fbf550 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,13 @@ //! 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)) { @@ -26,9 +30,7 @@ fn span_to_snippet_failure_skips_diagnostic(unresolvable_span_fixture: (SourceMa } #[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/module_must_have_inner_docs/src/tests/ui.rs b/crates/module_must_have_inner_docs/src/tests/ui.rs index 33e522bf4..7782bbd25 100644 --- a/crates/module_must_have_inner_docs/src/tests/ui.rs +++ b/crates/module_must_have_inner_docs/src/tests/ui.rs @@ -12,8 +12,8 @@ use whitaker_common::test_support::with_locale; /// /// - `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 /// 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..0998a1fc0 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 @@ -15,6 +15,4 @@ async fn unrelated_tokio_test() { value.expect("actual Tokio tests should still be allowed"); } -fn main() { - parse_config(); -} +fn main() { parse_config(); } diff --git a/crates/no_expect_outside_tests/src/behaviour.rs b/crates/no_expect_outside_tests/src/behaviour.rs index 2c94e0084..9d95f5e8c 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, summarise_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, summarise_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,19 @@ 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() } } #[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 +102,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 summarise the context")] -fn when_summarise(world: &ContextWorld) { - world.evaluate(); -} +fn when_summarise(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.rs b/crates/no_expect_outside_tests/src/context.rs index d892f1ad3..509c1f9f6 100644 --- a/crates/no_expect_outside_tests/src/context.rs +++ b/crates/no_expect_outside_tests/src/context.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 /// @@ -267,8 +272,7 @@ pub(crate) fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { return false; } - attr.meta_item_list() - .is_some_and(check_cfg_attr_for_test) + attr.meta_item_list().is_some_and(check_cfg_attr_for_test) } fn meta_item_inner_contains_test(item: MetaItemInner) -> bool { @@ -288,23 +292,20 @@ 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() - .is_some_and(|items| { - items - .iter() - .cloned() - .any(|item| meta_item_inner_contains_test_with_polarity(item, !is_positive)) - }); - } - - meta.meta_item_list() - .is_some_and(|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)) - }) + .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 { diff --git a/crates/no_expect_outside_tests/src/context/tests.rs b/crates/no_expect_outside_tests/src/context/tests.rs index 3a527df66..3ed91a899 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")] @@ -183,9 +180,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 @@ -257,9 +252,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 7357a2093..a364ca2cc 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)] @@ -50,9 +54,7 @@ const TIED_ARTEFACTS: [ArtefactSpec<'static>; 2] = [ /// rstest fixture that creates a uniquely named temporary directory for a /// selection test. #[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 @@ -122,15 +124,11 @@ 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 { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.0); - } + fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } /// Creates an empty `.rlib` fixture file at `directory/file_name` and returns @@ -153,7 +151,11 @@ fn set_modified_time(path: &Path, seconds_since_epoch: u64) { .metadata() .expect("rlib fixture metadata should be readable") .accessed(); - let times = existing_accessed.map_or_else(|_| std::fs::FileTimes::new(), |accessed| std::fs::FileTimes::new().set_accessed(accessed)) + let times = existing_accessed + .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/diagnostics.rs b/crates/no_expect_outside_tests/src/diagnostics.rs index 9222c54ed..08378e1fd 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> { @@ -231,9 +223,10 @@ fn fallback_messages( } fn context_label(summary: &ContextSummary) -> ContextLabel { - let label = summary - .function_name - .as_ref().map_or_else(|| "the surrounding scope".to_owned(), |name| format!("function `{name}`")); + 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 a76b39798..77d5e3f85 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, summarise_context}; -use crate::diagnostics::{DiagnosticContext, emit_diagnostic}; +use crate::{ + context::{collect_context, is_cfg_test_attribute, summarise_context}, + diagnostics::{DiagnosticContext, emit_diagnostic}, +}; dylint_linting::impl_late_lint! { pub NO_EXPECT_OUTSIDE_TESTS, diff --git a/crates/no_expect_outside_tests/src/driver/tests.rs b/crates/no_expect_outside_tests/src/driver/tests.rs index 121d8f1bc..b97123599 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 @@ -166,36 +168,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/lib_ui_tests.rs b/crates/no_expect_outside_tests/src/lib_ui_tests.rs index 42c9b0999..4b0913358 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}; @@ -85,7 +88,11 @@ fn run_example_under_test_harness(spec: &ExampleHarnessRun<'_>) { 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 ) }); diff --git a/crates/no_expect_outside_tests/src/tests.rs b/crates/no_expect_outside_tests/src/tests.rs index f569b8b4c..576fe98b9 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::summarise_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::summarise_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 1e6cd8e58..741d5fb1e 100644 --- a/crates/no_expect_outside_tests/src/tests/localization.rs +++ b/crates/no_expect_outside_tests/src/tests/localization.rs @@ -5,16 +5,24 @@ //! 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, + 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 @@ -92,9 +100,7 @@ impl LocalizationWorld { } #[fixture] -fn world() -> LocalizationWorld { - LocalizationWorld::default() -} +fn world() -> LocalizationWorld { LocalizationWorld::default() } #[given("the locale {locale} is selected")] fn given_locale(world: &LocalizationWorld, locale: String) { @@ -114,9 +120,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) { @@ -129,14 +133,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 localise the expect diagnostic")] fn when_localize(world: &LocalizationWorld) { @@ -202,39 +202,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) { @@ -261,6 +247,4 @@ fn execute_localization( localised_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 10e7761c1..59695a6d9 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,15 @@ //! Edge-case localization tests covering unusual receiver labels. +use rstest::rstest; + use super::{ - ContextLabel, Localizer, NoExpectMessages, ReceiverCategory, ReceiverLabel, localised_messages, + 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_std_fs_operations/src/behaviour.rs b/crates/no_std_fs_operations/src/behaviour.rs index 4b3cca1ee..d3d053621 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, localised_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, localised_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) { - self.operation = operation.to_owned(); - } + fn set_operation(&mut self, operation: &str) { self.operation = operation.to_owned(); } - 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 localise the std::fs diagnostic")] -fn when_localise(world: &WorldCell) { - world.borrow_mut().resolve(); -} +fn when_localise(world: &WorldCell) { world.borrow_mut().resolve(); } #[then("the primary mentions {snippet}")] fn then_primary(world: &WorldCell, snippet: String) { @@ -120,26 +113,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/config_tests.rs b/crates/no_std_fs_operations/src/config_tests.rs index f03432377..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() { diff --git a/crates/no_std_fs_operations/src/diagnostics.rs b/crates/no_std_fs_operations/src/diagnostics.rs index cd68c8710..2608500e0 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<'_>, @@ -58,11 +65,14 @@ fn fallback_messages(operation: &str) -> StdFsMessages { let note = concat!( "std::fs reads the ambient working directory, ", "so it bypasses the capability model enforced by cap-std and camino." - ).to_owned(); + ) + .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 ea096af0d..ccaf2438c 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, + }, +}; pub struct NoStdFsOperations { localizer: Localizer, @@ -147,9 +156,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 85f18a156..736d3b0eb 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 247ec4971..506de8976 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); @@ -35,9 +34,7 @@ impl ExclusionWorld { type WorldCell = RefCell; #[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) { @@ -45,9 +42,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.rs b/crates/no_std_fs_operations/src/usage.rs index 356b976d1..07a7090d9 100644 --- a/crates/no_std_fs_operations/src/usage.rs +++ b/crates/no_std_fs_operations/src/usage.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. @@ -152,9 +147,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 4b4183b4b..76caefc98 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/integration_exclusion.rs b/crates/no_std_fs_operations/tests/integration_exclusion.rs index 0741591ec..982c0d61b 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}; @@ -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(); @@ -400,7 +403,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 ) })?; 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 4e0580abc..d51af196f 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; @@ -15,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/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs index 55890ea40..e20285e62 100644 --- a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs +++ b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs @@ -11,7 +11,8 @@ #[cfg(test)] #[expect( dead_code, - reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." + reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. \ + Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." )] fn aliased_test_crate_non_companion_subject(value: i32) { let _ = Some(value).unwrap_or_else(|| panic!("aliased non-companion {value}")); @@ -24,11 +25,11 @@ mod aliased_test_crate_non_companion_subject { #[expect( dead_code, - reason = "proves an aliased `test` import alone is not an rstest companion; never invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." + reason = "proves an aliased `test` import alone is not an rstest companion; never \ + invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap \ + item 2.2.9." )] - fn unrelated_item() { - let _ = test_harness::black_box(1); - } + fn unrelated_item() { let _ = test_harness::black_box(1); } } fn main() {} diff --git a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs index 757396f3e..c0af50c6f 100644 --- a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs +++ b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs @@ -18,7 +18,8 @@ #[cfg(test)] #[expect( dead_code, - reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." + reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. \ + Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." )] fn hand_written_test_companion_subject(value: i32) { let _ = Some(value).unwrap_or_else(|| panic!("handwritten companion {value}")); diff --git a/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs b/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs index 5e1a41e56..ace766446 100644 --- a/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs +++ b/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs @@ -39,9 +39,7 @@ fn pass_unwrap_in_rstest_harness(value: i32) { #[cfg(all(test, windows))] mod pass_unwrap_in_rstest_harness { #[test] - fn case_1() { - super::pass_unwrap_in_rstest_harness(1); - } + fn case_1() { super::pass_unwrap_in_rstest_harness(1); } } fn main() {} diff --git a/crates/no_unwrap_or_else_panic/src/context.rs b/crates/no_unwrap_or_else_panic/src/context.rs index 5b3d20548..ec8d153e0 100644 --- a/crates/no_unwrap_or_else_panic/src/context.rs +++ b/crates/no_unwrap_or_else_panic/src/context.rs @@ -31,10 +31,7 @@ use rustc_span::sym; /// Summarize the context for a given HIR node. #[cfg(feature = "dylint-driver")] -pub(crate) fn summarise_context( - cx: &LateContext<'_>, - hir_id: hir::HirId, -) -> ContextSummary { +pub(crate) fn summarise_context(cx: &LateContext<'_>, hir_id: hir::HirId) -> ContextSummary { let mut entries = Vec::new(); let mut has_cfg_test = false; @@ -166,8 +163,7 @@ fn is_cfg_test_attribute(attr: &hir::Attribute) -> bool { return false; } - attr.meta_item_list() - .is_some_and(check_cfg_attr_for_test) + attr.meta_item_list().is_some_and(check_cfg_attr_for_test) } #[cfg(feature = "dylint-driver")] @@ -210,14 +206,12 @@ 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() - .is_some_and(|items| { - items - .iter() - .cloned() - .any(|item| meta_item_inner_contains_test_with_polarity(item, !is_positive)) - }); + 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() 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 6dd9bbc2a..42740ade9 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_linting::impl_late_lint! { pub NO_UNWRAP_OR_ELSE_PANIC, Deny, @@ -28,9 +32,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/lib_ui_tests.rs b/crates/no_unwrap_or_else_panic/src/lib_ui_tests.rs index fefe4e244..50707b7aa 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))] @@ -58,7 +58,8 @@ fn ui() { }) .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}\", message: {error} }}" ) }); } @@ -93,7 +94,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/panic_detector.rs b/crates/no_unwrap_or_else_panic/src/panic_detector.rs index 433213d7c..975c23214 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 26dc69887..3a625dd19 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 f22741da7..db3c291dc 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 { @@ -29,9 +33,7 @@ impl DecisionWorld { } #[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) { @@ -46,9 +48,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) { @@ -80,24 +80,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) { @@ -110,31 +102,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/examples/collection_zero_diagnostic.rs b/crates/rstest_helper_should_be_fixture/examples/collection_zero_diagnostic.rs index 01e152378..677c000e0 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 @@ -50,9 +50,7 @@ struct Builder<'a> { } impl Builder<'_> { - fn build(&self, suffix: &str) -> String { - format!("{}-{suffix}", self.fixture) - } + fn build(&self, suffix: &str) -> String { format!("{}-{suffix}", self.fixture) } } #[rstest] @@ -67,9 +65,7 @@ fn rstest_helper_call_collection_stays_silent(fixture: &str) { assert_eq!(built, "fixture-suffix"); } -const fn nested_helper(fixture: &str) -> &str { - fixture -} +const fn nested_helper(fixture: &str) -> &str { fixture } #[rstest] #[case("first")] 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 39704c9d1..31ea406e7 100644 --- a/crates/rstest_helper_should_be_fixture/src/collector_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs @@ -6,19 +6,23 @@ //! 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}; use super::{ - CallSiteCollector, CallSiteLocation, CallSiteRecord, literal_text_atom, + CallSiteCollector, + CallSiteLocation, + CallSiteRecord, + literal_text_atom, should_skip_arg_for_unrecoverable_span, }; -use proptest::prelude::*; - #[test] fn collector_iterates_callees_in_definition_path_order() { let mut collector = CallSiteCollector::default(); @@ -139,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) @@ -169,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 e6c67636e..27b1c8013 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 7b81020fb..3b2e44639 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.rs b/crates/rstest_helper_should_be_fixture/tests/ui.rs index bb8fa6e12..4e7373761 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui.rs @@ -11,14 +11,16 @@ #[cfg(feature = "dylint-driver")] extern crate rustc_driver; +use std::{ + path::PathBuf, + sync::atomic::{AtomicU64, Ordering}, +}; + use dylint_testing::ui::Test; +use harness_lock::ExampleHarnessLock; use rstest::rstest; -use std::path::PathBuf; -use std::sync::atomic::{AtomicU64, Ordering}; use whitaker_common::test_support::{run_test_runner, with_env_var}; -use harness_lock::ExampleHarnessLock; - // Internal test-only hook mirrored in the lint driver. It asks // `check_crate_post` to append redacted, shape-only passive collection // summaries for harness assertions without making the lint user-visible. @@ -41,7 +43,8 @@ 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", + "callee=Builder::<'_>::build;records=2\nfingerprint=unsupported,fixture-local\\ + nfingerprint=unsupported,fixture-local", "callee=helper;records=2", "callee=nested_helper;records=5", "fingerprint=unsupported,fixture-local", @@ -94,7 +97,8 @@ impl ExampleHarness { }) .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}\", message: {error} }}" ) }); } 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 3b3914b01..ef2bd6025 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,15 +6,18 @@ //! 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 diff --git a/crates/test_must_not_have_example/src/behaviour.rs b/crates/test_must_not_have_example/src/behaviour.rs index 95128fe07..60ad1f964 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,34 +14,24 @@ 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() } } #[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) { @@ -57,26 +49,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 da409e52d..bca9c5461 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 af5de74a2..06b945f4b 100644 --- a/crates/test_must_not_have_example/src/heuristics.rs +++ b/crates/test_must_not_have_example/src/heuristics.rs @@ -86,9 +86,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/test_must_not_have_example/src/lib_ui_tests.rs b/crates/test_must_not_have_example/src/lib_ui_tests.rs index 1cf040e87..a3f8096ba 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,9 +1,9 @@ //! 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] @@ -15,7 +15,8 @@ fn ui() { }) .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}\", message: {error} }}" ) }); } 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 262c1e8fa..34bdca996 100644 --- a/crates/whitaker_clones_core/src/ast/lowering_tests.rs +++ b/crates/whitaker_clones_core/src/ast/lowering_tests.rs @@ -1,19 +1,27 @@ //! 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, + LoweringLimits, + MAX_AST_DEPTH, + MAX_AST_NODES, + kind_id, + leaf_class, validate_covering_node_budget, }; use crate::{ - AstError, ByteSpan, Production, + AstError, + 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 c8b24da23..50ee9a6ae 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() -> AstResult<()> { @@ -155,13 +168,19 @@ fn canonical_hash_is_stable_for_equivalent_trees() { #[rstest] fn canonical_hash_is_sensitive_to_child_order() { let reordered = reordered_tree().expect("static test span should be valid"); - assert_ne!(canonical_hash(&expect_feature_tree()), canonical_hash(&reordered)); + assert_ne!( + canonical_hash(&expect_feature_tree()), + canonical_hash(&reordered) + ); } #[rstest] fn canonical_hash_is_sensitive_to_leaf_class() { let different = different_leaf_tree().expect("static test span should be valid"); - assert_ne!(canonical_hash(&expect_feature_tree()), canonical_hash(&different)); + assert_ne!( + canonical_hash(&expect_feature_tree()), + canonical_hash(&different) + ); } fn expect_feature_tree() -> NormalizedTree { 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 3fc4839a4..528f1a251 100644 --- a/crates/whitaker_clones_core/src/index/tests.rs +++ b/crates/whitaker_clones_core/src/index/tests.rs @@ -2,12 +2,18 @@ use rstest::{fixture, rstest}; -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 @@ -34,19 +40,13 @@ fn multi_band_config() -> LshConfig { } #[fixture] -fn shared_signature() -> MinHashSignature { - sketch(&[1, 2, 3, 4]) -} +fn shared_signature() -> MinHashSignature { sketch(&[1, 2, 3, 4]) } #[fixture] -fn distinct_signature() -> MinHashSignature { - sketch(&[8, 9, 10, 11]) -} +fn distinct_signature() -> MinHashSignature { sketch(&[8, 9, 10, 11]) } #[fixture] -fn identical_signature() -> MinHashSignature { - sketch(&[5, 7, 11, 13]) -} +fn identical_signature() -> MinHashSignature { sketch(&[5, 7, 11, 13]) } struct FragmentIds { alpha: FragmentId, @@ -234,8 +234,7 @@ fn canonical_ordering_across_multiple_pairs_and_bands( shared_signature: MinHashSignature, distinct_signature: MinHashSignature, ) { - let config = - LshConfig::new(4, MINHASH_SIZE.div_euclid(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 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 fbef6598c..e08507828 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 2411479c5..05003bf65 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 24b656745..1af60c5ff 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 artifact 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 189d955f0..d7beac327 100644 --- a/crates/whitaker_clones_core/tests/ast_boundary.rs +++ b/crates/whitaker_clones_core/tests/ast_boundary.rs @@ -206,9 +206,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 @@ -294,7 +292,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 d1ebd71a7..62d8c4f94 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, }; #[derive(Debug, Clone, Copy)] @@ -82,9 +87,7 @@ struct AstFeatureWorld { } #[fixture] -fn world() -> AstFeatureWorld { - AstFeatureWorld::default() -} +fn world() -> AstFeatureWorld { AstFeatureWorld::default() } fn whole_source_hash(source: &str) -> Result { let span = ByteSpan::new(source, 0, source.len() as u32)?; diff --git a/crates/whitaker_clones_core/tests/build_script_parsing.rs b/crates/whitaker_clones_core/tests/build_script_parsing.rs index 741814fa4..f90ba384a 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 832448edb..710c0eb43 100644 --- a/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs +++ b/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs @@ -16,9 +16,7 @@ struct CandidatePairWorld { } #[fixture] -fn world() -> CandidatePairWorld { - CandidatePairWorld::default() -} +fn world() -> CandidatePairWorld { CandidatePairWorld::default() } fn with_pair(world: &CandidatePairWorld, assert_fn: impl FnOnce(&CandidatePair)) { let pair = world.pair.borrow(); @@ -67,21 +65,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 4541aa0c4..d6aacd834 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, }; #[derive(Debug, Default)] @@ -20,9 +26,7 @@ struct MinHashLshWorld { } #[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(); @@ -166,41 +170,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 a11090df1..b144a3b1c 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}; @@ -34,9 +39,7 @@ impl Default for Run0World { } #[fixture] -fn world() -> Run0World { - Run0World::default() -} +fn world() -> Run0World { Run0World::default() } fn with_results(world: &Run0World, assert_fn: impl FnOnce(&[SarifResult])) { let run = world.run.borrow(); @@ -275,31 +278,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 83686881f..cd52b5a8d 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, }; @@ -29,9 +35,7 @@ struct TokenPassWorld { } #[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(); @@ -266,31 +270,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 b9a75975b..21f777b04 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::{ArtifactLocation, Location, PhysicalLocation, Region}; +use crate::{ + error::SarifError, + model::location::{ArtifactLocation, 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.artifact_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 76911f6a5..4cd77959c 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 e8d1b944c..3e63126bf 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::{Artifact, Invocation, Run, Tool, ToolComponent}; +use crate::model::{ + descriptor::ReportingDescriptor, + result::SarifResult, + run::{Artifact, 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, Artifact, ArtifactLocation}; + /// use whitaker_sarif::{Artifact, ArtifactLocation, RunBuilder}; /// /// let run = RunBuilder::new("tool", "1.0") /// .with_artifact(Artifact { @@ -175,14 +180,13 @@ 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}; #[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 bf4cde4a9..7e8029366 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::{ - Artifact, ArtifactLocation, Invocation, Level, Location, Message, MultiformatMessageString, - PhysicalLocation, Region, RelatedLocation, ReportingDescriptor, Run, SarifLog, SarifResult, - Tool, ToolComponent, + Artifact, + ArtifactLocation, + 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 5af374968..76d90ca29 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 1f64460f1..11d8ffa89 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 c3cd50d4a..178eddb40 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, ArtifactLocation, Region}; +/// use whitaker_sarif::{ArtifactLocation, Location, PhysicalLocation, Region}; /// /// let loc = Location { /// physical_location: PhysicalLocation { diff --git a/crates/whitaker_sarif/src/model/mod.rs b/crates/whitaker_sarif/src/model/mod.rs index a38b1493c..20b250ccc 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 -//! [`Artifact`]. +//! - [`run`] — [`Run`], [`Tool`], [`ToolComponent`], [`Invocation`], and [`Artifact`]. //! - [`result`] — [`SarifResult`], [`Level`], and [`Message`]. -//! - [`location`] — [`Location`], [`PhysicalLocation`], -//! [`ArtifactLocation`], [`Region`], and [`RelatedLocation`]. -//! - [`descriptor`] — [`ReportingDescriptor`] and -//! [`MultiformatMessageString`]. +//! - [`location`] — [`Location`], [`PhysicalLocation`], [`ArtifactLocation`], [`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 3d07164d7..c7ed2d4fe 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 48a7f9ec7..89c86653d 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. /// 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 1e94ac17f..11b720796 100644 --- a/crates/whitaker_sarif/src/test_support.rs +++ b/crates/whitaker_sarif/src/test_support.rs @@ -4,9 +4,11 @@ //! It exists solely to avoid duplicating test helper logic between the //! `merge::tests` unit tests and the `tests/` integration tests. -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}, +}; /// Builds a [`SarifResult`] with a fingerprint, location, and region. /// diff --git a/crates/whitaker_sarif/src/whitaker_properties.rs b/crates/whitaker_sarif/src/whitaker_properties.rs index 24d799c57..69b42c516 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 093a7091d..94aa6bab0 100644 --- a/crates/whitaker_sarif/tests/sarif_behaviour.rs +++ b/crates/whitaker_sarif/tests/sarif_behaviour.rs @@ -8,14 +8,22 @@ 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, }; -use whitaker_sarif::model::descriptor::ReportingDescriptor; -use whitaker_sarif::model::run::Run; - #[derive(Debug, Default)] struct SarifWorld { pending_run: RefCell>, @@ -34,9 +42,7 @@ struct SarifWorld { } #[fixture] -fn world() -> SarifWorld { - SarifWorld::default() -} +fn world() -> SarifWorld { SarifWorld::default() } // -- Helper functions (match-based to avoid expect/unwrap) -- @@ -243,9 +249,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) { @@ -290,7 +294,11 @@ fn then_result_rule_id(world: &SarifWorld, rule_id: String) { #[then("the result level is warning")] fn then_result_level_warning(world: &SarifWorld) { with_result(world, |result| { - assert_eq!(result.level, Level::Warning, "result level should be warning"); + assert_eq!( + result.level, + Level::Warning, + "result level should be warning" + ); }); } @@ -320,7 +328,11 @@ fn then_json_has_k(world: &SarifWorld, k: usize) { #[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, "merged result count should match"); + assert_eq!( + merged.results.len(), + count, + "merged result count should match" + ); }); } @@ -370,41 +382,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 fdb6f812c..64afc1878 100644 --- a/installer/src/artefact/extraction.rs +++ b/installer/src/artefact/extraction.rs @@ -113,10 +113,12 @@ fn validate_entry_path(path: &Path) -> Result<(), ExtractionError> { #[cfg(test)] mod tests { - 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 5ce246d0f..de1e51fd1 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 { @@ -146,9 +140,10 @@ fn validate_git_sha(value: &str) -> Result<()> { #[cfg(test)] mod tests { - 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 1adcc02da..c24cb04ae 100644 --- a/installer/src/artefact/manifest_parser.rs +++ b/installer/src/artefact/manifest_parser.rs @@ -48,9 +48,10 @@ pub fn parse_manifest(json: &str) -> Result { #[cfg(test)] mod tests { - 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/manifest_tests.rs b/installer/src/artefact/manifest_tests.rs index 784f4d044..214d7c3bf 100644 --- a/installer/src/artefact/manifest_tests.rs +++ b/installer/src/artefact/manifest_tests.rs @@ -1,9 +1,10 @@ //! Tests for manifest schema types. -use super::*; use rstest::{fixture, rstest}; use serde_json::Value; +use super::*; + #[fixture] fn sample_provenance() -> ManifestProvenance { ManifestProvenance { diff --git a/installer/src/artefact/naming.rs b/installer/src/artefact/naming.rs index abc19f45b..440d549d9 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 { @@ -96,9 +89,10 @@ impl fmt::Display for ArtefactName { #[cfg(test)] mod tests { - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[fixture] fn sample_name() -> ArtefactName { ArtefactName::new( 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 a485b2503..fd3f2b297 100644 --- a/installer/src/artefact/packaging_tests.rs +++ b/installer/src/artefact/packaging_tests.rs @@ -1,18 +1,15 @@ //! Unit tests for the artefact packaging module. -use super::*; use rstest::{fixture, rstest}; use tempfile::TempDir; +use super::*; + #[fixture] -fn temp_dir() -> TempDir { - TempDir::new().expect("temp dir creation succeeds") -} +fn temp_dir() -> TempDir { TempDir::new().expect("temp dir creation succeeds") } #[fixture] -fn sample_git_sha() -> GitSha { - GitSha::try_from("abc1234").expect("valid sha") -} +fn sample_git_sha() -> GitSha { GitSha::try_from("abc1234").expect("valid sha") } #[fixture] fn sample_toolchain() -> ToolchainChannel { diff --git a/installer/src/artefact/schema_version.rs b/installer/src/artefact/schema_version.rs index 3e67f848d..e812dfd3f 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 f6ed3e33e..496f13292 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. @@ -126,13 +120,12 @@ fn validate_sha256(value: &str) -> Result<()> { #[cfg(test)] mod tests { - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[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 7e408cfce..df27372ba 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,28 +128,23 @@ 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 { - 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 74851ce3d..0488c3ea9 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,16 +108,15 @@ 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 { - 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 dbe99da6f..bf10b0297 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 b08fbff2a..1ae0dae61 100644 --- a/installer/src/bin/package_dependency_binary.rs +++ b/installer/src/bin/package_dependency_binary.rs @@ -1,16 +1,19 @@ //! Package dependency binaries and shared provenance assets for release uploads. -use clap::{Parser, Subcommand}; use std::path::PathBuf; + +use clap::{Parser, Subcommand}; 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, }; -use whitaker_installer::installer_packaging::TargetTriple; /// Package repository-hosted dependency binaries for release publication. #[derive(Parser, Debug)] @@ -111,10 +114,11 @@ fn run(cli: Cli) -> Result<(), CliError> { #[cfg(test)] mod tests { - 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 02a1582ee..1915950b2 100644 --- a/installer/src/bin/package_installer_bin.rs +++ b/installer/src/bin/package_installer_bin.rs @@ -4,12 +4,13 @@ //! [`whitaker_installer::installer_packaging::package_installer`] invoked //! by the release CI workflow to create binstall-compatible archives. -use clap::Parser; use std::path::PathBuf; + +use clap::Parser; 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}, }; /// Package the `whitaker-installer` binary into a release archive. @@ -73,10 +74,11 @@ fn run(cli: Cli) -> Result<(), CliError> { #[cfg(test)] mod tests { - 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 5c6317893..e326a796f 100644 --- a/installer/src/bin/package_lints.rs +++ b/installer/src/bin/package_lints.rs @@ -4,20 +4,25 @@ //! invoked by both the Makefile `package-lints` target and the //! rolling-release CI workflow. +use std::{ + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; + use clap::Parser; -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, + }, + 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::resolution::{LINT_CRATES, SUITE_CRATE}; /// Package prebuilt lint libraries into `.tar.zst` archives following /// the ADR-001 naming convention and write a sidecar @@ -187,9 +192,7 @@ fn validate_iso8601(ts: &str) -> Result<(), PackageCliError> { Ok(()) } -const fn has_valid_length(b: &[u8]) -> bool { - b.len() == 20 -} +const 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' @@ -246,10 +249,12 @@ const fn civil_from_epoch(epoch_secs: u64) -> (u32, u32, u32) { #[cfg(test)] mod tests { - 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/binstall_metadata_tests.rs b/installer/src/binstall_metadata_tests.rs index 2a7bc2a25..89c97d9ec 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().expect("load installer Cargo.toml"); diff --git a/installer/src/builder.rs b/installer/src/builder.rs index fd08c4e19..5b7cd6dbb 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). @@ -229,9 +233,10 @@ pub const fn library_prefix() -> &'static str { #[cfg(test)] mod tests { - 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 b9227257d..efd25bacf 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")] @@ -155,15 +155,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/cli_tests.rs b/installer/src/cli_tests.rs index 8b732bd26..17f2abf9d 100644 --- a/installer/src/cli_tests.rs +++ b/installer/src/cli_tests.rs @@ -1,8 +1,9 @@ //! 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"]); diff --git a/installer/src/crate_name.rs b/installer/src/crate_name.rs index 5a384cc7e..82f462d27 100644 --- a/installer/src/crate_name.rs +++ b/installer/src/crate_name.rs @@ -17,52 +17,39 @@ 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 { - 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 e14ef7ed8..0e46b9676 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]) -> NamedTempFile { let mut file = NamedTempFile::new().expect("create temp file"); @@ -187,9 +189,7 @@ mod tests { } /// 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) -> impl Read { file.reopen().expect("reopen temp file") } #[rstest] #[case(404, true)] diff --git a/installer/src/dependency_binaries/install/downloader.rs b/installer/src/dependency_binaries/install/downloader.rs index 7d01ae5ea..40fc4b19b 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) { @@ -295,12 +303,14 @@ fn open_destination_dir(destination: &Utf8Path) -> io::Result<(Dir, &str)> { 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 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/downloader_boundary_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs index a45210ad1..5cd83d63a 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs @@ -5,29 +5,29 @@ //! 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; +// `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::{collections::HashMap, io::Read, path::PathBuf, time::Duration}; + +use camino::Utf8Path; +use cap_std::{ambient_authority, fs_utf8::Dir}; +use rstest::{fixture, rstest}; +use sha2::{Digest, Sha256}; +use tempfile::TempDir; + // 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 super::{ + downloader::download_from_urls, + http_test_server::{CannedResponse, LocalServer}, + installer::DependencyBinaryInstallError, +}; use crate::hex::to_lower_hex; -use camino::Utf8Path; -use cap_std::ambient_authority; -use cap_std::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; /// A short-timeout agent fixture for driving the local server. #[fixture] @@ -47,17 +47,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/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 17bd35bd1..04f582c77 100644 --- a/installer/src/dependency_binaries/install/http_test_server.rs +++ b/installer/src/dependency_binaries/install/http_test_server.rs @@ -5,14 +5,19 @@ //! 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}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; +use std::{ + collections::HashMap, + io, + io::{BufRead, BufReader, Write}, + net::{TcpListener, TcpStream}, + sync::{ + Arc, + Mutex, + 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 +78,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 { self.requested.lock().expect("lock requested paths").clone() diff --git a/installer/src/dependency_binaries/install/installer.rs b/installer/src/dependency_binaries/install/installer.rs index dd8afcd53..002858cc0 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 6b82a999f..ff206f6ad 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 ce675353f..8693a083d 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()); /// ``` @@ -293,9 +277,10 @@ pub fn find_dependency_binary( #[cfg(test)] mod tests { - 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 0451b682e..ca860d47e 100644 --- a/installer/src/dependency_packaging_tests.rs +++ b/installer/src/dependency_packaging_tests.rs @@ -1,15 +1,24 @@ //! 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::{ + 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, @@ -28,9 +37,7 @@ fn windows_target() -> TargetTriple { } #[fixture] -fn temp_dir() -> tempfile::TempDir { - tempfile::tempdir().expect("temp dir") -} +fn temp_dir() -> tempfile::TempDir { tempfile::tempdir().expect("temp dir") } #[test] fn archive_format_matches_target_platform() { diff --git a/installer/src/deps.rs b/installer/src/deps.rs index 005e11923..8f5d80062 100644 --- a/installer/src/deps.rs +++ b/installer/src/deps.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/install.rs b/installer/src/deps/install.rs index 1c4b03792..2ea37d401 100644 --- a/installer/src/deps/install.rs +++ b/installer/src/deps/install.rs @@ -1,14 +1,21 @@ //! 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, }; pub(super) struct RepositoryInstallContext<'a> { diff --git a/installer/src/deps/install_tests.rs b/installer/src/deps/install_tests.rs index 8630d4a8e..5f2ac572f 100644 --- a/installer/src/deps/install_tests.rs +++ b/installer/src/deps/install_tests.rs @@ -1,10 +1,12 @@ //! 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, + 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 bd754f878..1d3bc001f 100644 --- a/installer/src/deps/path_tests.rs +++ b/installer/src/deps/path_tests.rs @@ -1,14 +1,26 @@ //! 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() { diff --git a/installer/src/deps/tests.rs b/installer/src/deps/tests.rs index 537f26017..af32b2c8f 100644 --- a/installer/src/deps/tests.rs +++ b/installer/src/deps/tests.rs @@ -1,16 +1,31 @@ //! Tests for Dylint tool dependency installation and fallback behaviour. -use super::*; -use super::install::{InstallMode, install_tool}; -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::test_utils::{ExpectedCall, StubDirs, StubExecutor, failure_output, success_output}; use std::path::PathBuf; +use super::{ + install::{InstallMode, install_tool}, + *, +}; +use crate::{ + 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, + }, +}; + fn install_options<'a>( repository_installer: &'a dyn DependencyBinaryInstaller, quiet: bool, diff --git a/installer/src/dirs.rs b/installer/src/dirs.rs index 7139ea0b7..6a028b0c5 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_dir() +/// let data_dir = dirs +/// .whitaker_data_dir() /// .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_dir(&self) -> Option { - Some(self.user_dirs.home_dir().to_owned()) - } + fn home_dir(&self) -> Option { Some(self.user_dirs.home_dir().to_owned()) } fn bin_dir(&self) -> Option { #[cfg(unix)] @@ -113,9 +112,7 @@ impl BaseDirs for SystemBaseDirs { self.home_dir().map(|h| h.join(".local").join("bin")) } - fn whitaker_data_dir(&self) -> Option { - Some(self.project_dirs.data_dir().to_owned()) - } + fn whitaker_data_dir(&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 e1f1273d5..8532599e0 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 b069213ee..59a12d5fa 100644 --- a/installer/src/git.rs +++ b/installer/src/git.rs @@ -4,13 +4,19 @@ //! 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::{ + process::{Command, Output, Stdio}, + time::Duration, +}; + use camino::Utf8Path; -use std::process::{Command, Output, Stdio}; -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.rs b/installer/src/install_flow.rs index 85e9aa087..5ad0b86b0 100644 --- a/installer/src/install_flow.rs +++ b/installer/src/install_flow.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 c520829aa..058bfe83f 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,22 +21,14 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home_dir(&self) -> Option { - None - } - fn bin_dir(&self) -> Option { - None - } - fn whitaker_data_dir(&self) -> Option { - self.data_dir.clone() - } + fn home_dir(&self) -> Option { None } + fn bin_dir(&self) -> Option { None } + fn whitaker_data_dir(&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()) -} +fn stub_detect_host_target() -> Result { Ok("x86_64-unknown-linux-gnu".to_owned()) } fn stub_resolve_destination_dir( _dirs: &dyn BaseDirs, diff --git a/installer/src/install_metrics.rs b/installer/src/install_metrics.rs index 725503406..726ba7e10 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 ed3b24b89..2421b7631 100644 --- a/installer/src/install_metrics_tests.rs +++ b/installer/src/install_metrics_tests.rs @@ -1,12 +1,15 @@ //! 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, + 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, @@ -26,7 +29,11 @@ fn metrics_path_fixture() -> MetricsPathFixture { #[test] fn zero_state_rates_are_zero() { let metrics = InstallMetrics::default(); - assert_eq!(metrics.download_rate_permille(), 0, "zero-state download rate"); + assert_eq!( + metrics.download_rate_permille(), + 0, + "zero-state download rate" + ); assert_eq!(metrics.build_rate_permille(), 0, "zero-state build rate"); } @@ -40,7 +47,11 @@ 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_eq!(metrics.download_rate_permille(), 500, "download rate permille"); + assert_eq!( + metrics.download_rate_permille(), + 500, + "download rate permille" + ); assert_eq!(metrics.build_rate_permille(), 500, "build rate permille"); } 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/installer_packaging_tests.rs b/installer/src/installer_packaging_tests.rs index fb1660123..a371e2d9c 100644 --- a/installer/src/installer_packaging_tests.rs +++ b/installer/src/installer_packaging_tests.rs @@ -1,10 +1,11 @@ //! Unit tests for installer binary archive packaging. +use std::{fs, io::Read}; + +use rstest::rstest; + use super::*; use crate::binstall_metadata; -use rstest::rstest; -use std::fs; -use std::io::Read; // --------------------------------------------------------------------------- // Shared fixtures 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 7eb9c315b..30657ac2f 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. /// @@ -177,11 +180,13 @@ where #[cfg(test)] mod tests { - use super::*; - use rstest::{fixture, rstest}; use std::fs; + + use rstest::{fixture, rstest}; use tempfile::TempDir; + use super::*; + // ------------------------------------------------------------------------- // Fixtures // ------------------------------------------------------------------------- diff --git a/installer/src/list_output.rs b/installer/src/list_output.rs index 7fef04f67..c5d5e9bbc 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); @@ -70,8 +69,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); @@ -146,12 +144,13 @@ pub struct LibraryEntry { #[cfg(test)] mod tests { - 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/main.rs b/installer/src/main.rs index ae32918ff..06920105d 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 caa341a8a..dd80ba91c 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"); @@ -176,10 +175,11 @@ impl DryRunInfo<'_> { #[cfg(test)] mod tests { - use super::*; use camino::Utf8PathBuf; use rstest::{fixture, rstest}; + use super::*; + /// Shared fixture providing a test library path. #[fixture] fn test_path() -> Utf8PathBuf { @@ -188,9 +188,7 @@ mod tests { /// Shared fixture providing a shell snippet for the test path. #[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 1a468ff3c..076b51390 100644 --- a/installer/src/pipeline_staging_tests.rs +++ b/installer/src/pipeline_staging_tests.rs @@ -1,13 +1,16 @@ //! 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 @@ -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; @@ -73,9 +74,10 @@ impl StagingTestContext { } fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> BuildResult { - use crate::builder::{library_extension, library_prefix}; 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"); let filename = format!("{}{}{}", library_prefix(), crate_name, library_extension()); @@ -89,9 +91,7 @@ fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> BuildResult { } #[fixture] -fn staging_ctx() -> StagingTestContext { - StagingTestContext::new() -} +fn staging_ctx() -> StagingTestContext { StagingTestContext::new() } fn assert_bumpy_road_lint_in_staging_output(experimental: bool) { let staging_ctx = StagingTestContext::new().with_experimental(experimental); diff --git a/installer/src/pipeline_tests.rs b/installer/src/pipeline_tests.rs index bbca11d01..da5952a07 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 // ------------------------------------------------------------------------- @@ -88,9 +91,7 @@ impl PipelineContextProvider for TestContext { /// Returns a default TestContext with owned paths and default settings for pipeline unit tests. #[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 564c13c0f..5251109b2 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. /// @@ -45,10 +47,12 @@ pub fn prebuilt_library_dir( #[cfg(test)] mod tests { + 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() { @@ -88,8 +92,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_dir().return_once(|| { diff --git a/installer/src/prebuilt_tests.rs b/installer/src/prebuilt_tests.rs index 7dd75a04c..457a61e47 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"; diff --git a/installer/src/resolution.rs b/installer/src/resolution.rs index e526ea79a..cdfc25802 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 @@ -146,9 +148,10 @@ pub fn validate_crate_names(names: &[CrateName], options: &CrateResolutionOption #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + /// Test configuration for resolve_crates variants. struct ResolveCratesCase { individual_lints: bool, diff --git a/installer/src/scanner.rs b/installer/src/scanner.rs index e8faf2c21..0f5863bbb 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")); @@ -239,10 +236,11 @@ pub fn lints_for_library_with_experimental( #[cfg(test)] mod tests { - 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 777b56b3c..23ca5eb7b 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(&self) -> Vec { - vec![CrateName::from(SUITE_CRATE)] - } + fn requested_suite_crates(&self) -> 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 5ae2f74a2..04bec4854 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.rs b/installer/src/test_utils.rs index 4d8cfb05f..54c125304 100644 --- a/installer/src/test_utils.rs +++ b/installer/src/test_utils.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). /// @@ -123,17 +125,11 @@ pub struct StubDirs { } impl BaseDirs for StubDirs { - fn home_dir(&self) -> Option { - None - } + fn home_dir(&self) -> Option { None } - fn bin_dir(&self) -> Option { - self.bin_dir.clone() - } + fn bin_dir(&self) -> Option { self.bin_dir.clone() } - fn whitaker_data_dir(&self) -> Option { - None - } + fn whitaker_data_dir(&self) -> Option { None } } /// Compute the SHA-256 hex digest of a byte slice for test fixtures. @@ -204,18 +200,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(); @@ -233,13 +231,11 @@ 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 { @@ -257,16 +253,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/test_utils/dependency_binary_helpers.rs b/installer/src/test_utils/dependency_binary_helpers.rs index b13f57501..916a6eb87 100644 --- a/installer/src/test_utils/dependency_binary_helpers.rs +++ b/installer/src/test_utils/dependency_binary_helpers.rs @@ -1,25 +1,30 @@ //! Test helpers for dependency binary installation tests. -use crate::dependency_binaries::find_dependency_binary; +#[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; + #[cfg(any(test, feature = "test-support"))] use crate::dependency_binaries::{ - DependencyBinary, DependencyBinaryInstallError, DependencyBinaryInstaller, + 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; +use crate::{ + dependency_binaries::find_dependency_binary, + error::Result, + test_utils::{ExpectedCall, failure_output, stdout_output, success_output}, +}; /// Repository installer test double that always reports a missing archive. #[cfg(any(test, feature = "test-support"))] @@ -355,13 +360,15 @@ fn post_primary_calls(cfg: &PostPrimaryConfig) -> Vec { return calls; } // binstall failed and cargo install also fails - 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)), - )] - }) + 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( diff --git a/installer/src/test_utils/dependency_binary_helpers_tests.rs b/installer/src/test_utils/dependency_binary_helpers_tests.rs index 50e417aa9..745abea0f 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, dependency_version, expected_calls, repository_verification_call, + ExpectedCallConfig, + dependency_version, + expected_calls, + repository_verification_call, }; #[test] diff --git a/installer/src/tests.rs b/installer/src/tests.rs index af1b98f2d..98d681866 100644 --- a/installer/src/tests.rs +++ b/installer/src/tests.rs @@ -2,20 +2,27 @@ 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, @@ -235,17 +242,11 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home_dir(&self) -> Option { - self.home_dir.clone() - } + fn home_dir(&self) -> Option { self.home_dir.clone() } - fn bin_dir(&self) -> Option { - self.bin_dir.clone() - } + fn bin_dir(&self) -> Option { self.bin_dir.clone() } - fn whitaker_data_dir(&self) -> Option { - self.data_dir.clone() - } + fn whitaker_data_dir(&self) -> Option { self.data_dir.clone() } } #[test] diff --git a/installer/src/tests/fast_path.rs b/installer/src/tests/fast_path.rs index 988b21fbb..666d4a799 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, diff --git a/installer/src/toolchain.rs b/installer/src/toolchain.rs index 5865ce98f..4ed64922a 100644 --- a/installer/src/toolchain.rs +++ b/installer/src/toolchain.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.rs b/installer/src/toolchain/tests/failure_mocks.rs index f1871f051..980a927c4 100644 --- a/installer/src/toolchain/tests/failure_mocks.rs +++ b/installer/src/toolchain/tests/failure_mocks.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 @@ -122,8 +124,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. @@ -215,8 +216,7 @@ 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 /// diff --git a/installer/src/toolchain/tests/mod.rs b/installer/src/toolchain/tests/mod.rs index 5d4b7a8d7..bc3e53a6a 100644 --- a/installer/src/toolchain/tests/mod.rs +++ b/installer/src/toolchain/tests/mod.rs @@ -3,18 +3,28 @@ mod failure_mocks; mod test_helpers; -use super::*; 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, }; +use super::*; + const CRANELIFT_COMPONENT: &str = "rustc-codegen-cranelift"; // Asserts that a parsing function rejects invalid contents with an diff --git a/installer/src/toolchain/tests/test_helpers.rs b/installer/src/toolchain/tests/test_helpers.rs index 2426a3edb..9686a903b 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 { diff --git a/installer/src/version.rs b/installer/src/version.rs index 8208b9987..fef60021c 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 d6325d5df..f094ed148 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"; @@ -203,13 +206,14 @@ fn is_cargo_workspace_root(cargo_toml: &Utf8Path) -> Result { #[cfg(test)] mod tests { - 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 078effce9..3c1ad2d15 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"); @@ -172,8 +178,7 @@ 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") - .is_some_and(|path| std::env::split_paths(&path).any(|p| p == dir)) + 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. @@ -211,9 +216,10 @@ pub fn path_instructions(bin_dir: &Path) -> String { #[cfg(test)] mod tests { - 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"); @@ -223,9 +229,10 @@ mod tests { #[cfg(unix)] #[test] fn generate_unix_scripts_create_executables() { - use camino::Utf8PathBuf; use std::os::unix::fs::PermissionsExt; + use camino::Utf8PathBuf; + let temp = TempDir::new().expect("failed to create temp dir"); let library_path = Utf8PathBuf::from("/tmp/dylint/lib"); diff --git a/installer/tests/behaviour_artefact.rs b/installer/tests/behaviour_artefact.rs index 221825876..4dfdb4cb7 100644 --- a/installer/tests/behaviour_artefact.rs +++ b/installer/tests/behaviour_artefact.rs @@ -7,17 +7,17 @@ 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 @@ -39,19 +39,14 @@ struct ArtefactWorld { } #[fixture] -fn world() -> ArtefactWorld { - ArtefactWorld::default() -} +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) where F: FnOnce(&ArtefactError) -> bool, { - assert!( - error.is_some(), - "expected {field_name} validation to fail" - ); + assert!(error.is_some(), "expected {field_name} validation to fail"); assert!( predicate(error.as_ref().expect("checked above")), "error variant mismatch for {field_name}" @@ -209,62 +204,46 @@ fn then_action_is_fallback(world: &mut ArtefactWorld) { 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 fc905ea44..704385c77 100644 --- a/installer/tests/behaviour_artefact_packaging.rs +++ b/installer/tests/behaviour_artefact_packaging.rs @@ -4,20 +4,26 @@ //! `artefact::packaging` module against ADR-001 rules. Tests use the //! rstest-bdd v0.5.0 mutable world pattern. +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 @@ -101,9 +107,7 @@ fn given_target(world: &mut PackagingWorld, triple: String) { } #[when("the artefact is packaged")] -fn when_packaged(world: &mut PackagingWorld) { - run_packaging(world); -} +fn when_packaged(world: &mut PackagingWorld) { run_packaging(world); } #[then("the archive exists with the expected ADR-001 filename")] fn then_archive_exists(world: &mut PackagingWorld) { @@ -217,9 +221,7 @@ fn given_no_files(world: &mut PackagingWorld) { } #[when("packaging is attempted")] -fn when_packaging_attempted(world: &mut PackagingWorld) { - run_packaging(world); -} +fn when_packaging_attempted(world: &mut PackagingWorld) { run_packaging(world); } #[then("a packaging error is returned")] fn then_packaging_error(world: &mut PackagingWorld) { @@ -276,9 +278,7 @@ fn then_manifest_files_contains(world: &mut PackagingWorld, name: String) { } #[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) { given_packaged_artefact(world); } #[then("it matches the ArtefactName string representation")] fn then_filename_matches_artefact_name(world: &mut PackagingWorld) { @@ -322,62 +322,46 @@ fn list_archive_entries(world: &PackagingWorld) -> Vec { 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 620a164df..0f92dfe9c 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, }; // --------------------------------------------------------------------------- @@ -31,9 +36,7 @@ struct BinstallWorld { } #[fixture] -fn world() -> BinstallWorld { - BinstallWorld::default() -} +fn world() -> BinstallWorld { BinstallWorld::default() } // --------------------------------------------------------------------------- // Step definitions @@ -183,54 +186,40 @@ fn then_no_invalid_placeholder(world: &mut BinstallWorld, placeholder: String) { 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 0bd1f57c0..a15475e3e 100644 --- a/installer/tests/behaviour_cli.rs +++ b/installer/tests/behaviour_cli.rs @@ -9,17 +9,28 @@ 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, + 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")] @@ -28,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) { @@ -45,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) { @@ -90,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 c95291d84..15d371ed1 100644 --- a/installer/tests/behaviour_cli/scenarios.rs +++ b/installer/tests/behaviour_cli/scenarios.rs @@ -1,24 +1,19 @@ //! 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)] -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 41df63546..986310363 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -1,15 +1,21 @@ //! Shared fixtures, command helpers, and assertions for CLI behaviour tests. -use super::prebuilt_markers::PREBUILT_INSTALL_MARKER; +use std::{ + cell::{Cell, Ref, RefCell}, + path::{Path, PathBuf}, + process::{Command, Output}, +}; + 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, +}; + +use super::prebuilt_markers::PREBUILT_INSTALL_MARKER; #[derive(Default)] pub(super) struct CliWorld { @@ -24,9 +30,7 @@ pub(super) struct CliWorld { } #[fixture] -pub(super) fn cli_world() -> CliWorld { - CliWorld::default() -} +pub(super) fn cli_world() -> CliWorld { CliWorld::default() } pub(super) fn workspace_root() -> PathBuf { PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) @@ -61,11 +65,13 @@ pub(super) fn is_toolchain_installed(channel: &str) -> bool { 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." + "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 ); } @@ -275,11 +281,13 @@ pub(super) fn assert_unknown_lint_message_is_shown(cli_world: &CliWorld) { assert!( !stderr.contains("Dry run - no files will be modified"), - "dry-run configuration output should not be printed on unknown-lint error, stderr: {stderr}" + "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}" + "dry-run configuration output should not be printed on unknown-lint error, stderr: \ + {stderr}" ); assert!( stderr.contains("lint crate nonexistent_lint not found"), @@ -297,11 +305,13 @@ pub(super) fn assert_experimental_lint_opt_in_message_is_shown(cli_world: &CliWo assert!( !stderr.contains("Dry run - no files will be modified"), - "dry-run configuration output should not be printed on experimental-lint error, stderr: {stderr}" + "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}" + "dry-run configuration output should not be printed on experimental-lint error, stderr: \ + {stderr}" ); assert!( stderr.contains( @@ -326,7 +336,8 @@ pub(super) fn assert_experimental_lint_dry_run_output_is_shown(cli_world: &CliWo !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}" + "experimental opt-in error should not be printed when --experimental is set, stderr: \ + {stderr}" ); } @@ -361,8 +372,8 @@ pub(super) fn assert_suite_library_is_staged(cli_world: &CliWorld) { 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={:?}", + "prebuilt marker found in stderr but no library matching '{needle}' in \ + {prebuilt_path:?}, entries={:?}", matching_files(&prebuilt_path, ""), ); return; @@ -375,9 +386,8 @@ pub(super) fn assert_suite_library_is_staged(cli_world: &CliWorld) { assert!( matches.len() == 1, - "expected exactly one suite library matching '{needle}' in \ - {staging_dir:?}, matches={matches:?}, entries={:?}, \ - stdout={}, stderr={stderr}", + "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), ); diff --git a/installer/tests/behaviour_core.rs b/installer/tests/behaviour_core.rs index 40f24f403..157946601 100644 --- a/installer/tests/behaviour_core.rs +++ b/installer/tests/behaviour_core.rs @@ -3,17 +3,24 @@ //! 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 @@ -28,9 +35,7 @@ struct CrateResolutionWorld { } #[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) { @@ -135,9 +140,7 @@ struct ValidationWorld { } #[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) { @@ -219,9 +222,7 @@ struct ToolchainWorld { } #[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) { @@ -287,9 +288,7 @@ struct SnippetWorld { } #[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) { @@ -337,9 +336,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) { @@ -347,39 +344,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 e486883c4..ab25c8875 100644 --- a/installer/tests/behaviour_dependency_binaries.rs +++ b/installer/tests/behaviour_dependency_binaries.rs @@ -1,25 +1,31 @@ //! 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, + expected_calls, + path_binary_location, + write_fake_binary, + }, }, }; @@ -90,9 +96,7 @@ struct DependencyBinaryWorld { } #[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) { @@ -308,19 +312,13 @@ fn then_provenance_contains(world: &mut DependencyBinaryWorld, expected: String) } #[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) { @@ -345,21 +343,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 a80c66b7e..62052d816 100644 --- a/installer/tests/behaviour_docs.rs +++ b/installer/tests/behaviour_docs.rs @@ -6,10 +6,11 @@ mod doc_extraction; +use std::cell::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; // --------------------------------------------------------------------------- @@ -25,9 +26,7 @@ struct TomlWorld { } #[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) { @@ -296,46 +295,34 @@ fn then_locale_configuration_present(toml_world: &TomlWorld) { 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 1513a6bce..3c3ad8ec7 100644 --- a/installer/tests/behaviour_install_metrics.rs +++ b/installer/tests/behaviour_install_metrics.rs @@ -1,15 +1,17 @@ //! 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, + InstallMetrics, + InstallMode, + RecordOutcome, + record_install_at_path, }; - #[derive(Default)] struct InstallMetricsWorld { _temp_dir: Option, @@ -21,9 +23,7 @@ struct InstallMetricsWorld { } #[fixture] -fn world() -> InstallMetricsWorld { - InstallMetricsWorld::default() -} +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"); @@ -92,9 +92,7 @@ fn when_build_recorded(world: &mut InstallMetricsWorld, millis: u64) { } #[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) { @@ -186,46 +184,34 @@ fn then_warning_text_contains(world: &mut InstallMetricsWorld, expected: String) 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 f1bc92e56..9864e5e87 100644 --- a/installer/tests/behaviour_installer_release.rs +++ b/installer/tests/behaviour_installer_release.rs @@ -4,13 +4,21 @@ //! 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, + }, }; // --------------------------------------------------------------------------- @@ -30,9 +38,7 @@ struct InstallerReleaseWorld { } #[fixture] -fn world() -> InstallerReleaseWorld { - InstallerReleaseWorld::default() -} +fn world() -> InstallerReleaseWorld { InstallerReleaseWorld::default() } // --------------------------------------------------------------------------- // Step definitions @@ -90,14 +96,10 @@ fn attempt_packaging(world: &mut InstallerReleaseWorld) { } #[when("the installer is packaged")] -fn when_installer_packaged(world: &mut InstallerReleaseWorld) { - attempt_packaging(world); -} +fn when_installer_packaged(world: &mut InstallerReleaseWorld) { attempt_packaging(world); } #[when("packaging is attempted")] -fn when_packaging_attempted(world: &mut InstallerReleaseWorld) { - attempt_packaging(world); -} +fn when_packaging_attempted(world: &mut InstallerReleaseWorld) { attempt_packaging(world); } #[then("the archive filename is \"{expected}\"")] fn then_archive_filename_is(world: &mut InstallerReleaseWorld, expected: String) { @@ -198,46 +200,34 @@ fn read_zip_entries(path: &std::path::Path) -> Vec { 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 fa845dfaf..70249486a 100644 --- a/installer/tests/behaviour_prebuilt.rs +++ b/installer/tests/behaviour_prebuilt.rs @@ -1,17 +1,21 @@ //! BDD tests for the prebuilt artefact download and verification workflow. +use std::{path::Path, sync::Mutex}; + 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"; @@ -336,54 +340,40 @@ fn then_destination_is_not_created(world: &mut PrebuiltWorld) { 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 814ee5127..06da5d6d8 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 @@ -25,9 +25,7 @@ struct StagingWorld { } #[fixture] -fn staging_world() -> StagingWorld { - StagingWorld::default() -} +fn staging_world() -> StagingWorld { StagingWorld::default() } #[given("a built library")] fn given_built_library(staging_world: &StagingWorld) { @@ -77,12 +75,13 @@ use staging_failure::staging_failure_world; #[cfg(unix)] mod staging_failure { - use super::*; - use std::fs; - use std::os::unix::fs::PermissionsExt; + use std::{fs, os::unix::fs::PermissionsExt}; + use tempfile::TempDir; use whitaker_installer::error::InstallerError; + use super::*; + pub struct StagingFailureWorld { stager: RefCell>, result: RefCell>>, @@ -103,9 +102,7 @@ mod staging_failure { } #[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(staging_failure_world: &StagingFailureWorld) { @@ -144,7 +141,8 @@ mod staging_failure { if let Ok(file) = std::fs::OpenOptions::new() .create_new(true) .write(true) - .open(&probe_path) { + .open(&probe_path) + { drop(file); let _ = std::fs::remove_file(&probe_path); staging_failure_world.skip_assertions.set(true); @@ -182,9 +180,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 5754d9307..75c3be340 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,20 +15,30 @@ 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. #[expect(unused_imports)] 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, }; // --------------------------------------------------------------------------- @@ -36,36 +46,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 75aabf440..96d15c1b2 100644 --- a/installer/tests/behaviour_workflows.rs +++ b/installer/tests/behaviour_workflows.rs @@ -3,14 +3,16 @@ //! 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}, + 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 { @@ -23,9 +25,7 @@ struct WorkflowWorld { } #[fixture] -fn world() -> WorkflowWorld { - WorkflowWorld::default() -} +fn world() -> WorkflowWorld { WorkflowWorld::default() } fn workspace_root() -> PathBuf { PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) @@ -59,9 +59,7 @@ fn is_toolchain_installed(channel: &str) -> bool { fn skip_scenario_when_toolchain_missing(world: &WorkflowWorld, channel: &str) { if !is_toolchain_installed(channel) { - eprintln!( - "Skipping scenario because rustup toolchain '{channel}' is not installed." - ); + eprintln!("Skipping scenario because rustup toolchain '{channel}' is not installed."); world.skip_assertions.set(true); rstest_bdd::skip!( "rustup toolchain '{channel}' is not installed.", @@ -121,14 +119,10 @@ fn given_dry_run_with_flag(world: &WorkflowWorld, flag: &str) { } #[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) { 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) { 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) { @@ -252,21 +246,13 @@ fn then_output_includes_library_path_instructions(world: &WorkflowWorld) { // --------------------------------------------------------------------------- #[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/support/mod.rs b/installer/tests/support/mod.rs index b5b6ba2cc..a328ec2e7 100644 --- a/installer/tests/support/mod.rs +++ b/installer/tests/support/mod.rs @@ -4,8 +4,11 @@ //! 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; @@ -112,7 +115,8 @@ fn parse_rustup_location_output(output: &std::process::Output) -> String { .lines() .next() .expect("rustup not found in PATH") - .trim().to_owned() + .trim() + .to_owned() } /// Locates the system rustup binary path. diff --git a/installer/tests/toolchain_steps/mod.rs b/installer/tests/toolchain_steps/mod.rs index 3a220dc77..b33ecfd1d 100644 --- a/installer/tests/toolchain_steps/mod.rs +++ b/installer/tests/toolchain_steps/mod.rs @@ -3,16 +3,24 @@ //! These step implementations are used by the scenarios in //! `behaviour_toolchain.rs` via rstest-bdd macros. +use std::{ + cell::{Cell, RefCell}, + process::{Command, Output}, +}; + 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, +use super::{ + prebuilt_markers::PREBUILT_INSTALL_MARKER, + support::{ + is_toolchain_installed, + is_toolchain_installed_in_env, + pinned_toolchain_channel, + setup_isolated_rustup, + workspace_root, + }, }; /// Non-existent toolchain channel used to exercise auto-install failure paths. @@ -151,9 +159,7 @@ fn assert_toolchain_installed_in_isolated_env(world: &ToolchainWorld) { // --------------------------------------------------------------------------- #[fixture] -pub fn world() -> ToolchainWorld { - ToolchainWorld::default() -} +pub fn world() -> ToolchainWorld { ToolchainWorld::default() } // --------------------------------------------------------------------------- // Given steps @@ -202,9 +208,7 @@ pub fn given_isolated_rustup_quiet(world: &ToolchainWorld) { } #[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) { 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) { @@ -327,7 +331,8 @@ pub fn then_suite_library_is_staged(world: &ToolchainWorld) { 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}" + "expected '{STAGING_OUTPUT_MARKER}' or '{PREBUILT_INSTALL_MARKER}' in staging output, \ + stderr: {stderr}" ); } 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/src/config.rs b/src/config.rs index fe7955f76..a0aaea372 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> { - normalise_locale(self.locale.as_deref()) - } + pub fn locale(&self) -> Option<&str> { normalise_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 04f6a4ccd..6f660a11d 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 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 10f1d10e2..11ab7bf54 100644 --- a/suite/tests/registration.rs +++ b/suite/tests/registration.rs @@ -2,11 +2,14 @@ #![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 { @@ -29,14 +32,10 @@ impl RegistrationWorld { } #[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) { @@ -109,11 +108,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 b763a1b1c..a025c30d4 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; @@ -15,14 +17,10 @@ use whitaker::SharedConfig; use whitaker_common::i18n::normalise_locale; #[fixture] -fn config_source() -> RefCell> { - RefCell::new(None) -} +fn config_source() -> RefCell> { RefCell::new(None) } #[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 +48,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 +65,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 6b07dd7e1..5a3af2ebd 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; @@ -19,9 +18,7 @@ struct LocaleWorld { } #[fixture] -fn world() -> LocaleWorld { - LocaleWorld::default() -} +fn world() -> LocaleWorld { LocaleWorld::default() } #[derive(Debug)] struct StepSource(LocaleSource); @@ -41,9 +38,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 { @@ -55,9 +50,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) { @@ -65,9 +58,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) { @@ -75,9 +66,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) { @@ -126,26 +115,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 a0e688d67..4cb7cb218 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)] @@ -58,14 +50,10 @@ impl Default for HarnessWorld { } #[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) { @@ -162,33 +150,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; } From 826d0dbe1ce65fa8138dbfa3427a0983f9a6bfbb Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 11:00:09 +0200 Subject: [PATCH 04/21] Correct en-GB-oxendict spellings in code symbols and prose Rename the `-ise`/`-isation` identifiers, call sites, documentation and Gherkin step text to the Oxford `-ize`/`-ization` forms the shared spelling policy requires, rather than exempting each line: - `normalise_*` -> `normalize_*`, `summarise_context*` -> `summarize_context*`, `localised_messages` -> `localized_messages`, and the matching test, fixture and step-function names; - `capitalise_segment`, `pluralisation`, `deserialises_*`, `recognises_*`, `finalised`, `parametrised` and `synthesised` likewise; - SARIF `Artifact`/`ArtifactLocation` -> `Artefact`/`ArtefactLocation`. The `artifactLocation` and `artifacts` property names are fixed by the OASIS SARIF 2.1.0 schema, so the fields pin their wire names with `serde(rename)` and the two literals remain exempt as serialized values; - the en-GB diagnostic note in `no_expect_outside_tests` now reads "recognized", with the byte-exact `.stderr` fixtures updated in step so the localized message and its fallback agree. Delete the unused deprecated `rasterise_signal` alias instead of renaming it; the workspace treats these as internal APIs, so no compatibility shim is needed. Prune the 205 now-dead `typos.local.toml` ignore patterns left behind. The 50 that remain cover genuinely external names only: Gaelic and Welsh locale prose, `cargo_metadata::CompilerArtifact`, GitHub Actions artifact actions, `act`'s `--artifact-server-path`, dylint UI fixture sources, and the two SARIF wire-format properties. Also repair the `#[path]` test declarations in `module_must_have_inner_docs`, which resolved relative to `src/driver/` after that module moved to `driver/mod.rs`, and adopt the shared `allow_fixture_expansion_lints` attribute for single-line rstest fixtures across the workspace. --- .config/nextest.toml | 4 +- Cargo.toml | 2 + common/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 | 16 - .../decomposition_advice/tests/propagation.rs | 5 + common/src/i18n/mod.rs | 2 +- common/src/i18n/selection.rs | 8 +- common/src/lib.rs | 2 +- .../tests/brain_trait_evaluation_behaviour.rs | 2 + common/tests/brain_trait_metrics_behaviour.rs | 2 + .../tests/brain_type_evaluation_behaviour.rs | 2 + common/tests/brain_type_metrics_behaviour.rs | 2 + .../tests/cognitive_complexity_behaviour.rs | 6 +- common/tests/complexity_signal_behaviour.rs | 2 + common/tests/context_behaviour.rs | 11 +- common/tests/cosine_threshold_behaviour.rs | 2 + .../decomposition_adjacency_behaviour.rs | 2 + .../tests/decomposition_advice_behaviour.rs | 2 + ...ecomposition_diagnostic_notes_behaviour.rs | 2 + ...composition_label_propagation_behaviour.rs | 2 + .../decomposition_vector_algebra_behaviour.rs | 2 + .../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 | 2 + .../tests/i18n_quality/ftl_smoke_behaviour.rs | 2 +- common/tests/i18n_quality/suite.rs | 8 +- common/tests/lcom4_behaviour.rs | 2 + common/tests/localizer_helpers.rs | 4 +- common/tests/method_extraction_behaviour.rs | 2 + common/tests/rstest_detection_behaviour.rs | 8 +- common/tests/rstest_fingerprint_behaviour.rs | 2 + .../tests/rstest_span_recovery_behaviour.rs | 2 + crates/bumpy_road_function/Cargo.toml | 1 + 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} | 35 +- .../src/driver/segment_builder.rs | 15 +- .../tests/analysis_behaviour.rs | 27 +- .../tests/features/bumpy_road.feature | 4 +- crates/conditional_max_n_branches/Cargo.toml | 1 + .../conditional_max_n_branches/src/driver.rs | 46 ++- .../src/lib_ui_tests.rs | 17 +- .../src/tests/behaviour.rs | 1 + crates/function_attrs_follow_docs/Cargo.toml | 1 + .../function_attrs_follow_docs/src/driver.rs | 2 +- .../src/tests/localization.rs | 9 +- .../src/tests/order_detection.rs | 2 + .../function_attrs_localization.feature | 12 +- crates/module_max_lines/Cargo.toml | 1 + crates/module_max_lines/src/driver.rs | 1 + crates/module_max_lines/src/lib.rs | 5 + crates/module_max_lines/src/lib_ui_tests.rs | 17 +- crates/module_must_have_inner_docs/Cargo.toml | 1 + .../src/{driver.rs => driver/mod.rs} | 8 +- .../src/tests/behaviour.rs | 1 + .../src/tests/span_to_snippet.rs | 1 + crates/no_expect_outside_tests/Cargo.toml | 1 + ...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 | 9 +- .../src/{context.rs => context/mod.rs} | 4 +- .../src/dependency_rlib_tests.rs | 1 + .../src/diagnostics.rs | 2 +- .../no_expect_outside_tests/src/driver/mod.rs | 4 +- crates/no_expect_outside_tests/src/tests.rs | 14 +- .../src/tests/localization.rs | 7 +- .../src/tests/receiver_type_edge_cases.rs | 4 +- .../tests/features/context_summary.feature | 14 +- .../tests/features/localization.feature | 16 +- ...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 +- .../tests/integration_exclusion.rs | 2 +- crates/no_unwrap_or_else_panic/Cargo.toml | 1 + .../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 | 2 +- .../src/tests/behaviour.rs | 1 + crates/test_must_not_have_example/Cargo.toml | 1 + .../src/behaviour.rs | 1 + .../src/lib_ui_tests.rs | 17 +- crates/whitaker_clones_core/Cargo.toml | 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 | 22 +- crates/whitaker_clones_core/src/run0/types.rs | 2 +- .../tests/ast_boundary.rs | 41 +- .../tests/ast_feature_extraction_behaviour.rs | 59 ++- .../tests/build_script_integration.rs | 65 +-- .../tests/build_script_parsing.rs | 48 ++- .../tests/candidate_pair_behaviour.rs | 20 +- .../tests/min_hash_lsh_behaviour.rs | 49 ++- .../tests/run0_sarif_behaviour.rs | 109 +++-- .../tests/token_pass_behaviour.rs | 43 +- crates/whitaker_sarif/Cargo.toml | 1 + .../src/builders/location_builder.rs | 14 +- .../src/builders/log_builder.rs | 2 +- .../src/builders/run_builder.rs | 24 +- crates/whitaker_sarif/src/lib.rs | 4 +- 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 | 51 +-- .../whitaker_sarif/tests/sarif_behaviour.rs | 2 + crates/whitaker_test_macros/Cargo.toml | 17 + crates/whitaker_test_macros/src/lib.rs | 32 ++ 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/Cargo.toml | 1 + installer/src/artefact/extraction.rs | 8 +- installer/src/artefact/packaging_tests.rs | 7 +- installer/src/artefact/schema_version.rs | 2 +- installer/src/artefact/sha256_digest.rs | 1 + .../src/bin/package_dependency_binary.rs | 20 +- installer/src/bin/package_installer_bin.rs | 15 +- installer/src/bin/package_lints.rs | 101 ++--- installer/src/binstall_metadata_tests.rs | 6 +- installer/src/cli.rs | 82 ++-- installer/src/cli_tests.rs | 62 ++- .../dependency_binaries/install/checksum.rs | 7 +- .../dependency_binaries/install/downloader.rs | 7 +- .../install/http_test_server.rs | 62 ++- installer/src/dependency_packaging_tests.rs | 1 + installer/src/{deps.rs => deps/mod.rs} | 0 installer/src/list_output.rs | 13 +- installer/src/main.rs | 26 +- installer/src/output.rs | 3 +- installer/src/pipeline_staging_tests.rs | 1 + installer/src/pipeline_tests.rs | 1 + .../test_utils/dependency_binary_helpers.rs | 33 +- .../dependency_binary_helpers_tests.rs | 7 +- .../src/{test_utils.rs => test_utils/mod.rs} | 0 installer/src/tests.rs | 10 +- installer/src/tests/fast_path.rs | 8 +- installer/tests/behaviour_artefact.rs | 186 ++++++--- .../tests/behaviour_artefact_packaging.rs | 388 +++++++++++------- installer/tests/behaviour_binstall.rs | 139 ++++--- installer/tests/behaviour_cli/support.rs | 104 +++-- installer/tests/behaviour_core.rs | 48 ++- .../tests/behaviour_dependency_binaries.rs | 13 +- installer/tests/behaviour_docs.rs | 1 + installer/tests/behaviour_install_metrics.rs | 1 + .../tests/behaviour_installer_release.rs | 1 + installer/tests/behaviour_prebuilt.rs | 4 +- installer/tests/behaviour_staging.rs | 2 + installer/tests/behaviour_workflows.rs | 1 + installer/tests/support/mod.rs | 4 +- installer/tests/toolchain_steps/mod.rs | 1 + src/config.rs | 10 +- src/lints/template/validation.rs | 12 +- suite/Cargo.toml | 1 + suite/tests/registration.rs | 3 +- tests/config_loading.rs | 6 +- tests/locale_resolution.rs | 5 +- tests/ui_harness.rs | 1 + typos.local.toml | 207 +--------- typos.toml | 203 +-------- 187 files changed, 1709 insertions(+), 1409 deletions(-) rename crates/bumpy_road_function/src/{driver.rs => driver/mod.rs} (85%) 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%) create mode 100644 crates/whitaker_test_macros/Cargo.toml create mode 100644 crates/whitaker_test_macros/src/lib.rs rename installer/src/{deps.rs => deps/mod.rs} (100%) rename installer/src/{test_utils.rs => test_utils/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.toml b/Cargo.toml index 1908c5852..b1518b03b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ 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" } +whitaker_test_macros = { path = "crates/whitaker_test_macros", version = "0.2.7" } temp-env = "0.3.6" tempfile = "3.19.1" wait-timeout = "0.2.1" @@ -94,6 +95,7 @@ rustc_span = { workspace = true, optional = true } dylint_linting = { workspace = true, optional = true } [dev-dependencies] +whitaker_test_macros = { workspace = true } whitaker-common = { workspace = true } rstest = { workspace = true } rstest-bdd = { workspace = true } diff --git a/common/Cargo.toml b/common/Cargo.toml index a89370b9a..8c3dc360e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -34,6 +34,7 @@ rstest-bdd-macros = { workspace = true } proptest = "1" regex = "1.10.4" logtest = "2.0.0" +whitaker_test_macros = { workspace = true } [lints] 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 f6b638938..54b46ce8a 100644 --- a/common/src/brain_trait_metrics/tests.rs +++ b/common/src/brain_trait_metrics/tests.rs @@ -97,7 +97,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"), ]; @@ -192,7 +192,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 2f375778f..eeae4df56 100644 --- a/common/src/complexity_signal.rs +++ b/common/src/complexity_signal.rs @@ -256,22 +256,6 @@ pub fn rasterize_signal( Ok(accumulate_signal_from_diff(diff.as_slice(), len)) } -/// Deprecated alias for [`rasterize_signal`]. -/// -/// # Errors -/// -/// Propagates the same [`SignalBuildError`] conditions as [`rasterize_signal`]: -/// a zero (non-one-based) line in the range, an inverted range, or a segment -/// outside the function range. -#[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/decomposition_advice/tests/propagation.rs b/common/src/decomposition_advice/tests/propagation.rs index 190c7188c..ca91e2536 100644 --- a/common/src/decomposition_advice/tests/propagation.rs +++ b/common/src/decomposition_advice/tests/propagation.rs @@ -1,6 +1,7 @@ //! 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}, @@ -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"]) } @@ -37,12 +39,15 @@ 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"]) } diff --git a/common/src/i18n/mod.rs b/common/src/i18n/mod.rs index 0b1ec30b9..d4dc5ad6c 100644 --- a/common/src/i18n/mod.rs +++ b/common/src/i18n/mod.rs @@ -89,7 +89,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 b6da8749b..738ae1fbc 100644 --- a/common/src/i18n/selection.rs +++ b/common/src/i18n/selection.rs @@ -85,7 +85,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( @@ -137,7 +137,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()) } @@ -216,7 +216,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 775d82225..4f102dd6b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -98,7 +98,7 @@ pub use i18n::{ branch_phrase, get_localizer_for_lint, noop_reporter, - normalise_locale, + normalize_locale, resolve_localizer, safe_resolve_message_set, supports_locale, diff --git a/common/tests/brain_trait_evaluation_behaviour.rs b/common/tests/brain_trait_evaluation_behaviour.rs index 1972b5216..5be7074b7 100644 --- a/common/tests/brain_trait_evaluation_behaviour.rs +++ b/common/tests/brain_trait_evaluation_behaviour.rs @@ -16,6 +16,7 @@ use whitaker_common::brain_trait_metrics::{ format_primary_message, }, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct EvaluationWorld { @@ -103,6 +104,7 @@ impl EvaluationWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> EvaluationWorld { EvaluationWorld::default() } diff --git a/common/tests/brain_trait_metrics_behaviour.rs b/common/tests/brain_trait_metrics_behaviour.rs index 3172353aa..05eb7b9fa 100644 --- a/common/tests/brain_trait_metrics_behaviour.rs +++ b/common/tests/brain_trait_metrics_behaviour.rs @@ -5,6 +5,7 @@ use std::cell::RefCell; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::brain_trait_metrics::{TraitMetrics, TraitMetricsBuilder}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Debug)] enum PendingTraitItem { @@ -25,6 +26,7 @@ struct TraitMetricsWorld { metrics: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> TraitMetricsWorld { TraitMetricsWorld::default() } diff --git a/common/tests/brain_type_evaluation_behaviour.rs b/common/tests/brain_type_evaluation_behaviour.rs index 3131e6807..22811c1bb 100644 --- a/common/tests/brain_type_evaluation_behaviour.rs +++ b/common/tests/brain_type_evaluation_behaviour.rs @@ -17,6 +17,7 @@ use whitaker_common::brain_type_metrics::{ format_primary_message, }, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct EvaluationWorld { @@ -94,6 +95,7 @@ impl EvaluationWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> EvaluationWorld { EvaluationWorld::default() } diff --git a/common/tests/brain_type_metrics_behaviour.rs b/common/tests/brain_type_metrics_behaviour.rs index 3deb6312f..ea98687d4 100644 --- a/common/tests/brain_type_metrics_behaviour.rs +++ b/common/tests/brain_type_metrics_behaviour.rs @@ -12,6 +12,7 @@ use whitaker_common::brain_type_metrics::{ foreign_reach_count, weighted_methods_count, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct MetricsWorld { @@ -52,6 +53,7 @@ impl Default for MetricsWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> MetricsWorld { MetricsWorld::default() } diff --git a/common/tests/cognitive_complexity_behaviour.rs b/common/tests/cognitive_complexity_behaviour.rs index 7652c240d..73053861e 100644 --- a/common/tests/cognitive_complexity_behaviour.rs +++ b/common/tests/cognitive_complexity_behaviour.rs @@ -5,6 +5,7 @@ use std::cell::{Cell, RefCell}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::brain_type_metrics::cognitive_complexity::CognitiveComplexityBuilder; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug)] struct CcWorld { @@ -21,6 +22,7 @@ impl Default for CcWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> CcWorld { CcWorld::default() } @@ -85,8 +87,8 @@ fn given_pop_nesting(world: &CcWorld) { // --- When steps --- -#[when("the complexity is finalised")] -fn when_finalised(world: &CcWorld) { +#[when("the complexity is finalized")] +fn when_finalized(world: &CcWorld) { let builder = world .builder .borrow_mut() diff --git a/common/tests/complexity_signal_behaviour.rs b/common/tests/complexity_signal_behaviour.rs index 05aa8423c..e9def4318 100644 --- a/common/tests/complexity_signal_behaviour.rs +++ b/common/tests/complexity_signal_behaviour.rs @@ -11,6 +11,7 @@ use whitaker_common::complexity_signal::{ rasterize_signal, smooth_moving_average, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct SignalWorld { @@ -96,6 +97,7 @@ impl SignalWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> SignalWorld { SignalWorld::default() } diff --git a/common/tests/context_behaviour.rs b/common/tests/context_behaviour.rs index e0294115f..80c9190bd 100644 --- a/common/tests/context_behaviour.rs +++ b/common/tests/context_behaviour.rs @@ -11,6 +11,7 @@ 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 { @@ -54,6 +55,7 @@ impl FunctionFixture { } } +#[allow_fixture_expansion_lints] #[fixture] fn function() -> FunctionFixture { FunctionFixture::new() } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -62,6 +64,7 @@ struct Evaluation { in_context: bool, } +#[allow_fixture_expansion_lints] #[fixture] fn evaluation() -> Evaluation { Evaluation::default() } @@ -80,7 +83,7 @@ fn given_tokio(function: &FunctionFixture) { #[given("a function without test attributes")] 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); } @@ -102,7 +105,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); } @@ -112,7 +115,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); } @@ -138,6 +141,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 505e9d9fa..ddf9c78ac 100644 --- a/common/tests/cosine_threshold_behaviour.rs +++ b/common/tests/cosine_threshold_behaviour.rs @@ -8,6 +8,7 @@ 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 { @@ -63,6 +64,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 ed5c93b69..95f3fe1a0 100644 --- a/common/tests/decomposition_adjacency_behaviour.rs +++ b/common/tests/decomposition_adjacency_behaviour.rs @@ -10,6 +10,7 @@ use whitaker_common::test_support::decomposition::{ EdgeInput, adjacency_report, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct AdjacencyWorld { @@ -18,6 +19,7 @@ struct AdjacencyWorld { result: RefCell>>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> AdjacencyWorld { AdjacencyWorld::default() } diff --git a/common/tests/decomposition_advice_behaviour.rs b/common/tests/decomposition_advice_behaviour.rs index 9e92788f1..e67e2592c 100644 --- a/common/tests/decomposition_advice_behaviour.rs +++ b/common/tests/decomposition_advice_behaviour.rs @@ -12,6 +12,7 @@ use whitaker_common::decomposition_advice::{ SuggestedExtractionKind, suggest_decomposition, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Clone)] struct CsvList(Vec); @@ -43,6 +44,7 @@ struct DecompositionWorld { suggestions: RefCell>>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> DecompositionWorld { DecompositionWorld::default() } diff --git a/common/tests/decomposition_diagnostic_notes_behaviour.rs b/common/tests/decomposition_diagnostic_notes_behaviour.rs index 52a708e0c..9a9a81e82 100644 --- a/common/tests/decomposition_diagnostic_notes_behaviour.rs +++ b/common/tests/decomposition_diagnostic_notes_behaviour.rs @@ -14,6 +14,7 @@ use whitaker_common::{ }, test_support::decomposition::{parser_serde_fs_fixture, transport_trait_fixture}, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Clone)] struct CsvList(Vec); @@ -59,6 +60,7 @@ struct DiagnosticNoteWorld { rendered_note: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> DiagnosticNoteWorld { DiagnosticNoteWorld::default() } diff --git a/common/tests/decomposition_label_propagation_behaviour.rs b/common/tests/decomposition_label_propagation_behaviour.rs index 7fc8293f6..f69744517 100644 --- a/common/tests/decomposition_label_propagation_behaviour.rs +++ b/common/tests/decomposition_label_propagation_behaviour.rs @@ -10,6 +10,7 @@ use whitaker_common::test_support::decomposition::{ LabelPropagationReport, label_propagation_report, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Debug)] struct CsvList(Vec); @@ -66,6 +67,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 e6abfc4f1..03a019000 100644 --- a/common/tests/decomposition_vector_algebra_behaviour.rs +++ b/common/tests/decomposition_vector_algebra_behaviour.rs @@ -8,6 +8,7 @@ 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 { @@ -63,6 +64,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 027526a25..6c3a692cc 100644 --- a/common/tests/i18n_behaviour.rs +++ b/common/tests/i18n_behaviour.rs @@ -9,6 +9,7 @@ use std::{borrow::Cow, cell::RefCell, collections::HashMap}; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; 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; @@ -58,6 +59,7 @@ fn branch_count_from_key(key: &str) -> Option<(String, u32)> { Some((base.to_owned(), value)) } +#[allow_fixture_expansion_lints] #[fixture] fn fixture() -> I18nFixture { I18nFixture::default() } 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 40fc7d993..1c61055b4 100644 --- a/common/tests/lcom4_behaviour.rs +++ b/common/tests/lcom4_behaviour.rs @@ -8,6 +8,7 @@ use std::{ use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::lcom4::{MethodInfo, cohesion_components}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct LcomWorld { @@ -26,6 +27,7 @@ impl LcomWorld { const fn result(&self) -> Option { self.result.get() } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> LcomWorld { LcomWorld::default() } diff --git a/common/tests/localizer_helpers.rs b/common/tests/localizer_helpers.rs index 17c5358e9..f94032171 100644 --- a/common/tests/localizer_helpers.rs +++ b/common/tests/localizer_helpers.rs @@ -27,6 +27,7 @@ use whitaker_common::{ }, test_support::with_locale, }; +use whitaker_test_macros::allow_fixture_expansion_lints; static ENVIRONMENT_LOCK: Mutex<()> = Mutex::new(()); @@ -90,7 +91,7 @@ impl HelperWorld { .borrow() .as_ref() .cloned() - .unwrap_or_else(|| panic!("localizer should be initialised")) + .unwrap_or_else(|| panic!("localizer should be initialized")) } fn assert_locale(&self, expected: &str) { @@ -178,6 +179,7 @@ impl HelperWorld { fn recorded_messages(&self) -> Vec { self.emitter.recorded_messages() } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> HelperWorld { HelperWorld::new() } diff --git a/common/tests/method_extraction_behaviour.rs b/common/tests/method_extraction_behaviour.rs index 841cc4067..c7717f11b 100644 --- a/common/tests/method_extraction_behaviour.rs +++ b/common/tests/method_extraction_behaviour.rs @@ -5,6 +5,7 @@ use std::cell::RefCell; use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::lcom4::{MethodInfo, MethodInfoBuilder}; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct ExtractionWorld { @@ -53,6 +54,7 @@ impl ExtractionWorld { fn calls_empty(&self) -> bool { self.with_result(|info| info.called_methods().is_empty()) } } +#[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 9b0347724..ac83dcad9 100644 --- a/common/tests/rstest_detection_behaviour.rs +++ b/common/tests/rstest_detection_behaviour.rs @@ -18,6 +18,7 @@ use whitaker_common::{ is_rstest_test_with, }, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Clone, Debug, Default)] struct DetectionWorld { @@ -109,6 +110,7 @@ impl DetectionWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> DetectionWorld { DetectionWorld::default() } @@ -186,17 +188,17 @@ 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(); } -#[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)); } diff --git a/common/tests/rstest_fingerprint_behaviour.rs b/common/tests/rstest_fingerprint_behaviour.rs index 4100d0238..82e5a0876 100644 --- a/common/tests/rstest_fingerprint_behaviour.rs +++ b/common/tests/rstest_fingerprint_behaviour.rs @@ -14,6 +14,7 @@ use whitaker_common::rstest::{ ParagraphNormalizer, StmtShape, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Default)] struct FingerprintWorld { @@ -55,6 +56,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 99d60b88e..1e7900d95 100644 --- a/common/tests/rstest_span_recovery_behaviour.rs +++ b/common/tests/rstest_span_recovery_behaviour.rs @@ -8,6 +8,7 @@ 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 { @@ -30,6 +31,7 @@ impl SpanRecoveryWorld { } } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> SpanRecoveryWorld { SpanRecoveryWorld::default() } diff --git a/crates/bumpy_road_function/Cargo.toml b/crates/bumpy_road_function/Cargo.toml index 01ba78caa..11ae9e98a 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/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index 404c7aaa8..14799709e 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::{Settings, Weights, normalise_settings}; +/// use bumpy_road_function::analysis::{Settings, Weights, normalize_settings}; /// /// 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 { @@ -114,9 +114,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 33c86245a..276449316 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 log::debug; use serde::Deserialize; diff --git a/crates/bumpy_road_function/src/driver/diagnostic.rs b/crates/bumpy_road_function/src/driver/diagnostic.rs index 5f34150e2..64a277b87 100644 --- a/crates/bumpy_road_function/src/driver/diagnostic.rs +++ b/crates/bumpy_road_function/src/driver/diagnostic.rs @@ -39,7 +39,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"), @@ -69,8 +69,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); } @@ -158,10 +161,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 85% rename from crates/bumpy_road_function/src/driver.rs rename to crates/bumpy_road_function/src/driver/mod.rs index f74c9fd9b..5ef5690cb 100644 --- a/crates/bumpy_road_function/src/driver.rs +++ b/crates/bumpy_road_function/src/driver/mod.rs @@ -17,7 +17,7 @@ use whitaker_common::{ i18n::MessageKey, }; -use crate::analysis::{Settings, detect_bumps, normalise_settings}; +use crate::analysis::{Settings, detect_bumps, normalize_settings}; const LINT_NAME: &str = "bumpy_road_function"; const MESSAGE_KEY: MessageKey<'static> = MessageKey::new(LINT_NAME); @@ -32,13 +32,26 @@ 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() +/// Registration glue expanded from `dylint_linting::impl_late_lint!`. +/// +/// 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 { + 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 registration::BUMPY_ROAD_FUNCTION; + /// Lint pass that caches configuration and localization for a crate. pub struct BumpyRoadFunction { settings: Settings, @@ -56,7 +69,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()); } @@ -104,7 +117,7 @@ impl BumpyRoadFunction { return; } - analyse_body(cx, target, &self.settings, &self.localizer); + analyse_body(cx, &target, &self.settings, &self.localizer); } } @@ -180,7 +193,7 @@ struct AnalysisTarget { fn analyse_body( cx: &LateContext<'_>, - target: AnalysisTarget, + target: &AnalysisTarget, settings: &Settings, localizer: &Localizer, ) { @@ -204,14 +217,14 @@ 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; } }; 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 bceec6876..767d5deb9 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); } @@ -209,12 +209,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, } @@ -227,8 +223,9 @@ pub(super) fn span_line_range(source_map: &SourceMap, span: Span) -> Option, bumps: RefCell>, settings: RefCell, - normalised: RefCell>, + normalized: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> World { World::default() } @@ -159,28 +160,28 @@ 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 = normalize_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"); + .expect("settings should be normalized"); assert_eq!(settings.threshold, threshold); } 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/conditional_max_n_branches/Cargo.toml b/crates/conditional_max_n_branches/Cargo.toml index 1017d300b..aef24cd72 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/driver.rs b/crates/conditional_max_n_branches/src/driver.rs index 659641dfc..3d98943d9 100644 --- a/crates/conditional_max_n_branches/src/driver.rs +++ b/crates/conditional_max_n_branches/src/driver.rs @@ -64,13 +64,26 @@ impl Default for ConditionalMaxNBranches { } } -dylint_linting::impl_late_lint! { - pub CONDITIONAL_MAX_N_BRANCHES, - Warn, - "complex conditionals should be decomposed when they exceed the configured branch limit", - ConditionalMaxNBranches::default() +/// Registration glue expanded from `dylint_linting::impl_late_lint!`. +/// +/// 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 { + use super::ConditionalMaxNBranches; + + dylint_linting::impl_late_lint! { + /// Lint flagging conditionals whose predicate exceeds the configured + /// number of short-circuit branches. + pub CONDITIONAL_MAX_N_BRANCHES, + Warn, + "complex conditionals should be decomposed when they exceed the configured branch limit", + ConditionalMaxNBranches::default() + } } +pub use registration::CONDITIONAL_MAX_N_BRANCHES; + impl<'tcx> LateLintPass<'tcx> for ConditionalMaxNBranches { fn check_crate(&mut self, _cx: &LateContext<'tcx>) { self.max_branches = load_configuration().max_branches.max(1); @@ -191,12 +204,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, } @@ -215,9 +224,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"), @@ -238,9 +250,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, @@ -253,7 +265,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/conditional_max_n_branches/src/lib_ui_tests.rs b/crates/conditional_max_n_branches/src/lib_ui_tests.rs index 5322b1316..c05dc47a6 100644 --- a/crates/conditional_max_n_branches/src/lib_ui_tests.rs +++ b/crates/conditional_max_n_branches/src/lib_ui_tests.rs @@ -12,15 +12,14 @@ use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_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 a6748ebdb..b3715fb46 100644 --- a/crates/conditional_max_n_branches/src/tests/behaviour.rs +++ b/crates/conditional_max_n_branches/src/tests/behaviour.rs @@ -31,6 +31,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 7f776a42b..94b5a09a6 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/driver.rs b/crates/function_attrs_follow_docs/src/driver.rs index 52ae4fcc5..3124c1a9c 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 3f8ead786..a643d2967 100644 --- a/crates/function_attrs_follow_docs/src/tests/localization.rs +++ b/crates/function_attrs_follow_docs/src/tests/localization.rs @@ -15,7 +15,7 @@ use super::{ Localizer, MESSAGE_KEY, attribute_fallback, - localised_messages, + localized_messages, }; #[derive(Default)] @@ -58,6 +58,7 @@ impl LocalizationWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> LocalizationWorld { LocalizationWorld::default() } @@ -85,7 +86,7 @@ fn given_attribute_fallback(world: &LocalizationWorld) { world.use_attribute_fal #[given("localization fails")] 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(); @@ -114,9 +115,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/function_attrs_follow_docs/src/tests/order_detection.rs b/crates/function_attrs_follow_docs/src/tests/order_detection.rs index 1e7684642..d2a02f67e 100644 --- a/crates/function_attrs_follow_docs/src/tests/order_detection.rs +++ b/crates/function_attrs_follow_docs/src/tests/order_detection.rs @@ -44,9 +44,11 @@ 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 } 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/Cargo.toml b/crates/module_max_lines/Cargo.toml index a181deac2..9a3c2b456 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 548592159..0c7808de8 100644 --- a/crates/module_max_lines/src/driver.rs +++ b/crates/module_max_lines/src/driver.rs @@ -251,6 +251,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_max_lines/src/lib_ui_tests.rs b/crates/module_max_lines/src/lib_ui_tests.rs index 7f84775f1..62e4989e4 100644 --- a/crates/module_max_lines/src/lib_ui_tests.rs +++ b/crates/module_max_lines/src/lib_ui_tests.rs @@ -12,15 +12,14 @@ use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_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 b97955b4a..b8c11ea9f 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/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 9a6d377ec..c08f2b9be 100644 --- a/crates/module_must_have_inner_docs/src/driver.rs +++ b/crates/module_must_have_inner_docs/src/driver/mod.rs @@ -329,17 +329,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/module_must_have_inner_docs/src/tests/behaviour.rs b/crates/module_must_have_inner_docs/src/tests/behaviour.rs index e4f2461df..5bd99dc0e 100644 --- a/crates/module_must_have_inner_docs/src/tests/behaviour.rs +++ b/crates/module_must_have_inner_docs/src/tests/behaviour.rs @@ -34,6 +34,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/span_to_snippet.rs b/crates/module_must_have_inner_docs/src/tests/span_to_snippet.rs index c73fbf550..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 @@ -29,6 +29,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/no_expect_outside_tests/Cargo.toml b/crates/no_expect_outside_tests/Cargo.toml index 76e1a3e8d..4453452f5 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/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 0998a1fc0..9eed34d87 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 9d95f5e8c..8cfd7859e 100644 --- a/crates/no_expect_outside_tests/src/behaviour.rs +++ b/crates/no_expect_outside_tests/src/behaviour.rs @@ -11,7 +11,7 @@ use whitaker_common::{ attributes::{Attribute, AttributeKind, AttributePath}, }; -use crate::context::{ContextSummary, summarise_context}; +use crate::context::{ContextSummary, summarize_context}; #[derive(Default)] struct ContextWorld { @@ -56,7 +56,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(), @@ -70,6 +70,7 @@ impl ContextWorld { fn should_skip_lint(&self) -> bool { *self.skip_lint.borrow() } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> ContextWorld { ContextWorld::default() } @@ -104,8 +105,8 @@ 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(); } -#[when("I summarise the context")] -fn when_summarise(world: &ContextWorld) { world.evaluate(); } +#[when("I summarize the context")] +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.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 509c1f9f6..b62980278 100644 --- a/crates/no_expect_outside_tests/src/context.rs +++ b/crates/no_expect_outside_tests/src/context/mod.rs @@ -114,7 +114,7 @@ fn has_test_ancestry( /// # Examples /// /// ```ignore -/// let summary = summarise_context( +/// let summary = summarize_context( /// &entries, /// has_test_context_ancestry, /// additional_test_attributes, @@ -123,7 +123,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/dependency_rlib_tests.rs b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs index a364ca2cc..96d7e6928 100644 --- a/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs +++ b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs @@ -53,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") } diff --git a/crates/no_expect_outside_tests/src/diagnostics.rs b/crates/no_expect_outside_tests/src/diagnostics.rs index 08378e1fd..0161f0a70 100644 --- a/crates/no_expect_outside_tests/src/diagnostics.rs +++ b/crates/no_expect_outside_tests/src/diagnostics.rs @@ -187,7 +187,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, diff --git a/crates/no_expect_outside_tests/src/driver/mod.rs b/crates/no_expect_outside_tests/src/driver/mod.rs index 77d5e3f85..ccb58e04d 100644 --- a/crates/no_expect_outside_tests/src/driver/mod.rs +++ b/crates/no_expect_outside_tests/src/driver/mod.rs @@ -21,7 +21,7 @@ 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, summarise_context}, + context::{collect_context, is_cfg_test_attribute, summarize_context}, diagnostics::{DiagnosticContext, emit_diagnostic}, }; @@ -121,7 +121,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 576fe98b9..f6e842319 100644 --- a/crates/no_expect_outside_tests/src/tests.rs +++ b/crates/no_expect_outside_tests/src/tests.rs @@ -8,7 +8,7 @@ use whitaker_common::{ attributes::{Attribute, AttributeKind, AttributePath}, }; -use crate::context::summarise_context; +use crate::context::summarize_context; fn function_entry(name: &str, attrs: Vec) -> ContextEntry { ContextEntry::new(name, ContextKind::Function, attrs) @@ -23,18 +23,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")); @@ -43,7 +43,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); @@ -59,7 +59,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 741d5fb1e..3a84dd27e 100644 --- a/crates/no_expect_outside_tests/src/tests/localization.rs +++ b/crates/no_expect_outside_tests/src/tests/localization.rs @@ -20,7 +20,7 @@ use super::{ ReceiverLabel, context_label, fallback_messages, - localised_messages, + localized_messages, }; use crate::context::ContextSummary; @@ -99,6 +99,7 @@ impl LocalizationWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> LocalizationWorld { LocalizationWorld::default() } @@ -138,7 +139,7 @@ fn given_no_function(world: &LocalizationWorld) { world.set_function(None); } #[given("localization fails")] 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(); @@ -244,7 +245,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 { 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 59695a6d9..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 @@ -8,7 +8,7 @@ use super::{ NoExpectMessages, ReceiverCategory, ReceiverLabel, - localised_messages, + localized_messages, }; #[rstest] @@ -32,7 +32,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 a9453c234..14469084e 100644 --- a/crates/no_expect_outside_tests/tests/features/context_summary.feature +++ b/crates/no_expect_outside_tests/tests/features/context_summary.feature @@ -1,33 +1,33 @@ -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 - 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 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 + 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/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_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 ca97638a1..a979d087e 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 d3d053621..bc36f1412 100644 --- a/crates/no_std_fs_operations/src/behaviour.rs +++ b/crates/no_std_fs_operations/src/behaviour.rs @@ -6,7 +6,7 @@ use rstest::fixture; use rstest_bdd_macros::{given, scenario, then, when}; use whitaker_common::i18n::{I18nError, Localizer, testing::FailingLookup}; -use crate::diagnostics::{StdFsMessages, localised_messages}; +use crate::diagnostics::{StdFsMessages, localized_messages}; #[derive(Default)] struct LocalizationWorld { @@ -28,10 +28,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); } @@ -78,8 +78,8 @@ fn given_operation(world: &WorldCell, operation: String) { #[given("localization fails")] fn given_failure(world: &WorldCell) { world.borrow_mut().mark_failure(); } -#[when("I localise the std::fs diagnostic")] -fn when_localise(world: &WorldCell) { world.borrow_mut().resolve(); } +#[when("I localize the std::fs diagnostic")] +fn when_localize(world: &WorldCell) { world.borrow_mut().resolve(); } #[then("the primary mentions {snippet}")] fn then_primary(world: &WorldCell, snippet: String) { diff --git a/crates/no_std_fs_operations/src/diagnostics.rs b/crates/no_std_fs_operations/src/diagnostics.rs index 2608500e0..b61a570d5 100644 --- a/crates/no_std_fs_operations/src/diagnostics.rs +++ b/crates/no_std_fs_operations/src/diagnostics.rs @@ -83,7 +83,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 506de8976..413a4acdd 100644 --- a/crates/no_std_fs_operations/src/exclusion_behaviour.rs +++ b/crates/no_std_fs_operations/src/exclusion_behaviour.rs @@ -33,6 +33,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 76caefc98..d0680b7a9 100644 --- a/crates/no_std_fs_operations/src/usage/tests.rs +++ b/crates/no_std_fs_operations/src/usage/tests.rs @@ -22,7 +22,7 @@ use super::{StdFsUsage, UsageCategory, label_is_std_fs}; #[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_std_fs_operations/tests/integration_exclusion.rs b/crates/no_std_fs_operations/tests/integration_exclusion.rs index 982c0d61b..0ee3b4194 100644 --- a/crates/no_std_fs_operations/tests/integration_exclusion.rs +++ b/crates/no_std_fs_operations/tests/integration_exclusion.rs @@ -421,7 +421,7 @@ fn non_excluded_crate_diagnostics_match_snapshot() -> anyhow::Result<()> { { Some( serde_json::to_value(message.message) - .context("failed to serialise diagnostic for snapshot"), + .context("failed to serialize diagnostic for snapshot"), ) } _ => None, diff --git a/crates/no_unwrap_or_else_panic/Cargo.toml b/crates/no_unwrap_or_else_panic/Cargo.toml index 2286b44f1..c390d3117 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/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 ec8d153e0..67649621f 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(cx: &LateContext<'_>, 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; diff --git a/crates/no_unwrap_or_else_panic/src/driver.rs b/crates/no_unwrap_or_else_panic/src/driver.rs index 42740ade9..01200f591 100644 --- a/crates/no_unwrap_or_else_panic/src/driver.rs +++ b/crates/no_unwrap_or_else_panic/src/driver.rs @@ -142,7 +142,7 @@ fn summarize_context_with_harness( 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/no_unwrap_or_else_panic/src/tests/behaviour.rs b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs index db3c291dc..2a30e2969 100644 --- a/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs +++ b/crates/no_unwrap_or_else_panic/src/tests/behaviour.rs @@ -32,6 +32,7 @@ impl DecisionWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> DecisionWorld { DecisionWorld::default() } diff --git a/crates/test_must_not_have_example/Cargo.toml b/crates/test_must_not_have_example/Cargo.toml index 58283da3a..492398ecf 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 60ad1f964..7c82a288f 100644 --- a/crates/test_must_not_have_example/src/behaviour.rs +++ b/crates/test_must_not_have_example/src/behaviour.rs @@ -24,6 +24,7 @@ impl DocumentationWorld { fn outcome(&self) -> Option { *self.outcome.borrow() } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> DocumentationWorld { DocumentationWorld::default() } 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 a3f8096ba..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 @@ -10,15 +10,14 @@ use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_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..4f99eeb71 100644 --- a/crates/whitaker_clones_core/Cargo.toml +++ b/crates/whitaker_clones_core/Cargo.toml @@ -31,6 +31,7 @@ rstest-bdd-macros = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } toml = { workspace = true } +whitaker_test_macros = { workspace = true } [build-dependencies] camino = { workspace = true } diff --git a/crates/whitaker_clones_core/src/index/tests.rs b/crates/whitaker_clones_core/src/index/tests.rs index 528f1a251..b07205a8f 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 super::{ CandidatePair, @@ -39,12 +40,15 @@ 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 e08507828..5642558ce 100644 --- a/crates/whitaker_clones_core/src/run0/emit.rs +++ b/crates/whitaker_clones_core/src/run0/emit.rs @@ -169,7 +169,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 05003bf65..6890324e1 100644 --- a/crates/whitaker_clones_core/src/run0/tests.rs +++ b/crates/whitaker_clones_core/src/run0/tests.rs @@ -16,11 +16,11 @@ use super::{ use crate::NormProfile; fn build_pair_and_accept( - left: FragmentInput<'_>, - right: FragmentInput<'_>, + left: &FragmentInput<'_>, + 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) } @@ -46,14 +46,14 @@ fn assert_region(id: &str, source: &str, range: std::ops::Range, expected #[test] fn boundary_threshold_accepts_type1_pair() { let accepted = build_pair_and_accept( - FragmentInput { + &FragmentInput { id: "alpha", profile: NormProfile::T1, file_uri: "src/a.rs", source_text: "fn a() {}\n", hashes: &[(1, 0..8), (2, 0..8)], }, - FragmentInput { + &FragmentInput { id: "beta", profile: NormProfile::T1, file_uri: "src/b.rs", @@ -74,14 +74,14 @@ fn boundary_threshold_accepts_type2_pair() { .unwrap_or_else(|error| panic!("unexpected threshold error: {error}")), ); let accepted = build_pair_and_accept( - FragmentInput { + &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)], }, - FragmentInput { + &FragmentInput { id: "beta", profile: NormProfile::T2, file_uri: "src/b.rs", @@ -98,14 +98,14 @@ fn boundary_threshold_accepts_type2_pair() { #[test] fn just_below_threshold_is_rejected() { let accepted = build_pair_and_accept( - FragmentInput { + &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)], }, - FragmentInput { + &FragmentInput { id: "beta", profile: NormProfile::T2, file_uri: "src/b.rs", @@ -190,12 +190,12 @@ fn emit_run0_uses_primary_and_related_locations() { .locations .first() .expect("primary location should be present"); - assert_eq!(location.physical_location.artifact_location.uri, "src/a.rs"); + assert_eq!(location.physical_location.artefact_location.uri, "src/a.rs"); let related = result .related_locations .first() .expect("related location should be present"); - assert_eq!(related.physical_location.artifact_location.uri, "src/b.rs"); + assert_eq!(related.physical_location.artefact_location.uri, "src/b.rs"); } #[test] diff --git a/crates/whitaker_clones_core/src/run0/types.rs b/crates/whitaker_clones_core/src/run0/types.rs index 1af60c5ff..1776f802d 100644 --- a/crates/whitaker_clones_core/src/run0/types.rs +++ b/crates/whitaker_clones_core/src/run0/types.rs @@ -66,7 +66,7 @@ impl TokenFragment { #[must_use] pub const fn profile(&self) -> NormProfile { self.profile } - /// Returns the source artifact URI used in SARIF output. + /// Returns the source artefact URI used in SARIF output. #[must_use] pub const fn file_uri(&self) -> &str { self.file_uri.as_str() } diff --git a/crates/whitaker_clones_core/tests/ast_boundary.rs b/crates/whitaker_clones_core/tests/ast_boundary.rs index d7beac327..11275ff5b 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,7 +136,7 @@ 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)) }) @@ -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; } } @@ -215,10 +216,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, @@ -241,7 +243,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; 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 62d8c4f94..157ae34b7 100644 --- a/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs +++ b/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs @@ -16,6 +16,7 @@ use whitaker_clones_core::{ canonical_hash, lower_span, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Clone, Copy)] enum SnippetName { @@ -86,32 +87,44 @@ 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")] @@ -123,10 +136,12 @@ fn when_candidate_span_is_lowered(world: &AstFeatureWorld) { return; }; let end = start + needle.len(); + let span_start = + u32::try_from(start).unwrap_or_else(|error| panic!("span start must fit in u32: {error}")); + let span_end = + u32::try_from(end).unwrap_or_else(|error| panic!("span end must fit in u32: {error}")); - 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; @@ -139,7 +154,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(()) @@ -156,20 +171,24 @@ fn then_lowered_root_kind_is(world: &AstFeatureWorld, kind: String) { let lowered = world.lowered.borrow(); let tree = lowered .as_ref() - .expect("lowered tree should be available after lowering"); + .unwrap_or_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) } @@ -178,14 +197,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 f90ba384a..862234a07 100644 --- a/crates/whitaker_clones_core/tests/build_script_parsing.rs +++ b/crates/whitaker_clones_core/tests/build_script_parsing.rs @@ -114,7 +114,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(()) } @@ -127,7 +133,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(()) } @@ -142,12 +150,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(()) } @@ -160,7 +168,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(()) } @@ -169,16 +180,17 @@ 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(()) } diff --git a/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs b/crates/whitaker_clones_core/tests/candidate_pair_behaviour.rs index 710c0eb43..11d13f5e5 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,15 +16,16 @@ 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}")] @@ -54,14 +56,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 d6aacd834..4e65f6b69 100644 --- a/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs +++ b/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs @@ -15,6 +15,7 @@ use whitaker_clones_core::{ LshIndex, MinHasher, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct MinHashLshWorld { @@ -25,6 +26,7 @@ struct MinHashLshWorld { candidate_error: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> MinHashLshWorld { MinHashLshWorld::default() } @@ -56,15 +58,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 { @@ -108,7 +110,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; }; @@ -133,7 +135,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" + ); }); } @@ -147,26 +153,39 @@ fn then_only_candidate_pair_is(world: &MinHashLshWorld, left: String, right: Str ); }; let expected = CandidatePair::new(FragmentId::from(left), FragmentId::from(right)) - .expect("distinct fragment IDs should form a canonical pair"); - assert_eq!(candidate, &expected); + .unwrap_or_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 b144a3b1c..b9a65cc75 100644 --- a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs +++ b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs @@ -16,6 +16,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 { @@ -38,15 +39,16 @@ 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)) { @@ -181,7 +183,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}")] @@ -190,7 +198,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" + ); }); } @@ -200,40 +211,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}")] @@ -242,13 +284,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!( "{}:{}-{}:{}", @@ -257,9 +297,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); }); } @@ -269,11 +312,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.artefact_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 cd52b5a8d..b7cb01106 100644 --- a/crates/whitaker_clones_core/tests/token_pass_behaviour.rs +++ b/crates/whitaker_clones_core/tests/token_pass_behaviour.rs @@ -17,6 +17,7 @@ use whitaker_clones_core::{ normalize, winnow, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct TokenPassWorld { @@ -34,6 +35,7 @@ struct TokenPassWorld { window_error: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> TokenPassWorld { TokenPassWorld::default() } @@ -69,12 +71,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}")] @@ -89,8 +91,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) => { @@ -103,8 +105,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) => { @@ -203,7 +205,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")] @@ -218,7 +224,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" + ); }); } @@ -228,7 +238,11 @@ 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" + ); }); } @@ -238,7 +252,7 @@ fn then_retained_hashes_are(world: &TokenPassWorld, hashes: String) { .split_whitespace() .map(str::parse::) .collect::, _>>() - .expect("expected hash list should be valid"); + .unwrap_or_else(|error| panic!("expected hash list must be valid: {error}")); with_retained(world, |retained| { assert_eq!( @@ -246,7 +260,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" ); }); } @@ -262,7 +277,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 515d90cf2..6542deaa1 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/src/builders/location_builder.rs b/crates/whitaker_sarif/src/builders/location_builder.rs index 21f777b04..64e05ba1f 100644 --- a/crates/whitaker_sarif/src/builders/location_builder.rs +++ b/crates/whitaker_sarif/src/builders/location_builder.rs @@ -2,7 +2,7 @@ use crate::{ error::SarifError, - model::location::{ArtifactLocation, Location, PhysicalLocation, Region}, + model::location::{ArtefactLocation, Location, PhysicalLocation, Region}, }; /// Fluent builder for constructing a [`Region`]. @@ -66,7 +66,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 const fn with_byte_offset(mut self, offset: usize) -> Self { self.byte_offset = Some(offset); @@ -170,7 +170,7 @@ impl RegionBuilder { /// .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 { @@ -197,7 +197,7 @@ impl LocationBuilder { self } - /// Sets the region within the artifact. + /// Sets the region within the artefact. #[must_use] pub const fn with_region(mut self, region: Region) -> Self { self.region = Some(region); @@ -209,7 +209,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, }, @@ -263,7 +263,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()); } @@ -289,7 +289,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 4cd77959c..ccbd3cc0e 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 3e63126bf..8ec7d244d 100644 --- a/crates/whitaker_sarif/src/builders/run_builder.rs +++ b/crates/whitaker_sarif/src/builders/run_builder.rs @@ -3,7 +3,7 @@ use crate::model::{ descriptor::ReportingDescriptor, result::SarifResult, - run::{Artifact, Invocation, Run, Tool, ToolComponent}, + run::{Artefact, Invocation, Run, Tool, ToolComponent}, }; /// Fluent builder for constructing a [`Run`]. @@ -24,7 +24,7 @@ pub struct RunBuilder { rules: Vec, invocations: Vec, results: Vec, - artifacts: Vec, + artefacts: Vec, } impl RunBuilder { @@ -38,7 +38,7 @@ impl RunBuilder { rules: Vec::new(), invocations: Vec::new(), results: Vec::new(), - artifacts: Vec::new(), + artefacts: Vec::new(), } } @@ -122,27 +122,27 @@ impl RunBuilder { self } - /// Appends an artifact reference. + /// Appends an artefact reference. /// /// # Examples /// /// ``` - /// use whitaker_sarif::{Artifact, ArtifactLocation, RunBuilder}; + /// use whitaker_sarif::{Artefact, ArtefactLocation, RunBuilder}; /// /// 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 } @@ -171,7 +171,7 @@ impl RunBuilder { }, invocations: self.invocations, results: self.results, - artifacts: self.artifacts, + artefacts: self.artefacts, } } } @@ -181,10 +181,12 @@ mod tests { //! Unit tests for [`RunBuilder`] construction and method chaining. use rstest::{fixture, rstest}; + use whitaker_test_macros::allow_fixture_expansion_lints; use super::*; use crate::rules::all_rules; + #[allow_fixture_expansion_lints] #[fixture] fn builder() -> RunBuilder { RunBuilder::new("tool", "1.0") } diff --git a/crates/whitaker_sarif/src/lib.rs b/crates/whitaker_sarif/src/lib.rs index 7e8029366..d4b07ccbf 100644 --- a/crates/whitaker_sarif/src/lib.rs +++ b/crates/whitaker_sarif/src/lib.rs @@ -30,8 +30,8 @@ pub use error::{Result, SarifError}; pub use merge::{WHITAKER_FRAGMENT_KEY, deduplicate_results, merge_runs}; // Model types pub use model::{ - Artifact, - ArtifactLocation, + Artefact, + ArtefactLocation, Invocation, Level, Location, diff --git a/crates/whitaker_sarif/src/merge.rs b/crates/whitaker_sarif/src/merge.rs index 76d90ca29..027af3f76 100644 --- a/crates/whitaker_sarif/src/merge.rs +++ b/crates/whitaker_sarif/src/merge.rs @@ -65,7 +65,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 { @@ -126,7 +126,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 /// @@ -151,12 +151,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()); } @@ -166,7 +166,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 178eddb40..ce6ba1130 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::{ArtifactLocation, Location, PhysicalLocation, Region}; +/// use whitaker_sarif::{ArtefactLocation, Location, PhysicalLocation, 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, @@ -172,7 +176,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()), }, @@ -203,7 +207,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 20b250ccc..89c00df58 100644 --- a/crates/whitaker_sarif/src/model/mod.rs +++ b/crates/whitaker_sarif/src/model/mod.rs @@ -4,9 +4,9 @@ //! specification subset used by Whitaker. Types are organized by concept: //! //! - [`log`] — top-level [`SarifLog`] container. -//! - [`run`] — [`Run`], [`Tool`], [`ToolComponent`], [`Invocation`], and [`Artifact`]. +//! - [`run`] — [`Run`], [`Tool`], [`ToolComponent`], [`Invocation`], and [`Artefact`]. //! - [`result`] — [`SarifResult`], [`Level`], and [`Message`]. -//! - [`location`] — [`Location`], [`PhysicalLocation`], [`ArtifactLocation`], [`Region`], and +//! - [`location`] — [`Location`], [`PhysicalLocation`], [`ArtefactLocation`], [`Region`], and //! [`RelatedLocation`]. //! - [`descriptor`] — [`ReportingDescriptor`] and [`MultiformatMessageString`]. //! @@ -20,7 +20,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 89c86653d..f7cc4634b 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}; @@ -27,7 +27,7 @@ use super::{descriptor::ReportingDescriptor, result::SarifResult}; /// }, /// invocations: Vec::new(), /// results: Vec::new(), -/// artifacts: Vec::new(), +/// artefacts: Vec::new(), /// }; /// assert_eq!(run.tool.driver.name, "whitaker_clones_cli"); /// ``` @@ -45,9 +45,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. @@ -116,27 +119,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")] @@ -146,7 +149,7 @@ pub struct Artifact { #[cfg(test)] mod tests { use super::*; - use crate::model::location::ArtifactLocation; + use crate::model::location::ArtefactLocation; #[test] fn run_round_trip() { @@ -164,7 +167,7 @@ mod tests { command_line: None, }], results: Vec::new(), - artifacts: Vec::new(), + artefacts: Vec::new(), }; match serde_json::to_string(&run) { Ok(json) => match serde_json::from_str::(&json) { @@ -188,13 +191,13 @@ mod tests { }, invocations: Vec::new(), results: Vec::new(), - artifacts: Vec::new(), + artefacts: 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("\"artefacts\"")); assert!(!json.contains("\"rules\"")); } Err(e) => panic!("failed to serialize: {e}"), @@ -202,17 +205,17 @@ mod tests { } #[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()), }; - match serde_json::to_string(&artifact) { - Ok(json) => match serde_json::from_str::(&json) { - Ok(parsed) => assert_eq!(artifact, parsed), + match serde_json::to_string(&artefact) { + Ok(json) => match serde_json::from_str::(&json) { + Ok(parsed) => assert_eq!(artefact, parsed), Err(e) => panic!("failed to deserialize: {e}"), }, Err(e) => panic!("failed to serialize: {e}"), diff --git a/crates/whitaker_sarif/tests/sarif_behaviour.rs b/crates/whitaker_sarif/tests/sarif_behaviour.rs index 94aa6bab0..90cbf6ca4 100644 --- a/crates/whitaker_sarif/tests/sarif_behaviour.rs +++ b/crates/whitaker_sarif/tests/sarif_behaviour.rs @@ -23,6 +23,7 @@ use whitaker_sarif::{ model::{descriptor::ReportingDescriptor, run::Run}, token_pass_path, }; +use whitaker_test_macros::allow_fixture_expansion_lints; #[derive(Debug, Default)] struct SarifWorld { @@ -41,6 +42,7 @@ struct SarifWorld { computed_path: RefCell>, } +#[allow_fixture_expansion_lints] #[fixture] fn world() -> SarifWorld { SarifWorld::default() } diff --git a/crates/whitaker_test_macros/Cargo.toml b/crates/whitaker_test_macros/Cargo.toml new file mode 100644 index 000000000..79f0d3bec --- /dev/null +++ b/crates/whitaker_test_macros/Cargo.toml @@ -0,0 +1,17 @@ +[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"] } + +[lints] +workspace = true 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/docs/developers-guide.md b/docs/developers-guide.md index 4f4ed35d4..53d792a50 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/Cargo.toml b/installer/Cargo.toml index 9e654f0aa..c6e4107a1 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 } rustix = { version = "1.1.4", features = ["process"] } mockall = { workspace = true } rstest = { workspace = true } diff --git a/installer/src/artefact/extraction.rs b/installer/src/artefact/extraction.rs index 64afc1878..21e3c5a75 100644 --- a/installer/src/artefact/extraction.rs +++ b/installer/src/artefact/extraction.rs @@ -139,8 +139,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 @@ -186,8 +186,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/packaging_tests.rs b/installer/src/artefact/packaging_tests.rs index fd3f2b297..8e1fd4233 100644 --- a/installer/src/artefact/packaging_tests.rs +++ b/installer/src/artefact/packaging_tests.rs @@ -5,9 +5,11 @@ use tempfile::TempDir; use super::*; +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn temp_dir() -> TempDir { TempDir::new().expect("temp dir creation succeeds") } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn sample_git_sha() -> GitSha { GitSha::try_from("abc1234").expect("valid sha") } @@ -278,8 +280,11 @@ fn packaging_produces_deterministic_digest(temp_dir: TempDir) { let output = package_artefact(¶ms).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" ); } diff --git a/installer/src/artefact/schema_version.rs b/installer/src/artefact/schema_version.rs index e812dfd3f..a3eec1230 100644 --- a/installer/src/artefact/schema_version.rs +++ b/installer/src/artefact/schema_version.rs @@ -94,7 +94,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 496f13292..85eba0f6e 100644 --- a/installer/src/artefact/sha256_digest.rs +++ b/installer/src/artefact/sha256_digest.rs @@ -124,6 +124,7 @@ mod tests { use super::*; + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn valid_digest() -> String { "a".repeat(64) } diff --git a/installer/src/bin/package_dependency_binary.rs b/installer/src/bin/package_dependency_binary.rs index 1ae0dae61..cbecddb30 100644 --- a/installer/src/bin/package_dependency_binary.rs +++ b/installer/src/bin/package_dependency_binary.rs @@ -1,6 +1,6 @@ //! Package dependency binaries and shared provenance assets for release uploads. -use std::path::PathBuf; +use std::{io::Write, path::PathBuf}; use clap::{Parser, Subcommand}; use thiserror::Error; @@ -13,6 +13,7 @@ use whitaker_installer::{ write_provenance_markdown, }, installer_packaging::TargetTriple, + output::write_stderr_line, }; /// Package repository-hosted dependency binaries for release publication. @@ -68,6 +69,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 @@ -75,7 +79,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); } } @@ -93,20 +97,24 @@ fn run(cli: Cli) -> Result<(), CliError> { .map_err(|error| CliError::Manifest(error.to_string()))? .cloned() .ok_or(CliError::UnknownPackage(package))?; - let target = TargetTriple::try_from(target.as_str())?; + let target_triple = TargetTriple::try_from(target.as_str())?; let output = package_dependency_binary(&DependencyPackageParams { dependency, - target, + target: target_triple, binary_path, output_dir, })?; - println!("Created {}", output.archive_path.display()); + writeln!( + std::io::stdout(), + "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()); + writeln!(std::io::stdout(), "Created {}", output.display())?; } } Ok(()) diff --git a/installer/src/bin/package_installer_bin.rs b/installer/src/bin/package_installer_bin.rs index 1915950b2..feb2a5426 100644 --- a/installer/src/bin/package_installer_bin.rs +++ b/installer/src/bin/package_installer_bin.rs @@ -4,13 +4,14 @@ //! [`whitaker_installer::installer_packaging::package_installer`] invoked //! by the release CI workflow to create binstall-compatible archives. -use std::path::PathBuf; +use std::{io::Write, path::PathBuf}; use clap::Parser; use thiserror::Error; use whitaker_installer::{ artefact::error::ArtefactError, installer_packaging::{InstallerPackagingError, TargetTriple, Version, package_installer}, + output::write_stderr_line, }; /// Package the `whitaker-installer` binary into a release archive. @@ -48,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); } } @@ -68,7 +73,11 @@ fn run(cli: Cli) -> Result<(), CliError> { }; let output = package_installer(¶ms)?; - println!("Created {}", output.archive_path.display()); + writeln!( + std::io::stdout(), + "Created {}", + output.archive_path.display() + )?; Ok(()) } diff --git a/installer/src/bin/package_lints.rs b/installer/src/bin/package_lints.rs index e326a796f..cc8e12cb2 100644 --- a/installer/src/bin/package_lints.rs +++ b/installer/src/bin/package_lints.rs @@ -5,6 +5,7 @@ //! rolling-release CI workflow. use std::{ + io::Write, path::{Path, PathBuf}, time::{SystemTime, UNIX_EPOCH}, }; @@ -21,6 +22,7 @@ use whitaker_installer::{ target::TargetTriple, toolchain_channel::ToolchainChannel, }, + output::write_stderr_line, resolution::{LINT_CRATES, SUITE_CRATE}, }; @@ -87,12 +89,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); } } @@ -124,6 +130,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, @@ -135,12 +142,12 @@ fn run(cli: PackageCli) -> Result<(), PackageCliError> { 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(()) } @@ -179,32 +186,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(()) -} - -const 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`). @@ -218,10 +230,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") } @@ -229,22 +241,19 @@ 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. -const 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)] diff --git a/installer/src/binstall_metadata_tests.rs b/installer/src/binstall_metadata_tests.rs index 89c97d9ec..2adf60e1d 100644 --- a/installer/src/binstall_metadata_tests.rs +++ b/installer/src/binstall_metadata_tests.rs @@ -100,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)); @@ -111,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.rs b/installer/src/cli.rs index efd25bacf..a2807936b 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. @@ -176,7 +206,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 @@ -197,25 +227,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, } } diff --git a/installer/src/cli_tests.rs b/installer/src/cli_tests.rs index 17f2abf9d..3031d87ed 100644 --- a/installer/src/cli_tests.rs +++ b/installer/src/cli_tests.rs @@ -8,17 +8,32 @@ use super::*; fn cli_parses_defaults() { let cli = Cli::parse_from(["whitaker-installer"]); assert!(cli.command.is_none()); + assert_default_lint_selection(&cli); + assert_default_execution_options(&cli); + assert_default_skip_flags(&cli); +} + +/// Assert that no target directory or lint selection is present by default. +fn assert_default_lint_selection(cli: &Cli) { 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!(!cli.install.lint_selection.individual_lints); + assert!(!cli.install.lint_selection.experimental); +} + +/// Assert that execution options default to a full, non-quiet build. +fn assert_default_execution_options(cli: &Cli) { + assert!(!cli.install.execution.cranelift); + assert!(!cli.install.execution.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 that no pipeline steps are skipped by default. +fn assert_default_skip_flags(cli: &Cli) { + assert!(!cli.install.skip.skip_deps); + assert!(!cli.install.skip.skip_wrapper); + assert!(!cli.install.skip.no_update); assert!(!cli.install.is_build_only); } @@ -86,7 +101,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"), @@ -113,7 +128,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")]; @@ -129,15 +147,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); @@ -166,10 +184,10 @@ 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!(!args.lint_selection.individual_lints); + assert!(!args.lint_selection.experimental); + assert!(!args.execution.cranelift); + assert!(!args.skip.skip_deps); } #[test] @@ -183,12 +201,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/dependency_binaries/install/checksum.rs b/installer/src/dependency_binaries/install/checksum.rs index 0e46b9676..ad49c19bc 100644 --- a/installer/src/dependency_binaries/install/checksum.rs +++ b/installer/src/dependency_binaries/install/checksum.rs @@ -263,8 +263,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 40fc4b19b..bd6990df7 100644 --- a/installer/src/dependency_binaries/install/downloader.rs +++ b/installer/src/dependency_binaries/install/downloader.rs @@ -348,9 +348,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 04f582c77..cd03c3f23 100644 --- a/installer/src/dependency_binaries/install/http_test_server.rs +++ b/installer/src/dependency_binaries/install/http_test_server.rs @@ -66,9 +66,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}"), @@ -88,8 +90,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. } } } @@ -123,9 +128,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; }; @@ -142,19 +155,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!( @@ -165,7 +183,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/dependency_packaging_tests.rs b/installer/src/dependency_packaging_tests.rs index ca860d47e..b38882deb 100644 --- a/installer/src/dependency_packaging_tests.rs +++ b/installer/src/dependency_packaging_tests.rs @@ -36,6 +36,7 @@ fn windows_target() -> TargetTriple { TargetTriple::try_from("x86_64-pc-windows-msvc").expect("valid target") } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn temp_dir() -> tempfile::TempDir { tempfile::tempdir().expect("temp dir") } 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/list_output.rs b/installer/src/list_output.rs index c5d5e9bbc..c5fb4275c 100644 --- a/installer/src/list_output.rs +++ b/installer/src/list_output.rs @@ -57,10 +57,15 @@ fn format_toolchain_section( /// 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() - .map(|lint| format!(" - {lint}\n")) - .collect(); + 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) } diff --git a/installer/src/main.rs b/installer/src/main.rs index 06920105d..36ed61fcb 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -60,7 +60,7 @@ 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. const fn resolve_additional_components(args: &InstallArgs) -> &'static [&'static str] { - if args.cranelift { + if args.execution.cranelift { &["rustc-codegen-cranelift"] } else { &[] @@ -107,12 +107,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) @@ -153,7 +153,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 @@ -186,9 +186,9 @@ fn run_dry(args: &InstallArgs, dirs: &dyn BaseDirs, stderr: &mut dyn Write) -> R verbosity: args.verbosity, quiet: args.quiet, skips: DryRunSkips { - deps: args.skip_deps, - wrapper: args.skip_wrapper, - update: args.no_update, + deps: args.skip.skip_deps, + wrapper: args.skip.skip_wrapper, + update: args.skip.no_update, }, jobs: args.jobs, crates: &requested_crates, @@ -240,10 +240,10 @@ fn ensure_whitaker_workspace( .and_then(|p| Utf8PathBuf::try_from(p).ok()); let Some(cwd) = cwd else { - return ensure_workspace(dirs, !args.no_update); + 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}...")); } @@ -254,7 +254,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. @@ -292,7 +292,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 { @@ -344,8 +344,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 dd80ba91c..6c30243ff 100644 --- a/installer/src/output.rs +++ b/installer/src/output.rs @@ -187,6 +187,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) } @@ -221,7 +222,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/installer/src/pipeline_staging_tests.rs b/installer/src/pipeline_staging_tests.rs index 076b51390..0e3a8abbb 100644 --- a/installer/src/pipeline_staging_tests.rs +++ b/installer/src/pipeline_staging_tests.rs @@ -90,6 +90,7 @@ fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> BuildResult { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn staging_ctx() -> StagingTestContext { StagingTestContext::new() } diff --git a/installer/src/pipeline_tests.rs b/installer/src/pipeline_tests.rs index da5952a07..ed7060e18 100644 --- a/installer/src/pipeline_tests.rs +++ b/installer/src/pipeline_tests.rs @@ -90,6 +90,7 @@ impl PipelineContextProvider for TestContext { } /// 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() } diff --git a/installer/src/test_utils/dependency_binary_helpers.rs b/installer/src/test_utils/dependency_binary_helpers.rs index 916a6eb87..5a248cf61 100644 --- a/installer/src/test_utils/dependency_binary_helpers.rs +++ b/installer/src/test_utils/dependency_binary_helpers.rs @@ -157,6 +157,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. @@ -165,10 +179,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). @@ -450,14 +462,15 @@ pub fn cargo_fallback_calls(tool: &str, config: &ExpectedCallConfig<'_>) -> Vec< 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)); diff --git a/installer/src/test_utils/dependency_binary_helpers_tests.rs b/installer/src/test_utils/dependency_binary_helpers_tests.rs index 745abea0f..f242f4f49 100644 --- a/installer/src/test_utils/dependency_binary_helpers_tests.rs +++ b/installer/src/test_utils/dependency_binary_helpers_tests.rs @@ -2,6 +2,7 @@ use crate::test_utils::dependency_binary_helpers::{ ExpectedCallConfig, + RepositoryVerification, dependency_version, expected_calls, repository_verification_call, @@ -42,8 +43,7 @@ fn expected_calls_include_repository_probe_for_cargo_dylint() { 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, }, @@ -62,8 +62,7 @@ fn expected_calls_omit_executor_verification_for_dylint_link() { 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, }, diff --git a/installer/src/test_utils.rs b/installer/src/test_utils/mod.rs similarity index 100% rename from installer/src/test_utils.rs rename to installer/src/test_utils/mod.rs diff --git a/installer/src/tests.rs b/installer/src/tests.rs index 98d681866..1c84f8be4 100644 --- a/installer/src/tests.rs +++ b/installer/src/tests.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, @@ -71,7 +71,13 @@ 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 { + lint_selection: LintSelectionFlags { + individual_lints: true, + ..LintSelectionFlags::default() + }, + ..InstallArgs::default() + }, true, false )] diff --git a/installer/src/tests/fast_path.rs b/installer/src/tests/fast_path.rs index 666d4a799..cbf37573b 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, @@ -49,9 +50,12 @@ 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, + execution: ExecutionFlags { + cranelift, + ..ExecutionFlags::default() + }, ..InstallArgs::default() }; diff --git a/installer/tests/behaviour_artefact.rs b/installer/tests/behaviour_artefact.rs index 4dfdb4cb7..9d035399e 100644 --- a/installer/tests/behaviour_artefact.rs +++ b/installer/tests/behaviour_artefact.rs @@ -3,7 +3,7 @@ //! //! 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}; @@ -38,19 +38,38 @@ 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 {field_name} validation to fail"); - assert!( - predicate(error.as_ref().expect("checked above")), - "error variant mismatch for {field_name}" - ); + let error = error.ok_or_else(|| format!("expected {field_name} validation to fail"))?; + if predicate(error) { + 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:?}")) + } } // --------------------------------------------------------------------------- @@ -58,32 +77,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}\"")] @@ -92,10 +128,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")] @@ -107,8 +143,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}\"")] @@ -117,10 +153,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")] @@ -129,47 +165,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")] @@ -178,9 +238,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")] @@ -189,11 +256,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 704385c77..0c6a02c9a 100644 --- a/installer/tests/behaviour_artefact_packaging.rs +++ b/installer/tests/behaviour_artefact_packaging.rs @@ -2,7 +2,7 @@ //! //! 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}; @@ -42,33 +42,49 @@ 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() +fn world() -> PackagingWorld { PackagingWorld::default() } + +/// 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() + .map(|dir| dir.path().to_path_buf()) + .ok_or_else(|| String::from("temp_dir set")) } -/// Return the temp directory path, creating one if needed. -fn temp_path(world: &PackagingWorld) -> PathBuf { +/// Fetch the packaging output, failing if packaging has not run successfully. +fn output_ref(world: &PackagingWorld) -> Result<&PackageOutput, String> { world - .temp_dir + .output .as_ref() - .expect("temp_dir set") - .path() - .to_path_buf() + .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"), @@ -78,6 +94,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(()) } // --------------------------------------------------------------------------- @@ -85,52 +124,55 @@ 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() @@ -138,180 +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 entry = entry.map_err(|e| format!("read archive entry: {e}"))?; + let path = 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 0f92dfe9c..75afaddd5 100644 --- a/installer/tests/behaviour_binstall.rs +++ b/installer/tests/behaviour_binstall.rs @@ -35,17 +35,36 @@ 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")) +} + // --------------------------------------------------------------------------- // Step definitions // --------------------------------------------------------------------------- #[given("the installer Cargo.toml is loaded")] -fn given_cargo_toml_loaded(world: &mut BinstallWorld) { - let table = load_cargo_toml().expect("load installer Cargo.toml"); - world.binstall_table = Some(extract_binstall_table(&table).expect("extract binstall table")); +fn given_cargo_toml_loaded(world: &mut BinstallWorld) -> Result<(), String> { + let table = load_cargo_toml().map_err(|e| format!("load installer Cargo.toml: {e}"))?; + world.binstall_table = + Some(extract_binstall_table(&table).map_err(|e| format!("extract binstall table: {e}"))?); + Ok(()) } #[given("target \"{target}\" and version \"{version}\"")] @@ -65,13 +84,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")] @@ -85,48 +104,62 @@ 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> { + let binstall = binstall_table(world)?; + let pkg_url = table_str(binstall, "pkg-url")?; + if pkg_url != PKG_URL_TEMPLATE { + return Err(format!( + "pkg-url mismatch: expected {PKG_URL_TEMPLATE}, got {pkg_url}" + )); + } + Ok(()) } #[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> { + let binstall = binstall_table(world)?; + let bin_dir = table_str(binstall, "bin-dir")?; + if bin_dir != BIN_DIR_TEMPLATE { + return Err(format!( + "bin-dir mismatch: expected {BIN_DIR_TEMPLATE}, got {bin_dir}" + )); + } + Ok(()) } #[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> { + let binstall = binstall_table(world)?; + let pkg_fmt = table_str(binstall, "pkg-fmt")?; + if pkg_fmt != expected { + return Err(format!( + "pkg-fmt mismatch: expected {expected}, got {pkg_fmt}" + )); + } + Ok(()) } #[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}\"")] @@ -158,24 +191,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/support.rs b/installer/tests/behaviour_cli/support.rs index 986310363..b03853ea2 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -29,14 +29,16 @@ 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 { @@ -64,10 +66,6 @@ pub(super) fn is_toolchain_installed(channel: &str) -> bool { 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 \ @@ -92,7 +90,8 @@ 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 temp_dir = + TempDir::new().unwrap_or_else(|error| panic!("failed to create temp dir: {error}")); let target_dir = temp_dir.path().to_string_lossy().to_string(); cli_world.temp_dir.replace(Some(temp_dir)); target_dir @@ -127,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)) @@ -211,13 +210,20 @@ 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 output = command + .output() + .unwrap_or_else(|error| panic!("failed to run whitaker-installer: {error}")); 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")) + Ref::map(output, |opt| { + let Some(output) = opt.as_ref() else { + panic!("output not set"); + }; + output + }) } fn assert_exit_status(cli_world: &CliWorld, expected_success: bool) { @@ -244,27 +250,46 @@ pub(super) fn assert_dry_run_output_is_shown(cli_world: &CliWorld) { return; } - let toolchain = cli_world.toolchain.borrow(); - let toolchain = toolchain.as_ref().expect("toolchain not set"); + 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")); - assert!(stderr.contains(&format!("Toolchain: {toolchain}"))); - assert!(stderr.contains("Crates to build:")); - assert!(stderr.contains("whitaker_suite")); + 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 = cli_world.temp_dir.borrow(); - let temp_dir = temp_dir.as_ref().expect("temp dir not set"); + 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}"))); + assert!( + stderr.contains(&format!("Target directory: {expected_target_dir}")), + "expected target directory line in stderr: {stderr}" + ); } pub(super) fn assert_cli_exits_with_error(cli_world: &CliWorld) { @@ -329,9 +354,18 @@ pub(super) fn assert_experimental_lint_dry_run_output_is_shown(cli_world: &CliWo 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("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" @@ -361,8 +395,10 @@ pub(super) fn assert_suite_library_is_staged(cli_world: &CliWorld) { 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 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) @@ -372,22 +408,26 @@ pub(super) fn assert_suite_library_is_staged(cli_world: &CliWorld) { 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={:?}", + "prebuilt marker found in stderr but no library matching '{needle}' in {}, \ + entries={:?}", + prebuilt_path.display(), 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 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 {staging_dir:?}, \ - matches={matches:?}, entries={:?}, stdout={}, stderr={stderr}", + "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 157946601..61e6be471 100644 --- a/installer/tests/behaviour_core.rs +++ b/installer/tests/behaviour_core.rs @@ -34,6 +34,7 @@ struct CrateResolutionWorld { resolved: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn crate_world() -> CrateResolutionWorld { CrateResolutionWorld::default() } @@ -139,6 +140,7 @@ struct ValidationWorld { error_message: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn validation_world() -> ValidationWorld { ValidationWorld::default() } @@ -200,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" @@ -221,6 +225,7 @@ struct ToolchainWorld { error: Cell, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn toolchain_world() -> ToolchainWorld { ToolchainWorld::default() } @@ -287,6 +292,7 @@ struct SnippetWorld { snippet: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn snippet_world() -> SnippetWorld { SnippetWorld::default() } @@ -305,25 +311,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 ab25c8875..2c33fcd13 100644 --- a/installer/tests/behaviour_dependency_binaries.rs +++ b/installer/tests/behaviour_dependency_binaries.rs @@ -22,6 +22,7 @@ use whitaker_installer::{ StubExecutor, dependency_binary_helpers::{ ExpectedCallConfig, + RepositoryVerification, expected_calls, path_binary_location, write_fake_binary, @@ -95,6 +96,7 @@ struct DependencyBinaryWorld { dependencies: Vec, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> DependencyBinaryWorld { DependencyBinaryWorld::default() } @@ -169,14 +171,21 @@ fn build_stub_executor(world: &DependencyBinaryWorld, tool: &str) -> StubExecuto world.repository_behaviour, Some(RepositoryInstallerBehaviour::Success) ) && !world.is_unsupported_target; + let repository_verification = match ( + expect_repository_verification, + world.should_repository_verification_fail, + ) { + (false, _) => RepositoryVerification::Skip, + (true, true) => RepositoryVerification::Fails, + (true, false) => RepositoryVerification::Succeeds, + }; StubExecutor::new(expected_calls( tool, &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(), }, diff --git a/installer/tests/behaviour_docs.rs b/installer/tests/behaviour_docs.rs index 62052d816..682e90426 100644 --- a/installer/tests/behaviour_docs.rs +++ b/installer/tests/behaviour_docs.rs @@ -25,6 +25,7 @@ struct TomlWorld { error: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn toml_world() -> TomlWorld { TomlWorld::default() } diff --git a/installer/tests/behaviour_install_metrics.rs b/installer/tests/behaviour_install_metrics.rs index 3c3ad8ec7..7140bda82 100644 --- a/installer/tests/behaviour_install_metrics.rs +++ b/installer/tests/behaviour_install_metrics.rs @@ -22,6 +22,7 @@ struct InstallMetricsWorld { summary_line: Option, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> InstallMetricsWorld { InstallMetricsWorld::default() } diff --git a/installer/tests/behaviour_installer_release.rs b/installer/tests/behaviour_installer_release.rs index 9864e5e87..88b3aa294 100644 --- a/installer/tests/behaviour_installer_release.rs +++ b/installer/tests/behaviour_installer_release.rs @@ -37,6 +37,7 @@ struct InstallerReleaseWorld { packaging_error: Option, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> InstallerReleaseWorld { InstallerReleaseWorld::default() } diff --git a/installer/tests/behaviour_prebuilt.rs b/installer/tests/behaviour_prebuilt.rs index 70249486a..9ad5de436 100644 --- a/installer/tests/behaviour_prebuilt.rs +++ b/installer/tests/behaviour_prebuilt.rs @@ -256,8 +256,8 @@ fn when_prebuilt_attempted(world: &mut PrebuiltWorld) { 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 06da5d6d8..ca4e0e5aa 100644 --- a/installer/tests/behaviour_staging.rs +++ b/installer/tests/behaviour_staging.rs @@ -24,6 +24,7 @@ struct StagingWorld { staged_name: RefCell, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn staging_world() -> StagingWorld { StagingWorld::default() } @@ -101,6 +102,7 @@ mod staging_failure { } } + #[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] pub fn staging_failure_world() -> StagingFailureWorld { StagingFailureWorld::default() } diff --git a/installer/tests/behaviour_workflows.rs b/installer/tests/behaviour_workflows.rs index 96d15c1b2..0d274c600 100644 --- a/installer/tests/behaviour_workflows.rs +++ b/installer/tests/behaviour_workflows.rs @@ -24,6 +24,7 @@ struct WorkflowWorld { _temp_dir: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> WorkflowWorld { WorkflowWorld::default() } diff --git a/installer/tests/support/mod.rs b/installer/tests/support/mod.rs index a328ec2e7..caa50f1b4 100644 --- a/installer/tests/support/mod.rs +++ b/installer/tests/support/mod.rs @@ -84,11 +84,11 @@ 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"); + .expect("failed to initialize isolated rustup environment"); assert!( init_output.status.success(), - "failed to initialise isolated rustup: {}", + "failed to initialize isolated rustup: {}", String::from_utf8_lossy(&init_output.stderr) ); diff --git a/installer/tests/toolchain_steps/mod.rs b/installer/tests/toolchain_steps/mod.rs index b33ecfd1d..d47ac526b 100644 --- a/installer/tests/toolchain_steps/mod.rs +++ b/installer/tests/toolchain_steps/mod.rs @@ -158,6 +158,7 @@ fn assert_toolchain_installed_in_isolated_env(world: &ToolchainWorld) { // Fixture // --------------------------------------------------------------------------- +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] pub fn world() -> ToolchainWorld { ToolchainWorld::default() } diff --git a/src/config.rs b/src/config.rs index a0aaea372..0314448f1 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 { /// 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> { normalise_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. @@ -131,7 +131,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. @@ -142,7 +142,7 @@ mod tests { } #[rstest] - fn deserialises_locale_override() { + fn deserializes_locale_override() { let source = "locale = \"cy\"\n"; let config = toml::from_str::(source) @@ -162,7 +162,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 6f660a11d..1a2b1badd 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/suite/Cargo.toml b/suite/Cargo.toml index 535b61dc5..aada2f3e7 100644 --- a/suite/Cargo.toml +++ b/suite/Cargo.toml @@ -50,6 +50,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 11ab7bf54..d03f88b93 100644 --- a/suite/tests/registration.rs +++ b/suite/tests/registration.rs @@ -1,6 +1,6 @@ +//! 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, @@ -31,6 +31,7 @@ impl RegistrationWorld { } } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> RegistrationWorld { RegistrationWorld::new() } diff --git a/tests/config_loading.rs b/tests/config_loading.rs index a025c30d4..a23088ed0 100644 --- a/tests/config_loading.rs +++ b/tests/config_loading.rs @@ -14,11 +14,13 @@ 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; +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn config_source() -> RefCell> { RefCell::new(None) } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn load_result() -> RefCell>> { RefCell::new(None) } @@ -146,7 +148,7 @@ fn assert_locale( expected: StepLocale, ) { let raw = expected.into_inner(); - let expected_value = normalise_locale(Some(raw.as_str())) + let expected_value = normalize_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() { diff --git a/tests/locale_resolution.rs b/tests/locale_resolution.rs index 5a3af2ebd..aacafb62b 100644 --- a/tests/locale_resolution.rs +++ b/tests/locale_resolution.rs @@ -7,7 +7,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 { @@ -17,6 +17,7 @@ struct LocaleWorld { resolution: RefCell>, } +#[whitaker_test_macros::allow_fixture_expansion_lints] #[fixture] fn world() -> LocaleWorld { LocaleWorld::default() } @@ -94,7 +95,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 4cb7cb218..dbbeae4e1 100644 --- a/tests/ui_harness.rs +++ b/tests/ui_harness.rs @@ -49,6 +49,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 36ac75fbc..876a0bfb3 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -12,90 +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!\\(location\\.physical_location\\.artifact_location\\.uri, \"src/a\\.rs\"\\);$", - "(?m)^ assert_eq!\\(related\\.physical_location\\.artifact_location\\.uri, \"src/b\\.rs\"\\);$", - "(?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 \\->$", @@ -107,154 +38,30 @@ 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$", "(?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)^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 5e9f3dd85..5a0c01a2d 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,108 +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!\\(location\\.physical_location\\.artifact_location\\.uri, \"src/a\\.rs\"\\);$", - "(?m)^ assert_eq!\\(normalised, expected\\);$", - "(?m)^ assert_eq!\\(related\\.physical_location\\.artifact_location\\.uri, \"src/b\\.rs\"\\);$", - "(?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$", @@ -236,47 +77,9 @@ extend-ignore-re = [ "(?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)^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 49b1fdfc6a41ac7b39a2267c5d24af715d0b1ec6 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 11:05:34 +0200 Subject: [PATCH 05/21] Move installer self-named modules to mod.rs Satisfy `self_named_module_files` for `install_flow`, `tests`, `toolchain`, and `toolchain::tests::failure_mocks`. Co-Authored-By: Claude Opus 5 (1M context) --- installer/src/{install_flow.rs => install_flow/mod.rs} | 0 installer/src/{tests.rs => tests/mod.rs} | 0 installer/src/{toolchain.rs => toolchain/mod.rs} | 0 .../toolchain/tests/{failure_mocks.rs => failure_mocks/mod.rs} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename installer/src/{install_flow.rs => install_flow/mod.rs} (100%) rename installer/src/{tests.rs => tests/mod.rs} (100%) rename installer/src/{toolchain.rs => toolchain/mod.rs} (100%) rename installer/src/toolchain/tests/{failure_mocks.rs => failure_mocks/mod.rs} (100%) 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/tests.rs b/installer/src/tests/mod.rs similarity index 100% rename from installer/src/tests.rs rename to installer/src/tests/mod.rs 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/toolchain/tests/failure_mocks.rs b/installer/src/toolchain/tests/failure_mocks/mod.rs similarity index 100% rename from installer/src/toolchain/tests/failure_mocks.rs rename to installer/src/toolchain/tests/failure_mocks/mod.rs From 976bca9e41cbd97573512eb17130a870862f3744 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 11:10:03 +0200 Subject: [PATCH 06/21] Fix installer lint findings in toolchain, install-flow, and workspace Rename shadowed bindings, borrow needlessly-owned parameters, replace slice indexing with slice patterns, drop the vestigial `Result` around prebuilt installation, and extract wrapper-script assertion helpers. Co-Authored-By: Claude Opus 5 (1M context) --- installer/src/install_flow/mod.rs | 34 +++++----- installer/src/install_flow/tests.rs | 33 +++++----- installer/src/main.rs | 14 ++-- .../src/toolchain/tests/failure_mocks/mod.rs | 38 ++++++----- installer/src/toolchain/tests/mod.rs | 21 +++--- installer/src/toolchain/tests/test_helpers.rs | 66 ++++++++++--------- installer/src/workspace.rs | 15 +++-- installer/src/wrapper.rs | 52 +++++++++------ 8 files changed, 148 insertions(+), 125 deletions(-) diff --git a/installer/src/install_flow/mod.rs b/installer/src/install_flow/mod.rs index 5ad0b86b0..595346dff 100644 --- a/installer/src/install_flow/mod.rs +++ b/installer/src/install_flow/mod.rs @@ -52,8 +52,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) }) } @@ -64,8 +64,8 @@ pub(crate) fn ensure_dylint_tools_with_options( 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) }) } @@ -108,14 +108,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, @@ -129,6 +132,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, @@ -139,27 +143,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; } }; @@ -168,7 +172,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; } }; @@ -181,7 +185,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, @@ -189,9 +193,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 058bfe83f..8b0ca523a 100644 --- a/installer/src/install_flow/tests.rs +++ b/installer/src/install_flow/tests.rs @@ -28,15 +28,12 @@ impl BaseDirs for TestBaseDirs { 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 { @@ -102,9 +99,9 @@ fn prune_prebuilt_libraries_keeps_only_requested_crates( #[case] removed: &[&str], ) { let StagingFixture { - _temp_dir: _, staging_path, toolchain, + .. } = staging_fixture; let foreign_path = staging_path.join("libforeign_lint@nightly-2026-05-28.so"); @@ -176,29 +173,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/main.rs b/installer/src/main.rs index 36ed61fcb..c9688ed60 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -81,7 +81,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( @@ -235,11 +235,11 @@ 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 { + let Some(cwd) = utf8_cwd else { return ensure_workspace(dirs, !args.skip.no_update); }; @@ -262,10 +262,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( diff --git a/installer/src/toolchain/tests/failure_mocks/mod.rs b/installer/src/toolchain/tests/failure_mocks/mod.rs index 980a927c4..db6b56f6b 100644 --- a/installer/src/toolchain/tests/failure_mocks/mod.rs +++ b/installer/src/toolchain/tests/failure_mocks/mod.rs @@ -64,7 +64,7 @@ fn setup_toolchain_install_failure_mocks( expect_toolchain_install( runner, seq, - ToolchainInstallExpectation { + &ToolchainInstallExpectation { channel, exit_code: 1, stderr: Some(TOOLCHAIN_INSTALL_FAILURE_MESSAGE), @@ -99,7 +99,7 @@ fn setup_toolchain_unusable_failure_mocks( expect_toolchain_install( runner, seq, - ToolchainInstallExpectation { + &ToolchainInstallExpectation { channel, exit_code: 0, stderr: None, @@ -134,16 +134,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, ); } @@ -151,7 +151,7 @@ pub(super) fn setup_failure_mocks( setup_toolchain_unusable_failure_mocks( runner, seq, - channel, + channel_name, setup.additional_components, ); } @@ -223,39 +223,43 @@ fn is_component_install_failed( /// 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 bc3e53a6a..7a07ce3ee 100644 --- a/installer/src/toolchain/tests/mod.rs +++ b/installer/src/toolchain/tests/mod.rs @@ -81,7 +81,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, @@ -191,11 +191,11 @@ fn install_components_with_failure_reports_all_components() { matches!( err, InstallerError::ToolchainComponentInstallFailed { - ref toolchain, - ref components, + toolchain: ref failed_toolchain, + components: ref failed_components, ref message, - } if toolchain == "nightly-2026-05-28" - && components == &expected_component_list + } if failed_toolchain == "nightly-2026-05-28" + && failed_components == &expected_component_list && message == COMPONENT_INSTALL_FAILURE_MESSAGE ), "expected ToolchainComponentInstallFailed with all components, got {err:?}" @@ -226,10 +226,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), ); } @@ -239,10 +239,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 9686a903b..d0f88d5ab 100644 --- a/installer/src/toolchain/tests/test_helpers.rs +++ b/installer/src/toolchain/tests/test_helpers.rs @@ -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/src/workspace.rs b/installer/src/workspace.rs index f094ed148..e2a01340f 100644 --- a/installer/src/workspace.rs +++ b/installer/src/workspace.rs @@ -326,8 +326,7 @@ mod tests { 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] @@ -337,8 +336,7 @@ mod tests { 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:?}" @@ -372,7 +370,7 @@ mod tests { 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(), + find_workspace_root(&temp_workspace.path).expect("workspace root should be found"), temp_workspace.path ); } @@ -382,7 +380,10 @@ mod tests { 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); + assert_eq!( + find_workspace_root(&subdir).expect("workspace root should be found"), + temp_workspace.path + ); } #[rstest] @@ -390,7 +391,7 @@ mod tests { write_cargo_toml(&temp_workspace.path, "not_a_workspace"); 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 3c1ad2d15..8692140c9 100644 --- a/installer/src/wrapper.rs +++ b/installer/src/wrapper.rs @@ -226,11 +226,33 @@ mod tests { assert!(!is_directory_in_path(temp.path())); } + /// Asserts that every user, group, and other execute bit is set on `path`. #[cfg(unix)] - #[test] - fn generate_unix_scripts_create_executables() { + fn assert_script_is_executable(path: &Path) { use std::os::unix::fs::PermissionsExt; + let perms = std::fs::metadata(path) + .expect("failed to read metadata") + .permissions(); + assert_eq!(perms.mode() & 0o111, 0o111, "script should be executable"); + } + + /// Asserts that the script at `path` contains every fragment in `fragments`. + #[cfg(unix)] + fn assert_script_contains(path: &Path, fragments: &[&str]) { + let content = std::fs::read_to_string(path).expect("failed to read script"); + 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; let temp = TempDir::new().expect("failed to create temp dir"); @@ -241,23 +263,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] From 224a6076d4f2eda03196cf376cc0323d426ff9bc Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 11:14:13 +0200 Subject: [PATCH 07/21] Clear remaining installer/src lint findings Replace slice indexing with fallible lookups and slice patterns, rename shadowed bindings, name wildcard match variants explicitly, elide a redundant lifetime, inline format arguments, compare archive extensions case-insensitively, and reshape the resolve-crates test case struct to drop its bool cluster. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/dependency_binaries/install/tests.rs | 2 +- installer/src/deps/path_tests.rs | 14 ++- installer/src/deps/tests.rs | 6 +- installer/src/install_metrics_tests.rs | 10 +- installer/src/installer_packaging_tests.rs | 20 +++- installer/src/pipeline_staging_tests.rs | 25 +++-- installer/src/pipeline_tests.rs | 11 ++- installer/src/prebuilt_tests.rs | 23 ++--- installer/src/resolution.rs | 95 +++++++++++-------- installer/src/scanner.rs | 5 +- installer/src/staged_suite.rs | 8 +- .../dependency_binary_helpers_tests.rs | 17 ++-- installer/src/tests/fast_path.rs | 6 +- installer/src/tests/mod.rs | 36 +++---- 14 files changed, 156 insertions(+), 122 deletions(-) diff --git a/installer/src/dependency_binaries/install/tests.rs b/installer/src/dependency_binaries/install/tests.rs index ff206f6ad..6c17f4608 100644 --- a/installer/src/dependency_binaries/install/tests.rs +++ b/installer/src/dependency_binaries/install/tests.rs @@ -160,7 +160,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/deps/path_tests.rs b/installer/src/deps/path_tests.rs index 1d3bc001f..6b5aa25d8 100644 --- a/installer/src/deps/path_tests.rs +++ b/installer/src/deps/path_tests.rs @@ -98,10 +98,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.first().ok_or_else(|| { + std::io::Error::other("fake PATH should contain at least one directory") + })?; #[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) }, @@ -154,10 +157,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.get(1).ok_or_else(|| { + std::io::Error::other("fake PATH should contain at least two directories") + })?; #[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) }, diff --git a/installer/src/deps/tests.rs b/installer/src/deps/tests.rs index af32b2c8f..2e12899ea 100644 --- a/installer/src/deps/tests.rs +++ b/installer/src/deps/tests.rs @@ -26,10 +26,10 @@ use crate::{ }, }; -fn install_options<'a>( - repository_installer: &'a dyn DependencyBinaryInstaller, +fn install_options( + repository_installer: &dyn DependencyBinaryInstaller, quiet: bool, -) -> DependencyInstallOptions<'a> { +) -> DependencyInstallOptions<'_> { let dirs = StubDirs { bin_dir: Some(PathBuf::from("/tmp/bin")), }; diff --git a/installer/src/install_metrics_tests.rs b/installer/src/install_metrics_tests.rs index 2421b7631..96ba13c61 100644 --- a/installer/src/install_metrics_tests.rs +++ b/installer/src/install_metrics_tests.rs @@ -184,11 +184,15 @@ fn concurrent_records_do_not_lose_updates(metrics_path_fixture: MetricsPathFixtu 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 a371e2d9c..fdfa918bf 100644 --- a/installer/src/installer_packaging_tests.rs +++ b/installer/src/installer_packaging_tests.rs @@ -11,6 +11,16 @@ use crate::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, @@ -87,7 +97,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"), @@ -193,13 +206,14 @@ fn package_installer_creates_archive( assert!(output.archive_path.exists(), "archive should exist"); assert_eq!(output.archive_name, expected_name); - let entries = if expected_name.ends_with(".tgz") { + let entries = if has_extension(expected_name, "tgz") { read_tgz_entry_paths(&output.archive_path) } else { read_zip_entry_names(&output.archive_path) }; 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] diff --git a/installer/src/pipeline_staging_tests.rs b/installer/src/pipeline_staging_tests.rs index 0e3a8abbb..5980d964c 100644 --- a/installer/src/pipeline_staging_tests.rs +++ b/installer/src/pipeline_staging_tests.rs @@ -114,19 +114,17 @@ fn assert_bumpy_road_lint_in_staging_output(experimental: bool) { #[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(); + 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"); @@ -140,8 +138,8 @@ fn stage_libraries_returns_correct_staging_path(staging_ctx: StagingTestContext) #[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(); + let quiet_ctx = staging_ctx.with_quiet(quiet); + let context = quiet_ctx.pipeline_context(); let build_results = vec![]; let mut stderr = Vec::new(); @@ -153,8 +151,7 @@ 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}" ); } } @@ -163,10 +160,10 @@ fn stage_libraries_respects_quiet_flag(staging_ctx: StagingTestContext, #[case] fn stage_libraries_stages_build_results(staging_ctx: StagingTestContext) { use crate::builder::{library_extension, library_prefix}; - let staging_ctx = staging_ctx.with_quiet(true); - let context = staging_ctx.pipeline_context(); + let quiet_ctx = staging_ctx.with_quiet(true); + let context = quiet_ctx.pipeline_context(); let build_results = vec![create_mock_library( - staging_ctx.target_dir(), + quiet_ctx.target_dir(), "whitaker_suite", )]; let mut stderr = Vec::new(); diff --git a/installer/src/pipeline_tests.rs b/installer/src/pipeline_tests.rs index ed7060e18..75f1a1000 100644 --- a/installer/src/pipeline_tests.rs +++ b/installer/src/pipeline_tests.rs @@ -89,7 +89,8 @@ 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() } @@ -163,7 +164,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![])); @@ -189,7 +193,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 457a61e47..6f36bd29e 100644 --- a/installer/src/prebuilt_tests.rs +++ b/installer/src/prebuilt_tests.rs @@ -51,7 +51,7 @@ fn test_fallback_scenario( "reason: {reason}" ); } - other => panic!("expected Fallback, got {other:?}"), + other @ PrebuiltResult::Success { .. } => panic!("expected Fallback, got {other:?}"), } } @@ -81,7 +81,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:?}"), } } @@ -118,21 +118,11 @@ 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 { + 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)); @@ -142,7 +132,6 @@ fn manifest_validation_errors_return_fallback() { }, expected_reason_substring, ); - eprintln!("manifest validation scenario passed: {case_name}"); } } @@ -215,6 +204,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/resolution.rs b/installer/src/resolution.rs index cdfc25802..fe7ea4217 100644 --- a/installer/src/resolution.rs +++ b/installer/src/resolution.rs @@ -152,49 +152,65 @@ mod tests { use super::*; - /// 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] @@ -225,12 +241,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 0f5863bbb..50c12b3cf 100644 --- a/installer/src/scanner.rs +++ b/installer/src/scanner.rs @@ -391,7 +391,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 23ca5eb7b..f3291f401 100644 --- a/installer/src/staged_suite.rs +++ b/installer/src/staged_suite.rs @@ -76,7 +76,7 @@ mod tests { } impl StagedSuiteSetup { - fn requested_suite_crates(&self) -> 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()) @@ -174,7 +174,7 @@ mod tests { fn staged_suite_installation_writes_placeholder_library_for_suite_requests( staged_suite_setup: StagedSuiteSetup, ) { - let requested_crates = staged_suite_setup.requested_suite_crates(); + let requested_crates = StagedSuiteSetup::requested_suite_crates(); let stager = staged_suite_setup.stager(); with_var(TEST_STAGE_SUITE_ENV, Some("1"), || { @@ -198,7 +198,7 @@ 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 requested_crates = StagedSuiteSetup::requested_suite_crates(); let blocked_path = staged_suite_setup.create_blocked_suite_output(); with_var(TEST_STAGE_SUITE_ENV, Some("1"), || { @@ -222,7 +222,7 @@ mod tests { fn staged_suite_installation_is_disabled_in_release_builds( staged_suite_setup: StagedSuiteSetup, ) { - let requested_crates = staged_suite_setup.requested_suite_crates(); + 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/test_utils/dependency_binary_helpers_tests.rs b/installer/src/test_utils/dependency_binary_helpers_tests.rs index f242f4f49..10d8aa27b 100644 --- a/installer/src/test_utils/dependency_binary_helpers_tests.rs +++ b/installer/src/test_utils/dependency_binary_helpers_tests.rs @@ -10,11 +10,8 @@ use crate::test_utils::dependency_binary_helpers::{ #[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"]); @@ -50,8 +47,9 @@ fn expected_calls_include_repository_probe_for_cargo_dylint() { ); 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] @@ -69,6 +67,7 @@ fn expected_calls_omit_executor_verification_for_dylint_link() { ); 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/tests/fast_path.rs b/installer/src/tests/fast_path.rs index cbf37573b..683f7f970 100644 --- a/installer/src/tests/fast_path.rs +++ b/installer/src/tests/fast_path.rs @@ -37,9 +37,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"), diff --git a/installer/src/tests/mod.rs b/installer/src/tests/mod.rs index 1c84f8be4..22e33e7a6 100644 --- a/installer/src/tests/mod.rs +++ b/installer/src/tests/mod.rs @@ -40,9 +40,9 @@ fn dependency_install_options<'a>( #[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")), } } @@ -242,26 +242,26 @@ 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() } + fn home_dir(&self) -> Option { self.home.clone() } - fn bin_dir(&self) -> Option { self.bin_dir.clone() } + fn bin_dir(&self) -> Option { self.bin.clone() } - fn whitaker_data_dir(&self) -> Option { self.data_dir.clone() } + fn whitaker_data_dir(&self) -> Option { self.data.clone() } } #[test] 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(); @@ -289,9 +289,9 @@ fn write_install_metrics_prints_summary_and_persists_metrics() { #[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(); @@ -311,9 +311,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(); From 28662e6590ad8b3f8d041e88968803981e1ae87b Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 11:29:43 +0200 Subject: [PATCH 08/21] Clear remaining workspace lint findings Bring the lint crates, the root crate, and the installer's directory abstraction into compliance with the hardened policy: - isolate `dylint_linting::impl_late_lint!` in a `mod declaration` per lint crate and re-export only the documented lint surface, so the macro's undocumentable ABI entry point and `get_lints` accessor carry one fulfilled, reasoned `#![expect(missing_docs)]` instead of the policy being disabled crate-wide; - isolate `newt_hype::base_newtype!` likewise for the explicit `Clone` impl it emits on a `Copy` type; - rename the `BaseDirs` trait methods to `home`/`executables`/ `whitaker_data` so the mockall-generated struct no longer shares a uniform field postfix, fixing `struct_field_names` at source; - replace the two `float_cmp` assertions with `to_bits()` comparisons, which is what "the configured value round-tripped unchanged" actually asserts; - add the missing assertion messages in the root crate's harness tests. Scope the Clippy gate to `--lib --bins --tests --benches` rather than `--all-targets`. Every file under a lint crate's `examples/` directory is a `fail_*`/`pass_*` Dylint UI fixture that deliberately contains the anti-patterns the suite detects; a fixture proving `expect_used` fires cannot itself forbid `expect_used`. They live in `examples/` rather than `ui/` because `dylint_testing::Test::example` builds them with each crate's dev-dependencies. `typecheck` still compiles them and the UI harness still lints them. --- AGENTS.md | 10 +- Makefile | 11 +- crates/bumpy_road_function/src/analysis.rs | 5 + crates/bumpy_road_function/src/driver/mod.rs | 30 +- .../src/driver/segment_builder.rs | 4 + crates/bumpy_road_function/src/lib.rs | 7 +- .../tests/analysis_behaviour.rs | 30 +- crates/bumpy_road_function/tests/ui.rs | 17 +- .../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 | 13 +- .../src/tests/order_detection.rs | 3 +- 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 +++- .../src/dependency_rlib_tests.rs | 11 +- .../no_expect_outside_tests/src/driver/mod.rs | 32 +- .../src/driver/tests.rs | 6 +- crates/no_expect_outside_tests/src/lib.rs | 7 +- .../src/lib_ui_tests.rs | 14 +- .../src/tests/localization.rs | 39 +- 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 +- .../tests/integration_exclusion.rs | 23 +- .../tests/test_support/mod.rs | 72 ++-- .../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/lib_ui_tests.rs | 19 +- .../src/panic_detector.rs | 6 +- crates/no_unwrap_or_else_panic/src/policy.rs | 18 +- .../src/tests/behaviour.rs | 6 +- .../src/collector_tests.rs | 4 +- .../src/driver.rs | 53 ++- .../src/driver_tests.rs | 8 +- .../src/lib.rs | 7 +- .../src/visitor.rs | 12 +- .../tests/ui.rs | 24 +- .../tests/ui/harness_lock.rs | 60 +-- .../tests/ui/lock_model.rs | 14 +- .../test_must_not_have_example/src/driver.rs | 62 ++- .../src/heuristics.rs | 5 +- crates/test_must_not_have_example/src/lib.rs | 7 +- docs/whitaker-dylint-suite-design.md | 2 +- .../dependency_binaries/install/installer.rs | 2 +- .../src/dependency_binaries/install/tests.rs | 2 +- installer/src/dirs.rs | 27 +- installer/src/install_flow/tests.rs | 6 +- installer/src/install_metrics.rs | 2 +- installer/src/list.rs | 2 +- installer/src/prebuilt_path.rs | 8 +- installer/src/test_utils/mod.rs | 8 +- installer/src/tests/fast_path.rs | 2 +- installer/src/tests/mod.rs | 6 +- installer/src/workspace.rs | 4 +- installer/src/wrapper.rs | 2 +- installer/tests/behaviour_artefact.rs | 4 +- .../tests/behaviour_artefact_packaging.rs | 4 +- installer/tests/behaviour_cli/support.rs | 4 +- .../tests/behaviour_dependency_binaries.rs | 210 ++++++---- installer/tests/behaviour_docs.rs | 218 ++++++----- installer/tests/behaviour_install_metrics.rs | 213 +++++++---- .../tests/behaviour_installer_release.rs | 134 ++++--- installer/tests/behaviour_prebuilt.rs | 157 +++++--- installer/tests/behaviour_staging.rs | 84 ++-- installer/tests/behaviour_toolchain.rs | 5 +- installer/tests/behaviour_workflows.rs | 203 ++++++---- installer/tests/doc_extraction/extraction.rs | 17 +- installer/tests/support/mod.rs | 123 +++--- installer/tests/toolchain_steps/mod.rs | 358 +++++++----------- .../tests/toolchain_steps/scenario_setup.rs | 176 +++++++++ suite/tests/registration.rs | 16 +- tests/config_loading.rs | 5 +- tests/ui_harness.rs | 11 +- 85 files changed, 2004 insertions(+), 1213 deletions(-) create mode 100644 installer/tests/toolchain_steps/scenario_setup.rs diff --git a/AGENTS.md b/AGENTS.md index 649329cb1..400cf9db1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,13 +147,17 @@ project: ```sh RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc --workspace --no-deps - cargo clippy --workspace --all-targets --all-features -- -D warnings + cargo clippy --workspace --lib --bins --tests --benches --all-features \ + -- -D warnings whitaker --all -- -p whitaker-common -p whitaker-installer \ -p whitaker_clones_core -p whitaker_sarif --all-targets --all-features ``` - building rustdoc with warnings denied, linting every target with all - features enabled and denying all Clippy warnings, and running the + building rustdoc with warnings denied, linting every target except the + Dylint UI fixtures under `examples/` (which deliberately contain the + anti-patterns the suite detects, so the workspace policy cannot apply to + them) with all features enabled and denying all Clippy warnings, and + running the Whitaker Dylint suite over the support crates (install via `cargo install whitaker-installer && whitaker-installer`). `make lint-clippy` runs the rustdoc and Clippy subset; `make lint-whitaker` diff --git a/Makefile b/Makefile index 18a69a627..7298e6065 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,15 @@ CARGO ?= $(or $(shell command -v cargo 2>/dev/null),$(shell [ -x "$(HOME)/.cargo CARGO_LOCKED ?= BUILD_JOBS ?= CARGO_FLAGS ?= --workspace --all-targets --all-features +# Dylint UI fixtures live under `examples/` because `dylint_testing::Test::example` +# builds them with each lint crate's dev-dependencies (tokio, rstest); `ui/` +# fixtures are standalone and cannot carry those. Every file there is a +# `fail_*`/`pass_*` fixture that deliberately contains the anti-patterns the +# suite detects, so the workspace lint policy cannot meaningfully apply to them +# -- a fixture proving `expect_used` fires cannot itself forbid `expect_used`. +# Lint every other target rather than `--all-targets`; `typecheck` still builds +# the fixtures, and the suite still lints them through the UI harness. +CLIPPY_FLAGS ?= --workspace --lib --bins --tests --benches --all-features TEST_EXCLUDES ?= --exclude rustc_ast --exclude rustc_attr_data_structures --exclude rustc_hir --exclude rustc_lint --exclude rustc_middle --exclude rustc_session --exclude rustc_span --exclude whitaker --exclude function_attrs_follow_docs --exclude module_max_lines --exclude no_expect_outside_tests TEST_CARGO_FLAGS ?= $(CARGO_FLAGS) $(TEST_EXCLUDES) NEXTEST_PROFILE ?= @@ -159,7 +168,7 @@ lint: lint-clippy lint-whitaker ## Run rustdoc, Clippy, and the Whitaker Dylint lint-clippy: ## Run rustdoc and Clippy with warnings denied RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) doc $(CARGO_LOCKED) --workspace --no-deps - $(CARGO) clippy $(CARGO_LOCKED) $(CARGO_FLAGS) -- $(RUST_FLAGS) + $(CARGO) clippy $(CARGO_LOCKED) $(CLIPPY_FLAGS) -- $(RUST_FLAGS) lint-whitaker: ## Run the Whitaker Dylint suite with warnings denied @export PATH="$$PATH:$(TOOL_PATH_SUFFIX)"; \ diff --git a/crates/bumpy_road_function/src/analysis.rs b/crates/bumpy_road_function/src/analysis.rs index 14799709e..953e09af0 100644 --- a/crates/bumpy_road_function/src/analysis.rs +++ b/crates/bumpy_road_function/src/analysis.rs @@ -224,6 +224,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 5ef5690cb..af679b4cb 100644 --- a/crates/bumpy_road_function/src/driver/mod.rs +++ b/crates/bumpy_road_function/src/driver/mod.rs @@ -32,12 +32,18 @@ use self::{ segment_builder::{SegmentBuilder, span_line_range}, }; -/// 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::BumpyRoadFunction; dylint_linting::impl_late_lint! { @@ -50,7 +56,7 @@ mod registration { } } -pub use registration::BUMPY_ROAD_FUNCTION; +pub use declaration::BUMPY_ROAD_FUNCTION; /// Lint pass that caches configuration and localization for a crate. pub struct BumpyRoadFunction { @@ -79,7 +85,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>) { @@ -87,7 +93,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>) { @@ -95,7 +101,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>) { @@ -107,17 +113,17 @@ 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; } - analyse_body(cx, &target, &self.settings, &self.localizer); + analyse_body(cx, target, &self.settings, &self.localizer); } } diff --git a/crates/bumpy_road_function/src/driver/segment_builder.rs b/crates/bumpy_road_function/src/driver/segment_builder.rs index 767d5deb9..046868850 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/bumpy_road_function/tests/analysis_behaviour.rs b/crates/bumpy_road_function/tests/analysis_behaviour.rs index d24329859..c21342d6a 100644 --- a/crates/bumpy_road_function/tests/analysis_behaviour.rs +++ b/crates/bumpy_road_function/tests/analysis_behaviour.rs @@ -76,9 +76,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] @@ -95,9 +97,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)] @@ -182,7 +186,14 @@ fn then_threshold(world: &World, threshold: f64) { .normalized .borrow() .expect("settings should be normalized"); - assert_eq!(settings.threshold, threshold); + // 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)] @@ -237,8 +248,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/ui.rs b/crates/bumpy_road_function/tests/ui.rs index ee9904e1a..02256b5b3 100644 --- a/crates/bumpy_road_function/tests/ui.rs +++ b/crates/bumpy_road_function/tests/ui.rs @@ -17,15 +17,14 @@ use whitaker_common::test_support::{prepare_fixture, run_fixtures_with, run_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/driver.rs b/crates/conditional_max_n_branches/src/driver.rs index 3d98943d9..ba7d27385 100644 --- a/crates/conditional_max_n_branches/src/driver.rs +++ b/crates/conditional_max_n_branches/src/driver.rs @@ -64,12 +64,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! { @@ -82,7 +88,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 3124c1a9c..45285d235 100644 --- a/crates/function_attrs_follow_docs/src/driver.rs +++ b/crates/function_attrs_follow_docs/src/driver.rs @@ -40,13 +40,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(); @@ -58,7 +76,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), ); } } @@ -67,7 +85,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), ); } } @@ -76,7 +94,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), ); } } @@ -96,9 +114,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..ca62ecc6e 100644 --- a/crates/function_attrs_follow_docs/src/lib.rs +++ b/crates/function_attrs_follow_docs/src/lib.rs @@ -12,11 +12,22 @@ #![cfg_attr(feature = "dylint-driver", feature(rustc_private))] +// `rustc_hir` attribute structures store feature lists in `ThinVec`, which is a +// `rustc_private` crate rather than a Cargo dependency. The unit tests need to +// name the type when constructing attribute fixtures. +#[cfg(all(test, feature = "dylint-driver"))] +extern crate thin_vec; + #[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/function_attrs_follow_docs/src/tests/order_detection.rs b/crates/function_attrs_follow_docs/src/tests/order_detection.rs index d2a02f67e..5fb7145f3 100644 --- a/crates/function_attrs_follow_docs/src/tests/order_detection.rs +++ b/crates/function_attrs_follow_docs/src/tests/order_detection.rs @@ -9,6 +9,7 @@ use rstest::{fixture, rstest}; use rstest_bdd_macros::{given, scenario, then, when}; use rustc_hir::attrs::{AttributeKind as HirAttributeKind, InlineAttr, OptimizeAttr}; use rustc_span::{BytePos, DUMMY_SP, Span}; +use thin_vec::ThinVec; use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath}; use super::{ @@ -231,7 +232,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/module_max_lines/src/driver.rs b/crates/module_max_lines/src/driver.rs index 0c7808de8..6b150debe 100644 --- a/crates/module_max_lines/src/driver.rs +++ b/crates/module_max_lines/src/driver.rs @@ -30,13 +30,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, @@ -60,9 +78,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); @@ -129,7 +146,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, @@ -157,8 +174,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 7e502bf83..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 @@ -37,7 +37,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`. @@ -46,7 +47,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; @@ -88,8 +92,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) } @@ -102,7 +110,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 c08f2b9be..6b7868360 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)] @@ -31,7 +30,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>; @@ -55,13 +71,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, @@ -82,9 +116,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() { @@ -195,16 +228,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 @@ -226,10 +260,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; @@ -285,8 +322,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 3bda7875e..a0215ba79 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`. @@ -158,14 +159,16 @@ const fn is_ident_start(ch: char) -> bool { const fn is_ident_continue(ch: char) -> bool { ch == '_' || ch.is_ascii_alphanumeric() } 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)) } @@ -196,7 +199,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( @@ -215,7 +218,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 b62980278..367b25a6f 100644 --- a/crates/no_expect_outside_tests/src/context/mod.rs +++ b/crates/no_expect_outside_tests/src/context/mod.rs @@ -200,10 +200,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) @@ -258,17 +258,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; } @@ -331,12 +331,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 3ed91a899..35e8622fd 100644 --- a/crates/no_expect_outside_tests/src/context/tests.rs +++ b/crates/no_expect_outside_tests/src/context/tests.rs @@ -51,6 +51,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 { @@ -58,7 +77,7 @@ struct HasTestAncestryCase { attr_fixture: AttributeFixture, is_function_item: bool, include_custom_attribute: bool, - expected: bool, + expected: AncestryOutcome, } // Common path constants @@ -272,8 +291,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 @@ -333,42 +356,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(|| { @@ -376,14 +399,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:?}" ); }); } @@ -393,7 +418,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); }); } @@ -402,7 +427,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); }); } @@ -411,7 +436,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); }); } @@ -420,7 +445,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); }); } @@ -429,7 +454,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/dependency_rlib_tests.rs b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs index 96d7e6928..5a5c9f004 100644 --- a/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs +++ b/crates/no_expect_outside_tests/src/dependency_rlib_tests.rs @@ -82,11 +82,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 +128,11 @@ impl TemporaryDirectory { } impl Drop for TemporaryDirectory { - fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } + fn drop(&mut self) { + // 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); + } } /// Creates an empty `.rlib` fixture file at `directory/file_name` and returns diff --git a/crates/no_expect_outside_tests/src/driver/mod.rs b/crates/no_expect_outside_tests/src/driver/mod.rs index ccb58e04d..756dca676 100644 --- a/crates/no_expect_outside_tests/src/driver/mod.rs +++ b/crates/no_expect_outside_tests/src/driver/mod.rs @@ -25,13 +25,31 @@ 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)] @@ -152,12 +170,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 b97123599..26a0b8b79 100644 --- a/crates/no_expect_outside_tests/src/driver/tests.rs +++ b/crates/no_expect_outside_tests/src/driver/tests.rs @@ -115,6 +115,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}", ); }); } @@ -131,7 +132,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_expect_outside_tests/src/lib_ui_tests.rs b/crates/no_expect_outside_tests/src/lib_ui_tests.rs index 4b0913358..a642015fb 100644 --- a/crates/no_expect_outside_tests/src/lib_ui_tests.rs +++ b/crates/no_expect_outside_tests/src/lib_ui_tests.rs @@ -84,7 +84,7 @@ 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( @@ -288,10 +288,14 @@ fn dependency_rlib_candidate( /// Returns `true` when `path` names a file that starts with `prefix` and has /// the `.rlib` extension. fn is_dependency_rlib(path: &Path, prefix: &str) -> 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 3a84dd27e..33d65d318 100644 --- a/crates/no_expect_outside_tests/src/tests/localization.rs +++ b/crates/no_expect_outside_tests/src/tests/localization.rs @@ -115,8 +115,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); } @@ -156,31 +160,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}`" ); } @@ -195,10 +199,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" + ), } } @@ -225,7 +232,7 @@ fn scenario_failure(world: LocalizationWorld) { let _ = world; } #[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(); @@ -233,8 +240,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_std_fs_operations/src/behaviour.rs b/crates/no_std_fs_operations/src/behaviour.rs index bc36f1412..34a5680dd 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 { self.localizer = Some(Localizer::new(Some(locale))); } - fn set_operation(&mut self, operation: &str) { self.operation = operation.to_owned(); } + fn set_operation(&mut self, operation: &str) { operation.clone_into(&mut self.operation); } const fn mark_failure(&mut self) { self.failing = true; } @@ -30,26 +30,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 } } @@ -85,21 +85,30 @@ fn when_localize(world: &WorldCell) { world.borrow_mut().resolve(); } 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}")] @@ -107,7 +116,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 b61a570d5..ee147ce40 100644 --- a/crates/no_std_fs_operations/src/diagnostics.rs +++ b/crates/no_std_fs_operations/src/diagnostics.rs @@ -23,7 +23,7 @@ use crate::{NO_STD_FS_OPERATIONS, usage::StdFsUsage}; pub(crate) fn emit_diagnostic( cx: &LateContext<'_>, span: Span, - usage: StdFsUsage, + usage: &StdFsUsage, localizer: &Localizer, ) { let mut args: Arguments<'static> = Arguments::default(); @@ -47,9 +47,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())); }), ); } @@ -76,7 +76,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() @@ -103,13 +103,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 ccaf2438c..bf339cb1d 100644 --- a/crates/no_std_fs_operations/src/driver.rs +++ b/crates/no_std_fs_operations/src/driver.rs @@ -27,6 +27,8 @@ use crate::{ }, }; +/// Lint pass that tracks localization and exclusion state while checking +/// `std::fs` usage. pub struct NoStdFsOperations { localizer: Localizer, excluded: bool, @@ -43,13 +45,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(); @@ -115,7 +135,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); @@ -162,7 +182,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 @@ -171,7 +191,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 @@ -184,12 +204,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 736d3b0eb..eb232e7fc 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 413a4acdd..2eeb5c2f8 100644 --- a/crates/no_std_fs_operations/src/exclusion_behaviour.rs +++ b/crates/no_std_fs_operations/src/exclusion_behaviour.rs @@ -25,9 +25,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 07a7090d9..8cfd243cb 100644 --- a/crates/no_std_fs_operations/src/usage/mod.rs +++ b/crates/no_std_fs_operations/src/usage/mod.rs @@ -142,8 +142,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. @@ -158,11 +157,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_std_fs_operations/tests/integration_exclusion.rs b/crates/no_std_fs_operations/tests/integration_exclusion.rs index 0ee3b4194..e40a88821 100644 --- a/crates/no_std_fs_operations/tests/integration_exclusion.rs +++ b/crates/no_std_fs_operations/tests/integration_exclusion.rs @@ -126,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; }; @@ -200,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| { @@ -221,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, @@ -249,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, @@ -358,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" ); @@ -412,15 +413,15 @@ 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) + serde_json::to_value(compiler_message.message) .context("failed to serialize diagnostic for snapshot"), ) } 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 d51af196f..84079d74b 100644 --- a/crates/no_std_fs_operations/tests/test_support/mod.rs +++ b/crates/no_std_fs_operations/tests/test_support/mod.rs @@ -220,12 +220,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 +238,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 +261,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 +275,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 +293,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/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 01200f591..920d53589 100644 --- a/crates/no_unwrap_or_else_panic/src/driver.rs +++ b/crates/no_unwrap_or_else_panic/src/driver.rs @@ -18,13 +18,31 @@ use crate::{ policy::{LintPolicy, should_flag}, }; -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 { @@ -108,7 +126,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/lib_ui_tests.rs b/crates/no_unwrap_or_else_panic/src/lib_ui_tests.rs index 50707b7aa..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 @@ -53,15 +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. @@ -72,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( 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 975c23214..01b1ad065 100644 --- a/crates/no_unwrap_or_else_panic/src/panic_detector.rs +++ b/crates/no_unwrap_or_else_panic/src/panic_detector.rs @@ -49,7 +49,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 } } @@ -84,12 +84,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 3a625dd19..a979c37ea 100644 --- a/crates/no_unwrap_or_else_panic/src/policy.rs +++ b/crates/no_unwrap_or_else_panic/src/policy.rs @@ -36,13 +36,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 { @@ -153,13 +153,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 2a30e2969..96a89b0df 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/collector_tests.rs b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs index 31ea406e7..51471a363 100644 --- a/crates/rstest_helper_should_be_fixture/src/collector_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/collector_tests.rs @@ -230,11 +230,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_owned(), spans) + (callee.to_owned(), recorded_spans) }) .collect() } diff --git a/crates/rstest_helper_should_be_fixture/src/driver.rs b/crates/rstest_helper_should_be_fixture/src/driver.rs index 27b1c8013..f5b4e4ae4 100644 --- a/crates/rstest_helper_should_be_fixture/src/driver.rs +++ b/crates/rstest_helper_should_be_fixture/src/driver.rs @@ -44,13 +44,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 @@ -135,10 +153,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, @@ -148,10 +166,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={}, \ @@ -207,7 +225,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); } @@ -264,12 +282,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 1a26693f8..843f8d26b 100644 --- a/crates/rstest_helper_should_be_fixture/src/driver_tests.rs +++ b/crates/rstest_helper_should_be_fixture/src/driver_tests.rs @@ -78,9 +78,9 @@ fn normalizes_numeric_thresholds_to_two() { #[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(str::to_owned).collect(); + let expected_owned: Vec = expected.into_iter().map(str::to_owned).collect(); - assert_eq!(normalized, expected); + assert_eq!(normalized, expected_owned); } #[rstest] @@ -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 3b2e44639..de7b835c3 100644 --- a/crates/rstest_helper_should_be_fixture/src/visitor.rs +++ b/crates/rstest_helper_should_be_fixture/src/visitor.rs @@ -130,7 +130,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)) }) } } @@ -205,11 +205,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/rstest_helper_should_be_fixture/tests/ui.rs b/crates/rstest_helper_should_be_fixture/tests/ui.rs index 4e7373761..a818dc15e 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui.rs @@ -75,22 +75,25 @@ 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(); }) @@ -98,7 +101,8 @@ impl ExampleHarness { .unwrap_or_else(|error| { panic!( "UI tests should execute without diffs: RunnerFailure {{ crate_name: \ - \"{crate_name}\", directory: \"{directory}\", message: {error} }}" + \"{crate_name}\", directory: \"{directory}\", lock: \"{lock_path}\", message: \ + {error} }}" ) }); } @@ -113,9 +117,11 @@ impl ExampleHarness { let summary_path = unique_summary_path(); with_env_var(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); + 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 ef2bd6025..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 @@ -38,6 +38,11 @@ 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 { @@ -54,30 +59,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)?; } } } @@ -88,7 +93,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 { @@ -100,10 +106,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); } } @@ -113,7 +120,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)); } @@ -157,10 +164,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"); @@ -233,10 +240,14 @@ 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] @@ -267,10 +278,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); } @@ -296,7 +307,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"); @@ -318,7 +329,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"); @@ -326,7 +337,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 ac3fcac2a..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 @@ -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/driver.rs b/crates/test_must_not_have_example/src/driver.rs index bca9c5461..dc46e3ae3 100644 --- a/crates/test_must_not_have_example/src/driver.rs +++ b/crates/test_must_not_have_example/src/driver.rs @@ -35,13 +35,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. @@ -95,7 +113,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, }, @@ -145,7 +163,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)); } } @@ -164,11 +182,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; } @@ -184,7 +198,7 @@ impl TestMustNotHaveExample { fn emit_violation( &self, cx: &LateContext<'_>, - function: FunctionSite<'_>, + function: &FunctionSite<'_>, violation: DocExampleViolation, ) { let messages = localized_messages(&self.localizer, function.name, violation); @@ -206,20 +220,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 06b945f4b..bba408d87 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/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/src/dependency_binaries/install/installer.rs b/installer/src/dependency_binaries/install/installer.rs index 002858cc0..7c896d7b0 100644 --- a/installer/src/dependency_binaries/install/installer.rs +++ b/installer/src/dependency_binaries/install/installer.rs @@ -144,7 +144,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/tests.rs b/installer/src/dependency_binaries/install/tests.rs index 6c17f4608..04570effc 100644 --- a/installer/src/dependency_binaries/install/tests.rs +++ b/installer/src/dependency_binaries/install/tests.rs @@ -31,7 +31,7 @@ fn run_install_scenario( let temp_dir = tempfile::tempdir().expect("temp dir"); 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) diff --git a/installer/src/dirs.rs b/installer/src/dirs.rs index 6a028b0c5..2faceb77c 100644 --- a/installer/src/dirs.rs +++ b/installer/src/dirs.rs @@ -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. @@ -102,17 +102,17 @@ impl SystemBaseDirs { } impl BaseDirs for SystemBaseDirs { - fn home_dir(&self) -> Option { Some(self.user_dirs.home_dir().to_owned()) } + 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 { Some(self.project_dirs.data_dir().to_owned()) } + fn whitaker_data(&self) -> Option { Some(self.project_dirs.data_dir().to_owned()) } } #[cfg(test)] @@ -124,16 +124,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(), @@ -153,7 +150,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!( @@ -171,7 +168,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")); }); @@ -183,7 +180,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/install_flow/tests.rs b/installer/src/install_flow/tests.rs index 8b0ca523a..9a86b03aa 100644 --- a/installer/src/install_flow/tests.rs +++ b/installer/src/install_flow/tests.rs @@ -21,9 +21,9 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home_dir(&self) -> Option { None } - fn bin_dir(&self) -> Option { None } - fn whitaker_data_dir(&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 726ba7e10..9d1be4146 100644 --- a/installer/src/install_metrics.rs +++ b/installer/src/install_metrics.rs @@ -266,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)) } diff --git a/installer/src/list.rs b/installer/src/list.rs index 30657ac2f..d80a9fb1c 100644 --- a/installer/src/list.rs +++ b/installer/src/list.rs @@ -77,7 +77,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")) } diff --git a/installer/src/prebuilt_path.rs b/installer/src/prebuilt_path.rs index 5251109b2..4946b74b6 100644 --- a/installer/src/prebuilt_path.rs +++ b/installer/src/prebuilt_path.rs @@ -27,7 +27,7 @@ pub fn prebuilt_library_dir( target: &str, ) -> Result { let data_dir = dirs - .whitaker_data_dir() + .whitaker_data() .ok_or_else(|| InstallerError::StagingFailed { reason: "could not determine Whitaker data directory".to_owned(), })?; @@ -57,7 +57,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") @@ -78,7 +78,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") @@ -95,7 +95,7 @@ mod tests { use std::{ffi::OsString, 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/test_utils/mod.rs b/installer/src/test_utils/mod.rs index 54c125304..38ebc93e2 100644 --- a/installer/src/test_utils/mod.rs +++ b/installer/src/test_utils/mod.rs @@ -120,16 +120,16 @@ 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 { None } + fn home(&self) -> Option { None } - fn bin_dir(&self) -> Option { self.bin_dir.clone() } + fn executables(&self) -> Option { self.bin_dir.clone() } - fn whitaker_data_dir(&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/fast_path.rs b/installer/src/tests/fast_path.rs index 683f7f970..685a5c7e5 100644 --- a/installer/src/tests/fast_path.rs +++ b/installer/src/tests/fast_path.rs @@ -67,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 22e33e7a6..fc344eb8c 100644 --- a/installer/src/tests/mod.rs +++ b/installer/src/tests/mod.rs @@ -248,11 +248,11 @@ struct TestBaseDirs { } impl BaseDirs for TestBaseDirs { - fn home_dir(&self) -> Option { self.home.clone() } + fn home(&self) -> Option { self.home.clone() } - fn bin_dir(&self) -> Option { self.bin.clone() } + fn executables(&self) -> Option { self.bin.clone() } - fn whitaker_data_dir(&self) -> Option { self.data.clone() } + fn whitaker_data(&self) -> Option { self.data.clone() } } #[test] diff --git a/installer/src/workspace.rs b/installer/src/workspace.rs index e2a01340f..57806cd18 100644 --- a/installer/src/workspace.rs +++ b/installer/src/workspace.rs @@ -61,7 +61,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()) } @@ -312,7 +312,7 @@ mod tests { 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 } diff --git a/installer/src/wrapper.rs b/installer/src/wrapper.rs index 8692140c9..18d84b301 100644 --- a/installer/src/wrapper.rs +++ b/installer/src/wrapper.rs @@ -69,7 +69,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()) })?; diff --git a/installer/tests/behaviour_artefact.rs b/installer/tests/behaviour_artefact.rs index 9d035399e..885acf51c 100644 --- a/installer/tests/behaviour_artefact.rs +++ b/installer/tests/behaviour_artefact.rs @@ -51,8 +51,8 @@ fn ensure_error_matches( where F: FnOnce(&ArtefactError) -> bool, { - let error = error.ok_or_else(|| format!("expected {field_name} validation to fail"))?; - if predicate(error) { + 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}")) diff --git a/installer/tests/behaviour_artefact_packaging.rs b/installer/tests/behaviour_artefact_packaging.rs index 0c6a02c9a..1b3e30e00 100644 --- a/installer/tests/behaviour_artefact_packaging.rs +++ b/installer/tests/behaviour_artefact_packaging.rs @@ -385,8 +385,8 @@ fn list_archive_entries(world: &PackagingWorld) -> Result, String> { .entries() .map_err(|e| format!("list archive entries: {e}"))? { - let entry = entry.map_err(|e| format!("read archive entry: {e}"))?; - let path = entry + 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() diff --git a/installer/tests/behaviour_cli/support.rs b/installer/tests/behaviour_cli/support.rs index b03853ea2..12e87002e 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -217,8 +217,8 @@ pub(super) fn run_installer_cli(cli_world: &CliWorld) { } pub(super) fn get_output(cli_world: &CliWorld) -> Ref<'_, Output> { - let output = cli_world.output.borrow(); - Ref::map(output, |opt| { + let output_slot = cli_world.output.borrow(); + Ref::map(output_slot, |opt| { let Some(output) = opt.as_ref() else { panic!("output not set"); }; diff --git a/installer/tests/behaviour_dependency_binaries.rs b/installer/tests/behaviour_dependency_binaries.rs index 2c33fcd13..2a93ceeb8 100644 --- a/installer/tests/behaviour_dependency_binaries.rs +++ b/installer/tests/behaviour_dependency_binaries.rs @@ -30,12 +30,25 @@ use whitaker_installer::{ }, }; +/// 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, } @@ -48,20 +61,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.executables().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{}/{}", @@ -84,7 +100,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, @@ -121,8 +136,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")] @@ -156,10 +170,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 { @@ -167,14 +182,16 @@ 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; - let repository_verification = match ( - expect_repository_verification, - world.should_repository_verification_fail, - ) { + Some(RepositoryInstallerBehaviour::SuccessWithFailedVerification) + ); + let repository_verification = match (expect_repository_verification, should_verification_fail) { (false, _) => RepositoryVerification::Skip, (true, true) => RepositoryVerification::Fails, (true, false) => RepositoryVerification::Succeeds, @@ -192,33 +209,36 @@ fn build_stub_executor(world: &DependencyBinaryWorld, tool: &str) -> StubExecuto )) } +/// 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) + .map_err(|error| format!("write fake dylint-link: {error}")) +} + 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).expect("write fake dylint-link"); - } 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", @@ -228,13 +248,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, @@ -248,16 +276,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")] @@ -265,22 +290,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}\"")] @@ -288,36 +327,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 682e90426..edac722ac 100644 --- a/installer/tests/behaviour_docs.rs +++ b/installer/tests/behaviour_docs.rs @@ -6,7 +6,7 @@ mod doc_extraction; -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; use doc_extraction::extraction::{DOC_TOML_BLOCKS, find_block_containing}; use rstest::fixture; @@ -46,7 +46,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 +55,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 +112,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 +127,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(toml::Value::as_integer) - .unwrap_or_else(|| panic!("expected {section}.{key} to be an 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 +165,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(toml::Value::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" - ); + .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 7140bda82..10bc84d28 100644 --- a/installer/tests/behaviour_install_metrics.rs +++ b/installer/tests/behaviour_install_metrics.rs @@ -14,7 +14,8 @@ use whitaker_installer::install_metrics::{ #[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, @@ -26,8 +27,41 @@ struct InstallMetricsWorld { #[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 +78,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,102 +122,136 @@ 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")] fn when_rates_calculated(world: &mut InstallMetricsWorld) { let _ = world; } #[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", + ) } #[then("download rate is {expected:u64} permille")] -fn then_download_rate(world: &mut InstallMetricsWorld, expected: u64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert_eq!( - metrics.download_rate_permille(), - expected, - "download rate permille mismatch" - ); +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:u64} permille")] -fn then_build_rate(world: &mut InstallMetricsWorld, expected: u64) { - let metrics = world.in_memory_metrics.as_ref().expect("metrics available"); - assert_eq!( - metrics.build_rate_permille(), - expected, - "build rate permille mismatch" - ); +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")] -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 88b3aa294..7572d5114 100644 --- a/installer/tests/behaviour_installer_release.rs +++ b/installer/tests/behaviour_installer_release.rs @@ -51,41 +51,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(), }; @@ -94,13 +107,18 @@ 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}\"")] fn then_archive_filename_is(world: &mut InstallerReleaseWorld, expected: String) { @@ -111,21 +129,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")] @@ -139,15 +161,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:?}")) + } } // --------------------------------------------------------------------------- @@ -155,7 +178,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), @@ -163,32 +193,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 9ad5de436..6176c5537 100644 --- a/installer/tests/behaviour_prebuilt.rs +++ b/installer/tests/behaviour_prebuilt.rs @@ -1,6 +1,9 @@ //! BDD tests for the prebuilt artefact download and verification workflow. -use std::{path::Path, sync::Mutex}; +use std::{ + path::Path, + sync::{Mutex, PoisonError}, +}; use camino::Utf8PathBuf; use clap::Parser; @@ -61,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 } => { @@ -77,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) @@ -121,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), @@ -209,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 @@ -238,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() @@ -250,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")] @@ -263,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 ca4e0e5aa..df56e4dd8 100644 --- a/installer/tests/behaviour_staging.rs +++ b/installer/tests/behaviour_staging.rs @@ -43,19 +43,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")] @@ -87,8 +90,8 @@ mod staging_failure { 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,7 +100,7 @@ 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), } } } @@ -107,34 +110,44 @@ mod staging_failure { 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 @@ -146,7 +159,12 @@ mod staging_failure { .open(&probe_path) { drop(file); - let _ = std::fs::remove_file(&probe_path); + 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. @@ -154,26 +172,32 @@ mod staging_failure { 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 // filesystem permissions. This is similar to how CI containers often run. if rustix::process::geteuid().is_root() { - 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 75c3be340..39fa32403 100644 --- a/installer/tests/behaviour_toolchain.rs +++ b/installer/tests/behaviour_toolchain.rs @@ -18,7 +18,10 @@ 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. -#[expect(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, diff --git a/installer/tests/behaviour_workflows.rs b/installer/tests/behaviour_workflows.rs index 0d274c600..2012d6fd6 100644 --- a/installer/tests/behaviour_workflows.rs +++ b/installer/tests/behaviour_workflows.rs @@ -5,7 +5,8 @@ use std::{ cell::{Cell, RefCell}, - path::PathBuf, + io::Write as _, + path::{Path, PathBuf}, process::{Command, Output}, }; @@ -21,30 +22,31 @@ 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() ) @@ -58,57 +60,67 @@ fn is_toolchain_installed(channel: &str) -> bool { .is_ok_and(|o| o.status.success()) } -fn skip_scenario_when_toolchain_missing(world: &WorkflowWorld, channel: &str) { +/// 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, +) -> Result<(), String> { if !is_toolchain_installed(channel) { - eprintln!("Skipping scenario because rustup toolchain '{channel}' is not installed."); + 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")) } // --------------------------------------------------------------------------- // 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![ @@ -117,26 +129,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 @@ -148,98 +165,120 @@ 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) { +fn then_cli_exits_successfully(world: &WorkflowWorld) -> 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("installation succeeds or is skipped")] -fn then_installation_succeeds_or_is_skipped(world: &WorkflowWorld) { +fn then_installation_succeeds_or_is_skipped(world: &WorkflowWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(world); - assert!( - output.status.success(), - "installation failed: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output = get_output(world)?; + if output.status.success() { + Ok(()) + } else { + Err(format!( + "installation failed: {}", + String::from_utf8_lossy(&output.stderr) + )) + } } #[then("dry-run output shows skip_deps is true")] -fn then_skip_deps_is_true(world: &WorkflowWorld) { +fn then_skip_deps_is_true(world: &WorkflowWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(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}" - ); + if stderr.contains("Skip deps: true") { + Ok(()) + } else { + Err(format!( + "expected skip_deps to be true in output, stderr: {stderr}" + )) + } } #[then("dry-run output shows no_update is true")] -fn then_no_update_is_true(world: &WorkflowWorld) { +fn then_no_update_is_true(world: &WorkflowWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(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}" - ); + if stderr.contains("No update: true") { + Ok(()) + } else { + Err(format!( + "expected no_update to be true in output, stderr: {stderr}" + )) + } } #[then("dry-run output shows skip_wrapper is true")] -fn then_skip_wrapper_is_true(world: &WorkflowWorld) { +fn then_skip_wrapper_is_true(world: &WorkflowWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(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}" - ); + if stderr.contains("Skip wrapper: true") { + Ok(()) + } else { + Err(format!( + "expected skip_wrapper to be true in output, stderr: {stderr}" + )) + } } #[then("output includes DYLINT_LIBRARY_PATH instructions")] -fn then_output_includes_library_path_instructions(world: &WorkflowWorld) { +fn then_output_includes_library_path_instructions(world: &WorkflowWorld) -> Result<(), String> { skip_if_needed!(world); - let output = get_output(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}" - ); + if stderr.contains("DYLINT_LIBRARY_PATH") { + Ok(()) + } else { + Err(format!( + "expected DYLINT_LIBRARY_PATH instructions in output, stderr: {stderr}" + )) + } } // --------------------------------------------------------------------------- diff --git a/installer/tests/doc_extraction/extraction.rs b/installer/tests/doc_extraction/extraction.rs index 5ca7eb1ba..17ce1b052 100644 --- a/installer/tests/doc_extraction/extraction.rs +++ b/installer/tests/doc_extraction/extraction.rs @@ -12,9 +12,9 @@ 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 { @@ -116,8 +116,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 +134,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 caa50f1b4..c16c923e3 100644 --- a/installer/tests/support/mod.rs +++ b/installer/tests/support/mod.rs @@ -13,19 +13,28 @@ 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. @@ -75,7 +84,11 @@ pub struct IsolatedRustupEnv { /// `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 @@ -84,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 initialize isolated rustup environment"); + .map_err(|error| format!("failed to initialize isolated rustup environment: {error}"))?; - assert!( - init_output.status.success(), - "failed to initialize 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"]) @@ -100,55 +114,72 @@ 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_owned() + .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. +/// +/// # 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. @@ -157,23 +188,27 @@ fn install_rustup_to_cargo_bin(rustup_path: &str, cargo_bin: &Path) { /// 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 d47ac526b..9edc1fdd1 100644 --- a/installer/tests/toolchain_steps/mod.rs +++ b/installer/tests/toolchain_steps/mod.rs @@ -3,28 +3,24 @@ //! These step implementations are used by the scenarios in //! `behaviour_toolchain.rs` via rstest-bdd macros. -use std::{ - cell::{Cell, RefCell}, - process::{Command, Output}, -}; +mod scenario_setup; + +use std::process::Command; use rstest::fixture; use rstest_bdd_macros::{given, then, when}; -use tempfile::TempDir; - -use super::{ - prebuilt_markers::PREBUILT_INSTALL_MARKER, - 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"; @@ -38,122 +34,14 @@ 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()); - - 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); -} - -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_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" - ); -} - // --------------------------------------------------------------------------- // Fixture // --------------------------------------------------------------------------- @@ -167,53 +55,42 @@ pub fn world() -> ToolchainWorld { ToolchainWorld::default() } // --------------------------------------------------------------------------- #[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"]) } // --------------------------------------------------------------------------- @@ -221,13 +98,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 @@ -241,8 +118,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(()) } // --------------------------------------------------------------------------- @@ -250,132 +130,160 @@ 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) { +pub fn then_no_install_message(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let out = get_combined_output_string(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}" - ); + let has_install_message = out_lc.contains(&needle) + || out_lc.contains(&channel.to_lowercase()) && out_lc.contains(TOOLCHAIN_INSTALLED_MARKER); + if has_install_message { + return Err(format!( + "expected no installation message for channel '{channel}' in output, got:\n{out}" + )); + } + Ok(()) } #[then("the toolchain installation message is shown")] -pub fn then_install_message_shown(world: &ToolchainWorld) { +pub fn then_install_message_shown(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let out = get_combined_output_string(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) + let has_success_marker = 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}" - ); + if has_success_marker { + Ok(()) + } else { + Err(format!( + "expected success marker for channel '{channel}' in output, got:\n{out}" + )) + } } #[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) { +pub fn then_error_mentions_install_failure(world: &ToolchainWorld) -> Result<(), String> { skip_if_needed!(world); - let stderr = get_stderr_string(world); - assert!( - stderr.contains(TOOLCHAIN_ERROR_MARKER), - "expected '{}' in stderr: {stderr}", - TOOLCHAIN_ERROR_MARKER - ); + let stderr = get_stderr_string(world)?; + if stderr.contains(TOOLCHAIN_ERROR_MARKER) { + Ok(()) + } else { + Err(format!( + "expected '{TOOLCHAIN_ERROR_MARKER}' in stderr: {stderr}" + )) + } } #[then("the error includes the toolchain name")] -pub fn then_error_includes_toolchain_name(world: &ToolchainWorld) { +pub fn then_error_includes_toolchain_name(world: &ToolchainWorld) -> Result<(), String> { 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}" - ); + let stderr = get_stderr_string(world)?; + if stderr.contains(FAKE_TOOLCHAIN) { + Ok(()) + } else { + Err(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..ccba940ce --- /dev/null +++ b/installer/tests/toolchain_steps/scenario_setup.rs @@ -0,0 +1,176 @@ +//! 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/suite/tests/registration.rs b/suite/tests/registration.rs index d03f88b93..8611a350e 100644 --- a/suite/tests/registration.rs +++ b/suite/tests/registration.rs @@ -50,13 +50,15 @@ fn when_register_suite(world: &RegistrationWorld) { register_suite_lints(&mut world.store.borrow_mut()); })) .map_err(|panic| { - if let Some(message) = panic.downcast_ref::<&str>() { - (*message).to_owned() - } else if let Some(message) = panic.downcast_ref::() { - message.clone() - } else { - "registration panicked with a non-string payload".to_owned() - } + 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; diff --git a/tests/config_loading.rs b/tests/config_loading.rs index a23088ed0..9740a6375 100644 --- a/tests/config_loading.rs +++ b/tests/config_loading.rs @@ -115,7 +115,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 dbbeae4e1..ae8b5ab0d 100644 --- a/tests/ui_harness.rs +++ b/tests/ui_harness.rs @@ -128,7 +128,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}"), @@ -142,7 +146,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 b9d380a1a4169b2abf5e4eb28cf46860d7050592 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 11:36:30 +0200 Subject: [PATCH 09/21] Keep Dylint UI fixtures out of rustfmt and packaging Three test failures traced to the new formatting profile rather than to any behavioural change: - `cargo fmt --all` formats `examples/` because those files are Cargo targets, and `format_strings` split a long `reason = "..."` across lines. That shifted the diagnostics one line down, so the byte-exact `.stderr` expectations no longer matched. Fixtures under `ui/` were never affected because they are not targets. Add a `.rustfmt.toml` `ignore` list so the `examples/` fixtures are equally immune, and restore the reformatted files; they now differ from `main` only by the two deliberate spelling corrections. - `cargo package -p whitaker-common` failed because the new `whitaker_test_macros` dev-dependency carried a version requirement while the crate is `publish = false`. Make the workspace entry path-only: Cargo strips versionless dev-dependencies when packaging. Also clear the two findings the Whitaker suite raised against its own sources, now that `make lint` runs it: - move the `#[must_use]` attributes in `i18n/diagnostics.rs` back after their doc comments, where a mechanical `clippy --fix` pass had wedged them between the summary line and the rest of the comment (`function_attrs_follow_docs`); - recover the poisoned lock in `env_test_guard` with `PoisonError::into_inner` instead of panicking (`no_unwrap_or_else_panic`). The mutex guards `()`, so a poisoned lock carries no corrupted state -- only the record that an earlier test panicked while holding the serialization token. --- .rustfmt.toml | 12 ++++++++++++ Cargo.toml | 5 ++++- common/src/i18n/diagnostics.rs | 8 ++++---- common/src/test_support/mod.rs | 13 ++++++------- .../fail_expect_in_rstest_non_test_module.rs | 4 ++-- .../fail_expect_in_tokio_crate_non_test_fn.rs | 4 +++- ...il_unwrap_in_aliased_test_crate_non_companion.rs | 11 +++++------ .../fail_unwrap_in_hand_written_test_companion.rs | 3 +-- .../examples/pass_unwrap_in_rstest_harness.rs | 4 +++- .../examples/collection_zero_diagnostic.rs | 8 ++++++-- 10 files changed, 46 insertions(+), 26 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index f7ad026ba..ddaf6641b 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -12,3 +12,15 @@ 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/Cargo.toml b/Cargo.toml index b1518b03b..31da331a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,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" } -whitaker_test_macros = { path = "crates/whitaker_test_macros", 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/src/i18n/diagnostics.rs b/common/src/i18n/diagnostics.rs index ba4f9aa8e..10fa6a15a 100644 --- a/common/src/i18n/diagnostics.rs +++ b/common/src/i18n/diagnostics.rs @@ -103,7 +103,6 @@ pub struct DiagnosticMessageSet { impl DiagnosticMessageSet { /// Construct a new set of lint diagnostic strings. - #[must_use] /// /// # Examples /// @@ -112,6 +111,7 @@ impl DiagnosticMessageSet { /// let messages = DiagnosticMessageSet::new("primary".into(), "note".into(), "help".into()); /// assert_eq!(messages.primary(), "primary"); /// ``` + #[must_use] pub const fn new(primary: String, note: String, help: String) -> Self { Self { primary, @@ -121,7 +121,6 @@ impl DiagnosticMessageSet { } /// Access the primary lint diagnostic. - #[must_use] /// /// # Examples /// @@ -134,10 +133,10 @@ impl DiagnosticMessageSet { /// # ); /// assert_eq!(messages.primary(), "primary"); /// ``` + #[must_use] pub fn primary(&self) -> &str { &self.primary } /// Access the note attached to the diagnostic. - #[must_use] /// /// # Examples /// @@ -150,10 +149,10 @@ impl DiagnosticMessageSet { /// # ); /// assert_eq!(messages.note(), "note"); /// ``` + #[must_use] pub fn note(&self) -> &str { &self.note } /// Access the help text attached to the diagnostic. - #[must_use] /// /// # Examples /// @@ -166,6 +165,7 @@ impl DiagnosticMessageSet { /// # ); /// assert_eq!(messages.help(), "help"); /// ``` + #[must_use] pub fn help(&self) -> &str { &self.help } /// Remove Unicode isolating marks inserted by Fluent placeholders. diff --git a/common/src/test_support/mod.rs b/common/src/test_support/mod.rs index 57dd188a8..52cdf777f 100644 --- a/common/src/test_support/mod.rs +++ b/common/src/test_support/mod.rs @@ -23,7 +23,7 @@ pub mod ui; use std::{ ffi::OsStr, - sync::{Mutex, MutexGuard, OnceLock}, + sync::{Mutex, MutexGuard, OnceLock, PoisonError}, }; pub use fixtures::{copy_directory, copy_fixture}; @@ -44,16 +44,15 @@ pub use ui::{ /// `temp_env::with_vars_unset` when the test would otherwise race with other /// cases changing the same global process state. /// -/// # Panics -/// -/// Panics when the lock is poisoned, which means an earlier test panicked -/// while mutating the environment; the process state can no longer be -/// trusted, so failing fast is the only safe response. +/// 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) } /// Runs `callback` with one environment variable temporarily set. 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 1ef4e8166..57280542c 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; -#[expect(dead_code)] +#[allow(dead_code)] fn parse() { let parsed = std::iter::once("value").next(); let _ = parsed.expect("ordinary code must not inherit rstest harness status"); } -#[expect(dead_code)] +#[allow(dead_code)] mod parse { pub const VERSION: &str = "1"; } 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 9eed34d87..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 @@ -15,4 +15,6 @@ async fn unrelated_tokio_test() { value.expect("actual Tokio tests should still be allowed"); } -fn main() { parse_config(); } +fn main() { + parse_config(); +} diff --git a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs index e20285e62..55890ea40 100644 --- a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs +++ b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_aliased_test_crate_non_companion.rs @@ -11,8 +11,7 @@ #[cfg(test)] #[expect( dead_code, - reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. \ - Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." + reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." )] fn aliased_test_crate_non_companion_subject(value: i32) { let _ = Some(value).unwrap_or_else(|| panic!("aliased non-companion {value}")); @@ -25,11 +24,11 @@ mod aliased_test_crate_non_companion_subject { #[expect( dead_code, - reason = "proves an aliased `test` import alone is not an rstest companion; never \ - invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap \ - item 2.2.9." + reason = "proves an aliased `test` import alone is not an rstest companion; never invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." )] - fn unrelated_item() { let _ = test_harness::black_box(1); } + fn unrelated_item() { + let _ = test_harness::black_box(1); + } } fn main() {} diff --git a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs index c0af50c6f..757396f3e 100644 --- a/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs +++ b/crates/no_unwrap_or_else_panic/examples/fail_unwrap_in_hand_written_test_companion.rs @@ -18,8 +18,7 @@ #[cfg(test)] #[expect( dead_code, - reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. \ - Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." + reason = "compiled by the Dylint UI harness solely to assert the emitted lint; never invoked. Migration to #[whitaker_support::dylint_expect] is tracked by roadmap item 2.2.9." )] fn hand_written_test_companion_subject(value: i32) { let _ = Some(value).unwrap_or_else(|| panic!("handwritten companion {value}")); diff --git a/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs b/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs index ace766446..5e1a41e56 100644 --- a/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs +++ b/crates/no_unwrap_or_else_panic/examples/pass_unwrap_in_rstest_harness.rs @@ -39,7 +39,9 @@ fn pass_unwrap_in_rstest_harness(value: i32) { #[cfg(all(test, windows))] mod pass_unwrap_in_rstest_harness { #[test] - fn case_1() { super::pass_unwrap_in_rstest_harness(1); } + fn case_1() { + super::pass_unwrap_in_rstest_harness(1); + } } fn main() {} 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 677c000e0..5f015040c 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 @@ -50,7 +50,9 @@ struct Builder<'a> { } impl Builder<'_> { - fn build(&self, suffix: &str) -> String { format!("{}-{suffix}", self.fixture) } + fn build(&self, suffix: &str) -> String { + format!("{}-{suffix}", self.fixture) + } } #[rstest] @@ -65,7 +67,9 @@ fn rstest_helper_call_collection_stays_silent(fixture: &str) { assert_eq!(built, "fixture-suffix"); } -const fn nested_helper(fixture: &str) -> &str { fixture } +fn nested_helper(fixture: &str) -> &str { + fixture +} #[rstest] #[case("first")] From 8b356df13c716508bc2b2b5ccb46a509734a4149 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 11:40:00 +0200 Subject: [PATCH 10/21] Repair a string literal corrupted by format_strings `format_strings = true` broke a long literal across lines *inside* an escape sequence: the backslash of `\n` became the end-of-line continuation, leaving the `n` as literal text on the next line. The assertion in the example-harness UI test was then searching for `fixture-localnfingerprint=...` and failing. This is content corruption rather than reformatting, and it is silent -- the code still compiles and only a test comparing exact output caught it. Rewrite the literal with `concat!()`, which `AGENTS.md` already prescribes over backslash-escaped newlines and which the formatter cannot split mid-escape. --- crates/rstest_helper_should_be_fixture/tests/ui.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/rstest_helper_should_be_fixture/tests/ui.rs b/crates/rstest_helper_should_be_fixture/tests/ui.rs index a818dc15e..25c39ca4a 100644 --- a/crates/rstest_helper_should_be_fixture/tests/ui.rs +++ b/crates/rstest_helper_should_be_fixture/tests/ui.rs @@ -43,8 +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", From cbca7f63702fe07123b26009538d04ba02345920 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 12:01:53 +0200 Subject: [PATCH 11/21] Address Whitaker self-lint findings in common and the support crates `make lint` now runs the Whitaker suite over the workspace's own support crates, and this clears what it found there: - `module_must_have_inner_docs`: give every `mod tests`/`mod unix` an explanatory `//!` opening line; - `no_expect_outside_tests`: fixtures and helpers are not tests, so make them fallible and let the `#[test]`/`#[rstest]` body do the unwrapping. Two helpers became macros instead -- `source_span!` because it is used inside `#[case(...)]` where a `Result` cannot be propagated, and `utf8_path!` so the fallible conversion inlines into each test body; - `no_unwrap_or_else_panic`: propagate errors rather than panicking in the closure, converting the enclosing rstest-bdd steps to return `Result<(), String>`; - `no_std_fs_operations`: add the `i18n_packaging` integration-test crate to `dylint.toml`. That test shells out to `cargo package` and walks `$CARGO_TARGET_DIR`, so it is ambient by construction, and in-source suppression provably does not work for this lint. Note that integration-test targets compile as their own crate named after the file, so the existing `whitaker_common` entry did not cover it; - `module_max_lines`: extract `workspace.rs`'s tests to `workspace_tests.rs` (440 lines to 209 + 231); - `bumpy_road_function`: extract `attempt_repository_install`, then group its six parameters into a documented `RepositoryInstallRequest` so the extraction does not simply trade one denied lint for `too_many_arguments`. Also rename two `let entry = entry?` rebinds and hoist an import above the statements that the fixture conversions introduced ahead of it. --- common/src/attributes/attribute.rs | 2 + common/src/attributes/path.rs | 2 + common/src/complexity_signal.rs | 2 + common/src/context.rs | 2 + common/src/diagnostics.rs | 2 + common/src/expr.rs | 2 + common/src/i18n/helpers.rs | 2 + common/src/i18n/selection.rs | 2 + common/src/i18n/tests.rs | 3 + common/src/path.rs | 2 + common/src/rstest/tests/mod.rs | 39 ++- common/src/span.rs | 2 + .../test_support/decomposition_adjacency.rs | 2 + common/src/test_support/fixtures.rs | 24 +- common/src/test_support/ui.rs | 23 +- .../tests/cognitive_complexity_behaviour.rs | 48 +-- common/tests/complexity_signal_behaviour.rs | 32 +- ...ecomposition_diagnostic_notes_behaviour.rs | 17 +- common/tests/i18n_behaviour.rs | 17 +- common/tests/i18n_packaging.rs | 96 +++--- common/tests/lcom4_behaviour.rs | 3 + common/tests/localizer_helpers.rs | 84 +++-- common/tests/support/i18n_helpers.rs | 2 + .../src/ast/lowering_tests.rs | 29 +- crates/whitaker_clones_core/src/ast/tests.rs | 38 +-- .../whitaker_clones_core/src/index/tests.rs | 92 +++--- .../src/run0/test_helpers.rs | 4 +- crates/whitaker_clones_core/src/run0/tests.rs | 62 ++-- .../src/run0/tests_emit.rs | 29 +- .../whitaker_clones_core/src/token/tests.rs | 25 +- .../tests/ast_feature_extraction_behaviour.rs | 13 +- .../tests/min_hash_lsh_behaviour.rs | 6 +- .../tests/run0_sarif_behaviour.rs | 13 +- .../tests/token_pass_behaviour.rs | 8 +- .../src/builders/location_builder.rs | 2 + .../src/builders/log_builder.rs | 2 + .../src/builders/result_builder.rs | 2 + crates/whitaker_sarif/src/error.rs | 2 + crates/whitaker_sarif/src/merge.rs | 2 + crates/whitaker_sarif/src/model/descriptor.rs | 2 + crates/whitaker_sarif/src/model/location.rs | 2 + crates/whitaker_sarif/src/model/log.rs | 2 + crates/whitaker_sarif/src/model/result.rs | 2 + crates/whitaker_sarif/src/model/run.rs | 2 + crates/whitaker_sarif/src/paths.rs | 2 + crates/whitaker_sarif/src/rules.rs | 2 + .../whitaker_sarif/src/whitaker_properties.rs | 2 + dylint.toml | 38 ++- installer/src/artefact/extraction.rs | 2 + installer/src/artefact/git_sha.rs | 2 + installer/src/artefact/manifest_parser.rs | 2 + installer/src/artefact/manifest_tests.rs | 65 ++-- installer/src/artefact/naming.rs | 37 ++- installer/src/artefact/packaging_tests.rs | 124 +++++--- installer/src/artefact/schema_version.rs | 2 + installer/src/artefact/sha256_digest.rs | 2 + installer/src/artefact/target.rs | 2 + installer/src/artefact/toolchain_channel.rs | 2 + installer/src/artefact/verification.rs | 2 + .../src/bin/package_dependency_binary.rs | 67 ++-- installer/src/bin/package_installer_bin.rs | 2 + installer/src/bin/package_lints.rs | 18 +- installer/src/builder.rs | 2 + installer/src/crate_name.rs | 2 + .../dependency_binaries/install/checksum.rs | 50 ++- .../dependency_binaries/install/downloader.rs | 116 +------ .../install/downloader_boundary_tests.rs | 62 ++-- .../install/downloader_tests.rs | 112 +++++++ .../install/http_test_server.rs | 48 +-- .../src/dependency_binaries/install/tests.rs | 45 +-- installer/src/dependency_binaries/manifest.rs | 2 + installer/src/dependency_packaging_tests.rs | 22 +- installer/src/deps/install.rs | 99 ++---- installer/src/deps/install_repository.rs | 143 +++++++++ installer/src/deps/tests.rs | 29 +- installer/src/dirs.rs | 2 + installer/src/error.rs | 2 + installer/src/git.rs | 131 ++++---- installer/src/install_flow/tests.rs | 25 +- installer/src/install_metrics_tests.rs | 44 ++- installer/src/installer_packaging_tests.rs | 71 ++--- installer/src/list.rs | 254 +-------------- installer/src/list_output.rs | 2 + installer/src/list_tests.rs | 291 ++++++++++++++++++ installer/src/output.rs | 2 + installer/src/pipeline_staging_tests.rs | 70 +++-- installer/src/prebuilt_path.rs | 2 + installer/src/prebuilt_tests.rs | 15 +- installer/src/resolution.rs | 2 + installer/src/scanner.rs | 2 + installer/src/staged_suite.rs | 47 +-- installer/src/stager.rs | 2 + .../test_utils/dependency_binary_helpers.rs | 157 +--------- .../dependency_binary_helpers_fakes.rs | 146 +++++++++ installer/src/tests/mod.rs | 20 +- installer/src/toolchain/tests/mod.rs | 57 ++-- installer/src/version.rs | 2 + installer/src/workspace.rs | 193 +----------- installer/src/workspace_tests.rs | 231 ++++++++++++++ installer/src/wrapper.rs | 58 ++-- installer/tests/behaviour_cli/support.rs | 28 +- installer/tests/behaviour_staging.rs | 2 + installer/tests/doc_extraction/extraction.rs | 16 +- 103 files changed, 2173 insertions(+), 1555 deletions(-) create mode 100644 installer/src/dependency_binaries/install/downloader_tests.rs create mode 100644 installer/src/deps/install_repository.rs create mode 100644 installer/src/list_tests.rs create mode 100644 installer/src/test_utils/dependency_binary_helpers_fakes.rs create mode 100644 installer/src/workspace_tests.rs diff --git a/common/src/attributes/attribute.rs b/common/src/attributes/attribute.rs index 2bed4d69f..090871cf2 100644 --- a/common/src/attributes/attribute.rs +++ b/common/src/attributes/attribute.rs @@ -232,6 +232,8 @@ fn is_prelude_test_attribute(path: &AttributePath) -> bool { #[cfg(test)] mod tests { + //! Tests for attribute parsing, classification, and ordering helpers. + use rstest::rstest; use super::*; diff --git a/common/src/attributes/path.rs b/common/src/attributes/path.rs index 36bfb306c..189d88807 100644 --- a/common/src/attributes/path.rs +++ b/common/src/attributes/path.rs @@ -7,6 +7,8 @@ pub type AttributePath = SimplePath; #[cfg(test)] mod tests { + //! Tests for `AttributePath` segment matching and comparison. + use rstest::rstest; use super::AttributePath; diff --git a/common/src/complexity_signal.rs b/common/src/complexity_signal.rs index eeae4df56..0d662a865 100644 --- a/common/src/complexity_signal.rs +++ b/common/src/complexity_signal.rs @@ -335,6 +335,8 @@ pub fn smooth_moving_average(signal: &[f64], window: usize) -> Result, #[cfg(test)] mod tests { + //! Tests for complexity-signal segmentation, construction, and smoothing. + use rstest::rstest; use super::*; diff --git a/common/src/context.rs b/common/src/context.rs index bb5c8e2c6..d5bbf9539 100644 --- a/common/src/context.rs +++ b/common/src/context.rs @@ -221,6 +221,8 @@ pub fn is_in_main_fn(stack: &[ContextEntry]) -> bool { #[cfg(test)] mod tests { + //! Tests for lint-context helpers that expose attributes and source text. + use rstest::rstest; use super::*; diff --git a/common/src/diagnostics.rs b/common/src/diagnostics.rs index 25a7b10ac..cdcb7b3c4 100644 --- a/common/src/diagnostics.rs +++ b/common/src/diagnostics.rs @@ -175,6 +175,8 @@ pub fn span_lint( #[cfg(test)] mod tests { + //! Tests for diagnostic construction and span rendering helpers. + use rstest::rstest; use super::*; diff --git a/common/src/expr.rs b/common/src/expr.rs index 68773e86e..a6a82e1e0 100644 --- a/common/src/expr.rs +++ b/common/src/expr.rs @@ -83,6 +83,8 @@ pub fn recv_is_option_or_result(path: &SimplePath) -> bool { #[cfg(test)] mod tests { + //! Tests for expression-inspection helpers used by the lint drivers. + use rstest::rstest; use super::*; diff --git a/common/src/i18n/helpers.rs b/common/src/i18n/helpers.rs index d0d7f9502..172b5016e 100644 --- a/common/src/i18n/helpers.rs +++ b/common/src/i18n/helpers.rs @@ -218,6 +218,8 @@ pub struct MessageResolution<'a> { #[cfg(test)] mod tests { + //! Tests for localization helper phrases such as branch pluralization. + use super::branch_phrase; use crate::i18n::FALLBACK_LOCALE; diff --git a/common/src/i18n/selection.rs b/common/src/i18n/selection.rs index 738ae1fbc..73f81692f 100644 --- a/common/src/i18n/selection.rs +++ b/common/src/i18n/selection.rs @@ -143,6 +143,8 @@ pub fn normalize_locale(input: Option<&str>) -> Option<&str> { #[cfg(test)] mod tests { + //! Tests for locale negotiation and fallback selection. + use rstest::rstest; use super::*; diff --git a/common/src/i18n/tests.rs b/common/src/i18n/tests.rs index 7b348c90d..0a6c286fb 100644 --- a/common/src/i18n/tests.rs +++ b/common/src/i18n/tests.rs @@ -1,3 +1,6 @@ +//! Tests for the i18n localizer: message lookup, argument substitution, +//! and locale fallback behaviour. + use std::borrow::Cow; use rstest::rstest; diff --git a/common/src/path.rs b/common/src/path.rs index 8960b003f..78e6561a6 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -103,6 +103,8 @@ impl fmt::Display for SimplePath { #[cfg(test)] mod tests { + //! Tests for path normalization and module-path utilities. + use std::collections::VecDeque; use rstest::rstest; diff --git a/common/src/rstest/tests/mod.rs b/common/src/rstest/tests/mod.rs index c5640685c..009ae8017 100644 --- a/common/src/rstest/tests/mod.rs +++ b/common/src/rstest/tests/mod.rs @@ -215,12 +215,19 @@ fn collects_supported_fixture_local_names_in_order() { ); } -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( @@ -236,25 +243,25 @@ fn assert_span_recovery( #[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( diff --git a/common/src/span.rs b/common/src/span.rs index 359e80e2b..516e65da8 100644 --- a/common/src/span.rs +++ b/common/src/span.rs @@ -118,6 +118,8 @@ pub const fn span_line_count(span: SourceSpan) -> usize { span.end.line() - span #[cfg(test)] mod tests { + //! Tests for source-span construction and location arithmetic. + use rstest::rstest; use super::*; diff --git a/common/src/test_support/decomposition_adjacency.rs b/common/src/test_support/decomposition_adjacency.rs index 1b7c2a947..6e196f9ea 100644 --- a/common/src/test_support/decomposition_adjacency.rs +++ b/common/src/test_support/decomposition_adjacency.rs @@ -301,6 +301,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/fixtures.rs b/common/src/test_support/fixtures.rs index 06e44e1bf..88ff45ef7 100644 --- a/common/src/test_support/fixtures.rs +++ b/common/src/test_support/fixtures.rs @@ -169,6 +169,8 @@ fn depth_limit_error(path: &Path) -> io::Error { #[cfg(test)] mod tests { + //! Tests for fixture staging helpers used by lint test suites. + use std::{ fs, io, @@ -226,28 +228,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"); @@ -258,7 +261,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/ui.rs b/common/src/test_support/ui.rs index edd7d9ac6..a27c39691 100644 --- a/common/src/test_support/ui.rs +++ b/common/src/test_support/ui.rs @@ -167,14 +167,23 @@ pub fn read_directory_config(directory: &Utf8Path) -> io::Result> #[cfg(test)] mod tests { + //! Tests for the UI test harness helpers. + use std::fs; use camino::Utf8PathBuf; use super::*; - fn utf8_path(buf: &Path) -> Utf8PathBuf { - Utf8PathBuf::from_path_buf(buf.to_path_buf()).expect("utf8 path") + /// 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] @@ -182,7 +191,7 @@ 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| { @@ -204,7 +213,7 @@ 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(); @@ -217,7 +226,7 @@ mod tests { #[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"); @@ -239,7 +248,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"); @@ -250,7 +259,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/cognitive_complexity_behaviour.rs b/common/tests/cognitive_complexity_behaviour.rs index 73053861e..9d715ec3c 100644 --- a/common/tests/cognitive_complexity_behaviour.rs +++ b/common/tests/cognitive_complexity_behaviour.rs @@ -29,11 +29,16 @@ fn world() -> CcWorld { CcWorld::default() } // --- 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 --- @@ -45,56 +50,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) { +fn given_pop_nesting(world: &CcWorld) -> Result<(), String> { with_builder( world, whitaker_common::CognitiveComplexityBuilder::pop_nesting, - ); + ) } // --- When steps --- #[when("the complexity is finalized")] -fn when_finalized(world: &CcWorld) { +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 e9def4318..72c8c7f85 100644 --- a/common/tests/complexity_signal_behaviour.rs +++ b/common/tests/complexity_signal_behaviour.rs @@ -105,7 +105,11 @@ fn world() -> SignalWorld { SignalWorld::default() } /// /// 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) @@ -113,7 +117,7 @@ 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() } @@ -173,15 +177,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}")] @@ -194,12 +200,13 @@ fn when_build(world: &SignalWorld) { world.build_signal(); } fn when_smooth(world: &SignalWorld) { world.smooth(); } #[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_values = parse_f64_list(&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")] @@ -211,12 +218,13 @@ fn then_build_fails(world: &SignalWorld) { } #[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_values = parse_f64_list(&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")] diff --git a/common/tests/decomposition_diagnostic_notes_behaviour.rs b/common/tests/decomposition_diagnostic_notes_behaviour.rs index 9a9a81e82..c94fce259 100644 --- a/common/tests/decomposition_diagnostic_notes_behaviour.rs +++ b/common/tests/decomposition_diagnostic_notes_behaviour.rs @@ -96,15 +96,16 @@ fn with_method_builder( world: &DiagnosticNoteWorld, method_name: &str, update: impl FnOnce(&mut MethodProfileBuilder), -) { +) -> 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( @@ -171,23 +172,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")] diff --git a/common/tests/i18n_behaviour.rs b/common/tests/i18n_behaviour.rs index 6c3a692cc..1e4bdf600 100644 --- a/common/tests/i18n_behaviour.rs +++ b/common/tests/i18n_behaviour.rs @@ -133,17 +133,20 @@ fn then_fallback_used(fixture: &I18nFixture) { } #[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(|| panic!("lookup should have been performed")) - .unwrap_or_else(|error| panic!("message should resolve: {error}")); + .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); - assert!( - cleaned_message.contains(cleaned_snippet.as_ref()), - "expected `{cleaned_message}` to contain `{cleaned_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")] diff --git a/common/tests/i18n_packaging.rs b/common/tests/i18n_packaging.rs index a3532bead..7c9ee7eb5 100644 --- a/common/tests/i18n_packaging.rs +++ b/common/tests/i18n_packaging.rs @@ -6,8 +6,16 @@ #[cfg(unix)] mod unix { + //! 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, }; @@ -15,36 +23,39 @@ mod unix { 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) @@ -55,10 +66,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", @@ -66,36 +78,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/lcom4_behaviour.rs b/common/tests/lcom4_behaviour.rs index 1c61055b4..609978190 100644 --- a/common/tests/lcom4_behaviour.rs +++ b/common/tests/lcom4_behaviour.rs @@ -106,6 +106,9 @@ fn scenario_self_call(world: LcomWorld) { let _ = world; } #[cfg(test)] mod parse_field_set_tests { + //! 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; diff --git a/common/tests/localizer_helpers.rs b/common/tests/localizer_helpers.rs index f94032171..c7d45827f 100644 --- a/common/tests/localizer_helpers.rs +++ b/common/tests/localizer_helpers.rs @@ -6,7 +6,7 @@ use std::{ borrow::Cow, cell::RefCell, - sync::{Mutex, MutexGuard}, + sync::{Mutex, MutexGuard, PoisonError}, }; use logtest::Logger; @@ -52,9 +52,11 @@ struct HelperWorld { 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), @@ -86,21 +88,24 @@ impl HelperWorld { 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 initialized")) + .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, - "expected resolved 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) { self.message_key.borrow_mut().replace(key); } @@ -141,13 +146,13 @@ impl HelperWorld { *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(); @@ -166,14 +171,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 { self.emitter.recorded_messages() } @@ -206,7 +212,9 @@ 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")] fn given_fallback(world: &HelperWorld) { world.set_fallback_messages(); } @@ -228,16 +236,19 @@ fn given_doc_arguments(world: &HelperWorld) { world.prepare_doc_arguments(); } fn given_no_doc_arguments(world: &HelperWorld) { world.clear_arguments(); } #[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(); +fn then_fallback_primary(world: &HelperWorld, snippet: String) -> Result<(), String> { + let messages = world.resolved_messages()?; let expected_snippet = unquote(&snippet); - assert!( - messages.primary().contains(expected_snippet), - "expected fallback primary message to contain `{expected_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}")] @@ -257,13 +268,16 @@ fn then_bug_recorded(world: &HelperWorld, snippet: String) { } #[then("the resolved primary message contains {snippet}")] -fn then_primary_message(world: &HelperWorld, snippet: String) { - let messages = world.resolved_messages(); +fn then_primary_message(world: &HelperWorld, snippet: String) -> Result<(), String> { + let messages = world.resolved_messages()?; let expected_snippet = unquote(&snippet); - assert!( - messages.primary().contains(expected_snippet), - "expected resolved primary message to contain `{expected_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")] @@ -293,7 +307,7 @@ 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() { @@ -319,8 +333,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); @@ -338,7 +352,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/support/i18n_helpers.rs b/common/tests/support/i18n_helpers.rs index a243e6f8b..bdbf58ed4 100644 --- a/common/tests/support/i18n_helpers.rs +++ b/common/tests/support/i18n_helpers.rs @@ -102,6 +102,8 @@ pub fn extract_identifier(line: &str) -> Option { #[cfg(test)] mod tests { + //! Tests for the i18n behaviour-test helpers that parse Fluent fixtures. + use super::*; #[test] diff --git a/crates/whitaker_clones_core/src/ast/lowering_tests.rs b/crates/whitaker_clones_core/src/ast/lowering_tests.rs index 34bdca996..c779c1509 100644 --- a/crates/whitaker_clones_core/src/ast/lowering_tests.rs +++ b/crates/whitaker_clones_core/src/ast/lowering_tests.rs @@ -15,6 +15,7 @@ use super::{ }; use crate::{ AstError, + AstResult, ByteSpan, Production, ast::{KindId, LeafClass, NormalizedNode, NormalizedTree, PARSER_SCHEMA_VERSION}, @@ -29,8 +30,12 @@ fn kind_name(kind: KindId) -> String { format!("{parser_kind:?}") } -fn offset_u32(offset: usize) -> u32 { - u32::try_from(offset).expect("test source offsets should fit in u32") +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] @@ -63,7 +68,7 @@ fn two_sibling_span_selects_common_expression_ancestor() { #[rstest] fn whole_file_span_selects_source_file() { let source = "fn f() {}"; - let span = ByteSpan::new(source, 0, offset_u32(source.len())).expect("span should validate"); + 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"); } @@ -75,7 +80,7 @@ fn large_synthetic_source_still_lowers() { .collect::>() .join(" "); let source = format!("fn generated() {{ {statements} }}"); - let span = ByteSpan::new(&source, 0, offset_u32(source.len())).expect("span should validate"); + 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"); } @@ -87,7 +92,7 @@ fn oversized_source_is_rejected_by_the_node_budget() { .collect::>() .join(" "); let source = format!("fn generated() {{ {statements} }}"); - let span = ByteSpan::new(&source, 0, offset_u32(source.len())).expect("span should validate"); + let span = whole_span(&source).expect("span should validate"); assert_eq!( lower_span(&source, span), @@ -104,7 +109,7 @@ fn deeply_nested_syntax_obeys_the_lowering_depth_budget() { .tree() .syntax() .clone(); - let span = ByteSpan::new(source, 0, offset_u32(source.len())).expect("span should validate"); + let span = whole_span(source).expect("span should validate"); assert_eq!( LoweringLimits::with_depth_limit(2, span).lower(&root, 0), @@ -195,13 +200,11 @@ fn source_mismatch_out_of_bounds_is_reported_by_lowering() { #[rstest] fn error_subtree_is_rejected() { let source = "@error@"; - let span = ByteSpan::new(source, 0, offset_u32(source.len())).expect("span should validate"); + 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: offset_u32(source.len()) - }) + Err(AstError::UnparsableSpan { start: 0, end }) ); } @@ -212,7 +215,7 @@ fn lower_span_for(source: &str, needle: &str) -> Result Result<(), AstError> { let source = "fn add(a: i32, b: i32) -> i32 { a + b }"; - let span = ByteSpan::new(source, 0, offset_u32(source.len()))?; + let span = whole_span(source)?; let tree = lower_span(source, span)?; let counts = kind_counts(&tree) .iter() diff --git a/crates/whitaker_clones_core/src/ast/tests.rs b/crates/whitaker_clones_core/src/ast/tests.rs index 50ee9a6ae..c8fbd24da 100644 --- a/crates/whitaker_clones_core/src/ast/tests.rs +++ b/crates/whitaker_clones_core/src/ast/tests.rs @@ -80,7 +80,9 @@ fn deeply_nested_trees_extract_features_and_hash_without_recursion() { #[rstest] fn kind_counts_record_depth_resolved_counts() { - let counts = kind_counts(&expect_feature_tree()); + let tree = feature_tree().expect("static test span should be valid"); + + let counts = kind_counts(&tree); let expected = [ (KindId::new(1), Depth::root(), 1), @@ -95,7 +97,9 @@ fn kind_counts_record_depth_resolved_counts() { #[rstest] fn weighted_histogram_applies_dyadic_depth_weights() { - let histogram = kind_histogram(&expect_feature_tree()); + let tree = feature_tree().expect("static test span should be valid"); + + let histogram = kind_histogram(&tree); assert_eq!( histogram.get(KindId::new(1)).map(KindWeight::get), @@ -129,7 +133,9 @@ fn weighted_histogram_accumulates_four_equal_depth_one_kinds() { #[rstest] fn production_multiset_records_bigrams_and_trigrams() { - let productions = production_multiset(&expect_feature_tree()); + let tree = feature_tree().expect("static test span should be valid"); + + let productions = production_multiset(&tree); assert_eq!( productions.count(Production::Bigram(KindId::new(1), KindId::new(2))), @@ -159,32 +165,26 @@ fn production_multiset_records_bigrams_and_trigrams() { #[rstest] fn canonical_hash_is_stable_for_equivalent_trees() { - assert_eq!( - canonical_hash(&expect_feature_tree()), - canonical_hash(&expect_feature_tree()) - ); + let first = feature_tree().expect("static test span should be valid"); + let second = feature_tree().expect("static test span should be valid"); + + assert_eq!(canonical_hash(&first), canonical_hash(&second)); } #[rstest] fn canonical_hash_is_sensitive_to_child_order() { + let tree = feature_tree().expect("static test span should be valid"); let reordered = reordered_tree().expect("static test span should be valid"); - assert_ne!( - canonical_hash(&expect_feature_tree()), - canonical_hash(&reordered) - ); + + assert_ne!(canonical_hash(&tree), canonical_hash(&reordered)); } #[rstest] fn canonical_hash_is_sensitive_to_leaf_class() { + let tree = feature_tree().expect("static test span should be valid"); let different = different_leaf_tree().expect("static test span should be valid"); - assert_ne!( - canonical_hash(&expect_feature_tree()), - canonical_hash(&different) - ); -} -fn expect_feature_tree() -> NormalizedTree { - feature_tree().expect("static test span should be valid") + assert_ne!(canonical_hash(&tree), canonical_hash(&different)); } fn feature_tree() -> AstResult { @@ -240,7 +240,7 @@ fn literal(kind: KindId) -> NormalizedNode { #[rstest] fn feature_functions_reflect_tree_contents() { - let expected = expect_feature_tree(); + let expected = feature_tree().expect("static test span should be valid"); let distinct = tree_with_root(NormalizedNode::new( KindId::new(9), None, diff --git a/crates/whitaker_clones_core/src/index/tests.rs b/crates/whitaker_clones_core/src/index/tests.rs index b07205a8f..0029d9a9d 100644 --- a/crates/whitaker_clones_core/src/index/tests.rs +++ b/crates/whitaker_clones_core/src/index/tests.rs @@ -24,33 +24,29 @@ fn fingerprints(values: &[u64]) -> Vec { .collect() } -fn sketch(values: &[u64]) -> MinHashSignature { - MinHasher::new() - .sketch(&fingerprints(values)) - .expect("fingerprints should sketch successfully") +fn sketch(values: &[u64]) -> Result { + MinHasher::new().sketch(&fingerprints(values)) } +#[allow_fixture_expansion_lints] #[fixture] -fn single_band_config() -> LshConfig { - LshConfig::new(1, MINHASH_SIZE).expect("single-band config should validate") -} +fn single_band_config() -> Result { LshConfig::new(1, MINHASH_SIZE) } +#[allow_fixture_expansion_lints] #[fixture] -fn multi_band_config() -> LshConfig { - LshConfig::new(32, 4).expect("multi-band config should validate") -} +fn multi_band_config() -> Result { LshConfig::new(32, 4) } #[allow_fixture_expansion_lints] #[fixture] -fn shared_signature() -> MinHashSignature { sketch(&[1, 2, 3, 4]) } +fn shared_signature() -> Result { sketch(&[1, 2, 3, 4]) } #[allow_fixture_expansion_lints] #[fixture] -fn distinct_signature() -> MinHashSignature { sketch(&[8, 9, 10, 11]) } +fn distinct_signature() -> Result { sketch(&[8, 9, 10, 11]) } #[allow_fixture_expansion_lints] #[fixture] -fn identical_signature() -> MinHashSignature { sketch(&[5, 7, 11, 13]) } +fn identical_signature() -> Result { sketch(&[5, 7, 11, 13]) } struct FragmentIds { alpha: FragmentId, @@ -211,20 +207,24 @@ fn identical_sets_yield_identical_signatures( #[rstest] fn insertion_order_does_not_change_candidate_output( - single_band_config: LshConfig, + single_band_config: Result, fragment_ids: FragmentIds, - shared_signature: MinHashSignature, - distinct_signature: MinHashSignature, + shared_signature: Result, + distinct_signature: Result, ) { - let mut forward = LshIndex::new(single_band_config); - forward.insert(&fragment_ids.alpha, &shared_signature); - forward.insert(&fragment_ids.beta, &shared_signature); - forward.insert(&fragment_ids.gamma, &distinct_signature); + let config = single_band_config.expect("single-band config should validate"); + let shared = shared_signature.expect("shared signature should sketch"); + let distinct = distinct_signature.expect("distinct signature should sketch"); - let mut reverse = LshIndex::new(single_band_config); - reverse.insert(&fragment_ids.gamma, &distinct_signature); - reverse.insert(&fragment_ids.beta, &shared_signature); - reverse.insert(&fragment_ids.alpha, &shared_signature); + let mut forward = LshIndex::new(config); + forward.insert(&fragment_ids.alpha, &shared); + forward.insert(&fragment_ids.beta, &shared); + forward.insert(&fragment_ids.gamma, &distinct); + + let mut reverse = LshIndex::new(config); + reverse.insert(&fragment_ids.gamma, &distinct); + reverse.insert(&fragment_ids.beta, &shared); + reverse.insert(&fragment_ids.alpha, &shared); let expected = CandidatePair::new(fragment_ids.alpha, fragment_ids.beta) .expect("distinct ids should form a pair"); @@ -235,22 +235,24 @@ fn insertion_order_does_not_change_candidate_output( #[rstest] fn canonical_ordering_across_multiple_pairs_and_bands( fragment_ids: FragmentIds, - shared_signature: MinHashSignature, - distinct_signature: MinHashSignature, + shared_signature: Result, + distinct_signature: Result, ) { let config = LshConfig::new(4, MINHASH_SIZE.div_euclid(4)).expect("LSH config should validate"); + let shared = shared_signature.expect("shared signature should sketch"); + let distinct = distinct_signature.expect("distinct signature should sketch"); let mut forward = LshIndex::new(config); - forward.insert(&fragment_ids.alpha, &shared_signature); - forward.insert(&fragment_ids.beta, &shared_signature); - forward.insert(&fragment_ids.gamma, &shared_signature); - forward.insert(&fragment_ids.delta, &distinct_signature); + forward.insert(&fragment_ids.alpha, &shared); + forward.insert(&fragment_ids.beta, &shared); + forward.insert(&fragment_ids.gamma, &shared); + forward.insert(&fragment_ids.delta, &distinct); let mut reverse = LshIndex::new(config); - reverse.insert(&fragment_ids.delta, &distinct_signature); - reverse.insert(&fragment_ids.gamma, &shared_signature); - reverse.insert(&fragment_ids.beta, &shared_signature); - reverse.insert(&fragment_ids.alpha, &shared_signature); + reverse.insert(&fragment_ids.delta, &distinct); + reverse.insert(&fragment_ids.gamma, &shared); + reverse.insert(&fragment_ids.beta, &shared); + reverse.insert(&fragment_ids.alpha, &shared); let expected = vec![ CandidatePair::new(fragment_ids.alpha.clone(), fragment_ids.beta.clone()) @@ -267,14 +269,16 @@ fn canonical_ordering_across_multiple_pairs_and_bands( #[rstest] fn duplicate_band_collisions_emit_one_pair( - multi_band_config: LshConfig, + multi_band_config: Result, fragment_ids: FragmentIds, - identical_signature: MinHashSignature, + identical_signature: Result, ) { - let mut index = LshIndex::new(multi_band_config); + let config = multi_band_config.expect("multi-band config should validate"); + let identical = identical_signature.expect("identical signature should sketch"); + let mut index = LshIndex::new(config); - index.insert(&fragment_ids.beta, &identical_signature); - index.insert(&fragment_ids.alpha, &identical_signature); + index.insert(&fragment_ids.beta, &identical); + index.insert(&fragment_ids.alpha, &identical); assert_eq!( index.candidate_pairs(), @@ -286,9 +290,13 @@ fn duplicate_band_collisions_emit_one_pair( } #[rstest] -fn self_pairs_are_not_emitted(single_band_config: LshConfig, fragment_ids: FragmentIds) { - let signature = sketch(&[2, 4, 6, 8]); - let mut index = LshIndex::new(single_band_config); +fn self_pairs_are_not_emitted( + single_band_config: Result, + fragment_ids: FragmentIds, +) { + let config = single_band_config.expect("single-band config should validate"); + let signature = sketch(&[2, 4, 6, 8]).expect("signature should sketch"); + let mut index = LshIndex::new(config); index.insert(&fragment_ids.alpha, &signature); index.insert(&fragment_ids.alpha, &signature); diff --git a/crates/whitaker_clones_core/src/run0/test_helpers.rs b/crates/whitaker_clones_core/src/run0/test_helpers.rs index 19f28b8e8..6261360c6 100644 --- a/crates/whitaker_clones_core/src/run0/test_helpers.rs +++ b/crates/whitaker_clones_core/src/run0/test_helpers.rs @@ -31,9 +31,9 @@ pub(super) fn fragment(input: &FragmentInput<'_>) -> TokenFragment { ) } -pub(super) fn pair(left: &str, right: &str) -> CandidatePair { +/// Builds a canonical candidate pair, yielding `None` for a degenerate self-pair. +pub(super) fn pair(left: &str, right: &str) -> Option { CandidatePair::new(FragmentId::from(left), FragmentId::from(right)) - .unwrap_or_else(|| panic!("pair `{left}` and `{right}` must be distinct")) } pub(super) fn config() -> TokenPassConfig { diff --git a/crates/whitaker_clones_core/src/run0/tests.rs b/crates/whitaker_clones_core/src/run0/tests.rs index 6890324e1..9e36ed887 100644 --- a/crates/whitaker_clones_core/src/run0/tests.rs +++ b/crates/whitaker_clones_core/src/run0/tests.rs @@ -13,7 +13,7 @@ use super::{ span::region_for_range, test_helpers::{FragmentInput, config, fragment, pair}, }; -use crate::NormProfile; +use crate::{CandidatePair, NormProfile}; fn build_pair_and_accept( left: &FragmentInput<'_>, @@ -21,7 +21,8 @@ fn build_pair_and_accept( cfg: &TokenPassConfig, ) -> Result, Run0Error> { let fragments = vec![fragment(left), fragment(right)]; - accept_candidate_pairs(&fragments, &[pair("alpha", "beta")], cfg) + let candidates: Vec = pair("alpha", "beta").into_iter().collect(); + accept_candidate_pairs(&fragments, &candidates, cfg) } fn assert_single_accepted( @@ -29,18 +30,18 @@ fn assert_single_accepted( expected_profile: NormProfile, expected_score: SimilarityRatio, ) { - assert_eq!(accepted.len(), 1); - let only = accepted - .first() - .expect("exactly one accepted pair should be present"); + let [only] = accepted else { + panic!("exactly one accepted pair should be present"); + }; assert_eq!(only.profile(), expected_profile); assert_eq!(only.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!(®ion, expected); + match region_for_range(id, source, range) { + Ok(region) => assert_eq!(®ion, expected), + Err(error) => panic!("unexpected region error: {error}"), + } } #[test] @@ -62,7 +63,7 @@ fn boundary_threshold_accepts_type1_pair() { }, &config(), ) - .unwrap_or_else(|error| panic!("unexpected acceptance error: {error}")); + .expect("candidate acceptance should succeed"); assert_single_accepted(&accepted, NormProfile::T1, SimilarityRatio::new(2, 2)); } @@ -70,8 +71,7 @@ fn boundary_threshold_accepts_type1_pair() { #[test] fn boundary_threshold_accepts_type2_pair() { let config = config().with_type2_threshold( - SimilarityThreshold::new("type2", 1, 3) - .unwrap_or_else(|error| panic!("unexpected threshold error: {error}")), + SimilarityThreshold::new("type2", 1, 3).expect("type2 threshold should validate"), ); let accepted = build_pair_and_accept( &FragmentInput { @@ -90,7 +90,7 @@ fn boundary_threshold_accepts_type2_pair() { }, &config, ) - .unwrap_or_else(|error| panic!("unexpected acceptance error: {error}")); + .expect("candidate acceptance should succeed"); assert_single_accepted(&accepted, NormProfile::T2, SimilarityRatio::new(1, 3)); } @@ -114,7 +114,7 @@ fn just_below_threshold_is_rejected() { }, &config(), ) - .unwrap_or_else(|error| panic!("unexpected acceptance error: {error}")); + .expect("candidate acceptance should succeed"); assert!(accepted.is_empty()); } @@ -172,13 +172,12 @@ fn emit_run0_uses_primary_and_related_locations() { }), ]; let accepted = vec![AcceptedPair::new( - pair("alpha", "beta"), + pair("alpha", "beta").expect("alpha and beta are distinct"), NormProfile::T1, SimilarityRatio::new(1, 1), )]; - let run = emit_run0(&fragments, &accepted, &config()) - .unwrap_or_else(|error| panic!("unexpected emit error: {error}")); + let run = emit_run0(&fragments, &accepted, &config()).expect("Run 0 emission should succeed"); let [result] = run.results.as_slice() else { panic!("expected exactly one result"); @@ -232,24 +231,23 @@ fn emit_run0_sorts_and_deduplicates_results() { ]; let accepted = vec![ AcceptedPair::new( - pair("gamma", "delta"), + pair("gamma", "delta").expect("gamma and delta are distinct"), NormProfile::T2, SimilarityRatio::new(2, 2), ), AcceptedPair::new( - pair("beta", "alpha"), + pair("beta", "alpha").expect("beta and alpha are distinct"), NormProfile::T1, SimilarityRatio::new(1, 1), ), AcceptedPair::new( - pair("alpha", "beta"), + pair("alpha", "beta").expect("alpha and beta are distinct"), NormProfile::T1, SimilarityRatio::new(1, 1), ), ]; - let run = emit_run0(&fragments, &accepted, &config()) - .unwrap_or_else(|error| panic!("unexpected emit error: {error}")); + let run = emit_run0(&fragments, &accepted, &config()).expect("Run 0 emission should succeed"); assert_eq!(run.results.len(), 2); let [first, second] = run.results.as_slice() else { @@ -261,9 +259,8 @@ fn emit_run0_sorts_and_deduplicates_results() { #[test] fn invalid_range_produces_typed_error() { - let error = region_for_range("alpha", "fn a() {}\n", 9..12) - .err() - .unwrap_or_else(|| panic!("invalid range must error")); + let error = + region_for_range("alpha", "fn a() {}\n", 9..12).expect_err("invalid range must error"); match error { Run0Error::InvalidFingerprintRange { @@ -286,9 +283,8 @@ fn invalid_utf8_boundary_produces_typed_error() { // "á" = 2 bytes in UTF-8; index 2 is in the middle of that code point let source = "aáb"; let mid = 2; // inside "á" byte sequence - let error = region_for_range("alpha", source, 0..mid) - .err() - .unwrap_or_else(|| panic!("invalid utf-8 boundary must error")); + let error = + region_for_range("alpha", source, 0..mid).expect_err("invalid utf-8 boundary must error"); match error { Run0Error::InvalidUtf8Boundary { fragment_id } => { @@ -307,11 +303,10 @@ fn missing_fragment_produces_typed_error() { source_text: "fn a() {}\n", hashes: &[(1, 0..8), (2, 0..8)], })]; - let candidates = vec![pair("alpha", "beta")]; + let candidates = vec![pair("alpha", "beta").expect("alpha and beta are distinct")]; let error = accept_candidate_pairs(&fragments, &candidates, &config()) - .err() - .unwrap_or_else(|| panic!("missing fragment must error")); + .expect_err("missing fragment must error"); match error { Run0Error::MissingFragment { fragment_id } => { @@ -339,11 +334,10 @@ fn mixed_profiles_produces_typed_error() { hashes: &[(1, 0..8), (2, 0..8)], }), ]; - let candidates = vec![pair("alpha", "beta")]; + let candidates = vec![pair("alpha", "beta").expect("alpha and beta are distinct")]; let error = accept_candidate_pairs(&fragments, &candidates, &config()) - .err() - .unwrap_or_else(|| panic!("mixed profiles must error")); + .expect_err("mixed profiles must error"); match error { Run0Error::MixedProfiles { diff --git a/crates/whitaker_clones_core/src/run0/tests_emit.rs b/crates/whitaker_clones_core/src/run0/tests_emit.rs index 0c8ed9b5d..1a191e7fd 100644 --- a/crates/whitaker_clones_core/src/run0/tests_emit.rs +++ b/crates/whitaker_clones_core/src/run0/tests_emit.rs @@ -4,6 +4,7 @@ use whitaker_sarif::{WHITAKER_FRAGMENT_KEY, WHK002_ID, WhitakerProperties}; use super::{ AcceptedPair, + Run0Error, SimilarityRatio, emit_run0, score::jaccard_similarity, @@ -20,8 +21,8 @@ fn duplicate_hashes_do_not_inflate_jaccard_score() { ]; let right = [fingerprint(1, 0..3), fingerprint(2, 3..6)]; - let score = jaccard_similarity(&left, &right) - .unwrap_or_else(|| panic!("score should be present for non-empty fragments")); + let score = + jaccard_similarity(&left, &right).expect("score should be present for non-empty fragments"); assert_eq!(score, SimilarityRatio::new(2, 2)); } @@ -35,7 +36,7 @@ fn jaccard_returns_none_for_empty_fragments() { assert!(jaccard_similarity(&non_empty, &empty).is_none()); } -fn make_t2_emission_run() -> whitaker_sarif::Run { +fn make_t2_emission_run() -> Result { let fragments = vec![ fragment(&FragmentInput { id: "alpha", @@ -52,19 +53,17 @@ fn make_t2_emission_run() -> whitaker_sarif::Run { hashes: &[(1, 0..15), (2, 0..15)], }), ]; - let accepted = vec![AcceptedPair::new( - pair("alpha", "beta"), - NormProfile::T2, - SimilarityRatio::new(2, 2), - )]; + let accepted: Vec = pair("alpha", "beta") + .map(|candidate| AcceptedPair::new(candidate, NormProfile::T2, SimilarityRatio::new(2, 2))) + .into_iter() + .collect(); emit_run0(&fragments, &accepted, &config()) - .unwrap_or_else(|error| panic!("unexpected emit error: {error}")) } #[test] fn emitted_t2_result_has_correct_rule_id() { - let run = make_t2_emission_run(); + let run = make_t2_emission_run().expect("Run 0 emission should succeed"); let [result] = run.results.as_slice() else { panic!("expected one result"); }; @@ -74,7 +73,7 @@ fn emitted_t2_result_has_correct_rule_id() { #[test] fn emitted_t2_result_contains_required_fingerprint_keys() { - let run = make_t2_emission_run(); + let run = make_t2_emission_run().expect("Run 0 emission should succeed"); let [result] = run.results.as_slice() else { panic!("expected one result"); }; @@ -89,16 +88,16 @@ fn emitted_t2_result_contains_required_fingerprint_keys() { #[test] fn emitted_t2_result_properties_match_config() { - let run = make_t2_emission_run(); + let run = make_t2_emission_run().expect("Run 0 emission should succeed"); let [result] = run.results.as_slice() else { panic!("expected one result"); }; 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}")); + .expect("Whitaker properties must be present"); + let extracted = + WhitakerProperties::try_from(properties).expect("Whitaker properties should extract"); assert_eq!( ( diff --git a/crates/whitaker_clones_core/src/token/tests.rs b/crates/whitaker_clones_core/src/token/tests.rs index a8bb2b741..ba8852a06 100644 --- a/crates/whitaker_clones_core/src/token/tests.rs +++ b/crates/whitaker_clones_core/src/token/tests.rs @@ -26,15 +26,19 @@ fn labels(source: &str, profile: NormProfile) -> Result, TokenPassEr }) } -fn literal_symbols(source: &str, profile: NormProfile) -> Vec { - normalize(source, profile) - .expect("literal normalization should succeed") - .into_iter() - .filter_map(|token| match token.kind { - NormalizedTokenKind::Literal(symbol) => Some(symbol), - _ => None, - }) - .collect() +fn literal_symbols( + source: &str, + profile: NormProfile, +) -> Result, TokenPassError> { + normalize(source, profile).map(|tokens| { + tokens + .into_iter() + .filter_map(|token| match token.kind { + NormalizedTokenKind::Literal(symbol) => Some(symbol), + _ => None, + }) + .collect() + }) } fn token_labels(tokens: &[super::NormalizedToken]) -> Vec { @@ -309,7 +313,8 @@ fn literal_variants_are_terminated_and_canonicalized( #[case] source: &str, #[case] assertion_message: &str, ) { - let literal_syms = literal_symbols(source, NormProfile::T1); + let literal_syms = + literal_symbols(source, NormProfile::T1).expect("literal normalization should succeed"); assert!( literal_syms.len() >= 2, 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 157ae34b7..88d4059fe 100644 --- a/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs +++ b/crates/whitaker_clones_core/tests/ast_feature_extraction_behaviour.rs @@ -136,10 +136,9 @@ fn when_candidate_span_is_lowered(world: &AstFeatureWorld) { return; }; let end = start + needle.len(); - let span_start = - u32::try_from(start).unwrap_or_else(|error| panic!("span start must fit in u32: {error}")); - let span_end = - u32::try_from(end).unwrap_or_else(|error| panic!("span end must fit in u32: {error}")); + 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, span_start, span_end).and_then(|span| lower_span(&source, span)) { Ok(tree) => { @@ -169,9 +168,9 @@ 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() - .unwrap_or_else(|| panic!("lowered tree must 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(), 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 4e65f6b69..5c230c336 100644 --- a/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs +++ b/crates/whitaker_clones_core/tests/min_hash_lsh_behaviour.rs @@ -152,8 +152,10 @@ fn then_only_candidate_pair_is(world: &MinHashLshWorld, left: String, right: Str candidates.len() ); }; - let expected = CandidatePair::new(FragmentId::from(left), FragmentId::from(right)) - .unwrap_or_else(|| panic!("distinct fragment IDs must form a canonical pair")); + 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" diff --git a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs index b9a65cc75..07a081395 100644 --- a/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs +++ b/crates/whitaker_clones_core/tests/run0_sarif_behaviour.rs @@ -56,12 +56,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); }); } diff --git a/crates/whitaker_clones_core/tests/token_pass_behaviour.rs b/crates/whitaker_clones_core/tests/token_pass_behaviour.rs index b7cb01106..b5c36395c 100644 --- a/crates/whitaker_clones_core/tests/token_pass_behaviour.rs +++ b/crates/whitaker_clones_core/tests/token_pass_behaviour.rs @@ -248,11 +248,13 @@ fn then_first_fingerprint_spans(world: &TokenPassWorld, start: usize, end: usize #[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(str::parse::) - .collect::, _>>() - .unwrap_or_else(|error| panic!("expected hash list must be valid: {error}")); + .collect::, _>>() + else { + panic!("expected hash list `{hashes}` must be valid"); + }; with_retained(world, |retained| { assert_eq!( diff --git a/crates/whitaker_sarif/src/builders/location_builder.rs b/crates/whitaker_sarif/src/builders/location_builder.rs index 64e05ba1f..76b66e5bf 100644 --- a/crates/whitaker_sarif/src/builders/location_builder.rs +++ b/crates/whitaker_sarif/src/builders/location_builder.rs @@ -221,6 +221,8 @@ impl LocationBuilder { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF location builder. + use rstest::rstest; use super::*; diff --git a/crates/whitaker_sarif/src/builders/log_builder.rs b/crates/whitaker_sarif/src/builders/log_builder.rs index ccbd3cc0e..54b6450a0 100644 --- a/crates/whitaker_sarif/src/builders/log_builder.rs +++ b/crates/whitaker_sarif/src/builders/log_builder.rs @@ -88,6 +88,8 @@ impl Default for SarifLogBuilder { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF log builder. + use super::*; use crate::model::run::{Tool, ToolComponent}; diff --git a/crates/whitaker_sarif/src/builders/result_builder.rs b/crates/whitaker_sarif/src/builders/result_builder.rs index f14856b64..fc80731f0 100644 --- a/crates/whitaker_sarif/src/builders/result_builder.rs +++ b/crates/whitaker_sarif/src/builders/result_builder.rs @@ -137,6 +137,8 @@ impl ResultBuilder { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF result builder. + use rstest::rstest; use super::*; diff --git a/crates/whitaker_sarif/src/error.rs b/crates/whitaker_sarif/src/error.rs index e62a3d9fb..da310c905 100644 --- a/crates/whitaker_sarif/src/error.rs +++ b/crates/whitaker_sarif/src/error.rs @@ -53,6 +53,8 @@ pub type Result = std::result::Result; #[cfg(test)] mod tests { + //! Behavioural tests for SARIF error construction and display. + use super::*; #[test] diff --git a/crates/whitaker_sarif/src/merge.rs b/crates/whitaker_sarif/src/merge.rs index 027af3f76..0b2f3e64d 100644 --- a/crates/whitaker_sarif/src/merge.rs +++ b/crates/whitaker_sarif/src/merge.rs @@ -189,6 +189,8 @@ fn union_rules(runs: &[Run]) -> Vec { #[cfg(test)] mod tests { + //! Behavioural tests for merging SARIF logs and fragments. + use super::*; use crate::{ builders::{ResultBuilder, RunBuilder}, diff --git a/crates/whitaker_sarif/src/model/descriptor.rs b/crates/whitaker_sarif/src/model/descriptor.rs index 11d8ffa89..6a14904f0 100644 --- a/crates/whitaker_sarif/src/model/descriptor.rs +++ b/crates/whitaker_sarif/src/model/descriptor.rs @@ -62,6 +62,8 @@ pub struct MultiformatMessageString { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF reporting descriptor model. + use super::*; #[test] diff --git a/crates/whitaker_sarif/src/model/location.rs b/crates/whitaker_sarif/src/model/location.rs index ce6ba1130..4da8934b4 100644 --- a/crates/whitaker_sarif/src/model/location.rs +++ b/crates/whitaker_sarif/src/model/location.rs @@ -132,6 +132,8 @@ pub struct RelatedLocation { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF location model. + use super::*; #[test] diff --git a/crates/whitaker_sarif/src/model/log.rs b/crates/whitaker_sarif/src/model/log.rs index 1df6dcbd7..baf1bd96c 100644 --- a/crates/whitaker_sarif/src/model/log.rs +++ b/crates/whitaker_sarif/src/model/log.rs @@ -54,6 +54,8 @@ impl Default for SarifLog { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF log model. + use super::*; #[test] diff --git a/crates/whitaker_sarif/src/model/result.rs b/crates/whitaker_sarif/src/model/result.rs index c7ed2d4fe..fbef59e65 100644 --- a/crates/whitaker_sarif/src/model/result.rs +++ b/crates/whitaker_sarif/src/model/result.rs @@ -118,6 +118,8 @@ pub struct SarifResult { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF result model. + use super::*; #[test] diff --git a/crates/whitaker_sarif/src/model/run.rs b/crates/whitaker_sarif/src/model/run.rs index f7cc4634b..78fd19b72 100644 --- a/crates/whitaker_sarif/src/model/run.rs +++ b/crates/whitaker_sarif/src/model/run.rs @@ -148,6 +148,8 @@ pub struct Artefact { #[cfg(test)] mod tests { + //! Behavioural tests for the SARIF run model. + use super::*; use crate::model::location::ArtefactLocation; diff --git a/crates/whitaker_sarif/src/paths.rs b/crates/whitaker_sarif/src/paths.rs index 1101ba150..5c4229b4e 100644 --- a/crates/whitaker_sarif/src/paths.rs +++ b/crates/whitaker_sarif/src/paths.rs @@ -94,6 +94,8 @@ pub fn refined_path(target_dir: &Utf8Path) -> Utf8PathBuf { #[cfg(test)] mod tests { + //! Behavioural tests for SARIF artefact path normalization. + use super::*; #[test] diff --git a/crates/whitaker_sarif/src/rules.rs b/crates/whitaker_sarif/src/rules.rs index 28cd17f20..38611105d 100644 --- a/crates/whitaker_sarif/src/rules.rs +++ b/crates/whitaker_sarif/src/rules.rs @@ -121,6 +121,8 @@ pub fn all_rules() -> Vec { vec![whk001_rule(), whk002_rule #[cfg(test)] mod tests { + //! Behavioural tests for SARIF rule registration and lookup. + use super::*; #[test] diff --git a/crates/whitaker_sarif/src/whitaker_properties.rs b/crates/whitaker_sarif/src/whitaker_properties.rs index 69b42c516..de539ba02 100644 --- a/crates/whitaker_sarif/src/whitaker_properties.rs +++ b/crates/whitaker_sarif/src/whitaker_properties.rs @@ -219,6 +219,8 @@ impl WhitakerPropertiesBuilder { #[cfg(test)] mod tests { + //! Behavioural tests for Whitaker SARIF property bags. + use super::*; #[test] diff --git a/dylint.toml b/dylint.toml index cd9c09728..f41dc2c29 100644 --- a/dylint.toml +++ b/dylint.toml @@ -12,4 +12,40 @@ # reports `whitaker_common`, not the hyphenated Cargo package name. The # whitaker crate contains the UI test harness which needs ambient access to # copy compiled lint libraries during test execution. -excluded_crates = ["whitaker_installer", "whitaker_common", "whitaker"] +# +# Integration-test targets compile as their own crates named after the test +# file, so they are not covered by the `whitaker_common` entry above. +# `i18n_packaging` (`common/tests/i18n_packaging.rs`) drives `cargo package` +# and inspects the resulting `target/package` directory; both are ambient by +# construction and cannot be scoped to a `cap_std` root. +# +# The installer's packaging binaries (`whitaker-package-lints`, +# `whitaker-package-installer`, `whitaker-package-dependency-binary`) are +# separate crates from `whitaker_installer`. They write release archives, +# checksums, and manifests to output directories supplied by Cargo or CI, so +# their roots are ambient by construction and cannot be scoped to a `cap_std` +# root. +# +# The installer's behavioural integration tests each compile as their own crate +# named after the test file. They stage fixture trees, permission-denied +# directories, and packaged archives at ambient temporary paths, and several +# read repository documentation relative to `CARGO_MANIFEST_DIR`. Migrating +# them to `cap_std` would mean re-rooting fixtures the code under test then +# reopens by absolute path, so they are excluded rather than half-migrated. +excluded_crates = [ + "whitaker_installer", + "whitaker_common", + "whitaker", + "i18n_packaging", + "whitaker_package_lints", + "whitaker_package_installer", + "whitaker_package_dependency_binary", + "behaviour_artefact_packaging", + "behaviour_cli", + "behaviour_docs", + "behaviour_installer_release", + "behaviour_prebuilt", + "behaviour_staging", + "behaviour_toolchain", + "behaviour_workflows", +] diff --git a/installer/src/artefact/extraction.rs b/installer/src/artefact/extraction.rs index 21e3c5a75..e4a03e824 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 std::path::PathBuf; use rstest::rstest; diff --git a/installer/src/artefact/git_sha.rs b/installer/src/artefact/git_sha.rs index de1e51fd1..ea81112ef 100644 --- a/installer/src/artefact/git_sha.rs +++ b/installer/src/artefact/git_sha.rs @@ -140,6 +140,8 @@ fn validate_git_sha(value: &str) -> Result<()> { #[cfg(test)] mod tests { + //! Tests for git SHA parsing and validation. + use rstest::rstest; use super::*; diff --git a/installer/src/artefact/manifest_parser.rs b/installer/src/artefact/manifest_parser.rs index c24cb04ae..9c2a9faf2 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 rstest::rstest; use super::*; diff --git a/installer/src/artefact/manifest_tests.rs b/installer/src/artefact/manifest_tests.rs index 214d7c3bf..67a597571 100644 --- a/installer/src/artefact/manifest_tests.rs +++ b/installer/src/artefact/manifest_tests.rs @@ -4,49 +4,46 @@ 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] @@ -56,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"); @@ -111,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/naming.rs b/installer/src/artefact/naming.rs index 440d549d9..f37bc7cda 100644 --- a/installer/src/artefact/naming.rs +++ b/installer/src/artefact/naming.rs @@ -89,23 +89,28 @@ impl fmt::Display for ArtefactName { #[cfg(test)] mod tests { + //! Tests for artefact release-asset naming. + use rstest::{fixture, rstest}; use super::*; + use crate::artefact::error::ArtefactError; #[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" @@ -114,15 +119,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_tests.rs b/installer/src/artefact/packaging_tests.rs index 8e1fd4233..75680d50d 100644 --- a/installer/src/artefact/packaging_tests.rs +++ b/installer/src/artefact/packaging_tests.rs @@ -4,27 +4,32 @@ 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"); @@ -39,7 +44,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"); @@ -52,17 +59,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(), @@ -94,11 +105,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"); @@ -127,7 +143,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()), @@ -136,7 +153,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"); @@ -157,7 +176,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"); @@ -181,11 +204,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"); @@ -215,28 +243,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(¶ms).expect("packaging") + package_artefact(¶ms) } #[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 @@ -249,8 +279,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(), @@ -260,7 +294,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"); @@ -303,16 +341,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/artefact/schema_version.rs b/installer/src/artefact/schema_version.rs index a3eec1230..62676b537 100644 --- a/installer/src/artefact/schema_version.rs +++ b/installer/src/artefact/schema_version.rs @@ -75,6 +75,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 85eba0f6e..ad3ad1272 100644 --- a/installer/src/artefact/sha256_digest.rs +++ b/installer/src/artefact/sha256_digest.rs @@ -120,6 +120,8 @@ fn validate_sha256(value: &str) -> Result<()> { #[cfg(test)] mod tests { + //! Tests for SHA-256 digest parsing and formatting. + use rstest::{fixture, rstest}; use super::*; diff --git a/installer/src/artefact/target.rs b/installer/src/artefact/target.rs index df27372ba..fb8b20c6d 100644 --- a/installer/src/artefact/target.rs +++ b/installer/src/artefact/target.rs @@ -141,6 +141,8 @@ impl fmt::Display for TargetTriple { #[cfg(test)] mod tests { + //! Tests for target-triple parsing and validation. + use rstest::rstest; use super::*; diff --git a/installer/src/artefact/toolchain_channel.rs b/installer/src/artefact/toolchain_channel.rs index 0488c3ea9..5ff05ef78 100644 --- a/installer/src/artefact/toolchain_channel.rs +++ b/installer/src/artefact/toolchain_channel.rs @@ -113,6 +113,8 @@ impl fmt::Display for ToolchainChannel { #[cfg(test)] mod tests { + //! Tests for toolchain channel parsing and validation. + use rstest::rstest; use super::*; diff --git a/installer/src/artefact/verification.rs b/installer/src/artefact/verification.rs index bf10b0297..60179d22b 100644 --- a/installer/src/artefact/verification.rs +++ b/installer/src/artefact/verification.rs @@ -79,6 +79,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 cbecddb30..13256c2b0 100644 --- a/installer/src/bin/package_dependency_binary.rs +++ b/installer/src/bin/package_dependency_binary.rs @@ -1,6 +1,9 @@ //! Package dependency binaries and shared provenance assets for release uploads. -use std::{io::Write, path::PathBuf}; +use std::{ + io::Write, + path::{Path, PathBuf}, +}; use clap::{Parser, Subcommand}; use thiserror::Error; @@ -92,36 +95,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_triple = TargetTriple::try_from(target.as_str())?; - let output = package_dependency_binary(&DependencyPackageParams { - dependency, - target: target_triple, - binary_path, - output_dir, - })?; - writeln!( - std::io::stdout(), - "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)?; - writeln!(std::io::stdout(), "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 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 feb2a5426..9db1ef0c4 100644 --- a/installer/src/bin/package_installer_bin.rs +++ b/installer/src/bin/package_installer_bin.rs @@ -83,6 +83,8 @@ fn run(cli: Cli) -> Result<(), CliError> { #[cfg(test)] mod tests { + //! Tests for the installer packaging command. + use clap::Parser; use rstest::rstest; diff --git a/installer/src/bin/package_lints.rs b/installer/src/bin/package_lints.rs index cc8e12cb2..fd9dc35b0 100644 --- a/installer/src/bin/package_lints.rs +++ b/installer/src/bin/package_lints.rs @@ -258,6 +258,8 @@ const fn civil_from_epoch(epoch_secs: u64) -> (i64, u64, u64) { #[cfg(test)] mod tests { + //! Tests for the lint-library packaging command. + use std::fs; use clap::Parser; @@ -288,8 +290,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] @@ -345,7 +347,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"); @@ -370,7 +376,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/builder.rs b/installer/src/builder.rs index 5b7cd6dbb..38d109ea1 100644 --- a/installer/src/builder.rs +++ b/installer/src/builder.rs @@ -233,6 +233,8 @@ pub const fn library_prefix() -> &'static str { #[cfg(test)] mod tests { + //! Tests for lint-library build orchestration. + use rstest::{fixture, rstest}; use super::*; diff --git a/installer/src/crate_name.rs b/installer/src/crate_name.rs index 82f462d27..8d36993f9 100644 --- a/installer/src/crate_name.rs +++ b/installer/src/crate_name.rs @@ -46,6 +46,8 @@ impl fmt::Display for CrateName { #[cfg(test)] mod tests { + //! Tests for crate-name normalization. + use std::collections::HashSet; use super::*; diff --git a/installer/src/dependency_binaries/install/checksum.rs b/installer/src/dependency_binaries/install/checksum.rs index ad49c19bc..741cdf493 100644 --- a/installer/src/dependency_binaries/install/checksum.rs +++ b/installer/src/dependency_binaries/install/checksum.rs @@ -181,15 +181,15 @@ mod tests { use super::*; /// 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] #[case(404, true)] @@ -235,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", @@ -264,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) } @@ -314,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 bd6990df7..8d853093a 100644 --- a/installer/src/dependency_binaries/install/downloader.rs +++ b/installer/src/dependency_binaries/install/downloader.rs @@ -300,117 +300,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 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] - 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_tests.rs b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs index 5cd83d63a..a255cdb13 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs @@ -55,14 +55,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()) } } @@ -73,15 +74,15 @@ impl DownloadHarness { #[fixture] fn download_harness( #[default(HashMap::new())] routes: HashMap, -) -> DownloadHarness { - let server = LocalServer::start(routes); - let temp = TempDir::new().expect("create temp dir"); +) -> std::io::Result { + let server = LocalServer::start(routes)?; + let temp = TempDir::new()?; let destination = temp.path().join("archive.tgz"); - DownloadHarness { + Ok(DownloadHarness { server, _temp: temp, destination, - } + }) } #[rstest] @@ -97,7 +98,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, @@ -112,6 +113,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) @@ -134,7 +136,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, @@ -165,7 +167,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", ); } @@ -181,7 +186,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, @@ -203,7 +208,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", ); } @@ -227,7 +235,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, @@ -253,7 +261,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", ); } @@ -271,7 +282,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, @@ -300,7 +311,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( @@ -321,7 +332,10 @@ 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", ); } @@ -333,7 +347,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_tests.rs b/installer/src/dependency_binaries/install/downloader_tests.rs new file mode 100644 index 000000000..e3999cad3 --- /dev/null +++ b/installer/src/dependency_binaries/install/downloader_tests.rs @@ -0,0 +1,112 @@ +//! Tests for downloader error mapping and archive checksum verification. + +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] +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/dependency_binaries/install/http_test_server.rs b/installer/src/dependency_binaries/install/http_test_server.rs index cd03c3f23..62497a91c 100644 --- a/installer/src/dependency_binaries/install/http_test_server.rs +++ b/installer/src/dependency_binaries/install/http_test_server.rs @@ -13,6 +13,7 @@ use std::{ sync::{ Arc, Mutex, + PoisonError, atomic::{AtomicBool, Ordering}, }, thread::{self, JoinHandle}, @@ -57,12 +58,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 = { @@ -72,18 +71,23 @@ 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 { format!("{}{path}", self.base_url) } 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() } } @@ -120,6 +124,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, @@ -129,14 +141,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 { @@ -156,7 +161,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; } } @@ -173,7 +179,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/tests.rs b/installer/src/dependency_binaries/install/tests.rs index 04570effc..92b598248 100644 --- a/installer/src/dependency_binaries/install/tests.rs +++ b/installer/src/dependency_binaries/install/tests.rs @@ -23,21 +23,26 @@ use crate::{dirs::MockBaseDirs, installer_packaging::TargetTriple}; 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_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 @@ -60,9 +65,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) @@ -77,24 +84,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) } @@ -121,12 +128,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()); @@ -143,12 +151,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") diff --git a/installer/src/dependency_binaries/manifest.rs b/installer/src/dependency_binaries/manifest.rs index 8693a083d..f9c3a602f 100644 --- a/installer/src/dependency_binaries/manifest.rs +++ b/installer/src/dependency_binaries/manifest.rs @@ -277,6 +277,8 @@ pub fn find_dependency_binary( #[cfg(test)] mod tests { + //! Tests for the embedded dependency-binary manifest. + use rstest::{fixture, rstest}; use super::*; diff --git a/installer/src/dependency_packaging_tests.rs b/installer/src/dependency_packaging_tests.rs index b38882deb..4cf0fe583 100644 --- a/installer/src/dependency_packaging_tests.rs +++ b/installer/src/dependency_packaging_tests.rs @@ -5,6 +5,7 @@ use std::fs; use rstest::{fixture, rstest}; use crate::{ + artefact::error::ArtefactError, dependency_binaries::find_dependency_binary, dependency_packaging::{ ArchiveFormat, @@ -26,19 +27,15 @@ 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") -} - -#[fixture] -fn windows_target() -> TargetTriple { - TargetTriple::try_from("x86_64-pc-windows-msvc").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 temp_dir() -> tempfile::TempDir { tempfile::tempdir().expect("temp dir") } +fn temp_dir() -> std::io::Result { tempfile::tempdir() } #[test] fn archive_format_matches_target_platform() { @@ -76,10 +73,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"); @@ -155,7 +155,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.rs b/installer/src/deps/install.rs index 2ea37d401..9b13f87a5 100644 --- a/installer/src/deps/install.rs +++ b/installer/src/deps/install.rs @@ -18,6 +18,16 @@ use crate::{ installer_packaging::TargetTriple, }; +#[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, @@ -94,62 +104,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 - ), - ); } } } @@ -304,32 +271,6 @@ fn run_cargo_install( 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(); diff --git a/installer/src/deps/install_repository.rs b/installer/src/deps/install_repository.rs new file mode 100644 index 000000000..960fa69bc --- /dev/null +++ b/installer/src/deps/install_repository.rs @@ -0,0 +1,143 @@ +//! 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/tests.rs b/installer/src/deps/tests.rs index 2e12899ea..0ed91956e 100644 --- a/installer/src/deps/tests.rs +++ b/installer/src/deps/tests.rs @@ -7,6 +7,7 @@ use super::{ *, }; use crate::{ + artefact::error::ArtefactError, dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}, installer_packaging::TargetTriple, test_utils::{ @@ -26,21 +27,23 @@ use crate::{ }, }; +const OPTIONS_MSG: &str = "dependency install options should build"; + fn install_options( repository_installer: &dyn DependencyBinaryInstaller, quiet: bool, -) -> DependencyInstallOptions<'_> { +) -> 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] @@ -73,7 +76,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"); @@ -105,7 +108,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"); @@ -139,7 +142,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"); @@ -169,7 +172,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"); @@ -202,7 +205,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"); @@ -242,7 +245,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"); @@ -286,7 +289,7 @@ 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"); }) @@ -340,7 +343,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"); @@ -382,7 +385,7 @@ 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"); }) diff --git a/installer/src/dirs.rs b/installer/src/dirs.rs index 2faceb77c..a96a35933 100644 --- a/installer/src/dirs.rs +++ b/installer/src/dirs.rs @@ -117,6 +117,8 @@ impl BaseDirs for SystemBaseDirs { #[cfg(test)] mod tests { + //! Tests for base-directory resolution. + use super::*; use crate::test_support::env_test_guard; diff --git a/installer/src/error.rs b/installer/src/error.rs index 8532599e0..c8b207a4d 100644 --- a/installer/src/error.rs +++ b/installer/src/error.rs @@ -277,6 +277,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 59a12d5fa..5dd8a9606 100644 --- a/installer/src/git.rs +++ b/installer/src/git.rs @@ -5,7 +5,9 @@ //! configurable timeout to prevent hangs on network issues. use std::{ - process::{Command, Output, Stdio}, + io::Read, + process::{Child, Command, Output, Stdio}, + thread::JoinHandle, time::Duration, }; @@ -85,6 +87,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()); @@ -92,69 +119,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(Option::unwrap_or_default) - }); - let stderr_thread = std::thread::spawn(move || -> std::io::Result { - stderr_pipe - .map(std::io::read_to_string) - .transpose() - .map(Option::unwrap_or_default) - }); - - if let Some(status) = child.wait_timeout(GIT_TIMEOUT)? { - // 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(), - }) - } else { - // Timeout - kill the process and wait for the reader threads to - // finish. Each result is discarded deliberately: the operation has - // already failed, so cleanup is best effort. - drop(child.kill()); - drop(child.wait()); - drop(stdout_thread.join()); - drop(stderr_thread.join()); - Err(InstallerError::Git { + }) +} + +/// 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!( - "operation timed out after {} seconds", - GIT_TIMEOUT.as_secs() - ), - }) + 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/install_flow/tests.rs b/installer/src/install_flow/tests.rs index 9a86b03aa..0b3c2b67b 100644 --- a/installer/src/install_flow/tests.rs +++ b/installer/src/install_flow/tests.rs @@ -53,27 +53,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] @@ -93,11 +92,12 @@ 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 { staging_path, toolchain, @@ -110,7 +110,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)); } diff --git a/installer/src/install_metrics_tests.rs b/installer/src/install_metrics_tests.rs index 96ba13c61..484e1c7d7 100644 --- a/installer/src/install_metrics_tests.rs +++ b/installer/src/install_metrics_tests.rs @@ -17,13 +17,13 @@ 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] @@ -56,7 +56,11 @@ fn record_install_updates_counts_and_duration() { } #[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, @@ -69,7 +73,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 @@ -93,7 +101,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( @@ -121,7 +133,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, @@ -142,7 +158,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 @@ -179,7 +199,11 @@ 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(); diff --git a/installer/src/installer_packaging_tests.rs b/installer/src/installer_packaging_tests.rs index fdfa918bf..8bd9350fd 100644 --- a/installer/src/installer_packaging_tests.rs +++ b/installer/src/installer_packaging_tests.rs @@ -5,7 +5,7 @@ use std::{fs, io::Read}; use rstest::rstest; use super::*; -use crate::binstall_metadata; +use crate::{artefact::error::ArtefactError, binstall_metadata}; // --------------------------------------------------------------------------- // Shared fixtures @@ -29,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. @@ -42,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() } @@ -200,16 +198,17 @@ 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(¶ms).expect("packaging should succeed"); assert!(output.archive_path.exists(), "archive should exist"); assert_eq!(output.archive_name, expected_name); let entries = if has_extension(expected_name, "tgz") { - read_tgz_entry_paths(&output.archive_path) + 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:?}"); let entry = entries.first().expect("archive should contain one entry"); @@ -286,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(¶ms).expect("packaging"); let file = fs::File::open(&output.archive_path).expect("open"); diff --git a/installer/src/list.rs b/installer/src/list.rs index d80a9fb1c..c148ab8fd 100644 --- a/installer/src/list.rs +++ b/installer/src/list.rs @@ -179,255 +179,5 @@ where } #[cfg(test)] -mod tests { - use std::fs; - - use rstest::{fixture, rstest}; - use tempfile::TempDir; - - use super::*; - - // ------------------------------------------------------------------------- - // 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_output.rs b/installer/src/list_output.rs index c5fb4275c..2e244a848 100644 --- a/installer/src/list_output.rs +++ b/installer/src/list_output.rs @@ -149,6 +149,8 @@ pub struct LibraryEntry { #[cfg(test)] mod tests { + //! Tests for human-readable and JSON list formatting. + use std::collections::BTreeMap; use camino::Utf8PathBuf; diff --git a/installer/src/list_tests.rs b/installer/src/list_tests.rs new file mode 100644 index 000000000..d02ad12fd --- /dev/null +++ b/installer/src/list_tests.rs @@ -0,0 +1,291 @@ +//! Tests for the `list` command handler and its path resolution helpers. + +use std::fs; + +use rstest::{fixture, rstest}; +use tempfile::TempDir; + +use super::*; + +// ------------------------------------------------------------------------- +// Fixtures +// ------------------------------------------------------------------------- + +/// A temporary directory converted to a UTF-8 path for test isolation. +struct TempTarget { + _temp: TempDir, + path: Utf8PathBuf, +} + +#[fixture] +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. +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, +) -> 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)?; + + let filename = format!( + "{}whitaker_suite@{toolchain}{}", + library_prefix(), + library_extension() + ); + + 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) -> 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, +) -> std::io::Result<()> { + create_mock_library_internal(target_dir, toolchain, MockLibraryKind::Prebuilt { target }) +} + +// ------------------------------------------------------------------------- +// run_list tests +// ------------------------------------------------------------------------- + +#[rstest] +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()), + }; + 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( + #[from(temp_target)] temp_target_res: std::io::Result, + #[case] json: bool, + #[case] expected: &[&str], +) { + 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()), + }; + 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( + #[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()), + }; + 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( + #[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()), + }; + 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( + #[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(), + "expected None for directory without rust-toolchain.toml" + ); +} + +#[rstest] +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" +"#; + 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( + #[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:?}"); + assert_eq!(result.expect("already checked"), temp_target.path); +} + +#[rstest] +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())); + + 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( + #[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"); + + 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/output.rs b/installer/src/output.rs index 6c30243ff..c14172ffb 100644 --- a/installer/src/output.rs +++ b/installer/src/output.rs @@ -175,6 +175,8 @@ impl DryRunInfo<'_> { #[cfg(test)] mod tests { + //! Tests for installer output rendering. + use camino::Utf8PathBuf; use rstest::{fixture, rstest}; diff --git a/installer/src/pipeline_staging_tests.rs b/installer/src/pipeline_staging_tests.rs index 5980d964c..78a393ff3 100644 --- a/installer/src/pipeline_staging_tests.rs +++ b/installer/src/pipeline_staging_tests.rs @@ -28,15 +28,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"), @@ -45,7 +45,7 @@ impl StagingTestContext { verbosity: 0, experimental: false, quiet: false, - } + }) } fn target_dir(&self) -> &Utf8Path { &self.target_dir } @@ -73,34 +73,36 @@ impl StagingTestContext { } } -fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> BuildResult { +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 { StagingTestContext::new() } +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 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", - )]; + 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"); @@ -113,7 +115,11 @@ fn assert_bumpy_road_lint_in_staging_output(experimental: bool) { } #[rstest] -fn stage_libraries_returns_correct_staging_path(staging_ctx: StagingTestContext) { +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![]; @@ -137,7 +143,11 @@ 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) { +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![]; @@ -157,15 +167,19 @@ fn stage_libraries_respects_quiet_flag(staging_ctx: StagingTestContext, #[case] } #[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_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", - )]; + 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 = @@ -186,7 +200,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(); diff --git a/installer/src/prebuilt_path.rs b/installer/src/prebuilt_path.rs index 4946b74b6..bcf10bf0c 100644 --- a/installer/src/prebuilt_path.rs +++ b/installer/src/prebuilt_path.rs @@ -47,6 +47,8 @@ pub fn prebuilt_library_dir( #[cfg(test)] mod tests { + //! Tests for prebuilt artefact path resolution. + use std::path::PathBuf; use rstest::rstest; diff --git a/installer/src/prebuilt_tests.rs b/installer/src/prebuilt_tests.rs index 6f36bd29e..8384e5717 100644 --- a/installer/src/prebuilt_tests.rs +++ b/installer/src/prebuilt_tests.rs @@ -21,11 +21,12 @@ 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 @@ -35,7 +36,8 @@ fn test_fallback_scenario( setup_mocks: impl FnOnce(&mut MockArtefactDownloader, &mut MockArtefactExtractor), expected_reason_substring: &str, ) { - 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 mut downloader = MockArtefactDownloader::new(); @@ -57,7 +59,8 @@ fn test_fallback_scenario( #[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); diff --git a/installer/src/resolution.rs b/installer/src/resolution.rs index fe7ea4217..0116cff18 100644 --- a/installer/src/resolution.rs +++ b/installer/src/resolution.rs @@ -148,6 +148,8 @@ pub fn validate_crate_names(names: &[CrateName], options: &CrateResolutionOption #[cfg(test)] mod tests { + //! Tests for release resolution. + use rstest::rstest; use super::*; diff --git a/installer/src/scanner.rs b/installer/src/scanner.rs index 50c12b3cf..c49f3da07 100644 --- a/installer/src/scanner.rs +++ b/installer/src/scanner.rs @@ -236,6 +236,8 @@ pub fn lints_for_library_with_experimental( #[cfg(test)] mod tests { + //! Tests for scanning installed lint libraries. + use rstest::rstest; use tempfile::TempDir; diff --git a/installer/src/staged_suite.rs b/installer/src/staged_suite.rs index f3291f401..ccc09a57b 100644 --- a/installer/src/staged_suite.rs +++ b/installer/src/staged_suite.rs @@ -82,18 +82,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) } } @@ -101,24 +98,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] @@ -141,9 +138,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( @@ -172,8 +171,10 @@ 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 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(); @@ -197,9 +198,15 @@ mod tests { #[cfg(debug_assertions)] #[rstest] - fn staged_suite_installation_surfaces_write_failures(staged_suite_setup: StagedSuiteSetup) { + 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(); + 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( @@ -220,8 +227,10 @@ 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 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 diff --git a/installer/src/stager.rs b/installer/src/stager.rs index 04bec4854..b554f11bb 100644 --- a/installer/src/stager.rs +++ b/installer/src/stager.rs @@ -139,6 +139,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.rs b/installer/src/test_utils/dependency_binary_helpers.rs index 5a248cf61..8c1c98aee 100644 --- a/installer/src/test_utils/dependency_binary_helpers.rs +++ b/installer/src/test_utils/dependency_binary_helpers.rs @@ -1,161 +1,24 @@ //! Test helpers for dependency binary installation tests. -#[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; -#[cfg(any(test, feature = "test-support"))] -use crate::dependency_binaries::{ - DependencyBinary, - DependencyBinaryInstallError, - DependencyBinaryInstaller, -}; -#[cfg(any(test, feature = "test-support"))] -use crate::dirs::BaseDirs; -#[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::{ dependency_binaries::find_dependency_binary, error::Result, test_utils::{ExpectedCall, failure_output, stdout_output, success_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. -/// -/// # Errors -/// -/// Returns any I/O error raised while writing the binary or setting its -/// permissions. -#[cfg(any(test, feature = "test-support"))] -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. -/// -/// # Errors -/// -/// Returns any I/O error raised while writing the binary or setting its -/// permissions. -#[cfg(any(test, feature = "test-support"))] -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)?.permissions(); - permissions.set_mode(mode); - fs::set_permissions(path, permissions)?; - } - #[cfg(not(unix))] - let _ = is_executable; - Ok(()) -} +#[path = "dependency_binary_helpers_fakes.rs"] +mod fakes; -#[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. -/// -/// # Errors -/// -/// Returns an I/O error when the fake directories cannot be created, the -/// `setup` closure fails, or the fake `PATH` cannot be joined. -#[cfg(any(test, feature = "test-support"))] -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()?, tempfile::tempdir()?]; - 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)) - .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. -/// -/// # Errors -/// -/// Returns an I/O error when the fake `PATH` cannot be prepared or the fake -/// binary cannot be written. -#[cfg(any(test, feature = "test-support"))] -pub fn with_fake_binary_on_path( - binary_name: &str, - run: impl FnOnce() -> T, -) -> std::io::Result { - with_fake_path( - |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. -#[cfg(any(test, feature = "test-support"))] -#[must_use] -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..cda8f7c8f --- /dev/null +++ b/installer/src/test_utils/dependency_binary_helpers_fakes.rs @@ -0,0 +1,146 @@ +//! 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. + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use crate::{ + dependency_binaries::{ + DependencyBinary, + DependencyBinaryInstallError, + DependencyBinaryInstaller, + }, + dirs::BaseDirs, + installer_packaging::TargetTriple, + 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. +/// +/// # 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. +/// +/// # 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)?.permissions(); + permissions.set_mode(mode); + fs::set_permissions(path, permissions)?; + } + #[cfg(not(unix))] + let _ = is_executable; + Ok(()) +} + +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. +/// +/// # 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()?, tempfile::tempdir()?]; + 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)) + .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. +/// +/// # 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| { + 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)] + { + directory.join(format!("{binary_name}.cmd")) + } + #[cfg(not(windows))] + { + directory.join(binary_name) + } +} diff --git a/installer/src/tests/mod.rs b/installer/src/tests/mod.rs index fc344eb8c..217766485 100644 --- a/installer/src/tests/mod.rs +++ b/installer/src/tests/mod.rs @@ -28,13 +28,16 @@ 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] @@ -130,7 +133,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()); @@ -176,7 +180,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()); @@ -225,7 +230,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"); diff --git a/installer/src/toolchain/tests/mod.rs b/installer/src/toolchain/tests/mod.rs index 7a07ce3ee..322e8ada2 100644 --- a/installer/src/toolchain/tests/mod.rs +++ b/installer/src/toolchain/tests/mod.rs @@ -71,7 +71,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(); @@ -97,11 +100,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] @@ -117,10 +116,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(); @@ -134,11 +139,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] @@ -151,7 +152,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] @@ -187,19 +191,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 { - toolchain: ref failed_toolchain, - components: ref failed_components, - ref message, - } if failed_toolchain == "nightly-2026-05-28" - && failed_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] diff --git a/installer/src/version.rs b/installer/src/version.rs index fef60021c..787ac7e37 100644 --- a/installer/src/version.rs +++ b/installer/src/version.rs @@ -44,6 +44,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 57806cd18..1b16bb7ab 100644 --- a/installer/src/workspace.rs +++ b/installer/src/workspace.rs @@ -205,194 +205,5 @@ fn is_cargo_workspace_root(cargo_toml: &Utf8Path) -> Result { } #[cfg(test)] -mod tests { - use std::{fs, path::PathBuf}; - - use rstest::{fixture, rstest}; - 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, - 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().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_eq!(result.expect("workspace path should resolve"), 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); - - let err = result.expect_err("resolution should fail without a data directory"); - 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).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); - 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).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"); - let result = find_workspace_root(&temp_workspace.path); - assert!(matches!( - result.expect_err("workspace lookup should fail"), - 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..33cdafc06 --- /dev/null +++ b/installer/src/workspace_tests.rs @@ -0,0 +1,231 @@ +//! Tests for workspace discovery and layout. + +use std::{fs, path::PathBuf}; + +use rstest::{fixture, rstest}; +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, + path: Utf8PathBuf, +} + +#[fixture] +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) -> std::io::Result<()> { + let cargo_toml = dir.join("Cargo.toml"); + fs::write( + cargo_toml, + format!("[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\n"), + ) +} + +#[rstest] +#[case::whitaker_project(Some("whitaker"), true)] +#[case::other_project(Some("other-project"), false)] +#[case::empty_dir(None, false)] +fn is_whitaker_workspace_detection( + #[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).expect("Cargo.toml should be written"); + } + 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( + #[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); + + assert_eq!(action, WorkspaceAction::UseCurrentDir(temp_workspace.path)); +} + +#[rstest] +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"); + + 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( + #[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"); + + 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( + #[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"); + + 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().return_const(data_dir); + mock +} + +#[rstest] +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_eq!(result.expect("workspace path should resolve"), expected_dir); +} + +#[rstest] +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); + + let err = result.expect_err("resolution should fail without a data directory"); + 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( + #[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)); +} + +// Tests for find_workspace_root + +fn write_workspace_cargo_toml(dir: &Utf8Path) -> std::io::Result<()> { + fs::write( + dir.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/*\"]\n", + ) +} + +#[rstest] +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).expect("workspace root should be found"), + temp_workspace.path + ); +} + +#[rstest] +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).expect("workspace root should be found"), + temp_workspace.path + ); +} + +#[rstest] +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.expect_err("workspace lookup should fail"), + InstallerError::WorkspaceNotFound { .. } + )); +} diff --git a/installer/src/wrapper.rs b/installer/src/wrapper.rs index 18d84b301..cc074a116 100644 --- a/installer/src/wrapper.rs +++ b/installer/src/wrapper.rs @@ -216,6 +216,8 @@ pub fn path_instructions(bin_dir: &Path) -> String { #[cfg(test)] mod tests { + //! Tests for the generated cargo-whitaker wrapper script. + use tempfile::TempDir; use super::*; @@ -226,28 +228,38 @@ mod tests { assert!(!is_directory_in_path(temp.path())); } - /// Asserts that every user, group, and other execute bit is set on `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)] - fn assert_script_is_executable(path: &Path) { - use std::os::unix::fs::PermissionsExt; - - let perms = std::fs::metadata(path) - .expect("failed to read metadata") - .permissions(); - assert_eq!(perms.mode() & 0o111, 0o111, "script should be executable"); + 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`. + /// Asserts that the script at `$path` contains every fragment in `$fragments`. #[cfg(unix)] - fn assert_script_contains(path: &Path, fragments: &[&str]) { - let content = std::fs::read_to_string(path).expect("failed to read script"); - for fragment in fragments { - assert!( - content.contains(fragment), - "script {} should contain {fragment}", - path.display() - ); - } + 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)] @@ -263,14 +275,14 @@ mod tests { assert!(whitaker_path.exists()); assert!(whitaker_ls_path.exists()); - assert_script_is_executable(&whitaker_path); - assert_script_contains( + assert_script_is_executable!(&whitaker_path); + assert_script_contains!( &whitaker_path, - &["DYLINT_LIBRARY_PATH", "cargo dylint", "$@"], + &["DYLINT_LIBRARY_PATH", "cargo dylint", "$@"] ); - assert_script_contains( + assert_script_contains!( &whitaker_ls_path, - &["DYLINT_LIBRARY_PATH", "cargo dylint list", "whitaker_suite"], + &["DYLINT_LIBRARY_PATH", "cargo dylint list", "whitaker_suite"] ); } diff --git a/installer/tests/behaviour_cli/support.rs b/installer/tests/behaviour_cli/support.rs index 12e87002e..2365ba7db 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -43,18 +43,19 @@ pub(super) fn workspace_root() -> PathBuf { 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 { @@ -90,8 +91,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().unwrap_or_else(|error| panic!("failed to create temp dir: {error}")); + 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 @@ -210,9 +212,9 @@ pub(super) fn run_installer_cli(cli_world: &CliWorld) { command.env(TEST_STAGE_SUITE_ENV, "1"); } - let output = command - .output() - .unwrap_or_else(|error| panic!("failed to run whitaker-installer: {error}")); + let Ok(output) = command.output() else { + panic!("whitaker-installer should run"); + }; cli_world.output.replace(Some(output)); } diff --git a/installer/tests/behaviour_staging.rs b/installer/tests/behaviour_staging.rs index df56e4dd8..69c6d0397 100644 --- a/installer/tests/behaviour_staging.rs +++ b/installer/tests/behaviour_staging.rs @@ -79,6 +79,8 @@ use staging_failure::staging_failure_world; #[cfg(unix)] mod staging_failure { + //! Unix-only world and steps for staging permission failures. + use std::{fs, os::unix::fs::PermissionsExt}; use tempfile::TempDir; diff --git a/installer/tests/doc_extraction/extraction.rs b/installer/tests/doc_extraction/extraction.rs index 17ce1b052..f4bf8596a 100644 --- a/installer/tests/doc_extraction/extraction.rs +++ b/installer/tests/doc_extraction/extraction.rs @@ -19,8 +19,9 @@ pub static DOC_TOML_BLOCKS: LazyLock> = LazyLock::new(|| { 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 @@ -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] From 6a62447b487880adba4cf14900f5091d3a1b0762 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 12:05:54 +0200 Subject: [PATCH 12/21] Clear the remaining Whitaker findings in the installer `make lint` now passes end to end: rustdoc, Clippy, and the Whitaker suite over the workspace's own support crates. - `no_expect_outside_tests`: convert the installer's fixtures and test helpers to return `Result`, with the `#[test]`/`#[rstest]` body doing the unwrapping. rstest fixtures are consumed as `#[from(fixture)] fixture_res: Result<..>` so the conversion does not trade the lint for `shadow_reuse` on a `let x = x?` rebind. - `no_unwrap_or_else_panic`: propagate rather than panic in the closure. - `module_max_lines`: split `list`, `downloader`, `workspace`, `dependency_binary_helpers` and `deps::tests` into sibling modules, each keeping an explanatory `//!` opening line. - `bumpy_road_function`: extract the complexity clusters, grouping parameters into named structs where an extraction would otherwise exceed the four-argument budget. - `no_std_fs_operations`: add the integration-test crates that stage fixtures ambiently to `dylint.toml`. In-source suppression does not work for this lint, and these targets compile as their own crate named after the file, so they are not covered by the package-level entries. --- dylint.toml | 1 + .../downloader_boundary_non_utf8_tests.rs | 80 +++++++ .../install/downloader_boundary_tests.rs | 76 +----- installer/src/deps/tests.rs | 149 +----------- installer/src/deps/tests_repository.rs | 172 ++++++++++++++ installer/src/pipeline_staging_tests.rs | 40 ++-- installer/src/prebuilt_tests.rs | 61 ++--- installer/tests/behaviour_cli/support.rs | 221 ++---------------- .../tests/behaviour_cli/support_assertions.rs | 213 +++++++++++++++++ 9 files changed, 542 insertions(+), 471 deletions(-) create mode 100644 installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs create mode 100644 installer/src/deps/tests_repository.rs create mode 100644 installer/tests/behaviour_cli/support_assertions.rs diff --git a/dylint.toml b/dylint.toml index f41dc2c29..1b3a1a140 100644 --- a/dylint.toml +++ b/dylint.toml @@ -43,6 +43,7 @@ excluded_crates = [ "behaviour_artefact_packaging", "behaviour_cli", "behaviour_docs", + "behaviour_install_metrics", "behaviour_installer_release", "behaviour_prebuilt", "behaviour_staging", 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..b4cc1a2f2 --- /dev/null +++ b/installer/src/dependency_binaries/install/downloader_boundary_non_utf8_tests.rs @@ -0,0 +1,80 @@ +//! Unix-only boundary tests rejecting non-UTF-8 archive destinations. + +use std::{collections::HashMap, io}; + +use super::{ + super::{ + downloader::{ + DependencyArchiveDownloader, + RepositoryArchiveDownloader, + download_from_urls, + }, + http_test_server::LocalServer, + installer::DependencyBinaryInstallError, + }, + agent, +}; + +#[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()).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. + 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 a255cdb13..6affab33e 100644 --- a/installer/src/dependency_binaries/install/downloader_boundary_tests.rs +++ b/installer/src/dependency_binaries/install/downloader_boundary_tests.rs @@ -5,10 +5,7 @@ //! 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. -// `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::{collections::HashMap, io::Read, path::PathBuf, time::Duration}; use camino::Utf8Path; @@ -17,11 +14,6 @@ use rstest::{fixture, rstest}; use sha2::{Digest, Sha256}; use tempfile::TempDir; -// 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::{ downloader::download_from_urls, http_test_server::{CannedResponse, LocalServer}, @@ -340,68 +332,8 @@ fn download_from_urls_rejects_a_malformed_checksum_sidecar( ); } +// 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)] -#[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()).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. - 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", - ); -} - -#[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}", - ); - } - other => panic!("expected an Io error, got {other:?}"), - } -} +#[path = "downloader_boundary_non_utf8_tests.rs"] +mod non_utf8; diff --git a/installer/src/deps/tests.rs b/installer/src/deps/tests.rs index 0ed91956e..78f891ce0 100644 --- a/installer/src/deps/tests.rs +++ b/installer/src/deps/tests.rs @@ -2,10 +2,7 @@ use std::path::PathBuf; -use super::{ - install::{InstallMode, install_tool}, - *, -}; +use super::*; use crate::{ artefact::error::ArtefactError, dependency_binaries::{DependencyBinaryInstallError, MockDependencyBinaryInstaller}, @@ -301,145 +298,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(OPTIONS_MSG), - ) - .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(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")); - 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..1ffe88d5b --- /dev/null +++ b/installer/src/deps/tests_repository.rs @@ -0,0 +1,172 @@ +//! Tests for repository-release installation of the Dylint dependency tools. + +use std::path::PathBuf; + +use super::{ + DependencyBinaryInstaller, + DependencyTool, + DylintToolStatus, + OPTIONS_MSG, + install::{InstallContext, InstallMode, install_tool, repository_install_context}, + install_dylint_tools_with_options, + install_options, +}; +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, + }, +}; + +/// 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(OPTIONS_MSG), + ) + .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(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")); + 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/pipeline_staging_tests.rs b/installer/src/pipeline_staging_tests.rs index 78a393ff3..87d9c488e 100644 --- a/installer/src/pipeline_staging_tests.rs +++ b/installer/src/pipeline_staging_tests.rs @@ -94,24 +94,30 @@ fn create_mock_library(target_dir: &Utf8Path, crate_name: &str) -> std::io::Resu #[fixture] fn staging_ctx() -> std::io::Result { StagingTestContext::new() } -fn assert_bumpy_road_lint_in_staging_output(experimental: bool) { - 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(); +/// 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] @@ -222,5 +228,5 @@ fn stage_libraries_logs_installed_lints_when_not_quiet( #[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/prebuilt_tests.rs b/installer/src/prebuilt_tests.rs index 8384e5717..11031fa0d 100644 --- a/installer/src/prebuilt_tests.rs +++ b/installer/src/prebuilt_tests.rs @@ -29,32 +29,37 @@ fn destination_dir() -> std::io::Result<(tempfile::TempDir, Utf8PathBuf)> { 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().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}" - ); +/// `$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 @ PrebuiltResult::Success { .. } => panic!("expected Fallback, got {other:?}"), - } + }}; } #[test] @@ -95,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() @@ -126,7 +131,7 @@ fn manifest_validation_errors_return_fallback() { ]; for (toolchain, target, expected_reason_substring) in test_cases { - test_fallback_scenario( + test_fallback_scenario!( |downloader, _extractor| { let manifest_json = prebuilt_manifest_json(toolchain, target, "a".repeat(64)); downloader @@ -140,7 +145,7 @@ fn manifest_validation_errors_return_fallback() { #[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)); @@ -159,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); diff --git a/installer/tests/behaviour_cli/support.rs b/installer/tests/behaviour_cli/support.rs index 2365ba7db..1bed8cb30 100644 --- a/installer/tests/behaviour_cli/support.rs +++ b/installer/tests/behaviour_cli/support.rs @@ -1,4 +1,4 @@ -//! 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}, @@ -15,8 +15,6 @@ use whitaker_installer::{ toolchain::parse_toolchain_channel, }; -use super::prebuilt_markers::PREBUILT_INSTALL_MARKER; - #[derive(Default)] pub(super) struct CliWorld { args: RefCell>, @@ -228,209 +226,16 @@ pub(super) fn get_output(cli_world: &CliWorld) -> Ref<'_, Output> { }) } -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_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(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"), - "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(super) fn assert_installation_succeeds_or_is_skipped(cli_world: &CliWorld) { - if cli_world.skip_assertions.get() { - return; - } +#[path = "support_assertions.rs"] +mod assertions; - let output = get_output(cli_world); - assert!( - output.status.success(), - "installation failed: {}", - String::from_utf8_lossy(&output.stderr) - ); -} - -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_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), - ); -} +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..c354934d2 --- /dev/null +++ b/installer/tests/behaviour_cli/support_assertions.rs @@ -0,0 +1,213 @@ +//! 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), + ); +} + +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) { + 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(crate) 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(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), + ); +} From 2c0a44acec0d5c3f0560bc580e320d1cbc7bdb3f Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 12:12:38 +0200 Subject: [PATCH 13/21] Scope the doctest gate away from the Dylint driver crates `make test` now runs the workspace doctests, which surfaced two things. Doctests compile as their own crate and do not inherit the lib's `#![cfg_attr(feature = "dylint-driver", feature(rustc_private))]`, so under `--all-features` every Dylint driver crate fails to link `rustc_driver`. Their examples are exercised by the unit and UI suites instead, so exclude them, mirroring the existing `nextest` exclusions. Also repair two doc examples in `installer/src/dirs.rs` left stale by the `BaseDirs` method rename. `leta rename` updates code references but not doc comments, and `cargo doc` only validates intra-doc links, so nothing caught these until the examples were actually compiled -- which is the point of adding the gate. --- Makefile | 12 +++++++++++- installer/src/dirs.rs | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7298e6065..8829d3fb5 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,16 @@ SPELLING_HELPER_PYTEST = PYTHONPATH=scripts $(SPELLING_PY_ENV) \ --with pytest-cov==7.0.0 python -m pytest WORKFLOW_TEST_VENV ?= .venv LINT_CRATES ?= bumpy_road_function conditional_max_n_branches function_attrs_follow_docs module_max_lines module_must_have_inner_docs no_expect_outside_tests test_must_not_have_example no_std_fs_operations no_unwrap_or_else_panic whitaker_suite +# Doctests compile as their own crate and do not inherit the lib's +# `#![cfg_attr(feature = "dylint-driver", feature(rustc_private))]`, so the +# Dylint driver crates cannot link `rustc_driver` from a doctest and fail with +# "use of unstable library feature `rustc_private`". Their examples are covered +# by the unit and UI suites instead, so exclude them from the doctest run. +DOCTEST_EXCLUDES ?= --exclude rustc_ast --exclude rustc_attr_data_structures \ + --exclude rustc_hir --exclude rustc_lint --exclude rustc_middle \ + --exclude rustc_session --exclude rustc_span --exclude whitaker \ + --exclude rstest_helper_should_be_fixture \ + $(foreach crate,$(LINT_CRATES),--exclude $(crate)) CARGO_DYLINT_VERSION ?= 6.0.1 DYLINT_LINK_VERSION ?= 6.0.1 # Host-tool installs run under this toolchain: the dylint 6.0.1 lockfile @@ -123,7 +133,7 @@ test: ## Run tests with warnings treated as errors WHITAKER_BACKUP=""; \ fi; \ RUSTFLAGS="-C prefer-dynamic -Z force-unstable-if-unmarked $(RUST_FLAGS)" $(CARGO) $(TEST_RUNNER) $(CARGO_LOCKED) $(TEST_CARGO_FLAGS) $(BUILD_JOBS) $(if $(NEXTEST_PROFILE),--profile $(NEXTEST_PROFILE)); \ - RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) test --workspace --doc --all-features $(BUILD_JOBS); \ + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) test --workspace --doc --all-features $(DOCTEST_EXCLUDES) $(BUILD_JOBS); \ if [ "$${ACT_WORKFLOW_TESTS:-0}" = "1" ]; then \ $(MAKE) workflow-test; \ fi diff --git a/installer/src/dirs.rs b/installer/src/dirs.rs index a96a35933..79caf4bab 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()); /// } /// ``` @@ -74,7 +74,7 @@ pub trait BaseDirs { /// /// let dirs = SystemBaseDirs::new().expect("failed to initialize directories"); /// let data_dir = dirs -/// .whitaker_data_dir() +/// .whitaker_data() /// .expect("could not determine data directory"); /// println!("Whitaker data at: {}", data_dir.display()); /// ``` From 006a9820620b89a7f03dd4eb39bf37edcdf590a8 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 12:17:18 +0200 Subject: [PATCH 14/21] Extract a neighbour-lookup helper in the adjacency behaviour tests `then_node_has_no_neighbours` and `then_neighbours_of_node_are_sorted` each repeated the same `neighbours_of` lookup and out-of-bounds conversion before their differing assertion, which CodeScene flagged as duplication. Add `with_neighbours_of_node` alongside `with_report` and layer it on top, so the successful-build handling stays in one place and each step is left with only its own assertion. Behaviour is unchanged: the step strings, the bounds-error text, the assertion messages, and the `is_sorted_by_key` check are all preserved verbatim. --- .../decomposition_adjacency_behaviour.rs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/common/tests/decomposition_adjacency_behaviour.rs b/common/tests/decomposition_adjacency_behaviour.rs index 95f3fe1a0..8c67c27dc 100644 --- a/common/tests/decomposition_adjacency_behaviour.rs +++ b/common/tests/decomposition_adjacency_behaviour.rs @@ -59,6 +59,19 @@ fn with_report( } } +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| { @@ -97,10 +110,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 { @@ -111,10 +121,7 @@ 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"))?; + with_neighbours_of_node(world, node, |neighbours| { let is_sorted = neighbours.is_sorted_by_key(|neighbour| neighbour.0); if is_sorted { Ok(()) From 62cef657d297e719780a0978f07df144c389a19f Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 12:21:27 +0200 Subject: [PATCH 15/21] Collapse the CLI default assertion blocks into named conditions CodeScene flagged four runs of adjacent bare assertions in the CLI default tests as large assertion blocks. Each was also silent about which field had drifted: a failure reported only the line number. Add `assert_default_conditions`, which takes `(name, condition)` pairs and names the offending field in the panic message, and express each group as a table of named pure boolean queries. The checks themselves are unchanged, including the `verbosity == 0` comparison, which becomes a boolean rather than an `assert_eq!` so it fits the same table. No `PartialEq`/`Eq` derives were added and the production CLI types are untouched; the tests still read each field individually. --- installer/src/cli_tests.rs | 60 ++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/installer/src/cli_tests.rs b/installer/src/cli_tests.rs index 3031d87ed..bec75e32b 100644 --- a/installer/src/cli_tests.rs +++ b/installer/src/cli_tests.rs @@ -13,28 +13,48 @@ fn cli_parses_defaults() { 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!(cli.install.target_dir.is_none()); - assert!(cli.install.lint.is_empty()); - assert!(!cli.install.lint_selection.individual_lints); - assert!(!cli.install.lint_selection.experimental); + assert_default_conditions(&[ + ("target_dir", cli.install.target_dir.is_none()), + ("lint", cli.install.lint.is_empty()), + ( + "lint_selection.individual_lints", + !cli.install.lint_selection.individual_lints, + ), + ( + "lint_selection.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!(!cli.install.execution.cranelift); - assert!(!cli.install.execution.dry_run); - assert_eq!(cli.install.verbosity, 0); - assert!(!cli.install.quiet); + assert_default_conditions(&[ + ("execution.cranelift", !cli.install.execution.cranelift), + ("execution.dry_run", !cli.install.execution.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!(!cli.install.skip.skip_deps); - assert!(!cli.install.skip.skip_wrapper); - assert!(!cli.install.skip.no_update); - assert!(!cli.install.is_build_only); + assert_default_conditions(&[ + ("skip.skip_deps", !cli.install.skip.skip_deps), + ("skip.skip_wrapper", !cli.install.skip.skip_wrapper), + ("skip.no_update", !cli.install.skip.no_update), + ("is_build_only", !cli.install.is_build_only), + ]); } #[test] @@ -184,10 +204,18 @@ fn cli_rejects_conflicting_flags(#[case] args: &[&str]) { #[test] fn install_args_default_is_valid() { let args = InstallArgs::default(); - assert!(!args.lint_selection.individual_lints); - assert!(!args.lint_selection.experimental); - assert!(!args.execution.cranelift); - assert!(!args.skip.skip_deps); + assert_default_conditions(&[ + ( + "lint_selection.individual_lints", + !args.lint_selection.individual_lints, + ), + ( + "lint_selection.experimental", + !args.lint_selection.experimental, + ), + ("execution.cranelift", !args.execution.cranelift), + ("skip.skip_deps", !args.skip.skip_deps), + ]); } #[test] From 07562a5e32ab9c8b0d26c6edcc4a0a9889074308 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 12:25:52 +0200 Subject: [PATCH 16/21] Extract a binstall value comparison helper `then_pkg_url_present`, `then_bin_dir_present` and `then_default_pkg_fmt` each repeated the same fetch-key-and-compare shape, which CodeScene flagged as duplication. Add `assert_binstall_value_equals` beside `table_str` and reduce each step to a single call. The helper reports a mismatch by returning `Err` rather than asserting. These steps return `Result`, so an `assert_eq!` here trips `clippy::panic_in_result_fn`, and returning the message matches how every other step in the file reports failure. The message text is unchanged, so existing failures read exactly as before. `then_windows_override_pkg_fmt` is left alone: it reads through the `overrides` table and carries its own error strings, so it does not fit this helper. --- installer/tests/behaviour_binstall.rs | 50 ++++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/installer/tests/behaviour_binstall.rs b/installer/tests/behaviour_binstall.rs index 75afaddd5..a1ed58c37 100644 --- a/installer/tests/behaviour_binstall.rs +++ b/installer/tests/behaviour_binstall.rs @@ -55,6 +55,29 @@ fn table_str<'a>(table: &'a Table, key: &str) -> Result<&'a str, String> { .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 // --------------------------------------------------------------------------- @@ -105,38 +128,17 @@ fn when_bin_dir_expanded(world: &mut BinstallWorld) { #[then("the pkg-url template is present")] fn then_pkg_url_present(world: &mut BinstallWorld) -> Result<(), String> { - let binstall = binstall_table(world)?; - let pkg_url = table_str(binstall, "pkg-url")?; - if pkg_url != PKG_URL_TEMPLATE { - return Err(format!( - "pkg-url mismatch: expected {PKG_URL_TEMPLATE}, got {pkg_url}" - )); - } - Ok(()) + 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) -> Result<(), String> { - let binstall = binstall_table(world)?; - let bin_dir = table_str(binstall, "bin-dir")?; - if bin_dir != BIN_DIR_TEMPLATE { - return Err(format!( - "bin-dir mismatch: expected {BIN_DIR_TEMPLATE}, got {bin_dir}" - )); - } - Ok(()) + 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) -> Result<(), String> { - let binstall = binstall_table(world)?; - let pkg_fmt = table_str(binstall, "pkg-fmt")?; - if pkg_fmt != expected { - return Err(format!( - "pkg-fmt mismatch: expected {expected}, got {pkg_fmt}" - )); - } - Ok(()) + assert_binstall_value_equals(world, "pkg-fmt", &expected, "pkg-fmt") } #[then("the x86_64-pc-windows-msvc override has pkg-fmt \"{expected}\"")] From b2c63faa7001ef815d98c58d080a9ad4fbda0638 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 21 Aug 2026 16:59:46 +0200 Subject: [PATCH 17/21] Deduplicate installer behaviour assertions Extract shared output assertions so related BDD steps retain their existing conditions while avoiding duplicated control flow. --- .../tests/behaviour_cli/support_assertions.rs | 74 +++++------ installer/tests/behaviour_workflows.rs | 122 ++++++++---------- installer/tests/toolchain_steps/mod.rs | 96 +++++++------- 3 files changed, 135 insertions(+), 157 deletions(-) diff --git a/installer/tests/behaviour_cli/support_assertions.rs b/installer/tests/behaviour_cli/support_assertions.rs index c354934d2..e5eb895d7 100644 --- a/installer/tests/behaviour_cli/support_assertions.rs +++ b/installer/tests/behaviour_cli/support_assertions.rs @@ -20,6 +20,30 @@ fn assert_exit_status(cli_world: &CliWorld, expected_success: bool) { ); } +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); } @@ -76,52 +100,18 @@ pub(crate) fn assert_cli_exits_with_error(cli_world: &CliWorld) { } pub(crate) 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}" + 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) { - 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}" + assert_error_output_is_shown( + cli_world, + "experimental-lint", + "experimental lint crate rstest_helper_should_be_fixture requires --experimental", ); } diff --git a/installer/tests/behaviour_workflows.rs b/installer/tests/behaviour_workflows.rs index 2012d6fd6..9e34c7689 100644 --- a/installer/tests/behaviour_workflows.rs +++ b/installer/tests/behaviour_workflows.rs @@ -114,6 +114,36 @@ fn get_output(world: &WorkflowWorld) -> Result, Strin .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 // --------------------------------------------------------------------------- @@ -189,96 +219,48 @@ fn when_installer_cli_run(world: &WorkflowWorld) -> Result<(), String> { #[then("the CLI exits successfully")] fn then_cli_exits_successfully(world: &WorkflowWorld) -> Result<(), String> { - skip_if_needed!(world); - - let output = get_output(world)?; - if output.status.success() { - Ok(()) - } else { - Err(format!( - "expected success, stderr: {}", - String::from_utf8_lossy(&output.stderr) - )) - } + require_successful_output(world, "expected success") } #[then("installation succeeds or is skipped")] fn then_installation_succeeds_or_is_skipped(world: &WorkflowWorld) -> Result<(), String> { - skip_if_needed!(world); - - let output = get_output(world)?; - if output.status.success() { - Ok(()) - } else { - Err(format!( - "installation failed: {}", - String::from_utf8_lossy(&output.stderr) - )) - } + require_successful_output(world, "installation failed") } #[then("dry-run output shows skip_deps is true")] fn then_skip_deps_is_true(world: &WorkflowWorld) -> Result<(), String> { - skip_if_needed!(world); - - let output = get_output(world)?; - let stderr = String::from_utf8_lossy(&output.stderr); - - if stderr.contains("Skip deps: true") { - Ok(()) - } else { - Err(format!( - "expected skip_deps to be true in output, stderr: {stderr}" - )) - } + 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) -> Result<(), String> { - skip_if_needed!(world); - - let output = get_output(world)?; - let stderr = String::from_utf8_lossy(&output.stderr); - - if stderr.contains("No update: true") { - Ok(()) - } else { - Err(format!( - "expected no_update to be true in output, stderr: {stderr}" - )) - } + 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) -> Result<(), String> { - skip_if_needed!(world); - - let output = get_output(world)?; - let stderr = String::from_utf8_lossy(&output.stderr); - - if stderr.contains("Skip wrapper: true") { - Ok(()) - } else { - Err(format!( - "expected skip_wrapper to be true in output, stderr: {stderr}" - )) - } + 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) -> Result<(), String> { - skip_if_needed!(world); - - let output = get_output(world)?; - let stderr = String::from_utf8_lossy(&output.stderr); - - if stderr.contains("DYLINT_LIBRARY_PATH") { - Ok(()) - } else { - Err(format!( - "expected DYLINT_LIBRARY_PATH instructions in output, stderr: {stderr}" - )) - } + require_stderr_contains( + world, + "DYLINT_LIBRARY_PATH", + "expected DYLINT_LIBRARY_PATH instructions in output", + ) } // --------------------------------------------------------------------------- diff --git a/installer/tests/toolchain_steps/mod.rs b/installer/tests/toolchain_steps/mod.rs index 9edc1fdd1..eb2eaf646 100644 --- a/installer/tests/toolchain_steps/mod.rs +++ b/installer/tests/toolchain_steps/mod.rs @@ -42,6 +42,49 @@ macro_rules! skip_if_needed { }; } +fn assert_toolchain_install_message_presence( + world: &ToolchainWorld, + expected_presence: bool, +) -> Result<(), String> { + skip_if_needed!(world); + + 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 assert_stderr_contains( + world: &ToolchainWorld, + expected: &str, + failure_message: impl FnOnce(&str) -> String, +) -> Result<(), String> { + skip_if_needed!(world); + + let stderr = get_stderr_string(world)?; + if stderr.contains(expected) { + Ok(()) + } else { + Err(failure_message(&stderr)) + } +} + // --------------------------------------------------------------------------- // Fixture // --------------------------------------------------------------------------- @@ -160,37 +203,12 @@ pub fn then_dry_run_shows_toolchain(world: &ToolchainWorld) -> Result<(), String #[then("no toolchain installation message is shown")] pub fn then_no_install_message(world: &ToolchainWorld) -> Result<(), String> { - 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 has_install_message = out_lc.contains(&needle) - || out_lc.contains(&channel.to_lowercase()) && out_lc.contains(TOOLCHAIN_INSTALLED_MARKER); - if has_install_message { - return Err(format!( - "expected no installation message for channel '{channel}' in output, got:\n{out}" - )); - } - Ok(()) + assert_toolchain_install_message_presence(world, false) } #[then("the toolchain installation message is shown")] pub fn then_install_message_shown(world: &ToolchainWorld) -> Result<(), String> { - 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 has_success_marker = out_lc.contains(&needle) - || (out_lc.contains("installed successfully") && out_lc.contains(&channel.to_lowercase())); - if has_success_marker { - Ok(()) - } else { - Err(format!( - "expected success marker for channel '{channel}' in output, got:\n{out}" - )) - } + assert_toolchain_install_message_presence(world, true) } #[then("installation succeeds or is skipped")] @@ -248,28 +266,16 @@ pub fn then_cli_exits_with_error(world: &ToolchainWorld) -> Result<(), String> { #[then("the error mentions toolchain installation failure")] pub fn then_error_mentions_install_failure(world: &ToolchainWorld) -> Result<(), String> { - skip_if_needed!(world); - let stderr = get_stderr_string(world)?; - if stderr.contains(TOOLCHAIN_ERROR_MARKER) { - Ok(()) - } else { - Err(format!( - "expected '{TOOLCHAIN_ERROR_MARKER}' in stderr: {stderr}" - )) - } + 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) -> Result<(), String> { - skip_if_needed!(world); - let stderr = get_stderr_string(world)?; - if stderr.contains(FAKE_TOOLCHAIN) { - Ok(()) - } else { - Err(format!( - "expected toolchain name '{FAKE_TOOLCHAIN}' in error output, stderr: {stderr}" - )) - } + assert_stderr_contains(world, FAKE_TOOLCHAIN, |stderr| { + format!("expected toolchain name '{FAKE_TOOLCHAIN}' in error output, stderr: {stderr}") + }) } #[then("the error output is minimal")] From 90ddca7c51a5c2e93f987e9edc7bc96eecd5450b Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 21 Aug 2026 17:02:58 +0200 Subject: [PATCH 18/21] Fix documentation blank-line lint failures --- docs/developers-guide.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 53d792a50..fab9517e2 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -1747,7 +1747,6 @@ 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 @@ -2089,7 +2088,6 @@ 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` From 02892cca8f33e865b2b8f422e4888af4287e7c22 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 28 Aug 2026 16:12:45 +0200 Subject: [PATCH 19/21] Fix no-default-features test lint Return diagnostic failures from the fallible integration test so the test keeps its detailed failure output without panicking. --- .../tests/no_default_features_build.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) 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(()) } From 172f2a8e3ab282686afcc8a392b3bd6659942bae Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 28 Aug 2026 16:36:35 +0200 Subject: [PATCH 20/21] Use native scanner doctest filename Build the documented filename from the public platform prefix and extension helpers so the example works on Windows, macOS, and Linux. --- installer/src/scanner.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/installer/src/scanner.rs b/installer/src/scanner.rs index c49f3da07..a8c42220a 100644 --- a/installer/src/scanner.rs +++ b/installer/src/scanner.rs @@ -134,15 +134,15 @@ fn scan_toolchain_release( } /// Parse a library filename to extract crate name and toolchain. -/// -/// Format: `{prefix}{crate_name}@{toolchain}{extension}` -/// -/// # Examples -/// /// ``` -/// use whitaker_installer::scanner::parse_library_filename; -/// -/// let result = parse_library_filename("libmodule_max_lines@nightly-2026-05-28.so"); +/// use whitaker_installer::{ +/// builder::{library_extension, library_prefix}, +/// scanner::parse_library_filename, +/// }; +/// let prefix = library_prefix(); +/// let extension = library_extension(); +/// let filename = format!("{prefix}module_max_lines@nightly-2026-05-28{extension}"); +/// let result = parse_library_filename(&filename); /// assert!(result.is_some()); /// let (crate_name, toolchain) = result.expect("valid library filename"); /// assert_eq!(crate_name.as_str(), "module_max_lines"); From afb1e4ac1387aff8e51fa7bbeb62d94f0f6c8970 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 28 Aug 2026 16:49:47 +0200 Subject: [PATCH 21/21] Regenerate lockfile after rebase Resolve the target branch's lockfile conflict from the manifest and preserve the current compatible dependency graph. --- Cargo.lock | 700 +++++++++++++++++++++-------------------------------- 1 file changed, 273 insertions(+), 427 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 524629a5a..17d862a7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ "cipher", "cpubits", @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -165,9 +165,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ "bytes", "cfg_aliases", @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "serde_core", @@ -207,9 +207,10 @@ dependencies = [ "rustc_span", "serde", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -238,9 +239,9 @@ dependencies = [ [[package]] name = "cap-primitives" -version = "3.4.5" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +checksum = "8e0bf07d379916947be6c4a07f43684153d710a2896c31f9e97781362895596c" dependencies = [ "ambient-authority", "fs-set-times", @@ -250,7 +251,7 @@ dependencies = [ "maybe-owned", "rustix", "rustix-linux-procfs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "winx", ] @@ -274,12 +275,12 @@ dependencies = [ [[package]] name = "cap-std" -version = "3.4.5" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +checksum = "a59e59fa26472d29680ece6a9f8ee8b0551a719a33df2f5240bde065ecbddfd7" dependencies = [ "camino", - "cap-primitives 3.4.5", + "cap-primitives 3.4.6", "io-extras 0.18.4", "io-lifetimes 2.0.4", "rustix", @@ -324,9 +325,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -387,7 +388,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -461,6 +462,7 @@ dependencies = [ "tempfile", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -515,18 +517,18 @@ checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -701,13 +703,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -724,9 +726,9 @@ checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" [[package]] name = "dylint" -version = "6.0.1" +version = "6.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c738fb72ea7d248df2995a31b3698beb63d0f1f9ca5a5dc0188c4b64cd0e86e4" +checksum = "d59232730e7347f7b56a021e943c7cbd57636f56866c2a95341aa6159e64dad0" dependencies = [ "anstyle", "anyhow", @@ -742,9 +744,9 @@ dependencies = [ [[package]] name = "dylint_internal" -version = "6.0.1" +version = "6.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9796d3441d7894cbaf4992640799efffc1661978f0cc1266ec788c32254fdfb3" +checksum = "bc1edfdeb8f1b20ee0adcd6fa30f1562aebe7ef49caf89a0befab488d62babfd" dependencies = [ "anstyle", "anyhow", @@ -754,17 +756,20 @@ dependencies = [ "home", "log", "regex", + "semver", "serde", "tar", + "tempfile", "thiserror 2.0.20", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", + "toml_edit", ] [[package]] name = "dylint_linting" -version = "6.0.1" +version = "6.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c213635b3eaa84f43b9b497377f17d9a8d4feb413bab1d82e03ad1c73e4f6d8c" +checksum = "9e9f65c0439f032da8c5febad832b033c6784f084a3fdc8c3b41e0ed727d5654" dependencies = [ "cargo_metadata", "dylint_internal", @@ -772,14 +777,14 @@ dependencies = [ "rustversion", "serde", "thiserror 2.0.20", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] name = "dylint_testing" -version = "6.0.1" +version = "6.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5f50ee06be9ebfb5b9538ba0d7bb08adc33e8f31c901e7d398de2a9b1ae290" +checksum = "6f7d8b8cb1558ef0d090882c6359e5e2d5571ca6633446fa2347fa48da7b27bb" dependencies = [ "anyhow", "cargo_metadata", @@ -795,9 +800,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encode_unicode" @@ -841,14 +846,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "filetime" @@ -871,15 +876,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", @@ -984,15 +989,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - [[package]] name = "fragile" version = "2.1.0" @@ -1010,7 +1006,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ "io-lifetimes 2.0.4", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1043,19 +1039,20 @@ dependencies = [ "serial_test", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1064,20 +1061,20 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" @@ -1087,9 +1084,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", @@ -1162,17 +1159,14 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.4" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" dependencies = [ "bitflags 2.13.1", "libc", "libgit2-sys", "log", - "openssl-probe", - "openssl-sys", - "url", ] [[package]] @@ -1183,9 +1177,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -1255,9 +1249,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1271,9 +1265,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -1325,114 +1319,11 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "ignore" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ffa3a0547a138e59ddd6fa3b7c672ed47e6ad6a3cd177984ff1116aa5ba742" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", @@ -1511,7 +1402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" dependencies = [ "io-lifetimes 2.0.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1521,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" dependencies = [ "io-lifetimes 3.0.1", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1538,9 +1429,9 @@ checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -1565,11 +1456,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.32" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", + "jiff-core", "jiff-static", "log", "portable-atomic", @@ -1577,12 +1469,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + [[package]] name = "jiff-static" -version = "0.2.32" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ + "jiff-core", "proc-macro2", "quote", "syn 2.0.119", @@ -1606,9 +1508,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -1635,39 +1537,23 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libgit2-sys" -version = "0.18.5+1.9.4" +version = "0.18.8+1.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" +checksum = "7f7c568b25d7489bc3fb2988ed69ab111d2944d2f5fec3d5c987fe545ea97b50" dependencies = [ "cc", "libc", - "libssh2-sys", "libz-sys", - "openssl-sys", "pkg-config", ] [[package]] name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "libc", -] - -[[package]] -name = "libssh2-sys" -version = "0.3.2" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c04141a07bb0c0bc461cb657808764de571702a59bc5c726c400ac9a7625e3ab" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" dependencies = [ - "cc", "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", ] [[package]] @@ -1688,12 +1574,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - [[package]] name = "lock_api" version = "0.4.14" @@ -1705,9 +1585,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "value-bag", ] @@ -1770,9 +1650,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -1833,6 +1713,7 @@ dependencies = [ "tempfile", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1854,6 +1735,7 @@ dependencies = [ "serial_test", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1884,6 +1766,7 @@ dependencies = [ "tokio", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1910,9 +1793,10 @@ dependencies = [ "serde_json", "serial_test", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1936,6 +1820,7 @@ dependencies = [ "temp-env", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -1975,24 +1860,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -2073,15 +1940,15 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" @@ -2092,15 +1959,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -2189,9 +2047,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -2223,9 +2081,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2380,9 +2238,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -2470,7 +2328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe104196f61dc8911a8da1b10005e9401e7c2e14ad9302e2de6688311c0beec7" dependencies = [ "camino", - "cap-std 3.4.5", + "cap-std 3.4.6", "cfg-if", "convert_case 0.6.0", "gherkin", @@ -2523,7 +2381,7 @@ dependencies = [ "rustc_session", "rustc_span", "serde", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "trybuild", "whitaker", "whitaker-common", @@ -2668,7 +2526,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2683,9 +2541,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -2698,18 +2556,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2792,7 +2650,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2839,7 +2697,7 @@ checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2919,12 +2777,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "strsim" version = "0.11.1" @@ -2960,26 +2812,15 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "sys-locale" version = "0.3.2" @@ -3002,9 +2843,9 @@ dependencies = [ [[package]] name = "target-triple" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" [[package]] name = "temp-env" @@ -3022,10 +2863,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3071,6 +2912,7 @@ dependencies = [ "serde", "whitaker", "whitaker-common", + "whitaker_test_macros", ] [[package]] @@ -3140,14 +2982,14 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] name = "time" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "js-sys", @@ -3165,9 +3007,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "serde_core", @@ -3186,13 +3028,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -3206,9 +3048,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -3237,14 +3079,15 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", + "toml_writer", "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.4", ] @@ -3304,7 +3147,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] @@ -3475,30 +3318,12 @@ dependencies = [ "log", ] -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - [[package]] name = "utf8-zero" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -3507,9 +3332,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "value-bag" -version = "1.13.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd4ec1eb1d240636e354a30110a1dfcb37047169a4d9bd6d9d3469df574b5c4" +checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" [[package]] name = "vcpkg" @@ -3559,9 +3384,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -3572,9 +3397,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3582,9 +3407,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -3595,18 +3420,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -3629,9 +3454,10 @@ dependencies = [ "rustc_span", "serde", "thiserror 2.0.20", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "whitaker-common", "whitaker-installer", + "whitaker_test_macros", ] [[package]] @@ -3653,6 +3479,7 @@ dependencies = [ "tempfile", "thiserror 2.0.20", "unic-langid", + "whitaker_test_macros", ] [[package]] @@ -3678,13 +3505,14 @@ dependencies = [ "temp-env", "tempfile", "thiserror 2.0.20", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "trybuild", "ureq", "wait-timeout", "whitaker-common", "whitaker-installer", + "whitaker_test_macros", "zip", "zstd", ] @@ -3706,9 +3534,10 @@ dependencies = [ "sha2", "tempfile", "thiserror 2.0.20", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "whitaker_sarif", + "whitaker_test_macros", ] [[package]] @@ -3723,6 +3552,7 @@ dependencies = [ "serde_json", "tempfile", "thiserror 2.0.20", + "whitaker_test_macros", ] [[package]] @@ -3748,6 +3578,16 @@ dependencies = [ "rustc_span", "test_must_not_have_example", "whitaker-common", + "whitaker_test_macros", +] + +[[package]] +name = "whitaker_test_macros" +version = "0.2.7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -3772,7 +3612,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3793,7 +3633,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3802,7 +3642,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -3820,14 +3669,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3836,48 +3702,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" @@ -3900,7 +3814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ "bitflags 2.13.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3909,12 +3823,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - [[package]] name = "xattr" version = "1.6.1" @@ -3925,43 +3833,20 @@ dependencies = [ "rustix", ] -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -3973,21 +3858,6 @@ name = "zerofrom" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] [[package]] name = "zeroize" @@ -3995,38 +3865,14 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "serde", - "yoke", "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", ] [[package]] @@ -4058,9 +3904,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij"