From 08edfd5381c3e16c1526ff8a3f19a6aea1d1f885 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 16 Sep 2026 16:55:14 +0200 Subject: [PATCH 1/4] Format Rust sources with the nightly profile Route Rust formatting through the pinned nightly profile while retaining the stable compiler for build, test, lint, documentation, and Whitaker work. Keep byte-exact UI fixtures outside the formatter and constrain the known rstest fixture exceptions through the formatter contract. --- AGENTS.md | 46 +++++++++++-------- Makefile | 6 ++- .../test_rust_formatter_toolchain.py | 41 +++++++++++++++++ docs/developers-guide.md | 14 ++++-- rust/.rustfmt.toml | 16 +++++++ .../src/fd_ownership_kani_proofs.rs | 14 ++++-- .../src/fd_ownership_model.rs | 22 ++++----- rust/cuprum-native-io/src/kani_proofs.rs | 4 +- rust/cuprum-native-io/src/lib.rs | 9 ++-- rust/cuprum-native-io/src/memory.rs | 7 ++- rust/cuprum-native-io/src/ownership_tests.rs | 6 ++- rust/cuprum-native-io/src/windows.rs | 10 ++-- rust/cuprum-rust/src/errors.rs | 20 ++++---- rust/cuprum-rust/src/lib.rs | 7 +-- rust/cuprum-rust/src/stream_pyfunctions.rs | 12 ++++- rust/cuprum-streams/src/buffer.rs | 4 +- .../src/consume_snapshot_tests.rs | 12 +++-- rust/cuprum-streams/src/errors.rs | 1 + rust/cuprum-streams/src/io_utils/mod.rs | 14 ++---- rust/cuprum-streams/src/io_utils/tests.rs | 23 +++++++--- .../src/io_utils/tracing_tests.rs | 20 ++++---- rust/cuprum-streams/src/lib.rs | 8 +--- rust/cuprum-streams/src/lib_tests.rs | 26 ++++++----- rust/cuprum-streams/src/pump_machine.rs | 8 +--- rust/cuprum-streams/src/splice/mod.rs | 6 +-- rust/cuprum-streams/src/splice/tests.rs | 12 +++-- rust/cuprum-streams/src/test_support.rs | 17 ++++--- rust/cuprum-streams/src/tracing_capture.rs | 38 +++++++-------- .../src/tracing_capture_tests.rs | 3 +- rust/cuprum-streams/src/utf8.rs | 28 +++++------ rust/cuprum-streams/src/utf8_kani_proofs.rs | 38 +++++++-------- rust/cuprum-streams/tests/compile_tests.rs | 4 +- 32 files changed, 289 insertions(+), 207 deletions(-) create mode 100644 rust/.rustfmt.toml diff --git a/AGENTS.md b/AGENTS.md index f9fb3ac23..064ed377e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -193,22 +193,31 @@ working on the Rust portions of the project: - `make check-fmt` executes: ```sh - cargo fmt --all -- --check + cargo +nightly-2026-05-28 fmt --all -- --check ``` validating formatting across the Rust workspace without modifying files. - - `make lint` executes: + - `make lint` executes its Rustdoc and Clippy steps through + `RUST_DEBUG_CARGO`, then runs Whitaker separately: ```sh - RUSTDOCFLAGS="-D warnings" cargo doc --no-deps - cargo clippy --all-targets --all-features -- -D warnings - RUSTFLAGS="-D warnings" whitaker --all -- --all-targets --all-features + cd rust && RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" \ + $(RUST_DEBUG_CARGO) doc --no-deps $(DOC_FLAGS) && \ + $(RUST_DEBUG_CARGO) clippy $(CLIPPY_FLAGS) + cd rust && $(LOCAL_TOOL_ENV) \ + RUSTFLAGS="$(WHITAKER_RUSTFLAGS)" $(WHITAKER) --all -- \ + $(WHITAKER_CARGO_FLAGS) yamllint --config-file .yamllint.yml .github/workflows actionlint ``` - linting every target with all features enabled, denying all Clippy - warnings, and validating GitHub Actions workflows. Keep `.yamllint.yml` + On Linux, `RUST_DEBUG_CARGO` selects `nightly-2026-08-23` with the explicit + `tools/dev-fast/config.toml` fragment; elsewhere it selects the prescribed + Cargo route. Whitaker never receives that fragment. The protected coverage, + release, verification, and MSRV paths also retain their separate toolchain + and linker policy. The target lints every target with all features enabled, + denies all Clippy warnings, and validates GitHub Actions workflows. Keep + `.yamllint.yml` compatible with GitHub's unquoted `on` trigger key and require each workflow to begin with `---`. CI must install yamllint with `uv tool` and use the pinned, checksum-verified actionlint @@ -221,20 +230,21 @@ working on the Rust portions of the project: ``` running the Rust test suite with warnings denied. Use `make fmt` - (`cargo fmt --all`) to apply formatting fixes reported by the formatter - check alongside the Python and Markdown formatters. + (`cargo +nightly-2026-05-28 fmt --all`) to apply formatting fixes reported + by the formatter check alongside the Python and Markdown formatters. - The project toolchain stays pinned to Rust `1.85.0` and declares `rustfmt`, `clippy`, and `rust-analyzer`. CI also provisions `nightly-2026-05-28` for the maintenance formatter, then restores the project toolchain. The current - `fmt` and `check-fmt` recipes still use the project toolchain. Linux-only - debug Rust work additionally uses the separately pinned dev-fast nightly - through `tools/dev-fast/config.toml`; run `make dev-fast-check` first, since - it requires both the `rustc-codegen-cranelift` and `clippy` components. The - stable toolchain remains mandatory for release, coverage, Whitaker, the - explicit `make msrv-check` verification target, and the macOS and Windows - platform alternatives. All Rust packages inherit Cargo's - `rust-version = "1.85.0"` contract, which also keeps MSRV-aware Clippy - findings valid when debug linting uses the dev-fast nightly. + `fmt` and `check-fmt` recipes explicitly select the pinned nightly only for + formatting. Linux-only debug Rust work additionally uses the separately + pinned dev-fast nightly through `tools/dev-fast/config.toml`; run + `make dev-fast-check` first, since it requires both the + `rustc-codegen-cranelift` and `clippy` components. The stable toolchain + remains mandatory for release, coverage, Whitaker, the explicit + `make msrv-check` verification target, and the macOS and Windows platform + alternatives. All Rust packages inherit Cargo's `rust-version = "1.85.0"` + contract, which also keeps MSRV-aware Clippy findings valid when debug + linting uses the dev-fast nightly. - Clippy warnings MUST be disallowed. - Fix any warnings emitted during tests in code instead of silencing them. - Where a function is too long, extract meaningfully named helper functions diff --git a/Makefile b/Makefile index 81bae9fc0..648704477 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,8 @@ DEVELOP_DEV_FAST_ENV = $(if $(DEVELOP_DEV_FAST_ENABLED),RUSTUP_TOOLCHAIN=$(DEV_F DEV_FAST_CHECK_COMMAND = test "$(DEV_FAST_HOST_IS_LINUX)" = yes || { printf '%s\n' 'dev-fast is supported only on Linux; use the stable backend on this host' >&2; exit 1; }; test -f "$(DEV_FAST_CONFIG_RELATIVE)" || { printf 'dev-fast configuration is missing: %s\n' "$(DEV_FAST_CONFIG_RELATIVE)" >&2; exit 1; }; command -v mold >/dev/null 2>&1 || { printf 'mold %s is required for Linux dev-fast builds\n' "$(DEV_FAST_MOLD_VERSION)" >&2; exit 1; }; mold --version | grep -q '^mold $(DEV_FAST_MOLD_VERSION_PATTERN)\($$\|[[:space:]]\)' || { printf 'mold %s is required for Linux dev-fast builds\n' "$(DEV_FAST_MOLD_VERSION)" >&2; exit 1; }; components="$$(rustup component list --installed --toolchain "$(DEV_FAST_TOOLCHAIN)")" || { printf 'cannot inspect the components installed for %s\n' "$(DEV_FAST_TOOLCHAIN)" >&2; exit 1; }; for component in $(DEV_FAST_REQUIRED_COMPONENTS); do printf '%s\n' "$$components" | grep -q "^$$component" || { printf 'install %s for %s before using dev-fast\n' "$$component" "$(DEV_FAST_TOOLCHAIN)" >&2; exit 1; }; done DEV_FAST_TEST_RUSTFLAGS = $(TEST_RUSTFLAGS) $(if $(DEV_FAST_HOST_IS_LINUX),-Clink-arg=-fuse-ld=mold) DEV_FAST_TEST_COMMAND = if $(LOCAL_TOOL_ENV) command -v cargo-nextest >/dev/null 2>&1; then cd $(RUST_DIR) && CARGO_BUILD_JOBS="$(TEST_CARGO_BUILD_JOBS)" RUSTFLAGS="$(DEV_FAST_TEST_RUSTFLAGS)" $(DEV_FAST_CARGO_COMMAND) nextest run $(TEST_FLAGS) $(BUILD_JOBS); else echo "cargo-nextest not found; falling back to cargo test." >&2; cd $(RUST_DIR) && CARGO_BUILD_JOBS="$(TEST_CARGO_BUILD_JOBS)" RUSTFLAGS="$(DEV_FAST_TEST_RUSTFLAGS)" $(DEV_FAST_CARGO_COMMAND) test $(TEST_FLAGS) $(BUILD_JOBS); fi +RUSTFMT_TOOLCHAIN ?= nightly-2026-05-28 +RUSTFMT_CARGO ?= $(CARGO) +$(RUSTFMT_TOOLCHAIN) WHITAKER ?= whitaker BUILD_JOBS ?= RUST_FLAGS ?= -D warnings @@ -272,13 +274,13 @@ endif fmt: ruff ## Format sources $(RUFF) format $(RUFF) check --select I --fix - cd $(RUST_DIR) && $(CARGO) fmt --all + cd $(RUST_DIR) && $(RUSTFMT_CARGO) fmt --all $(LOCAL_TOOL_ENV) $(MDTABLEFIX) --in-place $(MDTABLEFIX_SELECT) $(MDTABLEFIX_EXTENSIONS) $(MDTABLEFIX_RULES) $(call run_markdownlint_files,$(MDLINT_FIX_COMMAND)) check-fmt: ruff ## Verify formatting $(RUFF) format --check - cd $(RUST_DIR) && $(CARGO) fmt --all -- --check + cd $(RUST_DIR) && $(RUSTFMT_CARGO) fmt --all -- --check $(LOCAL_TOOL_ENV) $(MDTABLEFIX) --check $(MDTABLEFIX_SELECT) $(MDTABLEFIX_EXTENSIONS) $(MDTABLEFIX_RULES) test-markdown-format: ## Validate the Markdown formatting Makefile contract diff --git a/cuprum/unittests/test_rust_formatter_toolchain.py b/cuprum/unittests/test_rust_formatter_toolchain.py index 2309f08f2..b9a64504d 100644 --- a/cuprum/unittests/test_rust_formatter_toolchain.py +++ b/cuprum/unittests/test_rust_formatter_toolchain.py @@ -3,6 +3,8 @@ from __future__ import annotations import re +import shutil +import subprocess # ruff: ignore[suspicious-subprocess-import] - expands fixed local Makefile recipes. import tomllib from tests.helpers.docs import repo_root @@ -244,3 +246,42 @@ def test_formatter_skips_are_limited_to_known_rstest_fixtures() -> None: assert observed == _FORMATTER_FIXTURE_SKIPS, ( "add a mutation proof before extending the formatter exception set" ) + + +def test_make_formatter_targets_select_the_pinned_nightly() -> None: + """Only formatter recipes select the pinned nightly cargo route.""" + make_executable = shutil.which("make") + assert make_executable is not None, "make must be available to expand recipes" + root = repo_root() + + def expanded_recipes(*targets: str) -> str: + """Return dry-run output for the fixed formatter contract targets.""" + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] + [make_executable, "--dry-run", "CARGO=probe-cargo", *targets], + check=True, + shell=False, + cwd=root, + capture_output=True, + encoding="utf-8", + ) + return completed.stdout + + formatter_recipes = expanded_recipes("fmt", "check-fmt") + formatter_lines = [ + line + for line in formatter_recipes.splitlines() + if line.startswith("cd rust && probe-cargo") + ] + expected_lines = [ + "cd rust && probe-cargo +nightly-2026-05-28 fmt --all", + "cd rust && probe-cargo +nightly-2026-05-28 fmt --all -- --check", + ] + assert formatter_lines == expected_lines, ( + "formatter targets must use only the injected pinned nightly cargo route" + ) + + non_formatter_recipes = expanded_recipes("lint", "test") + for formatter_line in expected_lines: + assert formatter_line not in non_formatter_recipes, ( + "lint and test must not select the formatter's nightly cargo route" + ) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index ca2969230..20e137314 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -355,9 +355,17 @@ The lint job installs Nixie and Whitaker through the pinned `leynos/shared-actions` installers. Nixie's installer also provisions Merman; the bootstrap uses Rust `1.95.0` because Merman CLI `0.7.0` requires that compiler. The job then restores Rust `1.85.0`, the project's supported -toolchain, before installing Whitaker and running the project gates. The -Whitaker action receives `WHITAKER_INSTALLER_VERSION` from the job environment -(`0.2.7`, the workflow's configured installer version). +toolchain, before installing Whitaker and running the project gates. On Linux, +standard debug Rust build, test, Rustdoc, and Clippy commands select +`nightly-2026-08-23` with the explicit `tools/dev-fast/config.toml` fragment. +The current `typecheck` target is Python-only and selects no Cargo fragment. +`fmt` and `check-fmt` alone select `nightly-2026-05-28` through +`rust/.rustfmt.toml`. Coverage, release, verification, MSRV, and Whitaker +commands, together with macOS and Windows, retain their prescribed +fragment-free or separately pinned toolchains. The stable pin declares +`rustfmt`, `clippy`, and `rust-analyzer` for local maintenance. The Whitaker +action receives `WHITAKER_INSTALLER_VERSION` from the job environment (`0.2.7`, +the workflow's configured installer version). cargo-nextest is no longer installed here at all. The coverage job is the only place it runs, and the shared action installs it from checksummed official diff --git a/rust/.rustfmt.toml b/rust/.rustfmt.toml new file mode 100644 index 000000000..a2c5d8f56 --- /dev/null +++ b/rust/.rustfmt.toml @@ -0,0 +1,16 @@ +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 + +ignore = ["cuprum-rust/tests/ui"] diff --git a/rust/cuprum-native-io/src/fd_ownership_kani_proofs.rs b/rust/cuprum-native-io/src/fd_ownership_kani_proofs.rs index 57ee51bfa..0d729ea15 100644 --- a/rust/cuprum-native-io/src/fd_ownership_kani_proofs.rs +++ b/rust/cuprum-native-io/src/fd_ownership_kani_proofs.rs @@ -2,10 +2,10 @@ //! //! The invariant under proof, over every modelled exit path: //! -//! - a reader passed through the production retention kernel is **never** closed by -//! Rust — it stays caller-owned on normal completion and error return; -//! - the `pump_stream` writer is **consumed** and closes exactly once on -//! every exit path, which is what signals EOF downstream. +//! - a reader passed through the production retention kernel is **never** closed by Rust — it stays +//! caller-owned on normal completion and error return; +//! - the `pump_stream` writer is **consumed** and closes exactly once on every exit path, which is +//! what signals EOF downstream. //! //! Scope. These are bounded-model proofs over the ownership *model* in //! [`super::fd_ownership_model`], not over real descriptors: Kani does not @@ -19,7 +19,11 @@ //! is bounded explicitly. use super::fd_ownership_model::{ - CloseLog, ExitMode, ModelFd, model_consume_stream, model_pump_stream, + CloseLog, + ExitMode, + ModelFd, + model_consume_stream, + model_pump_stream, model_with_borrowed_reader, }; diff --git a/rust/cuprum-native-io/src/fd_ownership_model.rs b/rust/cuprum-native-io/src/fd_ownership_model.rs index 205f46074..c8f2ddce3 100644 --- a/rust/cuprum-native-io/src/fd_ownership_model.rs +++ b/rust/cuprum-native-io/src/fd_ownership_model.rs @@ -6,9 +6,10 @@ //! real panic-unwind are proved by this model: native regressions exercise //! those paths, including the historical trailing-`mem::forget` mistake. -use crate::memory::with_retained_owner; use core::cell::Cell; +use crate::memory::with_retained_owner; + /// Records how many times the modelled descriptor was closed. /// /// A real close is an unmodellable side effect, so the model counts them. @@ -26,13 +27,9 @@ impl CloseLog { } /// Number of closes recorded so far. - pub(crate) const fn closes(self) -> u32 { - self.closes.into_inner() - } + pub(crate) const fn closes(self) -> u32 { self.closes.into_inner() } - fn record_close(&self) { - self.closes.set(self.closes.get().saturating_add(1)); - } + fn record_close(&self) { self.closes.set(self.closes.get().saturating_add(1)); } } /// A modelled *owning* descriptor handle. @@ -47,15 +44,11 @@ pub(crate) struct ModelFd<'log> { impl<'log> ModelFd<'log> { /// Reconstruct an owning handle that reports closes to `log`. - pub(crate) const fn new(log: &'log CloseLog) -> Self { - Self { log } - } + pub(crate) const fn new(log: &'log CloseLog) -> Self { Self { log } } } impl Drop for ModelFd<'_> { - fn drop(&mut self) { - self.log.record_close(); - } + fn drop(&mut self) { self.log.record_close(); } } /// Represents an ordinary operation error, not panic-unwind. @@ -118,9 +111,10 @@ mod tests { //! Concrete unit tests for the ownership model, mirroring the bounded //! Kani proofs so the contract is exercised by `make test` as well. - use super::{CloseLog, ExitMode, ModelFd, model_consume_stream, model_pump_stream}; use rstest::rstest; + use super::{CloseLog, ExitMode, ModelFd, model_consume_stream, model_pump_stream}; + #[rstest] #[case(ExitMode::Normal)] #[case(ExitMode::Error)] diff --git a/rust/cuprum-native-io/src/kani_proofs.rs b/rust/cuprum-native-io/src/kani_proofs.rs index a0af61759..b9ee4836c 100644 --- a/rust/cuprum-native-io/src/kani_proofs.rs +++ b/rust/cuprum-native-io/src/kani_proofs.rs @@ -13,9 +13,7 @@ use super::{ struct Resource<'a>(&'a Cell); impl Drop for Resource<'_> { - fn drop(&mut self) { - self.0.set(self.0.get() + 1); - } + fn drop(&mut self) { self.0.set(self.0.get() + 1); } } #[kani::proof] diff --git a/rust/cuprum-native-io/src/lib.rs b/rust/cuprum-native-io/src/lib.rs index befa1cf0b..4ebea514b 100644 --- a/rust/cuprum-native-io/src/lib.rs +++ b/rust/cuprum-native-io/src/lib.rs @@ -18,13 +18,14 @@ mod fd_ownership_model; #[cfg(unix)] pub use std::os::fd::{AsFd as AsStream, BorrowedFd as BorrowedStream, OwnedFd as OwnedStream}; +#[cfg(unix)] +use std::os::fd::{AsRawFd, FromRawFd}; #[cfg(windows)] pub use std::os::windows::io::{ - AsHandle as AsStream, BorrowedHandle as BorrowedStream, OwnedHandle as OwnedStream, + AsHandle as AsStream, + BorrowedHandle as BorrowedStream, + OwnedHandle as OwnedStream, }; - -#[cfg(unix)] -use std::os::fd::{AsRawFd, FromRawFd}; #[cfg(windows)] use std::os::windows::io::{FromRawHandle, RawHandle}; diff --git a/rust/cuprum-native-io/src/memory.rs b/rust/cuprum-native-io/src/memory.rs index d3e587fe8..2d3a9e134 100644 --- a/rust/cuprum-native-io/src/memory.rs +++ b/rust/cuprum-native-io/src/memory.rs @@ -27,15 +27,14 @@ pub(crate) fn with_retained_owner(value: T, operation: impl FnOnce(&mut T) #[cfg(test)] mod tests { //! Real unwinding through the production retention frame. - use super::with_retained_owner; use core::cell::Cell; use std::panic::{AssertUnwindSafe, catch_unwind}; + use super::with_retained_owner; + struct Close<'a>(&'a Cell); impl Drop for Close<'_> { - fn drop(&mut self) { - self.0.set(self.0.get() + 1); - } + fn drop(&mut self) { self.0.set(self.0.get() + 1); } } #[test] diff --git a/rust/cuprum-native-io/src/ownership_tests.rs b/rust/cuprum-native-io/src/ownership_tests.rs index d022fe47c..df3887ae4 100644 --- a/rust/cuprum-native-io/src/ownership_tests.rs +++ b/rust/cuprum-native-io/src/ownership_tests.rs @@ -4,10 +4,12 @@ use std::os::fd::{AsRawFd, IntoRawFd}; #[cfg(windows)] use std::os::windows::io::{AsRawHandle, IntoRawHandle}; -use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::{ + panic::{AssertUnwindSafe, catch_unwind}, + sync::{LockResult, Mutex, MutexGuard}, +}; use rstest::{fixture, rstest}; -use std::sync::{LockResult, Mutex, MutexGuard}; use super::{adopt_writer, borrow, borrow_reader, fd_is_open, pipe, read_once, with_owned_writer}; diff --git a/rust/cuprum-native-io/src/windows.rs b/rust/cuprum-native-io/src/windows.rs index 00236ba00..32bb15dce 100644 --- a/rust/cuprum-native-io/src/windows.rs +++ b/rust/cuprum-native-io/src/windows.rs @@ -1,15 +1,17 @@ //! Windows borrowed I/O, pipe ownership, and native handle observation. -use std::io::{self, Read, Write}; -use std::os::windows::io::{AsRawHandle, FromRawHandle}; +use std::{ + io::{self, Read, Write}, + os::windows::io::{AsRawHandle, FromRawHandle}, +}; +#[cfg(test)] +use windows_sys::Win32::Foundation::{ERROR_INVALID_HANDLE, GetHandleInformation}; use windows_sys::Win32::System::Pipes::CreatePipe; #[cfg(test)] use crate::PlatformFd; use crate::{BorrowedStream, OwnedStream}; -#[cfg(test)] -use windows_sys::Win32::Foundation::{ERROR_INVALID_HANDLE, GetHandleInformation}; /// Create two uniquely owned, non-inheritable anonymous pipe handles. /// diff --git a/rust/cuprum-rust/src/errors.rs b/rust/cuprum-rust/src/errors.rs index 0e3f4e6de..c7003a697 100644 --- a/rust/cuprum-rust/src/errors.rs +++ b/rust/cuprum-rust/src/errors.rs @@ -1,9 +1,9 @@ //! Map safe stream errors to Python exceptions without losing OS codes. -use cuprum_streams::PumpError; -use pyo3::PyErr; -use pyo3::exceptions::PyOSError; use std::io; +use cuprum_streams::PumpError; +use pyo3::{PyErr, exceptions::PyOSError}; + pub(crate) fn pump_error_to_py_err(err: PumpError) -> PyErr { match err { PumpError::Io(io_err) => io_error_to_py_err(io_err), @@ -38,9 +38,7 @@ fn strip_os_error_suffix(message: &str, code: i32) -> String { /// through `io::ErrorKind`, taken from the authoritative source rather than a /// parallel table. #[cfg(unix)] -fn os_error_to_py_err(code: i32, strerror: String) -> PyErr { - PyOSError::new_err((code, strerror)) -} +fn os_error_to_py_err(code: i32, strerror: String) -> PyErr { PyOSError::new_err((code, strerror)) } /// Build a Python `OSError` from a Win32 error code and its message. /// @@ -103,10 +101,12 @@ fn raw_os_error_parts(err: &io::Error) -> Option<(i32, String)> { mod tests { //! Unit tests for the canonical `PumpError` taxonomy: variant mapping, //! the non-fatal write predicate, and stable display messages. - use super::PumpError; - use rstest::rstest; use std::io; + use rstest::rstest; + + use super::PumpError; + #[test] fn io_errors_round_trip_their_kind() { let source = io::Error::new(io::ErrorKind::BrokenPipe, "downstream closed"); @@ -252,8 +252,8 @@ mod tests { fn a_synthesized_error_has_no_code_to_preserve(#[case] err: io::Error) { assert!( super::raw_os_error_parts(&err).is_none(), - "an error with no raw_os_error must select PyO3's ErrorKind mapping \ - rather than a fabricated code: {err:?}", + "an error with no raw_os_error must select PyO3's ErrorKind mapping rather than a \ + fabricated code: {err:?}", ); } diff --git a/rust/cuprum-rust/src/lib.rs b/rust/cuprum-rust/src/lib.rs index 0f6ef5d52..8f0b4d2a0 100644 --- a/rust/cuprum-rust/src/lib.rs +++ b/rust/cuprum-rust/src/lib.rs @@ -6,8 +6,7 @@ use cuprum_native_io::PlatformFd; use cuprum_streams::{BufferSize, PumpError, consume_stream, pump_stream}; -use pyo3::exceptions::PyValueError; -use pyo3::prelude::*; +use pyo3::{exceptions::PyValueError, prelude::*}; mod errors; #[cfg(test)] mod fd_tests; @@ -30,9 +29,7 @@ fn validate_buffer_size(size: i64) -> PyResult { #[must_use] #[doc(hidden)] #[pyfunction] -pub const fn is_available() -> bool { - true -} +pub const fn is_available() -> bool { true } #[expect( clippy::allow_attributes, diff --git a/rust/cuprum-rust/src/stream_pyfunctions.rs b/rust/cuprum-rust/src/stream_pyfunctions.rs index 30f162d6f..15b968569 100644 --- a/rust/cuprum-rust/src/stream_pyfunctions.rs +++ b/rust/cuprum-rust/src/stream_pyfunctions.rs @@ -1,8 +1,16 @@ //! Contain the Python stream exports and their generated `PyO3` wrappers. use super::{ - BufferSize, PumpError, PyResult, Python, ReaderFd, consume_stream, convert_fd, pump_stream, - pyfunction, validate_buffer_size, + BufferSize, + PumpError, + PyResult, + Python, + ReaderFd, + consume_stream, + convert_fd, + pump_stream, + pyfunction, + validate_buffer_size, }; /// Run a prepared stream operation after validating its buffer size. diff --git a/rust/cuprum-streams/src/buffer.rs b/rust/cuprum-streams/src/buffer.rs index ca8c6c198..e6623fd04 100644 --- a/rust/cuprum-streams/src/buffer.rs +++ b/rust/cuprum-streams/src/buffer.rs @@ -65,9 +65,7 @@ impl BufferBucket { } /// The rounded size in bytes. - const fn bytes(self) -> usize { - self.0 - } + const fn bytes(self) -> usize { self.0 } } #[cfg(test)] diff --git a/rust/cuprum-streams/src/consume_snapshot_tests.rs b/rust/cuprum-streams/src/consume_snapshot_tests.rs index 7e2d035ff..25cf108c8 100644 --- a/rust/cuprum-streams/src/consume_snapshot_tests.rs +++ b/rust/cuprum-streams/src/consume_snapshot_tests.rs @@ -20,9 +20,12 @@ //! Each case feeds a fixed payload through a real pipe, so the decode is driven //! by actual descriptor reads rather than a synthetic byte buffer. -use crate::errors::PumpError; -use crate::test_support::{make_pipe, write_all_to}; -use crate::{BufferSize, consume_stream_files}; +use crate::{ + BufferSize, + consume_stream_files, + errors::PumpError, + test_support::{make_pipe, write_all_to}, +}; /// Decode `payload` through the real `consume_stream_files` read loop using a /// pipe, reading `buffer_size` bytes at a time. @@ -130,9 +133,10 @@ mod properties { //! Payloads stay small so the unbuffered write in `consume` cannot fill the //! pipe and block, which is the precondition the helper documents. - use super::consume; use proptest::prelude::*; + use super::consume; + /// The largest payload a single unbuffered pipe write must accept. const MAX_PAYLOAD: usize = 512; /// The largest read buffer the properties exercise. diff --git a/rust/cuprum-streams/src/errors.rs b/rust/cuprum-streams/src/errors.rs index 94c0c1e91..6e1ea3034 100644 --- a/rust/cuprum-streams/src/errors.rs +++ b/rust/cuprum-streams/src/errors.rs @@ -1,5 +1,6 @@ //! Semantic errors for safe stream orchestration. use std::io; + use thiserror::Error; /// Semantic error for stream pump and consume operations. diff --git a/rust/cuprum-streams/src/io_utils/mod.rs b/rust/cuprum-streams/src/io_utils/mod.rs index 6df4072d3..0c99dc568 100644 --- a/rust/cuprum-streams/src/io_utils/mod.rs +++ b/rust/cuprum-streams/src/io_utils/mod.rs @@ -5,11 +5,9 @@ //! errors like broken pipes. Failures are reported through the crate's //! canonical [`PumpError`] taxonomy. -use std::cell::Cell; -use std::io; +use std::{cell::Cell, io}; -use crate::errors::PumpError; -use crate::pump_machine::WriteEvent; +use crate::{errors::PumpError, pump_machine::WriteEvent}; thread_local! { /// EINTR retries observed on the read path of the current operation. @@ -31,14 +29,10 @@ pub(crate) fn reset_retry_counters() { } /// EINTR retries accumulated on the read path since the last reset. -pub(crate) fn read_retry_count() -> u64 { - READ_RETRIES.with(Cell::get) -} +pub(crate) fn read_retry_count() -> u64 { READ_RETRIES.with(Cell::get) } /// EINTR retries accumulated on the write path since the last reset. -pub(crate) fn write_retry_count() -> u64 { - WRITE_RETRIES.with(Cell::get) -} +pub(crate) fn write_retry_count() -> u64 { WRITE_RETRIES.with(Cell::get) } /// Increment the current operation's read-path `EINTR` retry counter. /// diff --git a/rust/cuprum-streams/src/io_utils/tests.rs b/rust/cuprum-streams/src/io_utils/tests.rs index 9fbdd5426..7374b5bc8 100644 --- a/rust/cuprum-streams/src/io_utils/tests.rs +++ b/rust/cuprum-streams/src/io_utils/tests.rs @@ -1,17 +1,26 @@ //! Direct tests for descriptor-backed I/O helper contracts. -use std::io; -use std::os::fd::OwnedFd; +use std::{io, os::fd::OwnedFd}; use proptest::prelude::*; +use rstest::{fixture, rstest}; use super::{ - PumpError, WriteOutcome, classify_write_outcome, classify_write_with, handle_write, - map_short_write_error, read_raw_fd, read_raw_fd_with, read_stream, write_all_unix_with, + PumpError, + WriteOutcome, + classify_write_outcome, + classify_write_with, + handle_write, + map_short_write_error, + read_raw_fd, + read_raw_fd_with, + read_stream, + write_all_unix_with, +}; +use crate::{ + pump_machine::WriteEvent, + test_support::{make_pipe, unwrap_err, unwrap_ok, write_all_to}, }; -use crate::pump_machine::WriteEvent; -use crate::test_support::{make_pipe, unwrap_err, unwrap_ok, write_all_to}; -use rstest::{fixture, rstest}; /// A fresh `pipe(2)` pair (`read_end`, `write_end`) for descriptor-backed /// tests, so the shared setup lives in one place rather than a repeated diff --git a/rust/cuprum-streams/src/io_utils/tracing_tests.rs b/rust/cuprum-streams/src/io_utils/tracing_tests.rs index bbaa1701d..b8e142005 100644 --- a/rust/cuprum-streams/src/io_utils/tracing_tests.rs +++ b/rust/cuprum-streams/src/io_utils/tracing_tests.rs @@ -17,7 +17,11 @@ use proptest::prelude::*; use tracing::Level; use super::{ - operation_span, read_raw_fd_with, read_retry_count, reset_retry_counters, write_all_unix_with, + operation_span, + read_raw_fd_with, + read_retry_count, + reset_retry_counters, + write_all_unix_with, write_retry_count, }; use crate::tracing_capture::capture; @@ -45,8 +49,7 @@ fn warn_filter_keeps_eintr_warning_context() { assert!( captured.event_has_fields(Level::WARN, &["operation", "buffer_size"]), - "EINTR warn event must retain operation + buffer_size context under a \ - warn filter", + "EINTR warn event must retain operation + buffer_size context under a warn filter", ); } @@ -63,8 +66,7 @@ fn error_filter_keeps_fatal_write_context() { assert!( captured.event_has_fields(Level::ERROR, &["operation", "buffer_size"]), - "fatal write error event must retain operation + buffer_size context \ - under an error filter", + "fatal write error event must retain operation + buffer_size context under an error filter", ); } @@ -84,8 +86,8 @@ fn error_filter_keeps_ordinary_fatal_write_context() { assert!( captured.event_has_fields(Level::ERROR, &["operation", "buffer_size"]), - "map_short_write_error's fatal event must retain operation + \ - buffer_size context under an error filter", + "map_short_write_error's fatal event must retain operation + buffer_size context under an \ + error filter", ); } @@ -105,8 +107,8 @@ fn error_filter_keeps_read_overflow_context() { assert!( captured.event_has_fields(Level::ERROR, &["operation", "buffer_size"]), - "read length-overflow error event must retain operation + buffer_size \ - context under an error filter", + "read length-overflow error event must retain operation + buffer_size context under an \ + error filter", ); } diff --git a/rust/cuprum-streams/src/lib.rs b/rust/cuprum-streams/src/lib.rs index 2353a1622..17eab8c34 100644 --- a/rust/cuprum-streams/src/lib.rs +++ b/rust/cuprum-streams/src/lib.rs @@ -61,13 +61,9 @@ impl BufferSize { /// /// # Errors /// Rejects non-positive sizes and sizes above the allocation cap. - pub fn new(size: i64) -> Result { - checked_buffer_size(size).map(Self) - } + pub fn new(size: i64) -> Result { checked_buffer_size(size).map(Self) } - const fn value(self) -> usize { - self.0 - } + const fn value(self) -> usize { self.0 } } /// Pump from a borrowed reader, consuming the uniquely owned writer. diff --git a/rust/cuprum-streams/src/lib_tests.rs b/rust/cuprum-streams/src/lib_tests.rs index 4726d562c..9f5cb0fbd 100644 --- a/rust/cuprum-streams/src/lib_tests.rs +++ b/rust/cuprum-streams/src/lib_tests.rs @@ -1,15 +1,20 @@ //! Tests for the borrowed file-descriptor ownership contract. -use crate::buffer::ALLOCATION_FAILED_CATEGORY; -use crate::errors::PumpError; -use crate::io_utils::{classify_write, read_stream}; -use crate::pump_machine::WriteEvent; -use crate::test_support::{make_pipe, read_all_from, unwrap_ok, write_all_to}; -use crate::tracing_capture::capture; -use crate::{BufferSize, consume_stream_files, pump_stream_files_readwrite}; use rstest::rstest; use tracing::Level; +use crate::{ + BufferSize, + buffer::ALLOCATION_FAILED_CATEGORY, + consume_stream_files, + errors::PumpError, + io_utils::{classify_write, read_stream}, + pump_machine::WriteEvent, + pump_stream_files_readwrite, + test_support::{make_pipe, read_all_from, unwrap_ok, write_all_to}, + tracing_capture::capture, +}; + /// Consuming a pipe records its byte total and zero read retries in the span. #[rstest] fn consume_records_total_bytes_and_retries_on_span() { @@ -135,8 +140,7 @@ fn allocation_failure_reports_a_bounded_category_event() { ("platform", "unix"), ], ), - "a refused allocation must emit its stable category and bounded buffer \ - size at error level", + "a refused allocation must emit its stable category and bounded buffer size at error level", ); } @@ -245,8 +249,8 @@ fn pump_drains_the_reader_after_the_writer_breaks() { "broken pipe; draining reader", &[("bytes_transferred", "0")], ), - "the writer-close latch must report the hang-up by name, with the zero \ - byte total that reached the hung-up downstream", + "the writer-close latch must report the hang-up by name, with the zero byte total that \ + reached the hung-up downstream", ); // The reader must have been drained to EOF: a further read returns zero diff --git a/rust/cuprum-streams/src/pump_machine.rs b/rust/cuprum-streams/src/pump_machine.rs index ef9c3f4d4..99d79d8b4 100644 --- a/rust/cuprum-streams/src/pump_machine.rs +++ b/rust/cuprum-streams/src/pump_machine.rs @@ -69,14 +69,10 @@ impl PumpState { } /// Bytes confirmed written downstream so far. - pub(crate) const fn total_written(self) -> u64 { - self.total_written - } + pub(crate) const fn total_written(self) -> u64 { self.total_written } /// Whether the writer is still accepting data. - pub(crate) const fn writer_open(self) -> bool { - self.writer_open - } + pub(crate) const fn writer_open(self) -> bool { self.writer_open } } /// Whether the pump loop continues or stops after an iteration. diff --git a/rust/cuprum-streams/src/splice/mod.rs b/rust/cuprum-streams/src/splice/mod.rs index 8c2b7444d..6a2256d99 100644 --- a/rust/cuprum-streams/src/splice/mod.rs +++ b/rust/cuprum-streams/src/splice/mod.rs @@ -15,11 +15,11 @@ //! Splice requires at least one pipe endpoint. Regular files, most sockets, //! and other file descriptor types return `EINVAL` and trigger the fallback. -use cuprum_native_io::{AsStream, BorrowedStream, borrow}; use std::io; -use crate::errors::PumpError; -use crate::io_utils::read_raw_fd; +use cuprum_native_io::{AsStream, BorrowedStream, borrow}; + +use crate::{errors::PumpError, io_utils::read_raw_fd}; /// Attempt to pump data using `splice()`. Returns None if splice is not supported. /// diff --git a/rust/cuprum-streams/src/splice/tests.rs b/rust/cuprum-streams/src/splice/tests.rs index 310711d95..18590cfb8 100644 --- a/rust/cuprum-streams/src/splice/tests.rs +++ b/rust/cuprum-streams/src/splice/tests.rs @@ -2,16 +2,20 @@ //! drain: full transfer between pipes, fallback signalling for //! unsupported descriptor types, and broken-pipe draining. -use std::io::{self, Write}; -use std::os::fd::OwnedFd; +use std::{ + io::{self, Write}, + os::fd::OwnedFd, +}; use proptest::prelude::*; use rstest::{fixture, rstest}; use tempfile::NamedTempFile; use super::{accumulate_splices, drain_reader, splice_once_with, try_splice_pump}; -use crate::errors::PumpError; -use crate::test_support::{make_pipe, read_all_from, unwrap_ok, write_all_to}; +use crate::{ + errors::PumpError, + test_support::{make_pipe, read_all_from, unwrap_ok, write_all_to}, +}; /// A connected `pipe(2)` pair: `(read_end, write_end)`. type PipePair = (OwnedFd, OwnedFd); diff --git a/rust/cuprum-streams/src/test_support.rs b/rust/cuprum-streams/src/test_support.rs index d3dbaa827..0ed477cd2 100644 --- a/rust/cuprum-streams/src/test_support.rs +++ b/rust/cuprum-streams/src/test_support.rs @@ -1,24 +1,23 @@ //! Shared Unix file-descriptor fixtures and assertions for Rust unit tests. +use std::{ + fmt::Debug, + io::{self, Read, Write}, + os::fd::OwnedFd, +}; + use cap_std::fs::File; -use std::fmt::Debug; -use std::io::{self, Read, Write}; -use std::os::fd::OwnedFd; /// Create an anonymous pipe as `(read_end, write_end)`. /// /// Returns the operating-system error from `pipe(2)` so fixtures can pass it /// to the test body, where the failure receives assertion context. -pub(crate) fn make_pipe() -> io::Result<(OwnedFd, OwnedFd)> { - cuprum_native_io::pipe() -} +pub(crate) fn make_pipe() -> io::Result<(OwnedFd, OwnedFd)> { cuprum_native_io::pipe() } /// Duplicate a typed descriptor into an independently owned [`File`]. /// /// The caller retains its descriptor; cloning failures propagate to the test. -pub(crate) fn dup_as_file(fd: &OwnedFd) -> io::Result { - fd.try_clone().map(File::from) -} +pub(crate) fn dup_as_file(fd: &OwnedFd) -> io::Result { fd.try_clone().map(File::from) } /// Write every byte of `payload` through a duplicated descriptor. /// diff --git a/rust/cuprum-streams/src/tracing_capture.rs b/rust/cuprum-streams/src/tracing_capture.rs index 2813cca5d..b3fe85d19 100644 --- a/rust/cuprum-streams/src/tracing_capture.rs +++ b/rust/cuprum-streams/src/tracing_capture.rs @@ -8,15 +8,23 @@ //! production filters and that the pump/consume loops record `total_bytes` and //! retry counts on their span. -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fmt::Debug; -use std::sync::{Arc, LazyLock, Mutex, MutexGuard, PoisonError}; - -use tracing::field::{Field, Visit}; -use tracing::level_filters::LevelFilter; -use tracing::span::{Attributes, Id, Record}; -use tracing::subscriber::{Interest, NoSubscriber}; -use tracing::{Dispatch, Event, Level, Metadata, Subscriber}; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + fmt::Debug, + sync::{Arc, LazyLock, Mutex, MutexGuard, PoisonError}, +}; + +use tracing::{ + Dispatch, + Event, + Level, + Metadata, + Subscriber, + field::{Field, Visit}, + level_filters::LevelFilter, + span::{Attributes, Id, Record}, + subscriber::{Interest, NoSubscriber}, +}; /// A captured event: its level, the field names reachable from the active span /// stack when it was emitted, and the values it carried itself. @@ -184,9 +192,7 @@ impl Subscriber for FilterCapture { } /// Add the entered span to the active context stack. - fn enter(&self, span: &Id) { - lock(&self.state).stack.push(span.into_u64()); - } + fn enter(&self, span: &Id) { lock(&self.state).stack.push(span.into_u64()); } /// Remove the most recently entered matching span from the context stack. fn exit(&self, span: &Id) { @@ -209,9 +215,7 @@ struct DormantSubscriber; impl Subscriber for DormantSubscriber { /// Keep the retained guard dormant so it never receives events. - fn enabled(&self, _metadata: &Metadata<'_>) -> bool { - false - } + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { false } /// Disable callsites while this is the registry's only dispatch. fn max_level_hint(&self) -> Option { @@ -221,9 +225,7 @@ impl Subscriber for DormantSubscriber { } /// Produce inert span identifiers because the guard retains no span state. - fn new_span(&self, attrs: &Attributes<'_>) -> Id { - NoSubscriber::new().new_span(attrs) - } + fn new_span(&self, attrs: &Attributes<'_>) -> Id { NoSubscriber::new().new_span(attrs) } /// Ignore records because the dormant guard retains no span state. fn record(&self, _span: &Id, _values: &Record<'_>) {} diff --git a/rust/cuprum-streams/src/tracing_capture_tests.rs b/rust/cuprum-streams/src/tracing_capture_tests.rs index 264be4839..c0b352fec 100644 --- a/rust/cuprum-streams/src/tracing_capture_tests.rs +++ b/rust/cuprum-streams/src/tracing_capture_tests.rs @@ -7,8 +7,7 @@ use proptest::prelude::*; use rstest::rstest; -use tracing::Level; -use tracing::callsite::Callsite; +use tracing::{Level, callsite::Callsite}; use super::{Captured, capture}; diff --git a/rust/cuprum-streams/src/utf8.rs b/rust/cuprum-streams/src/utf8.rs index 8e548f005..be9eabe57 100644 --- a/rust/cuprum-streams/src/utf8.rs +++ b/rust/cuprum-streams/src/utf8.rs @@ -9,9 +9,7 @@ pub(crate) struct ValidUpTo(usize); impl ValidUpTo { - pub(crate) const fn value(self) -> usize { - self.0 - } + pub(crate) const fn value(self) -> usize { self.0 } } /// Marker indicating whether this is the final chunk of input. @@ -19,13 +17,9 @@ impl ValidUpTo { pub(crate) struct FinalChunk(bool); impl FinalChunk { - pub(crate) const fn new(is_final: bool) -> Self { - Self(is_final) - } + pub(crate) const fn new(is_final: bool) -> Self { Self(is_final) } - pub(crate) const fn is_final(self) -> bool { - self.0 - } + pub(crate) const fn is_final(self) -> bool { self.0 } } /// Decode pending bytes as UTF-8, replacing invalid sequences. @@ -182,14 +176,14 @@ mod tests { fn incomplete_tail_strategy() -> impl Strategy> { prop_oneof![ - Just(vec![0xC2]), - Just(vec![0xDF]), - Just(vec![0xE0, 0xA0]), - Just(vec![0xE1, 0x80]), - Just(vec![0xEF, 0xBF]), - Just(vec![0xF0, 0x90, 0x80]), - Just(vec![0xF1, 0x80, 0x80]), - Just(vec![0xF4, 0x8F, 0xBF]), + Just(vec![0xc2]), + Just(vec![0xdf]), + Just(vec![0xe0, 0xa0]), + Just(vec![0xe1, 0x80]), + Just(vec![0xef, 0xbf]), + Just(vec![0xf0, 0x90, 0x80]), + Just(vec![0xf1, 0x80, 0x80]), + Just(vec![0xf4, 0x8f, 0xbf]), ] } diff --git a/rust/cuprum-streams/src/utf8_kani_proofs.rs b/rust/cuprum-streams/src/utf8_kani_proofs.rs index 2d7afc7bf..81f63ba8c 100644 --- a/rust/cuprum-streams/src/utf8_kani_proofs.rs +++ b/rust/cuprum-streams/src/utf8_kani_proofs.rs @@ -12,18 +12,18 @@ fn decode_single_chunk(input: &[u8], final_chunk: bool) -> (String, Vec) { fn is_valid_incomplete_utf8_sequence(pending: &[u8]) -> bool { match pending { [first] => matches!(first, 0xC2..=0xDF | 0xE0..=0xEF | 0xF0..=0xF4), - [0xE0, second] => matches!(second, 0xA0..=0xBF), - [0xE1..=0xEC, second] => matches!(second, 0x80..=0xBF), - [0xED, second] => matches!(second, 0x80..=0x9F), - [0xEE..=0xEF, second] => matches!(second, 0x80..=0xBF), - [0xF0, second] => matches!(second, 0x90..=0xBF), - [0xF1..=0xF3, second] => matches!(second, 0x80..=0xBF), - [0xF4, second] => matches!(second, 0x80..=0x8F), - [0xF0, second, third] => matches!(second, 0x90..=0xBF) && matches!(third, 0x80..=0xBF), - [0xF1..=0xF3, second, third] => { - matches!(second, 0x80..=0xBF) && matches!(third, 0x80..=0xBF) + [0xe0, second] => matches!(second, 0xa0..=0xbf), + [0xe1..=0xec, second] => matches!(second, 0x80..=0xbf), + [0xed, second] => matches!(second, 0x80..=0x9f), + [0xee..=0xef, second] => matches!(second, 0x80..=0xbf), + [0xf0, second] => matches!(second, 0x90..=0xbf), + [0xf1..=0xf3, second] => matches!(second, 0x80..=0xbf), + [0xf4, second] => matches!(second, 0x80..=0x8f), + [0xf0, second, third] => matches!(second, 0x90..=0xbf) && matches!(third, 0x80..=0xbf), + [0xf1..=0xf3, second, third] => { + matches!(second, 0x80..=0xbf) && matches!(third, 0x80..=0xbf) } - [0xF4, second, third] => matches!(second, 0x80..=0x8F) && matches!(third, 0x80..=0xBF), + [0xf4, second, third] => matches!(second, 0x80..=0x8f) && matches!(third, 0x80..=0xbf), _ => false, } } @@ -31,16 +31,16 @@ fn is_valid_incomplete_utf8_sequence(pending: &[u8]) -> bool { #[kani::proof] #[kani::unwind(5)] fn single_chunk_matches_from_utf8_lossy() { - let input = [b'a', 0xFF, b'b']; + let input = [b'a', 0xff, b'b']; let (output, pending) = decode_single_chunk(&input, true); let output_bytes = output.as_bytes(); kani::cover!(output_bytes.len() == 5, "exercise invalid UTF-8 payloads"); kani::assert(output_bytes.len() == 5, "replacement output length"); kani::assert(output_bytes[0] == b'a', "valid prefix is preserved"); - kani::assert(output_bytes[1] == 0xEF, "replacement byte 1"); - kani::assert(output_bytes[2] == 0xBF, "replacement byte 2"); - kani::assert(output_bytes[3] == 0xBD, "replacement byte 3"); + kani::assert(output_bytes[1] == 0xef, "replacement byte 1"); + kani::assert(output_bytes[2] == 0xbf, "replacement byte 2"); + kani::assert(output_bytes[3] == 0xbd, "replacement byte 3"); kani::assert(output_bytes[4] == b'b', "valid suffix is preserved"); kani::assert( pending.is_empty(), @@ -51,7 +51,7 @@ fn single_chunk_matches_from_utf8_lossy() { #[kani::proof] #[kani::unwind(5)] fn two_chunk_boundaries_match_from_utf8_lossy() { - let input = [0xC2, 0xA2]; + let input = [0xc2, 0xa2]; let split_point = 1_usize; let (left, right) = input.split_at(split_point); let chunks = [left, right]; @@ -63,8 +63,8 @@ fn two_chunk_boundaries_match_from_utf8_lossy() { "exercise split points that bisect valid multi-byte sequences" ); kani::assert(output_bytes.len() == 2, "two chunks decode one scalar"); - kani::assert(output_bytes[0] == 0xC2, "first scalar byte is preserved"); - kani::assert(output_bytes[1] == 0xA2, "second scalar byte is preserved"); + kani::assert(output_bytes[0] == 0xc2, "first scalar byte is preserved"); + kani::assert(output_bytes[1] == 0xa2, "second scalar byte is preserved"); kani::assert( pending.is_empty(), "final chunk must leave no pending bytes", @@ -74,7 +74,7 @@ fn two_chunk_boundaries_match_from_utf8_lossy() { #[kani::proof] #[kani::unwind(5)] fn pending_state_is_valid_incomplete_utf8() { - let input = [0xE0, 0xA0]; + let input = [0xe0, 0xa0]; let (_output, pending) = decode_single_chunk(&input, false); kani::assert( diff --git a/rust/cuprum-streams/tests/compile_tests.rs b/rust/cuprum-streams/tests/compile_tests.rs index f52935fdc..4215cc7b3 100644 --- a/rust/cuprum-streams/tests/compile_tests.rs +++ b/rust/cuprum-streams/tests/compile_tests.rs @@ -1,6 +1,4 @@ //! Compile contracts for safe stream policy. #![forbid(unsafe_code)] #[test] -fn transition_privacy() { - trybuild::TestCases::new().compile_fail("tests/ui/fail/*.rs"); -} +fn transition_privacy() { trybuild::TestCases::new().compile_fail("tests/ui/fail/*.rs"); } From 4f483da67cb160c07605b0ea7a784feaff9d8cca Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 17 Sep 2026 15:52:53 +0200 Subject: [PATCH 2/4] Strengthen formatter routing contracts Require every lint and test Cargo recipe to stay off the formatter nightly while retaining the Linux dev-fast route. Document that Makefile owns the formatter toolchain pin and rustfmt.toml owns formatter behaviour. --- .../test_rust_formatter_toolchain.py | 19 +++++++++++++++---- docs/developers-guide.md | 15 ++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/cuprum/unittests/test_rust_formatter_toolchain.py b/cuprum/unittests/test_rust_formatter_toolchain.py index b9a64504d..cf553110b 100644 --- a/cuprum/unittests/test_rust_formatter_toolchain.py +++ b/cuprum/unittests/test_rust_formatter_toolchain.py @@ -280,8 +280,19 @@ def expanded_recipes(*targets: str) -> str: "formatter targets must use only the injected pinned nightly cargo route" ) - non_formatter_recipes = expanded_recipes("lint", "test") - for formatter_line in expected_lines: - assert formatter_line not in non_formatter_recipes, ( - "lint and test must not select the formatter's nightly cargo route" + expected_debug_route = ( + "RUSTUP_TOOLCHAIN=nightly-2026-08-23 probe-cargo " + "--config ../tools/dev-fast/config.toml" + ) + for target in ("lint", "test"): + target_recipes = expanded_recipes(target) + cargo_lines = [ + line for line in target_recipes.splitlines() if "probe-cargo" in line + ] + assert cargo_lines, f"{target} must expand at least one Cargo command" + assert all( + "probe-cargo +nightly-2026-05-28" not in line for line in cargo_lines + ), f"{target} must not select the formatter's nightly Cargo route" + assert any(expected_debug_route in line for line in cargo_lines), ( + f"{target} must retain the Linux dev-fast Cargo route" ) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 20e137314..2ea0b5c34 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -359,13 +359,14 @@ toolchain, before installing Whitaker and running the project gates. On Linux, standard debug Rust build, test, Rustdoc, and Clippy commands select `nightly-2026-08-23` with the explicit `tools/dev-fast/config.toml` fragment. The current `typecheck` target is Python-only and selects no Cargo fragment. -`fmt` and `check-fmt` alone select `nightly-2026-05-28` through -`rust/.rustfmt.toml`. Coverage, release, verification, MSRV, and Whitaker -commands, together with macOS and Windows, retain their prescribed -fragment-free or separately pinned toolchains. The stable pin declares -`rustfmt`, `clippy`, and `rust-analyzer` for local maintenance. The Whitaker -action receives `WHITAKER_INSTALLER_VERSION` from the job environment (`0.2.7`, -the workflow's configured installer version). +`fmt` and `check-fmt` alone select `nightly-2026-05-28` through `RUSTFMT_CARGO` +in the Makefile. `rust/.rustfmt.toml` configures formatter behaviour only; +change the Makefile when changing the formatter toolchain. Coverage, release, +verification, MSRV, and Whitaker commands, together with macOS and Windows, +retain their prescribed fragment-free or separately pinned toolchains. The +stable pin declares `rustfmt`, `clippy`, and `rust-analyzer` for local +maintenance. The Whitaker action receives `WHITAKER_INSTALLER_VERSION` from the +job environment (`0.2.7`, the workflow's configured installer version). cargo-nextest is no longer installed here at all. The coverage job is the only place it runs, and the shared action installs it from checksummed official From 2f14e943a2e9e574a3a90b5538e8f3cbbc0ce5e1 Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 19 Sep 2026 20:50:08 +0200 Subject: [PATCH 3/4] Assert the dev-fast route only on Linux The formatter contract required the Linux dev-fast Cargo fragment from every lint and test recipe, but the Makefile routes debug work through dev-fast only when `uname -s` reports Linux and falls back to the bare injected Cargo elsewhere. The unconditional assertion therefore failed on macOS and Windows hosts, where those recipes contain no fragment at all. Branch on the host: require the fragment on Linux and assert its absence everywhere else. Keep the formatter route and the formatter-nightly rejection checks unconditional, since neither is platform-gated. Co-Authored-By: Claude Code --- .../test_rust_formatter_toolchain.py | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/cuprum/unittests/test_rust_formatter_toolchain.py b/cuprum/unittests/test_rust_formatter_toolchain.py index cf553110b..fa1f89f89 100644 --- a/cuprum/unittests/test_rust_formatter_toolchain.py +++ b/cuprum/unittests/test_rust_formatter_toolchain.py @@ -5,6 +5,7 @@ import re import shutil import subprocess # ruff: ignore[suspicious-subprocess-import] - expands fixed local Makefile recipes. +import sys import tomllib from tests.helpers.docs import repo_root @@ -44,6 +45,10 @@ # Only these characters can begin a lexeme: `/` for comments, `r` for raw # strings, `"` for strings, and `'` for character literals. _LEXEME_STARTS = frozenset("/r\"'") +# The Makefile gates dev-fast on `uname -s`, which reports Linux for the hosts +# `sys.platform` reports as `linux*`. Keeping the check explicit means a broken +# host probe in the Makefile fails the contract below instead of mirroring it. +_HOST_IS_LINUX = sys.platform.startswith("linux") def test_formatter_toolchain_precedes_the_project_toolchain( @@ -284,6 +289,10 @@ def expanded_recipes(*targets: str) -> str: "RUSTUP_TOOLCHAIN=nightly-2026-08-23 probe-cargo " "--config ../tools/dev-fast/config.toml" ) + dev_fast_fragments = ( + "RUSTUP_TOOLCHAIN=nightly-2026-08-23", + "--config ../tools/dev-fast/config.toml", + ) for target in ("lint", "test"): target_recipes = expanded_recipes(target) cargo_lines = [ @@ -293,6 +302,16 @@ def expanded_recipes(*targets: str) -> str: assert all( "probe-cargo +nightly-2026-05-28" not in line for line in cargo_lines ), f"{target} must not select the formatter's nightly Cargo route" - assert any(expected_debug_route in line for line in cargo_lines), ( - f"{target} must retain the Linux dev-fast Cargo route" - ) + # The Makefile routes debug work through dev-fast only on Linux and falls + # back to the bare injected Cargo elsewhere, so the fragment is required + # on Linux and must be absent on every other host. + if _HOST_IS_LINUX: + assert any(expected_debug_route in line for line in cargo_lines), ( + f"{target} must retain the Linux dev-fast Cargo route" + ) + else: + assert all( + fragment not in line + for line in cargo_lines + for fragment in dev_fast_fragments + ), f"{target} must omit the Linux-only dev-fast Cargo route off Linux" From 12f5373fdfbe4c2c5f2c91e769b5b77091cb6563 Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 19 Sep 2026 21:19:55 +0200 Subject: [PATCH 4/4] Extract the formatter route assertions CodeScene scored `test_make_formatter_targets_select_the_pinned_nightly` at 10 against a threshold of 9: the host branch added last commit put the test one decision point over the limit, and the reviewed code path is the test's own control flow rather than production behaviour worth suppressing. Move each per-target route check into a module-level helper that names what it asserts. The test now reads as the three contracts it states -- the formatter route is absent, the host-appropriate debug route is present or absent, and each target expands at least one Cargo command -- and the complexity moves into helpers small enough to stay under the threshold. Both helper branches keep their mutation proofs: the non-Linux helper still rejects a dev-fast line, and the Linux helper still rejects a fragment-free one. --- .../test_rust_formatter_toolchain.py | 75 +++++++++++++------ 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/cuprum/unittests/test_rust_formatter_toolchain.py b/cuprum/unittests/test_rust_formatter_toolchain.py index fa1f89f89..b6562f287 100644 --- a/cuprum/unittests/test_rust_formatter_toolchain.py +++ b/cuprum/unittests/test_rust_formatter_toolchain.py @@ -49,6 +49,55 @@ # `sys.platform` reports as `linux*`. Keeping the check explicit means a broken # host probe in the Makefile fails the contract below instead of mirroring it. _HOST_IS_LINUX = sys.platform.startswith("linux") +_EXPECTED_DEBUG_ROUTE = ( + "RUSTUP_TOOLCHAIN=nightly-2026-08-23 probe-cargo " + "--config ../tools/dev-fast/config.toml" +) +_DEV_FAST_FRAGMENTS = ( + "RUSTUP_TOOLCHAIN=nightly-2026-08-23", + "--config ../tools/dev-fast/config.toml", +) + + +def _assert_target_avoids_the_formatter_route( + target: str, cargo_lines: list[str] +) -> None: + """Assert one lint or test target keeps the formatter's nightly route out. + + Parameters + ---------- + target + Makefile target whose expanded recipe is being checked. + cargo_lines + Cargo command lines from that target's dry-run recipe. + """ + assert all("probe-cargo +nightly-2026-05-28" not in line for line in cargo_lines), ( + f"{target} must not select the formatter's nightly Cargo route" + ) + + +def _assert_target_debug_route(target: str, cargo_lines: list[str]) -> None: + """Assert one lint or test target carries the host-appropriate debug route. + + The Makefile routes debug work through dev-fast only on Linux and falls back + to the bare injected Cargo elsewhere, so the fragment is required on Linux + and must be absent on every other host. + + Parameters + ---------- + target + Makefile target whose expanded recipe is being checked. + cargo_lines + Cargo command lines from that target's dry-run recipe. + """ + if _HOST_IS_LINUX: + assert any(_EXPECTED_DEBUG_ROUTE in line for line in cargo_lines), ( + f"{target} must retain the Linux dev-fast Cargo route" + ) + return + assert all( + fragment not in line for line in cargo_lines for fragment in _DEV_FAST_FRAGMENTS + ), f"{target} must omit the Linux-only dev-fast Cargo route off Linux" def test_formatter_toolchain_precedes_the_project_toolchain( @@ -285,33 +334,11 @@ def expanded_recipes(*targets: str) -> str: "formatter targets must use only the injected pinned nightly cargo route" ) - expected_debug_route = ( - "RUSTUP_TOOLCHAIN=nightly-2026-08-23 probe-cargo " - "--config ../tools/dev-fast/config.toml" - ) - dev_fast_fragments = ( - "RUSTUP_TOOLCHAIN=nightly-2026-08-23", - "--config ../tools/dev-fast/config.toml", - ) for target in ("lint", "test"): target_recipes = expanded_recipes(target) cargo_lines = [ line for line in target_recipes.splitlines() if "probe-cargo" in line ] assert cargo_lines, f"{target} must expand at least one Cargo command" - assert all( - "probe-cargo +nightly-2026-05-28" not in line for line in cargo_lines - ), f"{target} must not select the formatter's nightly Cargo route" - # The Makefile routes debug work through dev-fast only on Linux and falls - # back to the bare injected Cargo elsewhere, so the fragment is required - # on Linux and must be absent on every other host. - if _HOST_IS_LINUX: - assert any(expected_debug_route in line for line in cargo_lines), ( - f"{target} must retain the Linux dev-fast Cargo route" - ) - else: - assert all( - fragment not in line - for line in cargo_lines - for fragment in dev_fast_fragments - ), f"{target} must omit the Linux-only dev-fast Cargo route off Linux" + _assert_target_avoids_the_formatter_route(target, cargo_lines) + _assert_target_debug_route(target, cargo_lines)