From 593d791cee10617aa3d7bd793513d1b7a57a54ea Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 17:18:20 +0200 Subject: [PATCH 01/21] Enforce the Rust lint baseline Centralize the workspace lint policy, route doctests through the debug contract, and retain the audited unsafe boundary with dynamic Cargo target checks. --- AGENTS.md | 4 +- Makefile | 4 +- cuprum/unittests/test_dev_fast_contract.py | 19 +++- docs/adr-011-audited-rust-boundaries.md | 20 ++-- docs/developers-guide.md | 33 ++++++- docs/repository-layout.md | 1 + docs/rust-boundary-verification.md | 20 ++-- pyproject.toml | 3 + rust/Cargo.toml | 10 ++ rust/clippy.toml | 20 ++++ rust/cuprum-streams/Cargo.toml | 77 +-------------- scripts/check_boundary_contract.py | 107 ++++++++++++++++----- scripts/tests/test_boundary_contract.py | 97 ++++++++++++++++--- tests/test_native_sdist.py | 8 +- 14 files changed, 291 insertions(+), 132 deletions(-) create mode 100644 rust/clippy.toml diff --git a/AGENTS.md b/AGENTS.md index 064ed377e..deb40b334 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -229,7 +229,9 @@ working on the Rust portions of the project: RUSTFLAGS="-D warnings" cargo test --all-targets --all-features ``` - running the Rust test suite with warnings denied. Use `make fmt` + running the Rust test suite with warnings denied. It then runs Cargo + doctests separately with all features, because Nextest does not execute + doctests. Use `make fmt` (`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`, diff --git a/Makefile b/Makefile index 648704477..03576d86e 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ RUSTFMT_CARGO ?= $(CARGO) +$(RUSTFMT_TOOLCHAIN) WHITAKER ?= whitaker BUILD_JOBS ?= RUST_FLAGS ?= -D warnings -RUSTDOC_FLAGS ?= -D warnings +RUSTDOC_FLAGS ?= --cfg docsrs -D warnings CARGO_FLAGS ?= --all-targets --all-features CLIPPY_FLAGS ?= $(CARGO_FLAGS) -- $(RUST_FLAGS) DOC_FLAGS ?= --jobs 1 @@ -70,6 +70,7 @@ DOC_FLAGS ?= --jobs 1 # billed for, and never above it. TEST_JOBS ?= 1 TEST_FLAGS ?= $(CARGO_FLAGS) --jobs $(TEST_JOBS) +DOCTEST_FLAGS ?= --workspace --doc --all-features $(BUILD_JOBS) TEST_RUSTFLAGS ?= $(RUST_FLAGS) -C codegen-units=1 WHITAKER_CARGO_FLAGS ?= $(CARGO_FLAGS) --jobs 1 WHITAKER_RUSTFLAGS ?= $(RUST_FLAGS) -C codegen-units=1 @@ -361,6 +362,7 @@ test-rust: $(RUST_DEBUG_PREREQUISITE) ## Run the Rust suite 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)" $(RUST_DEBUG_CARGO) test $(TEST_FLAGS) $(BUILD_JOBS); \ fi + cd $(RUST_DIR) && CARGO_BUILD_JOBS="$(TEST_CARGO_BUILD_JOBS)" RUSTFLAGS="$(DEV_FAST_TEST_RUSTFLAGS)" $(RUST_DEBUG_CARGO) test $(DOCTEST_FLAGS) msrv-check: ## Verify every Rust target compiles on the published MSRV cd $(RUST_DIR) && $(MSRV_CARGO_COMMAND) check --workspace --all-targets --all-features diff --git a/cuprum/unittests/test_dev_fast_contract.py b/cuprum/unittests/test_dev_fast_contract.py index 7404b082f..83246de69 100644 --- a/cuprum/unittests/test_dev_fast_contract.py +++ b/cuprum/unittests/test_dev_fast_contract.py @@ -198,7 +198,7 @@ def test_linux_debug_routes_select_the_fragment_and_injected_cargo() -> None: direct_cargo = [ line for line in routing.splitlines() if "probe-cargo --config" in line ] - assert sum(line.count("probe-cargo --config") for line in direct_cargo) == 7, ( + assert sum(line.count("probe-cargo --config") for line in direct_cargo) == 8, ( "each standard and explicit debug Cargo invocation must select one fragment" ) assert all( @@ -212,6 +212,23 @@ def test_linux_debug_routes_select_the_fragment_and_injected_cargo() -> None: ) +def test_rust_doctests_are_a_separate_debug_routed_gate() -> None: + """Keep doctests covered when nextest owns ordinary Rust tests.""" + output = _dry_run("test-rust", variables={"DEV_FAST_HOST_IS_LINUX": "yes"}) + doctest = next( + line + for line in _routing_lines(output) + if "test --workspace --doc --all-features" in line + ) + assert "probe-cargo --config ../tools/dev-fast/config.toml" in doctest, ( + "doctests must use the explicit Linux debug configuration" + ) + assert 'RUSTFLAGS="-D warnings' in doctest, "doctests must deny warnings" + assert "-Clink-arg=-fuse-ld=mold" in doctest, ( + "doctests must retain the selected linker" + ) + + @pytest.mark.parametrize( "flags", [ diff --git a/docs/adr-011-audited-rust-boundaries.md b/docs/adr-011-audited-rust-boundaries.md index c008de89e..f1ebd6899 100644 --- a/docs/adr-011-audited-rust-boundaries.md +++ b/docs/adr-011-audited-rust-boundaries.md @@ -117,9 +117,12 @@ focused tests (`/tmp/issue379-reader-validation-before.log` and representability and non-negativity; it does not establish resource validity, lifetime, or exclusive ownership. -Nine safe-crate unsafe-rejection probes, including a future target without a -crate-level attribute, are covered by the contract gate. The normal callback -compile regression passed with the expected `&W` versus `&mut W` diagnostic in +The contract derives every automatic and explicitly configured Cargo target +root, requiring each to carry `#![forbid(unsafe_code)]`; its future-target +negative cases cover binary, build-script, example, integration-test, and +benchmark roots. The current two safe roots receive six deliberate +unsafe-rejection probes. The normal callback compile regression passed with the +expected `&W` versus `&mut W` diagnostic in `/tmp/issue379-round25-trybuild-normal.log`; the final fault run passed its controls and detected all four mutations in `/tmp/issue379-round27-faults.log`. Hosted Windows and macOS runtime execution passed on 2026-09-15: the "Rust @@ -183,9 +186,8 @@ suppressing its owner drop. This avoids carrying the former extension-wide `std::fs` lint exclusion into either extracted crate; `cap-std` and its handle adapters are trusted dependencies, not verifier-proved implementations. -The safe crate also declares `unsafe_code = "forbid"` in Cargo's lint table, so -future automatically discovered test, example, and benchmark targets inherit -the prohibition. Cargo cannot override an individual workspace-inherited lint; -the safe manifest therefore carries the workspace lint tables plus this one -addition. The boundary-contract gate requires exact policy parity, preventing -that required duplication from weakening other lints or drifting silently. +Cargo cannot override an individual workspace-inherited lint. The safe crate +therefore keeps its `#![forbid(unsafe_code)]` source-level prohibition while +all three members inherit the shared workspace tables. The boundary-contract +gate checks both the common manifest inheritance and every discovered safe +target root, including future automatic Cargo targets. diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 2ea0b5c34..877347100 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -3344,7 +3344,8 @@ make test ``` `make test` runs the Python pytest batches before the crate tests through -`cargo nextest`, including proptest cases compiled under `#[cfg(test)]`. Run +`cargo nextest`, including proptest cases compiled under `#[cfg(test)]`. It +then runs Cargo doctests separately because Nextest does not execute them. Run the complete Rust lint and formatting gates before committing Rust changes: ```bash @@ -3352,6 +3353,30 @@ make check-fmt make lint ``` +### Rust lint baseline + +`rust/Cargo.toml` is the authoritative shared Rust lint table and every +workspace member declares `[lints] workspace = true`. `rust/clippy.toml` holds +the shared thresholds: cognitive complexity 9, four arguments, 70 lines, and +four nesting levels. New workspace members must inherit these tables rather +than replicate or weaken them. + +The baseline follows Concordat revision +`cf4333c3f38b55bf3d4d5d2663e2147e2da1cec8` with local extensions for +`missing_assert_message` and environment-method injection. `std::env` readers +and mutators are disallowed in favour of injected environment or +working-directory seams. Fix findings at source; `#[allow]` is not permitted, +and a reasoned `#[expect]` is reserved for macro-expansion artefacts or genuine +floating-point arithmetic. + +The workspace cannot forbid unsafe code globally: `cuprum-native-io` and +`cuprum-rust` are the audited syscall and FFI boundaries. `cuprum-streams` +keeps its explicit crate-level prohibition, which the boundary contract checks. +The Rust 1.85.0 maintenance toolchain provides `rustfmt`, `clippy`, and +`rust-analyzer`; Linux debug Rustdoc, Clippy, and doctests use the separately +pinned dev-fast configuration, while MSRV, coverage, release, verification, and +Whitaker retain their prescribed routes. + ### Rustfmt fixture compatibility The formatter maintenance pin is `nightly-2026-05-28`; Continuous Integration @@ -4068,7 +4093,7 @@ checks the members that result. `uv build --sdist` archive must retain: - the workspace manifests and pins — `rust/Cargo.toml`, `rust/Cargo.lock`, - `rust/rust-toolchain.toml`, and `rust/dylint.toml`; + `rust/clippy.toml`, `rust/rust-toolchain.toml`, and `rust/dylint.toml`; - each crate manifest — `rust/cuprum-rust/Cargo.toml`, `rust/cuprum-streams/Cargo.toml`, and `rust/cuprum-native-io/Cargo.toml`; - all three crate source trees, including each crate's `src/` and the @@ -4079,8 +4104,8 @@ sdist carries sources rather than a multi-gigabyte build cache. Maturin builds its archive from the same workspace, and the `[tool.maturin].include` entries carry the two files it would not otherwise -copy — `rust/rust-toolchain.toml` and `rust/dylint.toml`, both restricted with -`format = "sdist"` so they never enter a wheel. +copy — `rust/clippy.toml`, `rust/rust-toolchain.toml`, and `rust/dylint.toml`, +all restricted with `format = "sdist"` so they never enter a wheel. `tests/test_native_sdist.py` verifies both frontends, and it is deliberately not a unit test: each case builds a genuine archive with `uv build --sdist` or diff --git a/docs/repository-layout.md b/docs/repository-layout.md index 1e47a1853..9483e7451 100644 --- a/docs/repository-layout.md +++ b/docs/repository-layout.md @@ -35,6 +35,7 @@ _Figure 1: Simplified repository tree for contributor orientation._ | `docs/` | User, maintainer, design, decision, roadmap, and reference documentation. | | `docs/execplans/` | Durable execution plans for non-trivial implementation work. | | `rust/` | Cargo workspace for Rust extension code and Rust-specific build/test targets. | +| `rust/clippy.toml` | Shared Clippy thresholds and environment-access policy used by every workspace member. | | `rust/cuprum-rust/` | Thin PyO3/maturin integration crate for the optional native backend; raw Python resource hand-off lives here. | | `rust/cuprum-streams/` | Safe stream orchestration, pump policy, state machine, errors, and checked UTF-8 decoding; unsafe Rust is forbidden. | | `rust/cuprum-native-io/` | Audited native descriptor/handle ownership, single-call I/O, progress kernels, and platform test fixtures. | diff --git a/docs/rust-boundary-verification.md b/docs/rust-boundary-verification.md index 89e3c5f3f..948d1010c 100644 --- a/docs/rust-boundary-verification.md +++ b/docs/rust-boundary-verification.md @@ -186,13 +186,19 @@ with its `Extension-gated tests (Windows Python/Rust boundary)` and runs add no Kani/Miri coverage. The second CodeRabbit review remains pending; the first review passed with 0 findings. -| Boundary and production surface | Property | Selected tool and executed subject | Assumptions and bounds | Native or integration evidence | Status and limitation | -| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `cuprum-native-io` borrowed and owned Unix/Windows handles | Borrowed readers remain caller-owned; transferred writers close once on normal, error, and real unwind; no released/reused descriptor is touched. | Kani exercises the production `with_owned_writer` drop counter where Verus cannot represent OS handles or destructors. The direct Verus resource assessment is recorded below. | Valid-live-resource, unique-transfer, and no-concurrent-close obligations remain caller contracts. Kani's symbolic exit is bounded and has coverage witnesses. The writer callback receives an immutable reference, so it cannot replace or move the owned resource out of the scope. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Round 27 Kani passed 7 native harnesses; Miri passed 13 tests with 0 ignored; the fault harness detected all 4 mutations. | -| `cuprum-native-io` read/write kernels | Buffers expose only initialized in-bounds bytes; short reads/writes, `EINTR`, zero progress, and checked byte accounting preserve valid state. | Verus verifies the exact production `progress.rs` bodies rendered by `scripts/render_boundary_proofs.py`; Kani checks the production progress kernels and ownership seam. | `checked_progress` rejects overflow and oversized lengths; `record_write_progress` commits tail/count only after both checks. Aggregate pump/consume tracing counts retain saturating-add semantics, so they are not exact beyond `u64::MAX`. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Verus passed 2 functions with 0 errors; Kani/Miri and native tests remain scoped as recorded above. Neither verifier proves syscall effects. | -| Linux `splice` adapter and fallback policy | Unsupported descriptors fall back; partial/interrupted transfers and broken-pipe drain preserve bytes and termination. | Kani/Verus cover the typed decision, progress, and accumulation seams; the kernel call itself is external. Miri covers applicable native crate paths but excludes unshimmed `libc::splice`. | Syscall return/error guarantees, symbolic chunk and trace bounds, and non-vacuity witnesses are explicit assumptions or bounds. | Round 35 local Rust and native checks passed; hosted Windows/macOS runtime execution was green in CI. | The syscall is not directly verified or Miri-interpreted because `libc::splice` has no supported shim. Round 27 Miri passed 13 tests with 0 ignored. | -| `cuprum-streams` safe policy and decoder | Safe crate and all test/helper targets reject unsafe Rust; state machine never writes after closure, stops on EOF, and checked UTF-8 replacement preserves valid prefixes. | Compiler contract checks the copied safe crate and compile tests. Kani covers pure pump policy; checked UTF-8 no longer contains `from_utf8_unchecked`. | No raw resource integers or pointers enter the safe crate; lifetime-bound typed native handles do enter through `AsStream`. Aggregate tracing counts retain saturating-add semantics; per-chunk checked progress is verified separately. | Round 35 Rust checks covered 115/115 tests with no skips; nine compiler probes passed. Hosted Windows/macOS runtime execution was green in CI. | Safe-policy/decoder Kani passed 11 harnesses. | -| `cuprum-rust` PyO3 integration | Generated wrappers expose stable signatures, map errors, release the GIL safely, and preserve the native resource contract. | PyO3 compile UI tests and Python integration tests. Verus/Miri exclude interpreter-generated ABI code; typed Rust seams are verified separately. | PyO3/CPython ABI, `Send`, interpreter lifecycle, and Python transport hand-off are trusted assumptions. | Round 35 Python checks recorded 1,568 passes and one skip; the extension-required suite recorded 80 passes and one skip. | Local integration checks are green; Python lifetime establishment remains outside this evidence. Hosted Windows/macOS runs pass. The first CodeRabbit review passed with 0 findings; the second review remains pending. | +The current boundary contract discovers all automatic and explicitly configured +Cargo target roots, requiring a source-level unsafe prohibition on each. Its +current measured run found the library and compile-contract target, compiled +six unsafe probes, and separately rejected unannotated future binary, +build-script, example, integration-test, and benchmark targets. + +| Boundary and production surface | Property | Selected tool and executed subject | Assumptions and bounds | Native or integration evidence | Status and limitation | +| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cuprum-native-io` borrowed and owned Unix/Windows handles | Borrowed readers remain caller-owned; transferred writers close once on normal, error, and real unwind; no released/reused descriptor is touched. | Kani exercises the production `with_owned_writer` drop counter where Verus cannot represent OS handles or destructors. The direct Verus resource assessment is recorded below. | Valid-live-resource, unique-transfer, and no-concurrent-close obligations remain caller contracts. Kani's symbolic exit is bounded and has coverage witnesses. The writer callback receives an immutable reference, so it cannot replace or move the owned resource out of the scope. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Round 27 Kani passed 7 native harnesses; Miri passed 13 tests with 0 ignored; the fault harness detected all 4 mutations. | +| `cuprum-native-io` read/write kernels | Buffers expose only initialized in-bounds bytes; short reads/writes, `EINTR`, zero progress, and checked byte accounting preserve valid state. | Verus verifies the exact production `progress.rs` bodies rendered by `scripts/render_boundary_proofs.py`; Kani checks the production progress kernels and ownership seam. | `checked_progress` rejects overflow and oversized lengths; `record_write_progress` commits tail/count only after both checks. Aggregate pump/consume tracing counts retain saturating-add semantics, so they are not exact beyond `u64::MAX`. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Verus passed 2 functions with 0 errors; Kani/Miri and native tests remain scoped as recorded above. Neither verifier proves syscall effects. | +| Linux `splice` adapter and fallback policy | Unsupported descriptors fall back; partial/interrupted transfers and broken-pipe drain preserve bytes and termination. | Kani/Verus cover the typed decision, progress, and accumulation seams; the kernel call itself is external. Miri covers applicable native crate paths but excludes unshimmed `libc::splice`. | Syscall return/error guarantees, symbolic chunk and trace bounds, and non-vacuity witnesses are explicit assumptions or bounds. | Round 35 local Rust and native checks passed; hosted Windows/macOS runtime execution was green in CI. | The syscall is not directly verified or Miri-interpreted because `libc::splice` has no supported shim. Round 27 Miri passed 13 tests with 0 ignored. | +| `cuprum-streams` safe policy and decoder | Every automatic or explicit Cargo target root rejects unsafe Rust; state machine never writes after closure, stops on EOF, and checked UTF-8 replacement preserves valid prefixes. | Compiler contract discovers safe roots, compiles deliberate probes against current roots, and rejects a future unannotated root. Kani covers pure pump policy; checked UTF-8 no longer contains `from_utf8_unchecked`. | No raw resource integers or pointers enter the safe crate; lifetime-bound typed native handles do enter through `AsStream`. Aggregate tracing counts retain saturating-add semantics; per-chunk checked progress is verified separately. | Round 35 Rust checks covered 115/115 tests with no skips. The current contract run found two roots, compiled six probes, and rejected all dynamic target cases. | Safe-policy/decoder Kani passed 11 harnesses. | +| `cuprum-rust` PyO3 integration | Generated wrappers expose stable signatures, map errors, release the GIL safely, and preserve the native resource contract. | PyO3 compile UI tests and Python integration tests. Verus/Miri exclude interpreter-generated ABI code; typed Rust seams are verified separately. | PyO3/CPython ABI, `Send`, interpreter lifecycle, and Python transport hand-off are trusted assumptions. | Round 35 Python checks recorded 1,568 passes and one skip; the extension-required suite recorded 80 passes and one skip. | Local integration checks are green; Python lifetime establishment remains outside this evidence. Hosted Windows/macOS runs pass. The first CodeRabbit review passed with 0 findings; the second review remains pending. | Round 22 also found a packaging boundary omission. A real `uv build --sdist` archive initially omitted the Rust workspace, and diff --git a/pyproject.toml b/pyproject.toml index 23341562e..84eba3ae3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -398,6 +398,7 @@ also_copy = [ "rust/cuprum-native-io/", "rust/Cargo.toml", "rust/Cargo.lock", + "rust/clippy.toml", "rust/dylint.toml", ] @@ -497,6 +498,7 @@ source-exclude = ["cuprum/unittests/**"] source-include = [ "rust/Cargo.toml", "rust/Cargo.lock", + "rust/clippy.toml", "rust/rust-toolchain.toml", "rust/dylint.toml", "rust/cuprum-rust/Cargo.toml", @@ -516,6 +518,7 @@ module-name = "cuprum._rust_backend_native" python-source = "." exclude = ["cuprum/unittests/**"] include = [ + { path = "rust/clippy.toml", format = "sdist" }, { path = "rust/rust-toolchain.toml", format = "sdist" }, { path = "rust/dylint.toml", format = "sdist" }, ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 12c5596fb..ba1fa9efa 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -15,8 +15,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" @@ -71,7 +73,15 @@ result_large_err = "deny" [workspace.lints.rust] missing_docs = "deny" +renamed_and_removed_lints = "deny" +unknown_lints = "deny" unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] } [workspace.lints.rustdoc] +bare_urls = "deny" +broken_intra_doc_links = "deny" +invalid_codeblock_attributes = "deny" +invalid_html_tags = "deny" missing_crate_level_docs = "deny" +private_intra_doc_links = "deny" +unescaped_backticks = "deny" diff --git a/rust/clippy.toml b/rust/clippy.toml new file mode 100644 index 000000000..57eb1ccf6 --- /dev/null +++ b/rust/clippy.toml @@ -0,0 +1,20 @@ +# Align with CodeScene's ceiling +cognitive-complexity-threshold = 9 # default is 25 +too-many-arguments-threshold = 4 # default is 7 +too-many-lines-threshold = 70 # default is 100 +excessive-nesting-threshold = 4 # default is off + +allow-expect-in-tests = true + +# The signed Netsuke baseline at 924cb215 requires explicit environment +# injection. The reasons appear in diagnostics, directing callers to the +# boundary that makes environment access testable. +disallowed-methods = [ + { path = "std::env::var", reason = "inject an environment reader" }, + { path = "std::env::var_os", reason = "inject an environment reader" }, + { path = "std::env::vars", reason = "inject an environment reader" }, + { path = "std::env::vars_os", reason = "inject an environment reader" }, + { path = "std::env::set_var", reason = "use a stub environment in tests" }, + { path = "std::env::remove_var", reason = "use a stub environment in tests" }, + { path = "std::env::set_current_dir", reason = "inject a base-directory seam; confine CWD changes to Command::current_dir" }, +] diff --git a/rust/cuprum-streams/Cargo.toml b/rust/cuprum-streams/Cargo.toml index 83250543c..6368c6750 100644 --- a/rust/cuprum-streams/Cargo.toml +++ b/rust/cuprum-streams/Cargo.toml @@ -23,75 +23,8 @@ rstest = "0.26.1" tempfile = "3.27.0" trybuild = { version = "1.0.116", features = ["diff"] } -# Cargo cannot override one lint while inheriting workspace lints. The -# boundary-contract gate requires these tables to match the workspace plus -# unsafe_code = "forbid", which applies to every current and future target. -[lints.clippy] -pedantic = { level = "warn", priority = -1 } - -# 1. hygiene -allow_attributes = "deny" -allow_attributes_without_reason = "deny" -blanket_clippy_restriction_lints = "deny" -cognitive_complexity = "deny" -needless_pass_by_value = "deny" -implicit_hasher = "deny" - -# 2. debugging leftovers -dbg_macro = "deny" -print_stdout = "deny" -print_stderr = "deny" - -# 3. panic-prone operations -unwrap_used = "deny" -expect_used = "deny" -indexing_slicing = "deny" -string_slice = "deny" -integer_division = "deny" -integer_division_remainder_used = "deny" -panic_in_result_fn = "deny" -unreachable = "deny" - -# 4. portability -host_endian_bytes = "deny" -little_endian_bytes = "deny" -big_endian_bytes = "deny" - -# 5. nursery idiom polish -let_underscore_must_use = "deny" -or_fun_call = "deny" -option_if_let_else = "deny" -self_named_module_files = "deny" -shadow_reuse = "deny" -shadow_same = "deny" -shadow_unrelated = "deny" -str_to_string = "deny" -string_lit_as_bytes = "deny" -try_err = "deny" -unneeded_field_pattern = "deny" -use_self = "deny" - -# 6. numerical foot-guns -float_arithmetic = "deny" -cast_possible_truncation = "deny" -cast_possible_wrap = "deny" -cast_precision_loss = "deny" -lossy_float_literal = "deny" - -# 7. API ergonomics -missing_const_for_fn = "deny" -must_use_candidate = "deny" -unused_async = "deny" - -# 8. Error handling -missing_panics_doc = "deny" -error_impl_error = "deny" -result_large_err = "deny" - -[lints.rust] -unsafe_code = "forbid" -missing_docs = "deny" -unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] } - -[lints.rustdoc] -missing_crate_level_docs = "deny" +# Safe-stream code retains `#![forbid(unsafe_code)]` in its crate root. The +# workspace intentionally does not impose that rule on the audited syscall and +# FFI boundaries, so every member can inherit one shared lint baseline here. +[lints] +workspace = true diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 1867a9cf1..c2af6e2e5 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -3,7 +3,7 @@ # requires-python = ">=3.13" # dependencies = ["cuprum==0.1.0"] # /// -"""Compile deliberate unsafe probes against a copy of the actual safe crate. +"""Check audited workspace lint inheritance and safe-crate unsafe boundaries. The workspace copy avoids touching a developer's sources while proving that the real library and integration-test targets reject unsafe code. Cargo keeps @@ -55,27 +55,93 @@ def check_members(manifest: str) -> None: raise ValueError(msg) -def check_safe_policy(workspace_manifest: str, safe_manifest: str) -> None: - """Require workspace lint parity plus an unrelaxable safe-target prohibition. +def check_member_lint_inheritance(workspace: Path) -> None: + """Require each audited package to inherit the workspace lint baseline. Parameters ---------- - workspace_manifest : str - Workspace manifest containing the shared lint policy. - safe_manifest : str - Safe crate manifest, whose lint policy applies to every Cargo target. + workspace : Path + Root of the Rust workspace containing audited member manifests. Raises ------ ValueError - If the safe crate weakens or diverges from the workspace policy. + If an audited package replaces or weakens the shared policy locally. """ - expected = tomllib.loads(workspace_manifest)["workspace"]["lints"] - expected["rust"]["unsafe_code"] = "forbid" - actual = tomllib.loads(safe_manifest)["lints"] - if actual != expected: - msg = "safe crate must retain workspace lints and forbid unsafe in all targets" + for member in BOUNDARIES | SAFE_CRATES: + manifest_path = workspace / member / "Cargo.toml" + manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8")) + if manifest.get("lints") != {"workspace": True}: + msg = f"{member} must inherit the workspace lint baseline" + raise ValueError(msg) + + +def safe_target_roots(workspace: Path) -> tuple[Path, ...]: + """Return every automatic and explicitly configured safe-crate target root. + + Cargo auto-discovers crate roots only at these locations. Explicit target + tables may select another path, which remains part of the safe boundary. + Keeping this scan local and deterministic lets the contract reject a new + target before a compiler invocation can hide it behind dependency output. + + Returns + ------- + tuple[Path, ...] + Existing automatic and explicitly configured crate-root source paths. + """ + crate = workspace / "cuprum-streams" + manifest = tomllib.loads((crate / "Cargo.toml").read_text(encoding="utf-8")) + automatic = [crate / "src/lib.rs", crate / "src/main.rs", crate / "build.rs"] + for directory in ("src/bin", "examples", "tests", "benches"): + root = crate / directory + automatic.extend(root.glob("*.rs")) + automatic.extend(root.glob("*/main.rs")) + explicit: list[Path] = [] + for target_type in ("lib", "bin", "example", "test", "bench"): + targets = manifest.get(target_type, []) + if isinstance(targets, dict): + targets = [targets] + explicit.extend( + crate / target["path"] + for target in targets + if isinstance(target, dict) and isinstance(target.get("path"), str) + ) + package = manifest.get("package", {}) + if isinstance(package, dict) and isinstance(package.get("build"), str): + explicit.append(crate / package["build"]) + return tuple(sorted({path for path in automatic + explicit if path.is_file()})) + + +def check_safe_policy(workspace: Path) -> tuple[Path, ...]: + """Require every safe-crate target root to forbid unsafe source code. + + The shared baseline deliberately omits ``unsafe_code = forbid`` because + the other audited members implement syscall and FFI boundaries. Every + automatic or explicit safe-crate target must therefore state the + non-negotiable source-level prohibition itself. + + Returns + ------- + tuple[Path, ...] + The checked source roots, for the compiler-probe phase. + + Raises + ------ + ValueError + If no source roots exist or any root omits the unsafe prohibition. + """ + targets = safe_target_roots(workspace) + if not targets: + msg = "safe crate has no discoverable Cargo target roots" raise ValueError(msg) + for target in targets: + source = target.read_text(encoding="utf-8") + if "#![forbid(unsafe_code)]" not in source: + msg = ( + f"safe target must forbid unsafe code: {target.relative_to(workspace)}" + ) + raise ValueError(msg) + return targets def _compile(workspace: Path) -> tuple[int, str]: @@ -104,18 +170,12 @@ def main() -> None: """Check the allowlist and actual compiler rejection on isolated source copies.""" manifest = (ROOT / "rust/Cargo.toml").read_text(encoding="utf-8") check_members(manifest) - check_safe_policy( - manifest, - (ROOT / "rust/cuprum-streams/Cargo.toml").read_text(encoding="utf-8"), - ) + check_member_lint_inheritance(ROOT / "rust") + safe_targets = check_safe_policy(ROOT / "rust") workspace = ROOT / ".cache/boundary-contract/workspace" if workspace.exists(): shutil.rmtree(workspace) shutil.copytree(ROOT / "rust", workspace, ignore=shutil.ignore_patterns("target")) - future_target = workspace / "cuprum-streams/tests/future_target.rs" - future_target.write_text( - "//! Probe automatic Cargo target lint inheritance.\n", encoding="utf-8" - ) logs = ROOT / "rust/target/boundary-verification" logs.mkdir(parents=True, exist_ok=True) code, output = _compile(workspace) @@ -127,9 +187,8 @@ def main() -> None: file=sys.stderr, ) raise SystemExit(code) - targets = ("src/lib.rs", "tests/compile_tests.rs", "tests/future_target.rs") - for target in targets: - _check_target(workspace, workspace / "cuprum-streams" / target, logs) + for target in safe_targets: + _check_target(workspace, workspace / target.relative_to(ROOT / "rust"), logs) def _unsafe_was_forbidden(code: int, output: str) -> bool: diff --git a/scripts/tests/test_boundary_contract.py b/scripts/tests/test_boundary_contract.py index b15b7df45..2654d78a9 100644 --- a/scripts/tests/test_boundary_contract.py +++ b/scripts/tests/test_boundary_contract.py @@ -12,14 +12,27 @@ import pytest from scripts import check_boundary_contract as contract -from scripts.check_boundary_contract import check_members, check_safe_policy +from scripts.check_boundary_contract import ( + check_member_lint_inheritance, + check_members, + check_safe_policy, + safe_target_roots, +) from scripts.tests.boundary_harness_support import copy_boundary_repository # The one diagnostic string that distinguishes a rejected probe from a compile # that failed for some unrelated reason. UNSAFE_MARKERS = ("forbid(unsafe_code)", "-F unsafe-code") -TARGETS = ("src/lib.rs", "tests/compile_tests.rs", "tests/future_target.rs") +TARGETS = ("src/lib.rs", "tests/compile_tests.rs") +FUTURE_TARGETS = ( + "src/main.rs", + "src/bin/future_target.rs", + "build.rs", + "examples/future_target.rs", + "tests/future_target.rs", + "benches/future_target.rs", +) class _RecordedCompile: @@ -78,22 +91,82 @@ def test_audited_members_are_accepted() -> None: ) -@pytest.mark.parametrize("replacement", ["deny", "allow"]) -def test_safe_targets_cannot_weaken_unsafe_policy(replacement: str) -> None: - """A target-wide deny can be locally relaxed, so only forbid is accepted.""" +def test_all_audited_members_inherit_the_workspace_lint_baseline() -> None: + """Every package must use the root tables without a local replacement.""" root = Path(__file__).resolve().parents[2] / "rust" - workspace = (root / "Cargo.toml").read_text(encoding="utf-8") - safe = (root / "cuprum-streams/Cargo.toml").read_text(encoding="utf-8") - check_safe_policy(workspace, safe) - weakened = safe.replace('unsafe_code = "forbid"', f'unsafe_code = "{replacement}"') - with pytest.raises(ValueError, match="forbid unsafe in all targets"): - check_safe_policy(workspace, weakened) + check_member_lint_inheritance(root) + + +@pytest.mark.parametrize( + "member", ["cuprum-rust", "cuprum-native-io", "cuprum-streams"] +) +def test_member_lint_inheritance_rejects_a_local_override( + member: str, tmp_path: Path +) -> None: + """No audited member may opt out of the shared lint baseline.""" + root = copy_boundary_repository(tmp_path) / "rust" + manifest = root / member / "Cargo.toml" + manifest.write_text( + manifest.read_text(encoding="utf-8").replace( + "[lints]\nworkspace = true", "[lints]\nworkspace = false", 1 + ), + encoding="utf-8", + ) + with pytest.raises(ValueError, match=f"{member} must inherit"): + check_member_lint_inheritance(root) + + +def test_safe_target_roots_cover_every_current_automatic_target(tmp_path: Path) -> None: + """The target scan includes the library and compile-contract integration test.""" + root = copy_boundary_repository(tmp_path) / "rust" + actual = tuple( + path.relative_to(root / "cuprum-streams").as_posix() + for path in safe_target_roots(root) + ) + assert actual == TARGETS, "the safe-target scanner must cover every Cargo root" + + +@pytest.mark.parametrize("target", FUTURE_TARGETS) +def test_new_safe_target_without_an_unsafe_prohibition_is_rejected( + target: str, tmp_path: Path +) -> None: + """A future automatic target cannot weaken the safe-crate boundary.""" + root = copy_boundary_repository(tmp_path) / "rust" + check_safe_policy(root) + future_target = root / "cuprum-streams" / target + future_target.parent.mkdir(parents=True, exist_ok=True) + future_target.write_text( + "//! Deliberately incomplete safety contract.\n", encoding="utf-8" + ) + with pytest.raises(ValueError, match="safe target must forbid unsafe code"): + check_safe_policy(root) + + +def test_configured_build_script_without_an_unsafe_prohibition_is_rejected( + tmp_path: Path, +) -> None: + """An explicit build-script path is also an unsafe-policy target root.""" + root = copy_boundary_repository(tmp_path) / "rust" + manifest = root / "cuprum-streams/Cargo.toml" + manifest.write_text( + manifest.read_text(encoding="utf-8").replace( + "publish = false", 'publish = false\nbuild = "tools/build.rs"' + ), + encoding="utf-8", + ) + target = root / "cuprum-streams/tools/build.rs" + target.parent.mkdir() + target.write_text( + "//! Deliberately incomplete safety contract.\n", encoding="utf-8" + ) + with pytest.raises(ValueError, match="safe target must forbid unsafe code"): + check_safe_policy(root) def test_main_probes_every_target_with_every_unsafe_form( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """The baseline compiles, then each target is probed with all three forms.""" + """The baseline compiles, then each explicit safe root receives every probe.""" root, compiler = _drive_contract(tmp_path, monkeypatch) for probe in contract.PROBES: diff --git a/tests/test_native_sdist.py b/tests/test_native_sdist.py index db8c6fc13..332331115 100644 --- a/tests/test_native_sdist.py +++ b/tests/test_native_sdist.py @@ -60,7 +60,13 @@ def test_source_distribution_retains_native_workspace( workspace = repo_root() / "rust" expected: set[tuple[str, ...]] = { ("rust", name) - for name in ("Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dylint.toml") + for name in ( + "Cargo.toml", + "Cargo.lock", + "clippy.toml", + "rust-toolchain.toml", + "dylint.toml", + ) } for crate in ("cuprum-rust", "cuprum-streams", "cuprum-native-io"): expected.add(("rust", crate, "Cargo.toml")) From da65e78eb07b7ccf35ba18e37c30448c99370a86 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 17:23:33 +0200 Subject: [PATCH 02/21] Simplify safe target discovery Separate automatic and explicit Cargo target resolution while retaining the unsafe-boundary contract for each target form. --- scripts/check_boundary_contract.py | 76 ++++++++++++++++++------- scripts/tests/test_boundary_contract.py | 27 +++++++++ 2 files changed, 84 insertions(+), 19 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index c2af6e2e5..5e02a9ee1 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -13,6 +13,7 @@ from __future__ import annotations import contextlib +import itertools import shutil import sys import tomllib @@ -32,6 +33,9 @@ "pub unsafe fn unsafe_probe() {}", "unsafe trait Probe {} unsafe impl Probe for () {}", ) +AUTOMATIC_TARGET_ROOTS = ("src/lib.rs", "src/main.rs", "build.rs") +AUTOMATIC_TARGET_DIRECTORIES = ("src/bin", "examples", "tests", "benches") +EXPLICIT_TARGET_TYPES = ("lib", "bin", "example", "test", "bench") def check_members(manifest: str) -> None: @@ -91,25 +95,59 @@ def safe_target_roots(workspace: Path) -> tuple[Path, ...]: """ crate = workspace / "cuprum-streams" manifest = tomllib.loads((crate / "Cargo.toml").read_text(encoding="utf-8")) - automatic = [crate / "src/lib.rs", crate / "src/main.rs", crate / "build.rs"] - for directory in ("src/bin", "examples", "tests", "benches"): - root = crate / directory - automatic.extend(root.glob("*.rs")) - automatic.extend(root.glob("*/main.rs")) - explicit: list[Path] = [] - for target_type in ("lib", "bin", "example", "test", "bench"): - targets = manifest.get(target_type, []) - if isinstance(targets, dict): - targets = [targets] - explicit.extend( - crate / target["path"] - for target in targets - if isinstance(target, dict) and isinstance(target.get("path"), str) - ) - package = manifest.get("package", {}) - if isinstance(package, dict) and isinstance(package.get("build"), str): - explicit.append(crate / package["build"]) - return tuple(sorted({path for path in automatic + explicit if path.is_file()})) + candidates = itertools.chain( + _automatic_target_roots(crate), _explicit_target_roots(crate, manifest) + ) + return tuple(sorted({path for path in candidates if path.is_file()})) + + +def _automatic_target_roots(crate: Path) -> cabc.Iterator[Path]: + """Yield roots Cargo discovers from the safe crate's conventional layout.""" + directory_roots = (crate / directory for directory in AUTOMATIC_TARGET_DIRECTORIES) + return itertools.chain( + (crate / path for path in AUTOMATIC_TARGET_ROOTS), + *(root.glob("*.rs") for root in directory_roots), + *( + (crate / directory).glob("*/main.rs") + for directory in AUTOMATIC_TARGET_DIRECTORIES + ), + ) + + +def _explicit_target_roots( + crate: Path, manifest: cabc.Mapping[str, object] +) -> cabc.Iterator[Path]: + """Yield crate roots declared through Cargo target or build-script fields.""" + target_definitions = itertools.chain.from_iterable( + _target_definitions(manifest.get(target_type)) + for target_type in EXPLICIT_TARGET_TYPES + ) + target_paths = ( + crate / path + for target in target_definitions + if isinstance((path := target.get("path")), str) + ) + return itertools.chain(target_paths, _build_script_target_root(crate, manifest)) + + +def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: + """Normalise a Cargo target table into a sequence of target definitions.""" + if isinstance(value, dict): + return (value,) + if isinstance(value, list): + return tuple(target for target in value if isinstance(target, dict)) + return () + + +def _build_script_target_root( + crate: Path, manifest: cabc.Mapping[str, object] +) -> tuple[Path, ...]: + """Return the package's explicitly configured build-script root, if any.""" + package = manifest.get("package") + if not isinstance(package, dict): + return () + build_script = package.get("build") + return (crate / build_script,) if isinstance(build_script, str) else () def check_safe_policy(workspace: Path) -> tuple[Path, ...]: diff --git a/scripts/tests/test_boundary_contract.py b/scripts/tests/test_boundary_contract.py index 2654d78a9..735ab943f 100644 --- a/scripts/tests/test_boundary_contract.py +++ b/scripts/tests/test_boundary_contract.py @@ -126,6 +126,33 @@ def test_safe_target_roots_cover_every_current_automatic_target(tmp_path: Path) assert actual == TARGETS, "the safe-target scanner must cover every Cargo root" +@pytest.mark.parametrize("target_type", ["lib", "bin", "example", "test", "bench"]) +def test_safe_target_roots_cover_explicit_cargo_target_tables( + target_type: str, tmp_path: Path +) -> None: + """Every explicit Cargo target type remains inside the safe boundary.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + manifest = crate / "Cargo.toml" + heading = f"[{target_type}]" if target_type == "lib" else f"[[{target_type}]]" + relative_path = f"tools/{target_type}_target.rs" + manifest.write_text( + manifest.read_text(encoding="utf-8") + + f'\n{heading}\npath = "{relative_path}"\n', + encoding="utf-8", + ) + target = crate / relative_path + target.parent.mkdir() + target.write_text( + "//! Explicit safe target.\n#![forbid(unsafe_code)]\n", + encoding="utf-8", + ) + + roots = safe_target_roots(root) + + assert target in roots, f"the explicit {target_type} target must be checked" + + @pytest.mark.parametrize("target", FUTURE_TARGETS) def test_new_safe_target_without_an_unsafe_prohibition_is_rejected( target: str, tmp_path: Path From 4d01e7f701d23c61ba4507fc9622f9ed25acf765 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 17:26:25 +0200 Subject: [PATCH 03/21] Clarify Cargo target table dispatch Use structural matching for the supported manifest shapes in the boundary contract resolver. --- scripts/check_boundary_contract.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 5e02a9ee1..82c4c2ad3 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -132,11 +132,13 @@ def _explicit_target_roots( def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: """Normalise a Cargo target table into a sequence of target definitions.""" - if isinstance(value, dict): - return (value,) - if isinstance(value, list): - return tuple(target for target in value if isinstance(target, dict)) - return () + match value: + case dict() as target: + return (target,) + case list() as targets: + return tuple(target for target in targets if isinstance(target, dict)) + case _: + return () def _build_script_target_root( From 702706aab97eaad758dbe1eb7b6f11e656ba94d3 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 17:29:00 +0200 Subject: [PATCH 04/21] Normalize boundary contract spelling Keep the new Cargo target helper documentation within the repository's Oxford spelling policy. --- scripts/check_boundary_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 82c4c2ad3..5b7f32de3 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -131,7 +131,7 @@ def _explicit_target_roots( def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: - """Normalise a Cargo target table into a sequence of target definitions.""" + """Normalize a Cargo target table into a sequence of target definitions.""" match value: case dict() as target: return (target,) From 5b97a797ac630319d71b2d8041578f99d58f4e51 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 18:30:41 +0200 Subject: [PATCH 05/21] Harden doctest and boundary contracts Use the pinned Linux Rustdoc route to display and deny doctest-body warnings. Make safe-target discovery match Cargo's automatic-target switches, fail closed for unreadable inputs, and retain the audited source-level unsafe prohibition. Add executable lint, routing, target-discovery, and warning contracts, then record the implementation evolution in ADR-011. --- Makefile | 3 +- cuprum/unittests/test_dev_fast_contract.py | 23 +- .../test_doctest_warning_contract.py | 73 ++++++ docs/adr-011-audited-rust-boundaries.md | 19 ++ docs/developers-guide.md | 7 +- docs/rust-boundary-verification.md | 3 + scripts/check_boundary_contract.py | 216 +++++++++++++---- .../tests/test_boundary_target_discovery.py | 220 ++++++++++++++++++ .../tests/test_rust_lint_baseline_contract.py | 191 +++++++++++++++ 9 files changed, 699 insertions(+), 56 deletions(-) create mode 100644 cuprum/unittests/test_doctest_warning_contract.py create mode 100644 scripts/tests/test_boundary_target_discovery.py create mode 100644 scripts/tests/test_rust_lint_baseline_contract.py diff --git a/Makefile b/Makefile index 03576d86e..62cafc2ac 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,7 @@ WHITAKER ?= whitaker BUILD_JOBS ?= RUST_FLAGS ?= -D warnings RUSTDOC_FLAGS ?= --cfg docsrs -D warnings +DOCTEST_RUSTDOC_FLAGS = $(RUSTDOC_FLAGS) $(if $(DEV_FAST_HOST_IS_LINUX),-Zunstable-options --display-doctest-warnings --doctest-build-arg=-D --doctest-build-arg=warnings) CARGO_FLAGS ?= --all-targets --all-features CLIPPY_FLAGS ?= $(CARGO_FLAGS) -- $(RUST_FLAGS) DOC_FLAGS ?= --jobs 1 @@ -362,7 +363,7 @@ test-rust: $(RUST_DEBUG_PREREQUISITE) ## Run the Rust suite 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)" $(RUST_DEBUG_CARGO) test $(TEST_FLAGS) $(BUILD_JOBS); \ fi - cd $(RUST_DIR) && CARGO_BUILD_JOBS="$(TEST_CARGO_BUILD_JOBS)" RUSTFLAGS="$(DEV_FAST_TEST_RUSTFLAGS)" $(RUST_DEBUG_CARGO) test $(DOCTEST_FLAGS) + cd $(RUST_DIR) && CARGO_BUILD_JOBS="$(TEST_CARGO_BUILD_JOBS)" RUSTDOCFLAGS="$(DOCTEST_RUSTDOC_FLAGS)" RUSTFLAGS="$(DEV_FAST_TEST_RUSTFLAGS)" $(RUST_DEBUG_CARGO) test $(DOCTEST_FLAGS) msrv-check: ## Verify every Rust target compiles on the published MSRV cd $(RUST_DIR) && $(MSRV_CARGO_COMMAND) check --workspace --all-targets --all-features diff --git a/cuprum/unittests/test_dev_fast_contract.py b/cuprum/unittests/test_dev_fast_contract.py index 83246de69..f5e65d134 100644 --- a/cuprum/unittests/test_dev_fast_contract.py +++ b/cuprum/unittests/test_dev_fast_contract.py @@ -62,6 +62,12 @@ def _routing_lines(output: str) -> list[str]: ] +def _assert_required_tokens(route: str, tokens: tuple[str, ...], subject: str) -> None: + """Require every token that makes a routed command's contract binding.""" + missing = tuple(token for token in tokens if token not in route) + assert not missing, f"{subject} omitted required routing tokens: {missing}" + + def _assert_fragment_free(route: str) -> None: """Reject any selected Linux development acceleration from a protected route.""" assert "--config" not in route, f"protected route selected Cargo config: {route}" @@ -220,12 +226,17 @@ def test_rust_doctests_are_a_separate_debug_routed_gate() -> None: for line in _routing_lines(output) if "test --workspace --doc --all-features" in line ) - assert "probe-cargo --config ../tools/dev-fast/config.toml" in doctest, ( - "doctests must use the explicit Linux debug configuration" - ) - assert 'RUSTFLAGS="-D warnings' in doctest, "doctests must deny warnings" - assert "-Clink-arg=-fuse-ld=mold" in doctest, ( - "doctests must retain the selected linker" + _assert_required_tokens( + doctest, + ( + "probe-cargo --config ../tools/dev-fast/config.toml", + 'RUSTFLAGS="-D warnings', + 'RUSTDOCFLAGS="--cfg docsrs -D warnings -Zunstable-options', + "--display-doctest-warnings", + "--doctest-build-arg=-D --doctest-build-arg=warnings", + "-Clink-arg=-fuse-ld=mold", + ), + "the Linux doctest route", ) diff --git a/cuprum/unittests/test_doctest_warning_contract.py b/cuprum/unittests/test_doctest_warning_contract.py new file mode 100644 index 000000000..d9e80b1e3 --- /dev/null +++ b/cuprum/unittests/test_doctest_warning_contract.py @@ -0,0 +1,73 @@ +"""Prove the Linux doctest route fails when a doctest emits a warning.""" + +from __future__ import annotations + +import os +import shutil +import subprocess # ruff: ignore[suspicious-subprocess-import] - this test executes a fixed pinned Cargo command. +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +DEV_FAST_TOOLCHAIN = "nightly-2026-08-23" +DOCTEST_RUSTDOC_FLAGS = ( + "--cfg docsrs -D warnings -Zunstable-options --display-doctest-warnings " + "--doctest-build-arg=-D --doctest-build-arg=warnings" +) +WARNING_DOCTEST = """//! Warning-bearing doctest fixture. + +/// ``` +/// #[deprecated(note = "doctest warning contract")] +/// fn legacy() {} +/// legacy(); +/// ``` +pub struct WarningFixture; +""" + + +@pytest.mark.skipif(sys.platform != "linux", reason="dev-fast doctests are Linux-only") +def test_pinned_doctest_route_rejects_a_warning(tmp_path: Path) -> None: + """The nightly rustdoc path exposes and denies a doctest-body warning.""" + crate = tmp_path / "warning-doctest" + source = crate / "src/lib.rs" + source.parent.mkdir(parents=True) + (crate / "Cargo.toml").write_text( + '[package]\nname = "warning-doctest"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + source.write_text(WARNING_DOCTEST, encoding="utf-8") + cargo = shutil.which("cargo") + assert cargo is not None, "the doctest contract requires Cargo on PATH" + environment = { + **os.environ, + "CARGO_TARGET_DIR": str(ROOT / "rust/target/doctest-warning-contract"), + "RUSTDOCFLAGS": DOCTEST_RUSTDOC_FLAGS, + "RUSTFLAGS": "-D warnings -Clink-arg=-fuse-ld=mold", + "RUSTUP_TOOLCHAIN": DEV_FAST_TOOLCHAIN, + } + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Cargo argv and checked-in fragment. + [ + cargo, + "--config", + str(ROOT / "tools/dev-fast/config.toml"), + "test", + "--doc", + "--manifest-path", + str(crate / "Cargo.toml"), + ], + capture_output=True, + check=False, + cwd=crate, + env=environment, + text=True, + ) + + assert completed.returncode != 0, "a doctest warning must fail the routed gate" + assert "use of deprecated function" in completed.stdout, ( + "the failure must come from the doctest warning rather than setup" + ) + assert "-D deprecated" in completed.stdout, ( + "rustdoc must deny the displayed doctest warning" + ) diff --git a/docs/adr-011-audited-rust-boundaries.md b/docs/adr-011-audited-rust-boundaries.md index f1ebd6899..fc8388482 100644 --- a/docs/adr-011-audited-rust-boundaries.md +++ b/docs/adr-011-audited-rust-boundaries.md @@ -191,3 +191,22 @@ therefore keeps its `#![forbid(unsafe_code)]` source-level prohibition while all three members inherit the shared workspace tables. The boundary-contract gate checks both the common manifest inheritance and every discovered safe target root, including future automatic Cargo targets. + +## Addendum (2026-09-20): Lint baseline and target discovery + +The workspace now owns the common Rust, Clippy, and Rustdoc lint baseline in +`rust/Cargo.toml`; every member inherits it. `rust/clippy.toml` records the +reviewed complexity thresholds and the environment-access injection policy. The +approved unsafe boundary remains deliberately narrower: the syscall and FFI +crates cannot use a workspace-wide `unsafe_code` prohibition, while every +effective `cuprum-streams` target root must declare `#![forbid(unsafe_code)]`. + +The contract implementation evolved from a fixed target list to loading the +safe crate's manifest and effective Cargo roots. It honours `autolib`, +`autobins`, `autoexamples`, `autotests`, `autobenches`, and `build = false`, +while retaining explicit `lib`, `bin`, `example`, `test`, `bench`, and custom +build-script paths. Filesystem access, UTF-8 decoding, and TOML parsing fail +closed with the affected path; pure discovery and policy evaluation remain +separately testable. The Linux debug doctest gate uses the pinned dev-fast +nightly rustdoc route to display and deny doctest-body warnings. Stable Rust +1.85.0 remains the published MSRV and retains its separate verification route. diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 877347100..6872c17b8 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -3372,7 +3372,10 @@ floating-point arithmetic. The workspace cannot forbid unsafe code globally: `cuprum-native-io` and `cuprum-rust` are the audited syscall and FFI boundaries. `cuprum-streams` keeps its explicit crate-level prohibition, which the boundary contract checks. -The Rust 1.85.0 maintenance toolchain provides `rustfmt`, `clippy`, and +The +[ADR-011 addendum](adr-011-audited-rust-boundaries.md#addendum-2026-09-20-lint-baseline-and-target-discovery) +records why that source-level contract complements the shared lint tables. The +Rust 1.85.0 maintenance toolchain provides `rustfmt`, `clippy`, and `rust-analyzer`; Linux debug Rustdoc, Clippy, and doctests use the separately pinned dev-fast configuration, while MSRV, coverage, release, verification, and Whitaker retain their prescribed routes. @@ -4103,7 +4106,7 @@ Cargo build output is excluded: `target` never appears in an archive, so an sdist carries sources rather than a multi-gigabyte build cache. Maturin builds its archive from the same workspace, and the -`[tool.maturin].include` entries carry the two files it would not otherwise +`[tool.maturin].include` entries carry the three files it would not otherwise copy — `rust/clippy.toml`, `rust/rust-toolchain.toml`, and `rust/dylint.toml`, all restricted with `format = "sdist"` so they never enter a wheel. diff --git a/docs/rust-boundary-verification.md b/docs/rust-boundary-verification.md index 948d1010c..63cc5b6d1 100644 --- a/docs/rust-boundary-verification.md +++ b/docs/rust-boundary-verification.md @@ -191,6 +191,9 @@ Cargo target roots, requiring a source-level unsafe prohibition on each. Its current measured run found the library and compile-contract target, compiled six unsafe probes, and separately rejected unannotated future binary, build-script, example, integration-test, and benchmark targets. +[ADR-011's 2026-09-20 addendum](adr-011-audited-rust-boundaries.md#addendum-2026-09-20-lint-baseline-and-target-discovery) +records the implementation evolution, including Cargo's automatic-target +switches and the I/O boundary around discovery. | Boundary and production surface | Property | Selected tool and executed subject | Assumptions and bounds | Native or integration evidence | Status and limitation | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 5b7f32de3..bb06379d3 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -15,6 +15,7 @@ import contextlib import itertools import shutil +import stat import sys import tomllib import typing as typ @@ -33,8 +34,16 @@ "pub unsafe fn unsafe_probe() {}", "unsafe trait Probe {} unsafe impl Probe for () {}", ) -AUTOMATIC_TARGET_ROOTS = ("src/lib.rs", "src/main.rs", "build.rs") -AUTOMATIC_TARGET_DIRECTORIES = ("src/bin", "examples", "tests", "benches") +AUTOMATIC_TARGET_ROOTS = ( + ("autolib", "src/lib.rs"), + ("autobins", "src/main.rs"), +) +AUTOMATIC_TARGET_DIRECTORIES = ( + ("autobins", "src/bin"), + ("autoexamples", "examples"), + ("autotests", "tests"), + ("autobenches", "benches"), +) EXPLICIT_TARGET_TYPES = ("lib", "bin", "example", "test", "bench") @@ -46,12 +55,19 @@ def check_members(manifest: str) -> None: manifest : str Workspace manifest text. - Raises - ------ - ValueError - If workspace membership differs from the audited set. """ - members = tomllib.loads(manifest)["workspace"]["members"] + _check_members_manifest(tomllib.loads(manifest)) + + +def _check_members_manifest(manifest: cabc.Mapping[str, object]) -> None: + """Evaluate the loaded workspace membership without filesystem access.""" + workspace = manifest.get("workspace") + members = workspace.get("members") if isinstance(workspace, dict) else None + if not isinstance(members, list) or not all( + isinstance(member, str) for member in members + ): + msg = "workspace membership differs from the audited boundary inventory" + raise ValueError(msg) if set(members) != BOUNDARIES | SAFE_CRATES or len(members) != len( BOUNDARIES | SAFE_CRATES ): @@ -74,7 +90,7 @@ def check_member_lint_inheritance(workspace: Path) -> None: """ for member in BOUNDARIES | SAFE_CRATES: manifest_path = workspace / member / "Cargo.toml" - manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8")) + manifest = _load_toml(manifest_path) if manifest.get("lints") != {"workspace": True}: msg = f"{member} must inherit the workspace lint baseline" raise ValueError(msg) @@ -93,29 +109,137 @@ def safe_target_roots(workspace: Path) -> tuple[Path, ...]: tuple[Path, ...] Existing automatic and explicitly configured crate-root source paths. """ + crate, manifest = _load_safe_manifest(workspace) + package = _package_table(manifest) + automatic = _load_automatic_target_roots(crate, package) + explicit = _explicit_target_roots(crate, manifest, package) + return discover_safe_target_roots(automatic, explicit) + + +def discover_safe_target_roots( + automatic: cabc.Iterable[Path], explicit: cabc.Iterable[Path] +) -> tuple[Path, ...]: + """Deduplicate loaded automatic and declared explicit Cargo target roots.""" + return tuple(sorted(set(itertools.chain(automatic, explicit)))) + + +def evaluate_safe_target_policy(sources: cabc.Mapping[Path, str]) -> tuple[Path, ...]: + """Require every loaded safe target source to forbid unsafe code.""" + if not sources: + msg = "safe crate has no discoverable Cargo target roots" + raise ValueError(msg) + for target, source in sources.items(): + if "#![forbid(unsafe_code)]" not in source: + msg = f"safe target must forbid unsafe code: {target}" + raise ValueError(msg) + return tuple(sources) + + +def _load_safe_manifest(workspace: Path) -> tuple[Path, cabc.Mapping[str, object]]: + """Load the safe crate manifest before evaluating its target policy.""" crate = workspace / "cuprum-streams" - manifest = tomllib.loads((crate / "Cargo.toml").read_text(encoding="utf-8")) - candidates = itertools.chain( - _automatic_target_roots(crate), _explicit_target_roots(crate, manifest) - ) - return tuple(sorted({path for path in candidates if path.is_file()})) + return crate, _load_toml(crate / "Cargo.toml") -def _automatic_target_roots(crate: Path) -> cabc.Iterator[Path]: - """Yield roots Cargo discovers from the safe crate's conventional layout.""" - directory_roots = (crate / directory for directory in AUTOMATIC_TARGET_DIRECTORIES) - return itertools.chain( - (crate / path for path in AUTOMATIC_TARGET_ROOTS), - *(root.glob("*.rs") for root in directory_roots), - *( - (crate / directory).glob("*/main.rs") - for directory in AUTOMATIC_TARGET_DIRECTORIES +def _load_toml(path: Path) -> cabc.Mapping[str, object]: + """Read and parse one manifest while retaining the affected path on failure.""" + text = _read_text(path) + try: + manifest = tomllib.loads(text) + except tomllib.TOMLDecodeError as error: + msg = f"cannot parse TOML manifest {path}: {error}" + raise ValueError(msg) from error + return manifest + + +def _read_text(path: Path) -> str: + """Read UTF-8 text, failing closed when the source cannot be inspected.""" + try: + return path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as error: + msg = f"cannot read {path}: {error}" + raise ValueError(msg) from error + + +def _load_automatic_target_roots( + crate: Path, package: cabc.Mapping[str, object] +) -> tuple[Path, ...]: + """Load the existing Cargo auto-target roots selected by package switches.""" + direct = ( + crate / path + for switch, path in AUTOMATIC_TARGET_ROOTS + if _automatic_target_is_enabled(package, switch) + ) + directories = ( + crate / directory + for switch, directory in AUTOMATIC_TARGET_DIRECTORIES + if _automatic_target_is_enabled(package, switch) + ) + candidates = itertools.chain( + direct, + itertools.chain.from_iterable( + _load_directory_target_roots(path) for path in directories ), + _load_default_build_script_root(crate, package), ) + return tuple(path for path in candidates if _is_regular_file(path)) + + +def _automatic_target_is_enabled( + package: cabc.Mapping[str, object], switch: str +) -> bool: + """Return whether Cargo's default-enabled automatic target switch is active.""" + return package.get(switch) is not False + + +def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: + """Load Cargo's flat and one-level-nested conventional target roots.""" + for entry in _directory_entries(directory): + if entry.suffix == ".rs": + yield entry + if _is_directory(entry): + yield entry / "main.rs" + + +def _directory_entries(directory: Path) -> tuple[Path, ...]: + """List one optional automatic-target directory without masking I/O errors.""" + try: + return tuple(directory.iterdir()) + except FileNotFoundError: + return () + except NotADirectoryError: + return () + except OSError as error: + msg = f"cannot inspect automatic target directory {directory}: {error}" + raise ValueError(msg) from error + + +def _is_regular_file(path: Path) -> bool: + """Check a candidate root while treating inspection failures as contract errors.""" + try: + return stat.S_ISREG(path.stat().st_mode) + except FileNotFoundError: + return False + except OSError as error: + msg = f"cannot inspect target root {path}: {error}" + raise ValueError(msg) from error + + +def _is_directory(path: Path) -> bool: + """Check a nested target directory without masking an access failure.""" + try: + return stat.S_ISDIR(path.stat().st_mode) + except FileNotFoundError: + return False + except OSError as error: + msg = f"cannot inspect target directory {path}: {error}" + raise ValueError(msg) from error def _explicit_target_roots( - crate: Path, manifest: cabc.Mapping[str, object] + crate: Path, + manifest: cabc.Mapping[str, object], + package: cabc.Mapping[str, object], ) -> cabc.Iterator[Path]: """Yield crate roots declared through Cargo target or build-script fields.""" target_definitions = itertools.chain.from_iterable( @@ -127,7 +251,9 @@ def _explicit_target_roots( for target in target_definitions if isinstance((path := target.get("path")), str) ) - return itertools.chain(target_paths, _build_script_target_root(crate, manifest)) + return itertools.chain( + target_paths, _configured_build_script_target_root(crate, package) + ) def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: @@ -141,13 +267,23 @@ def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: return () -def _build_script_target_root( - crate: Path, manifest: cabc.Mapping[str, object] -) -> tuple[Path, ...]: - """Return the package's explicitly configured build-script root, if any.""" +def _package_table(manifest: cabc.Mapping[str, object]) -> cabc.Mapping[str, object]: + """Return the package table or an empty table for malformed optional input.""" package = manifest.get("package") - if not isinstance(package, dict): - return () + return package if isinstance(package, dict) else {} + + +def _load_default_build_script_root( + crate: Path, package: cabc.Mapping[str, object] +) -> tuple[Path, ...]: + """Return Cargo's default build script unless the manifest disables it.""" + return (crate / "build.rs",) if package.get("build", True) is True else () + + +def _configured_build_script_target_root( + crate: Path, package: cabc.Mapping[str, object] +) -> tuple[Path, ...]: + """Return a non-default package build-script target, if one is declared.""" build_script = package.get("build") return (crate / build_script,) if isinstance(build_script, str) else () @@ -165,23 +301,10 @@ def check_safe_policy(workspace: Path) -> tuple[Path, ...]: tuple[Path, ...] The checked source roots, for the compiler-probe phase. - Raises - ------ - ValueError - If no source roots exist or any root omits the unsafe prohibition. """ targets = safe_target_roots(workspace) - if not targets: - msg = "safe crate has no discoverable Cargo target roots" - raise ValueError(msg) - for target in targets: - source = target.read_text(encoding="utf-8") - if "#![forbid(unsafe_code)]" not in source: - msg = ( - f"safe target must forbid unsafe code: {target.relative_to(workspace)}" - ) - raise ValueError(msg) - return targets + sources = {target: _read_text(target) for target in targets} + return evaluate_safe_target_policy(sources) def _compile(workspace: Path) -> tuple[int, str]: @@ -208,8 +331,7 @@ def _compile(workspace: Path) -> tuple[int, str]: def main() -> None: """Check the allowlist and actual compiler rejection on isolated source copies.""" - manifest = (ROOT / "rust/Cargo.toml").read_text(encoding="utf-8") - check_members(manifest) + _check_members_manifest(_load_toml(ROOT / "rust/Cargo.toml")) check_member_lint_inheritance(ROOT / "rust") safe_targets = check_safe_policy(ROOT / "rust") workspace = ROOT / ".cache/boundary-contract/workspace" diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py new file mode 100644 index 000000000..520cc809a --- /dev/null +++ b/scripts/tests/test_boundary_target_discovery.py @@ -0,0 +1,220 @@ +"""Exercise safe Cargo target discovery without compiling a fixture workspace.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from scripts import check_boundary_contract as contract +from scripts.check_boundary_contract import ( + check_safe_policy, + discover_safe_target_roots, + evaluate_safe_target_policy, + safe_target_roots, +) +from scripts.tests.boundary_harness_support import copy_boundary_repository + +AUTO_DISABLED_TARGETS = ( + ("autolib", "src/lib.rs"), + ("autobins", "src/main.rs"), + ("autobins", "src/bin/flat.rs"), + ("autobins", "src/bin/nested/main.rs"), + ("autoexamples", "examples/example.rs"), + ("autotests", "tests/integration.rs"), + ("autobenches", "benches/benchmark.rs"), +) +AUTOMATIC_PROPERTY_TARGETS = ( + "src/lib.rs", + "src/bin/nested/main.rs", +) +EXPLICIT_PROPERTY_TARGETS = ( + "src/lib.rs", + "tools/explicit.rs", + "tools/build.rs", +) +SAFE_SOURCE = "//! Safe target.\n#![forbid(unsafe_code)]\n" + + +def _write_target(crate: Path, relative_path: str, source: str = SAFE_SOURCE) -> Path: + """Create one synthetic Cargo target source and return its absolute path.""" + target = crate / relative_path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(source, encoding="utf-8") + return target + + +def _set_package_field(manifest: Path, field: str) -> None: + """Add one tested package field beside the existing package metadata.""" + manifest.write_text( + manifest.read_text(encoding="utf-8").replace( + "publish = false", f"publish = false\n{field}", 1 + ), + encoding="utf-8", + ) + + +@pytest.mark.parametrize(("switch", "target"), AUTO_DISABLED_TARGETS) +def test_disabled_automatic_targets_are_not_checked( + switch: str, target: str, tmp_path: Path +) -> None: + """Cargo's package auto-target switches remove only their matching roots.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + target_path = _write_target(crate, target) + _set_package_field(crate / "Cargo.toml", f"{switch} = false") + + roots = safe_target_roots(root) + + assert target_path not in roots, f"{switch} = false must disable {target}" + check_safe_policy(root) + + +def test_build_false_disables_the_default_build_script(tmp_path: Path) -> None: + """Cargo's ``build = false`` removes the otherwise automatic build script.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + build_script = _write_target(crate, "build.rs") + assert build_script in safe_target_roots(root), ( + "the default build script is automatic" + ) + + _set_package_field(crate / "Cargo.toml", "build = false") + + assert build_script not in safe_target_roots(root), ( + "build = false must remove the automatic build-script target" + ) + check_safe_policy(root) + + +def test_explicit_target_without_unsafe_prohibition_is_rejected(tmp_path: Path) -> None: + """An explicit target does not inherit the safe crate's source attribute.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + manifest = crate / "Cargo.toml" + manifest.write_text( + manifest.read_text(encoding="utf-8") + + '\n[[bin]]\nname = "unsafe-contract-probe"\npath = "tools/probe.rs"\n', + encoding="utf-8", + ) + _write_target(crate, "tools/probe.rs", "//! Missing safety attribute.\n") + + with pytest.raises(ValueError, match="safe target must forbid unsafe code"): + check_safe_policy(root) + + +def test_malformed_manifest_fails_closed_with_its_path(tmp_path: Path) -> None: + """Target discovery must report malformed TOML instead of losing coverage.""" + root = copy_boundary_repository(tmp_path) / "rust" + manifest = root / "cuprum-streams/Cargo.toml" + manifest.write_text("[package\n", encoding="utf-8") + + with pytest.raises(ValueError, match=r"cannot parse TOML manifest .*Cargo.toml"): + safe_target_roots(root) + + +def test_non_utf8_manifest_fails_closed_with_its_path(tmp_path: Path) -> None: + """Manifest decoding failure is an explicit unmeasured-contract error.""" + root = copy_boundary_repository(tmp_path) / "rust" + manifest = root / "cuprum-streams/Cargo.toml" + manifest.write_bytes(b"\xff") + + with pytest.raises(ValueError, match=r"cannot read .*Cargo.toml"): + safe_target_roots(root) + + +def test_unreadable_source_fails_closed_with_its_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A source read error is an unmeasured boundary, never an omitted target.""" + root = copy_boundary_repository(tmp_path) / "rust" + target = root / "cuprum-streams/src/lib.rs" + original_read_text = Path.read_text + + def read_text(path: Path, *args: object, **kwargs: object) -> str: + """Refuse only the selected target while retaining real fixture reads.""" + if path == target: + raise PermissionError + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", read_text) + + with pytest.raises(ValueError, match=r"cannot read .*src/lib.rs"): + check_safe_policy(root) + + +def test_unreadable_automatic_target_directory_fails_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Directory access failure must not be treated as an empty target set.""" + root = copy_boundary_repository(tmp_path) / "rust" + directory = root / "cuprum-streams/tests" + original_iterdir = Path.iterdir + + def iterdir(path: Path) -> object: + """Refuse only the directory whose targets the contract must inspect.""" + if path == directory: + raise PermissionError + return original_iterdir(path) + + monkeypatch.setattr(Path, "iterdir", iterdir) + + with pytest.raises( + ValueError, match=r"cannot inspect automatic target directory .*tests" + ): + safe_target_roots(root) + + +def test_probe_rejection_cannot_replace_source_policy( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The orchestrator rejects a missing attribute before it can append a probe.""" + root = copy_boundary_repository(tmp_path) + target = root / "rust/cuprum-streams/src/lib.rs" + target.write_text("//! Missing safety attribute.\n", encoding="utf-8") + compile_calls: list[Path] = [] + + def compile_workspace(workspace: Path) -> tuple[int, str]: + """Record a forbidden probe attempt without invoking Cargo.""" + compile_calls.append(workspace) + return 1, "probe rejected" + + monkeypatch.setattr(contract, "ROOT", root) + monkeypatch.setattr(contract, "_compile", compile_workspace) + + with pytest.raises(ValueError, match="safe target must forbid unsafe code"): + contract.main() + assert not compile_calls, "source policy must fail before the probe seam runs" + + +@settings(max_examples=24, deadline=None) +@given( + st.lists(st.sampled_from(AUTOMATIC_PROPERTY_TARGETS), max_size=6), + st.lists(st.sampled_from(EXPLICIT_PROPERTY_TARGETS), max_size=6), +) +def test_discovery_is_order_independent_and_policy_requires_every_root( + automatic_paths: list[str], explicit_paths: list[str] +) -> None: + """Generated automatic, nested, explicit, and build roots stay deduplicated.""" + crate = Path("/synthetic/cuprum-streams") + automatic = tuple(crate / path for path in automatic_paths) + explicit = tuple(crate / path for path in explicit_paths) + expected = tuple(sorted({*automatic, *explicit})) + + discovered = discover_safe_target_roots(automatic, explicit) + + assert discovered == expected, "the pure discovery oracle must ignore input order" + if not discovered: + with pytest.raises(ValueError, match="no discoverable Cargo target roots"): + evaluate_safe_target_policy({}) + return + safe_sources = dict.fromkeys(discovered, SAFE_SOURCE) + assert evaluate_safe_target_policy(safe_sources) == discovered, ( + "policy evaluation must retain every discovered root" + ) + missing_attribute = dict(safe_sources) + missing_attribute[discovered[0]] = "//! Missing safety attribute.\n" + with pytest.raises(ValueError, match="safe target must forbid unsafe code"): + evaluate_safe_target_policy(missing_attribute) diff --git a/scripts/tests/test_rust_lint_baseline_contract.py b/scripts/tests/test_rust_lint_baseline_contract.py new file mode 100644 index 000000000..c655dca28 --- /dev/null +++ b/scripts/tests/test_rust_lint_baseline_contract.py @@ -0,0 +1,191 @@ +"""Keep the committed Rust lint baseline complete and executable.""" + +from __future__ import annotations + +import shutil +import subprocess # ruff: ignore[suspicious-subprocess-import] - this test expands a fixed local Make recipe. +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +CLIPPY_LINTS = { + "allow_attributes", + "allow_attributes_without_reason", + "blanket_clippy_restriction_lints", + "cognitive_complexity", + "disallowed_methods", + "needless_pass_by_value", + "implicit_hasher", + "missing_assert_message", + "dbg_macro", + "print_stdout", + "print_stderr", + "unwrap_used", + "expect_used", + "indexing_slicing", + "string_slice", + "integer_division", + "integer_division_remainder_used", + "panic_in_result_fn", + "unreachable", + "host_endian_bytes", + "little_endian_bytes", + "big_endian_bytes", + "let_underscore_must_use", + "or_fun_call", + "option_if_let_else", + "self_named_module_files", + "shadow_reuse", + "shadow_same", + "shadow_unrelated", + "str_to_string", + "string_lit_as_bytes", + "try_err", + "unneeded_field_pattern", + "use_self", + "float_arithmetic", + "cast_possible_truncation", + "cast_possible_wrap", + "cast_precision_loss", + "lossy_float_literal", + "missing_const_for_fn", + "must_use_candidate", + "unused_async", + "missing_panics_doc", + "error_impl_error", + "result_large_err", +} +RUST_LINTS = {"missing_docs", "renamed_and_removed_lints", "unknown_lints"} +RUSTDOC_LINTS = { + "bare_urls", + "broken_intra_doc_links", + "invalid_codeblock_attributes", + "invalid_html_tags", + "missing_crate_level_docs", + "private_intra_doc_links", + "unescaped_backticks", +} +DISALLOWED_METHODS = { + "std::env::var", + "std::env::var_os", + "std::env::vars", + "std::env::vars_os", + "std::env::set_var", + "std::env::remove_var", + "std::env::set_current_dir", +} +EXPECTED_PEDANTIC = {"level": "warn", "priority": -1} +EXPECTED_UNEXPECTED_CFGS = {"level": "warn", "check-cfg": ["cfg(kani)"]} +EXPECTED_CLIPPY_OPTIONS = { + "cognitive-complexity-threshold": 9, + "too-many-arguments-threshold": 4, + "too-many-lines-threshold": 70, + "excessive-nesting-threshold": 4, + "allow-expect-in-tests": True, +} + + +def _cargo_manifest() -> dict[str, object]: + """Load the workspace manifest that owns the shared lint policy.""" + with (ROOT / "rust/Cargo.toml").open("rb") as manifest: + return tomllib.load(manifest) + + +def _clippy_config() -> dict[str, object]: + """Load the local Clippy configuration beside the workspace manifest.""" + with (ROOT / "rust/clippy.toml").open("rb") as config: + return tomllib.load(config) + + +def _dry_run_test_rust() -> str: + """Expand the Linux doctest recipe without running Cargo or prerequisites.""" + make = shutil.which("make") + assert make is not None, "the Make contract requires GNU Make on PATH" + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Make argv. + [ + make, + "--dry-run", + "test-rust", + "CARGO=probe-cargo", + "DEV_FAST_HOST_IS_LINUX=yes", + "BUILD_JOBS=--jobs 7", + ], + capture_output=True, + check=True, + cwd=ROOT, + text=True, + ) + return completed.stdout + + +def test_workspace_declares_every_required_rust_lint_at_the_expected_level() -> None: + """The baseline must not lose a denied lint during future table edits.""" + workspace = _cargo_manifest()["workspace"] + assert isinstance(workspace, dict), "the workspace table must be a TOML mapping" + lints = workspace["lints"] + assert isinstance(lints, dict), "the workspace must declare a lint table" + clippy = lints["clippy"] + rust = lints["rust"] + rustdoc = lints["rustdoc"] + assert isinstance(clippy, dict), "the workspace must declare Clippy lints" + assert isinstance(rust, dict), "the workspace must declare Rust lints" + assert isinstance(rustdoc, dict), "the workspace must declare Rustdoc lints" + assert clippy["pedantic"] == EXPECTED_PEDANTIC, ( + "pedantic must remain the warn-level base" + ) + assert {name: clippy[name] for name in CLIPPY_LINTS} == dict.fromkeys( + CLIPPY_LINTS, "deny" + ), "every required Clippy lint must stay denied" + assert {name: rust[name] for name in RUST_LINTS} == dict.fromkeys( + RUST_LINTS, "deny" + ), "every required Rust lint must stay denied" + assert {name: rustdoc[name] for name in RUSTDOC_LINTS} == dict.fromkeys( + RUSTDOC_LINTS, "deny" + ), "every required Rustdoc lint must stay denied" + assert "unsafe_code" not in rust, ( + "the audited syscall and FFI boundaries preclude a workspace-wide unsafe ban" + ) + assert rust["unexpected_cfgs"] == EXPECTED_UNEXPECTED_CFGS, ( + "the Kani conditional compilation contract must remain reachable" + ) + + +def test_clippy_configuration_keeps_the_approved_thresholds_and_methods() -> None: + """Thresholds and environment injection must stay auditable local policy.""" + config = _clippy_config() + assert { + name: config[name] + for name in ( + "cognitive-complexity-threshold", + "too-many-arguments-threshold", + "too-many-lines-threshold", + "excessive-nesting-threshold", + "allow-expect-in-tests", + ) + } == EXPECTED_CLIPPY_OPTIONS, "the approved Clippy options must not drift" + methods = config["disallowed-methods"] + assert isinstance(methods, list), "disallowed methods must remain a TOML list" + assert {method["path"] for method in methods if isinstance(method, dict)} == ( + DISALLOWED_METHODS + ), "the seven environment methods must stay disallowed" + assert all( + isinstance(method, dict) + and isinstance(method.get("reason"), str) + and method["reason"] + for method in methods + ), "each disallowed environment method needs a local injection rationale" + + +def test_doctest_recipe_passes_full_rustdoc_warning_flags_and_jobs() -> None: + """Require the separate gate to use pinned nightly rustdoc warnings.""" + output = _dry_run_test_rust() + doctest = next( + line + for line in output.splitlines() + if "test --workspace --doc --all-features --jobs 7" in line + ) + assert ( + 'RUSTDOCFLAGS="--cfg docsrs -D warnings -Zunstable-options ' + "--display-doctest-warnings --doctest-build-arg=-D " + '--doctest-build-arg=warnings"' + ) in doctest, "the doctest recipe must retain every rustdoc warning flag" From bf294d436930d2fca601c1cebef016ba0c578752 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 18:34:59 +0200 Subject: [PATCH 06/21] Type the boundary read-text test double Match Path.read_text's optional encoding and error parameters so the unreadable-source contract remains type-safe. --- scripts/tests/test_boundary_target_discovery.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index 520cc809a..3cd0ba404 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -133,11 +133,13 @@ def test_unreadable_source_fails_closed_with_its_path( target = root / "cuprum-streams/src/lib.rs" original_read_text = Path.read_text - def read_text(path: Path, *args: object, **kwargs: object) -> str: + def read_text( + path: Path, encoding: str | None = None, errors: str | None = None + ) -> str: """Refuse only the selected target while retaining real fixture reads.""" if path == target: raise PermissionError - return original_read_text(path, *args, **kwargs) + return original_read_text(path, encoding=encoding, errors=errors) monkeypatch.setattr(Path, "read_text", read_text) From b9270f330cc88078af7a3fc8fafb7588b25e37ac Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 18:55:25 +0200 Subject: [PATCH 07/21] Decouple the doctest warning regression Exercise the pinned nightly Rustdoc warning flags with its standard backend so every Python test runner can prove that the former RUSTFLAGS route passes and the corrected route rejects the deprecated doctest. --- .../test_doctest_warning_contract.py | 86 +++++++++++++------ 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/cuprum/unittests/test_doctest_warning_contract.py b/cuprum/unittests/test_doctest_warning_contract.py index d9e80b1e3..cd609f634 100644 --- a/cuprum/unittests/test_doctest_warning_contract.py +++ b/cuprum/unittests/test_doctest_warning_contract.py @@ -6,11 +6,13 @@ import shutil import subprocess # ruff: ignore[suspicious-subprocess-import] - this test executes a fixed pinned Cargo command. import sys -from pathlib import Path +import typing as typ import pytest -ROOT = Path(__file__).resolve().parents[2] +if typ.TYPE_CHECKING: + from pathlib import Path + DEV_FAST_TOOLCHAIN = "nightly-2026-08-23" DOCTEST_RUSTDOC_FLAGS = ( "--cfg docsrs -D warnings -Zunstable-options --display-doctest-warnings " @@ -27,31 +29,23 @@ """ -@pytest.mark.skipif(sys.platform != "linux", reason="dev-fast doctests are Linux-only") -def test_pinned_doctest_route_rejects_a_warning(tmp_path: Path) -> None: - """The nightly rustdoc path exposes and denies a doctest-body warning.""" - crate = tmp_path / "warning-doctest" - source = crate / "src/lib.rs" - source.parent.mkdir(parents=True) - (crate / "Cargo.toml").write_text( - '[package]\nname = "warning-doctest"\nversion = "0.1.0"\nedition = "2024"\n', - encoding="utf-8", - ) - source.write_text(WARNING_DOCTEST, encoding="utf-8") - cargo = shutil.which("cargo") - assert cargo is not None, "the doctest contract requires Cargo on PATH" - environment = { - **os.environ, - "CARGO_TARGET_DIR": str(ROOT / "rust/target/doctest-warning-contract"), - "RUSTDOCFLAGS": DOCTEST_RUSTDOC_FLAGS, - "RUSTFLAGS": "-D warnings -Clink-arg=-fuse-ld=mold", +def _run_doctests( + crate: Path, cargo: str, rustdoc_flags: str | None +) -> subprocess.CompletedProcess[str]: + """Run the pinned nightly with the standard backend and stated Rustdoc flags.""" + environment: dict[str, str] = dict(os.environ) + environment.update({ + "CARGO_TARGET_DIR": str(crate / "target"), + "RUSTFLAGS": "-D warnings", "RUSTUP_TOOLCHAIN": DEV_FAST_TOOLCHAIN, - } - completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Cargo argv and checked-in fragment. + }) + if rustdoc_flags is not None: + environment["RUSTDOCFLAGS"] = rustdoc_flags + else: + environment.pop("RUSTDOCFLAGS", None) + return subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Cargo argv and pinned toolchain. [ cargo, - "--config", - str(ROOT / "tools/dev-fast/config.toml"), "test", "--doc", "--manifest-path", @@ -64,10 +58,46 @@ def test_pinned_doctest_route_rejects_a_warning(tmp_path: Path) -> None: text=True, ) - assert completed.returncode != 0, "a doctest warning must fail the routed gate" - assert "use of deprecated function" in completed.stdout, ( - "the failure must come from the doctest warning rather than setup" + +def _diagnostics(completed: subprocess.CompletedProcess[str]) -> str: + """Return every Cargo diagnostic stream for setup and failure assertions.""" + return completed.stdout + completed.stderr + + +@pytest.mark.skipif( + sys.platform != "linux", reason="the production doctest warning route is Linux-only" +) +def test_pinned_doctest_route_rejects_a_warning(tmp_path: Path) -> None: + """Only the corrected nightly Rustdoc route rejects a doctest warning.""" + crate = tmp_path / "warning-doctest" + source = crate / "src/lib.rs" + source.parent.mkdir(parents=True) + (crate / "Cargo.toml").write_text( + '[package]\nname = "warning-doctest"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + source.write_text(WARNING_DOCTEST, encoding="utf-8") + cargo = shutil.which("cargo") + assert cargo is not None, "the doctest contract requires Cargo on PATH" + former_route = _run_doctests(crate, cargo, rustdoc_flags=None) + former_diagnostics = _diagnostics(former_route) + assert former_route.returncode == 0, ( + "the former RUSTFLAGS-only route must pass the warning-bearing doctest:\n" + f"{former_diagnostics}" + ) + assert "test result: ok" in former_diagnostics, ( + "the former route must execute the doctest rather than only build setup" + ) + + corrected_route = _run_doctests(crate, cargo, DOCTEST_RUSTDOC_FLAGS) + corrected_diagnostics = _diagnostics(corrected_route) + assert corrected_route.returncode != 0, ( + "the corrected Rustdoc route must reject a doctest warning:\n" + f"{corrected_diagnostics}" + ) + assert "error: use of deprecated function" in corrected_diagnostics, ( + "the corrected route must fail on the doctest warning rather than setup" ) - assert "-D deprecated" in completed.stdout, ( + assert "-D deprecated" in corrected_diagnostics, ( "rustdoc must deny the displayed doctest warning" ) From 849bdfe947b60722d00a07813dbe870978244a3d Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 19:23:32 +0200 Subject: [PATCH 08/21] Cover inferred Cargo target roots Discover pathless explicit target tables with Cargo's conventional source paths and retain the unsafe boundary for each resolved root. Make doctest diagnostics deterministic, include the lint contract in the standard test selection, and distinguish current evidence from the historical probe count. --- Makefile | 1 + .../test_doctest_warning_contract.py | 5 ++ docs/rust-boundary-verification.md | 48 +++++++------ scripts/check_boundary_contract.py | 71 +++++++++++-------- .../tests/test_boundary_target_discovery.py | 50 +++++++++++++ 5 files changed, 123 insertions(+), 52 deletions(-) diff --git a/Makefile b/Makefile index 62cafc2ac..af4e76410 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,7 @@ PYTEST_TARGETS ?= cuprum/unittests/test_*.py \ tests/test_ci_*.py \ tests/test_native_sdist.py \ scripts/tests/test_boundary_*.py \ + scripts/tests/test_rust_lint_baseline_contract.py \ tests/behaviour/test_[a-h]*.py \ tests/behaviour/test_[i-r]*.py \ tests/behaviour/test_[s-z]*.py diff --git a/cuprum/unittests/test_doctest_warning_contract.py b/cuprum/unittests/test_doctest_warning_contract.py index cd609f634..b8f96ae41 100644 --- a/cuprum/unittests/test_doctest_warning_contract.py +++ b/cuprum/unittests/test_doctest_warning_contract.py @@ -46,6 +46,8 @@ def _run_doctests( return subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Cargo argv and pinned toolchain. [ cargo, + "--color", + "never", "test", "--doc", "--manifest-path", @@ -95,6 +97,9 @@ def test_pinned_doctest_route_rejects_a_warning(tmp_path: Path) -> None: "the corrected Rustdoc route must reject a doctest warning:\n" f"{corrected_diagnostics}" ) + assert "\x1b" not in corrected_diagnostics, ( + "the synthetic Cargo route must disable colour for deterministic diagnostics" + ) assert "error: use of deprecated function" in corrected_diagnostics, ( "the corrected route must fail on the doctest warning rather than setup" ) diff --git a/docs/rust-boundary-verification.md b/docs/rust-boundary-verification.md index 63cc5b6d1..a54b1a996 100644 --- a/docs/rust-boundary-verification.md +++ b/docs/rust-boundary-verification.md @@ -170,38 +170,40 @@ do not prove PyO3 internals or the CPython ABI. Table 3. Verification evidence by boundary. -Round 35 integrated local checks are green: the Python suite recorded 1,568 -passes and one skip in `/tmp/issue379-round35-test.log`, the Rust suite -recorded 115/115 tests with no skips, and the extension-required suite recorded -80 passes and one skip. The nine unsafe-code compiler probes, Windows -cross-Clippy, and development build also passed. The cached Kani setup check -had already passed in Round 33, which remains retained in the preceding -reports. Hosted Windows/macOS runtime execution passed on 2026-09-15: the -`Rust boundary verification` run 35004943625 succeeded with its -`Native contracts (windows-2022)`, `Native contracts (macos-latest)`, and -`Native contracts (ubuntu-latest)` jobs, and the `CI` run 35004943782 succeeded -with its `Extension-gated tests (Windows Python/Rust boundary)` and +Historical Round 35 integrated local checks are green: the Python suite +recorded 1,568 passes and one skip in `/tmp/issue379-round35-test.log`, the +Rust suite recorded 115/115 tests with no skips, and the extension-required +suite recorded 80 passes and one skip. That historical run included nine +unsafe-code compiler probes, Windows cross-Clippy, and the development build. +The cached Kani setup check had already passed in Round 33, which remains +retained in the preceding reports. Hosted Windows/macOS runtime execution +passed on 2026-09-15: the `Rust boundary verification` run 35004943625 +succeeded with its `Native contracts (windows-2022)`, +`Native contracts (macos-latest)`, and `Native contracts (ubuntu-latest)` jobs, +and the `CI` run 35004943782 succeeded with its +`Extension-gated tests (Windows Python/Rust boundary)` and `Extension-gated tests (Python/Rust boundary)` jobs. The boundary workflow's `extended` Kani/Miri job is skipped on pull requests by design, so these hosted runs add no Kani/Miri coverage. The second CodeRabbit review remains pending; the first review passed with 0 findings. -The current boundary contract discovers all automatic and explicitly configured -Cargo target roots, requiring a source-level unsafe prohibition on each. Its -current measured run found the library and compile-contract target, compiled -six unsafe probes, and separately rejected unannotated future binary, -build-script, example, integration-test, and benchmark targets. +The current boundary contract is distinct from Round 35's historical nine-probe +run. It discovers all automatic and explicitly configured Cargo target roots, +requiring a source-level unsafe prohibition on each. Its current measured run +found the library and compile-contract target, compiled six unsafe probes, and +separately rejected unannotated future binary, build-script, example, +integration-test, and benchmark targets. [ADR-011's 2026-09-20 addendum](adr-011-audited-rust-boundaries.md#addendum-2026-09-20-lint-baseline-and-target-discovery) records the implementation evolution, including Cargo's automatic-target switches and the I/O boundary around discovery. -| Boundary and production surface | Property | Selected tool and executed subject | Assumptions and bounds | Native or integration evidence | Status and limitation | -| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `cuprum-native-io` borrowed and owned Unix/Windows handles | Borrowed readers remain caller-owned; transferred writers close once on normal, error, and real unwind; no released/reused descriptor is touched. | Kani exercises the production `with_owned_writer` drop counter where Verus cannot represent OS handles or destructors. The direct Verus resource assessment is recorded below. | Valid-live-resource, unique-transfer, and no-concurrent-close obligations remain caller contracts. Kani's symbolic exit is bounded and has coverage witnesses. The writer callback receives an immutable reference, so it cannot replace or move the owned resource out of the scope. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Round 27 Kani passed 7 native harnesses; Miri passed 13 tests with 0 ignored; the fault harness detected all 4 mutations. | -| `cuprum-native-io` read/write kernels | Buffers expose only initialized in-bounds bytes; short reads/writes, `EINTR`, zero progress, and checked byte accounting preserve valid state. | Verus verifies the exact production `progress.rs` bodies rendered by `scripts/render_boundary_proofs.py`; Kani checks the production progress kernels and ownership seam. | `checked_progress` rejects overflow and oversized lengths; `record_write_progress` commits tail/count only after both checks. Aggregate pump/consume tracing counts retain saturating-add semantics, so they are not exact beyond `u64::MAX`. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Verus passed 2 functions with 0 errors; Kani/Miri and native tests remain scoped as recorded above. Neither verifier proves syscall effects. | -| Linux `splice` adapter and fallback policy | Unsupported descriptors fall back; partial/interrupted transfers and broken-pipe drain preserve bytes and termination. | Kani/Verus cover the typed decision, progress, and accumulation seams; the kernel call itself is external. Miri covers applicable native crate paths but excludes unshimmed `libc::splice`. | Syscall return/error guarantees, symbolic chunk and trace bounds, and non-vacuity witnesses are explicit assumptions or bounds. | Round 35 local Rust and native checks passed; hosted Windows/macOS runtime execution was green in CI. | The syscall is not directly verified or Miri-interpreted because `libc::splice` has no supported shim. Round 27 Miri passed 13 tests with 0 ignored. | -| `cuprum-streams` safe policy and decoder | Every automatic or explicit Cargo target root rejects unsafe Rust; state machine never writes after closure, stops on EOF, and checked UTF-8 replacement preserves valid prefixes. | Compiler contract discovers safe roots, compiles deliberate probes against current roots, and rejects a future unannotated root. Kani covers pure pump policy; checked UTF-8 no longer contains `from_utf8_unchecked`. | No raw resource integers or pointers enter the safe crate; lifetime-bound typed native handles do enter through `AsStream`. Aggregate tracing counts retain saturating-add semantics; per-chunk checked progress is verified separately. | Round 35 Rust checks covered 115/115 tests with no skips. The current contract run found two roots, compiled six probes, and rejected all dynamic target cases. | Safe-policy/decoder Kani passed 11 harnesses. | -| `cuprum-rust` PyO3 integration | Generated wrappers expose stable signatures, map errors, release the GIL safely, and preserve the native resource contract. | PyO3 compile UI tests and Python integration tests. Verus/Miri exclude interpreter-generated ABI code; typed Rust seams are verified separately. | PyO3/CPython ABI, `Send`, interpreter lifecycle, and Python transport hand-off are trusted assumptions. | Round 35 Python checks recorded 1,568 passes and one skip; the extension-required suite recorded 80 passes and one skip. | Local integration checks are green; Python lifetime establishment remains outside this evidence. Hosted Windows/macOS runs pass. The first CodeRabbit review passed with 0 findings; the second review remains pending. | +| Boundary and production surface | Property | Selected tool and executed subject | Assumptions and bounds | Native or integration evidence | Status and limitation | +| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cuprum-native-io` borrowed and owned Unix/Windows handles | Borrowed readers remain caller-owned; transferred writers close once on normal, error, and real unwind; no released/reused descriptor is touched. | Kani exercises the production `with_owned_writer` drop counter where Verus cannot represent OS handles or destructors. The direct Verus resource assessment is recorded below. | Valid-live-resource, unique-transfer, and no-concurrent-close obligations remain caller contracts. Kani's symbolic exit is bounded and has coverage witnesses. The writer callback receives an immutable reference, so it cannot replace or move the owned resource out of the scope. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Round 27 Kani passed 7 native harnesses; Miri passed 13 tests with 0 ignored; the fault harness detected all 4 mutations. | +| `cuprum-native-io` read/write kernels | Buffers expose only initialized in-bounds bytes; short reads/writes, `EINTR`, zero progress, and checked byte accounting preserve valid state. | Verus verifies the exact production `progress.rs` bodies rendered by `scripts/render_boundary_proofs.py`; Kani checks the production progress kernels and ownership seam. | `checked_progress` rejects overflow and oversized lengths; `record_write_progress` commits tail/count only after both checks. Aggregate pump/consume tracing counts retain saturating-add semantics, so they are not exact beyond `u64::MAX`. | Round 35 Rust checks covered 115/115 tests with no skips. Hosted Windows/macOS runtime execution was green in CI. | Verus passed 2 functions with 0 errors; Kani/Miri and native tests remain scoped as recorded above. Neither verifier proves syscall effects. | +| Linux `splice` adapter and fallback policy | Unsupported descriptors fall back; partial/interrupted transfers and broken-pipe drain preserve bytes and termination. | Kani/Verus cover the typed decision, progress, and accumulation seams; the kernel call itself is external. Miri covers applicable native crate paths but excludes unshimmed `libc::splice`. | Syscall return/error guarantees, symbolic chunk and trace bounds, and non-vacuity witnesses are explicit assumptions or bounds. | Round 35 local Rust and native checks passed; hosted Windows/macOS runtime execution was green in CI. | The syscall is not directly verified or Miri-interpreted because `libc::splice` has no supported shim. Round 27 Miri passed 13 tests with 0 ignored. | +| `cuprum-streams` safe policy and decoder | Every automatic or explicit Cargo target root rejects unsafe Rust; state machine never writes after closure, stops on EOF, and checked UTF-8 replacement preserves valid prefixes. | Compiler contract discovers safe roots, compiles deliberate probes against current roots, and rejects a future unannotated root. Kani covers pure pump policy; checked UTF-8 no longer contains `from_utf8_unchecked`. | No raw resource integers or pointers enter the safe crate; lifetime-bound typed native handles do enter through `AsStream`. Aggregate tracing counts retain saturating-add semantics; per-chunk checked progress is verified separately. | Historical Round 35 Rust checks covered 115/115 tests with no skips. The later current contract run found two roots, compiled six probes, and rejected all dynamic target cases. | Safe-policy/decoder Kani passed 11 harnesses. | +| `cuprum-rust` PyO3 integration | Generated wrappers expose stable signatures, map errors, release the GIL safely, and preserve the native resource contract. | PyO3 compile UI tests and Python integration tests. Verus/Miri exclude interpreter-generated ABI code; typed Rust seams are verified separately. | PyO3/CPython ABI, `Send`, interpreter lifecycle, and Python transport hand-off are trusted assumptions. | Round 35 Python checks recorded 1,568 passes and one skip; the extension-required suite recorded 80 passes and one skip. | Local integration checks are green; Python lifetime establishment remains outside this evidence. Hosted Windows/macOS runs pass. The first CodeRabbit review passed with 0 findings; the second review remains pending. | Round 22 also found a packaging boundary omission. A real `uv build --sdist` archive initially omitted the Rust workspace, and diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index bb06379d3..93b954b1b 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -45,17 +45,16 @@ ("autobenches", "benches"), ) EXPLICIT_TARGET_TYPES = ("lib", "bin", "example", "test", "bench") +INFERRED_TARGET_DIRECTORIES = { + "bin": "src/bin", + "example": "examples", + "test": "tests", + "bench": "benches", +} def check_members(manifest: str) -> None: - """Require an explicit review before the approved crate set can grow. - - Parameters - ---------- - manifest : str - Workspace manifest text. - - """ + """Require review before the approved crate set can grow.""" _check_members_manifest(tomllib.loads(manifest)) @@ -242,20 +241,46 @@ def _explicit_target_roots( package: cabc.Mapping[str, object], ) -> cabc.Iterator[Path]: """Yield crate roots declared through Cargo target or build-script fields.""" - target_definitions = itertools.chain.from_iterable( - _target_definitions(manifest.get(target_type)) + target_roots = itertools.chain.from_iterable( + ( + (crate / path,) + if isinstance((path := target.get("path")), str) + else _inferred_target_roots(crate, target_type, target, package) + ) for target_type in EXPLICIT_TARGET_TYPES - ) - target_paths = ( - crate / path - for target in target_definitions - if isinstance((path := target.get("path")), str) + for target in _target_definitions(manifest.get(target_type)) ) return itertools.chain( - target_paths, _configured_build_script_target_root(crate, package) + target_roots, _configured_build_script_target_root(crate, package) ) +def _inferred_target_roots( + crate: Path, + target_type: str, + target: cabc.Mapping[str, object], + package: cabc.Mapping[str, object], +) -> tuple[Path, ...]: + """Return existing conventional paths Cargo may infer for a target table.""" + match target_type: + case "lib": + candidates = (crate / "src/lib.rs",) + case "bin" | "example" | "test" | "bench": + name = target.get("name") + if not isinstance(name, str): + return () + directory = INFERRED_TARGET_DIRECTORIES[target_type] + candidates = ( + crate / directory / f"{name}.rs", + crate / directory / name / "main.rs", + ) + if target_type == "bin" and name == package.get("name"): + candidates = (crate / "src/main.rs", *candidates) + case _: + return () + return tuple(path for path in candidates if _is_regular_file(path)) + + def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: """Normalize a Cargo target table into a sequence of target definitions.""" match value: @@ -289,19 +314,7 @@ def _configured_build_script_target_root( def check_safe_policy(workspace: Path) -> tuple[Path, ...]: - """Require every safe-crate target root to forbid unsafe source code. - - The shared baseline deliberately omits ``unsafe_code = forbid`` because - the other audited members implement syscall and FFI boundaries. Every - automatic or explicit safe-crate target must therefore state the - non-negotiable source-level prohibition itself. - - Returns - ------- - tuple[Path, ...] - The checked source roots, for the compiler-probe phase. - - """ + """Require every effective safe target root to forbid unsafe source code.""" targets = safe_target_roots(workspace) sources = {target: _read_text(target) for target in targets} return evaluate_safe_target_policy(sources) diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index 3cd0ba404..2e5a0bb4c 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -35,6 +35,18 @@ "tools/explicit.rs", "tools/build.rs", ) +PATHLESS_EXPLICIT_TARGETS = ( + ("lib", None, "src/lib.rs"), + ("bin", "cuprum-streams", "src/main.rs"), + ("bin", "flat", "src/bin/flat.rs"), + ("bin", "nested", "src/bin/nested/main.rs"), + ("example", "flat", "examples/flat.rs"), + ("example", "nested", "examples/nested/main.rs"), + ("test", "flat", "tests/flat.rs"), + ("test", "nested", "tests/nested/main.rs"), + ("bench", "flat", "benches/flat.rs"), + ("bench", "nested", "benches/nested/main.rs"), +) SAFE_SOURCE = "//! Safe target.\n#![forbid(unsafe_code)]\n" @@ -56,6 +68,16 @@ def _set_package_field(manifest: Path, field: str) -> None: ) +def _append_pathless_target(manifest: Path, target_type: str, name: str | None) -> None: + """Declare a target table whose source path Cargo must infer.""" + table = "[lib]" if target_type == "lib" else f"[[{target_type}]]" + name_entry = "" if name is None else f'\nname = "{name}"' + manifest.write_text( + manifest.read_text(encoding="utf-8") + f"\n{table}{name_entry}\n", + encoding="utf-8", + ) + + @pytest.mark.parametrize(("switch", "target"), AUTO_DISABLED_TARGETS) def test_disabled_automatic_targets_are_not_checked( switch: str, target: str, tmp_path: Path @@ -105,6 +127,34 @@ def test_explicit_target_without_unsafe_prohibition_is_rejected(tmp_path: Path) check_safe_policy(root) +@pytest.mark.parametrize(("target_type", "name", "target"), PATHLESS_EXPLICIT_TARGETS) +def test_pathless_explicit_target_is_discovered_and_checked( + target_type: str, name: str | None, target: str, tmp_path: Path +) -> None: + """Cargo-inferred source paths must remain inside the safe-target contract.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + manifest = crate / "Cargo.toml" + _set_package_field( + manifest, + "\n".join(( + "autolib = false", + "autobins = false", + "autoexamples = false", + "autotests = false", + "autobenches = false", + )), + ) + _append_pathless_target(manifest, target_type, name) + target_path = _write_target(crate, target, "//! Missing safety attribute.\n") + + assert target_path in safe_target_roots(root), ( + "the pathless Cargo target table must discover its inferred source" + ) + with pytest.raises(ValueError, match="safe target must forbid unsafe code"): + check_safe_policy(root) + + def test_malformed_manifest_fails_closed_with_its_path(tmp_path: Path) -> None: """Target discovery must report malformed TOML instead of losing coverage.""" root = copy_boundary_repository(tmp_path) / "rust" From f8071cc3c8833b663465a3c8a5136b5d38ed3307 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 19:32:21 +0200 Subject: [PATCH 09/21] Exempt Cargo's color flag from spelling Record the fixed external command spelling with a context-specific pattern, regenerate the tracked Typos configuration, and prove ordinary prose remains subject to the Oxford spelling rule. --- .../tests/test_rust_lint_baseline_contract.py | 28 +++++++++++++++++++ typos.local.toml | 3 ++ typos.toml | 1 + 3 files changed, 32 insertions(+) diff --git a/scripts/tests/test_rust_lint_baseline_contract.py b/scripts/tests/test_rust_lint_baseline_contract.py index c655dca28..ece63e37f 100644 --- a/scripts/tests/test_rust_lint_baseline_contract.py +++ b/scripts/tests/test_rust_lint_baseline_contract.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import shutil import subprocess # ruff: ignore[suspicious-subprocess-import] - this test expands a fixed local Make recipe. import tomllib @@ -97,6 +98,12 @@ def _clippy_config() -> dict[str, object]: return tomllib.load(config) +def _typos_local_config() -> dict[str, object]: + """Load local spelling exemptions that supplement the shared dictionary.""" + with (ROOT / "typos.local.toml").open("rb") as config: + return tomllib.load(config) + + def _dry_run_test_rust() -> str: """Expand the Linux doctest recipe without running Cargo or prerequisites.""" make = shutil.which("make") @@ -176,6 +183,27 @@ def test_clippy_configuration_keeps_the_approved_thresholds_and_methods() -> Non ), "each disallowed environment method needs a local injection rationale" +def test_cargo_flag_exemption_is_limited_to_the_external_flag() -> None: + """The Cargo CLI spelling must not exempt ordinary American-spelled prose.""" + patterns = _typos_local_config()["patterns"] + assert isinstance(patterns, dict), "local spelling patterns must be a mapping" + ignored = patterns["ignore"] + assert isinstance(ignored, list), "local spelling ignore patterns must be a list" + pattern = next( + entry + for entry in ignored + if isinstance(entry, str) and entry.startswith("--col") + ) + external_flag = '"--col' + 'or",' + assert re.search(pattern, external_flag), ( + "the external Cargo flag must match its documented exemption" + ) + ordinary_prose = "".join(("col", "or in ordinary prose")) + assert re.search(pattern, ordinary_prose) is None, ( + "ordinary prose must retain the Oxford-spelling check" + ) + + def test_doctest_recipe_passes_full_rustdoc_warning_flags_and_jobs() -> None: """Require the separate gate to use pinned nightly rustdoc warnings.""" output = _dry_run_test_rust() diff --git a/typos.local.toml b/typos.local.toml index 6c5d7a6e5..1866e17de 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -40,6 +40,9 @@ ignore = [ # backticks should still be spell-checked. '\bbuild_native_wheel_artifact\b', '--artifact-server-path(?:[=\s"`]|$)', + # Cargo fixes this command-line flag's US spelling; ordinary prose remains + # checked. + '--color(?:[=,\s"]|$)', '`color`', '\brust-analyzer\b', # `mold` is the fixed external linker name. Typos tokenizes words before diff --git a/typos.toml b/typos.toml index a35018209..d5a6c6421 100644 --- a/typos.toml +++ b/typos.toml @@ -37,6 +37,7 @@ extend-ignore-re = [ "\"mold\\\\t--version\"", "--artifact-name(?:[=\\s\"`]|$)", "--artifact-server-path(?:[=\\s\"`]|$)", + "--color(?:[=,\\s\"]|$)", "-fuse-ld=mold", "/artifacts(?:[/?#\"`]|$)", "Azure\\s+Architecture\\s+Center\\s*\\|\\s*Microsoft\\s+Learn\\b", From 9098fb8fabe00a5a38cbfc487d814a330c80b7e1 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 20:24:23 +0200 Subject: [PATCH 10/21] Match Cargo target overrides in boundary checks Exclude conventional roots replaced by explicit target paths, and retain separate unsafe-probe evidence for targets sharing a basename. Metadata-backed contract tests cover each target type and preserve the safe-boundary policy. --- scripts/check_boundary_contract.py | 133 +++++++++--------- scripts/tests/test_boundary_contract.py | 45 +++++- .../tests/test_boundary_target_discovery.py | 78 ++++++++++ 3 files changed, 186 insertions(+), 70 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 93b954b1b..cd7aa2321 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -12,6 +12,7 @@ from __future__ import annotations +import base64 import contextlib import itertools import shutil @@ -34,10 +35,7 @@ "pub unsafe fn unsafe_probe() {}", "unsafe trait Probe {} unsafe impl Probe for () {}", ) -AUTOMATIC_TARGET_ROOTS = ( - ("autolib", "src/lib.rs"), - ("autobins", "src/main.rs"), -) +AUTOMATIC_TARGET_ROOTS = (("autolib", "src/lib.rs"), ("autobins", "src/main.rs")) AUTOMATIC_TARGET_DIRECTORIES = ( ("autobins", "src/bin"), ("autoexamples", "examples"), @@ -45,12 +43,6 @@ ("autobenches", "benches"), ) EXPLICIT_TARGET_TYPES = ("lib", "bin", "example", "test", "bench") -INFERRED_TARGET_DIRECTORIES = { - "bin": "src/bin", - "example": "examples", - "test": "tests", - "bench": "benches", -} def check_members(manifest: str) -> None: @@ -59,7 +51,6 @@ def check_members(manifest: str) -> None: def _check_members_manifest(manifest: cabc.Mapping[str, object]) -> None: - """Evaluate the loaded workspace membership without filesystem access.""" workspace = manifest.get("workspace") members = workspace.get("members") if isinstance(workspace, dict) else None if not isinstance(members, list) or not all( @@ -96,34 +87,54 @@ def check_member_lint_inheritance(workspace: Path) -> None: def safe_target_roots(workspace: Path) -> tuple[Path, ...]: - """Return every automatic and explicitly configured safe-crate target root. + """Return effective automatic and explicit Cargo target roots. - Cargo auto-discovers crate roots only at these locations. Explicit target - tables may select another path, which remains part of the safe boundary. - Keeping this scan local and deterministic lets the contract reject a new - target before a compiler invocation can hide it behind dependency output. + Explicit tables replace matching conventional roots, as Cargo metadata does. Returns ------- tuple[Path, ...] - Existing automatic and explicitly configured crate-root source paths. + Effective crate-root source paths. """ - crate, manifest = _load_safe_manifest(workspace) - package = _package_table(manifest) + crate = workspace / "cuprum-streams" + manifest = _load_toml(crate / "Cargo.toml") + package = manifest.get("package") + package = package if isinstance(package, dict) else {} automatic = _load_automatic_target_roots(crate, package) explicit = _explicit_target_roots(crate, manifest, package) - return discover_safe_target_roots(automatic, explicit) + overrides = _automatic_roots_overridden_by_explicit_targets( + crate, manifest, package, automatic + ) + active_automatic = (path for path in automatic if path not in overrides) + return discover_safe_target_roots(active_automatic, explicit) def discover_safe_target_roots( automatic: cabc.Iterable[Path], explicit: cabc.Iterable[Path] ) -> tuple[Path, ...]: - """Deduplicate loaded automatic and declared explicit Cargo target roots.""" + """Deduplicate automatic and explicit crate roots. + + Returns + ------- + tuple[Path, ...] + Sorted effective crate-root source paths. + """ return tuple(sorted(set(itertools.chain(automatic, explicit)))) def evaluate_safe_target_policy(sources: cabc.Mapping[Path, str]) -> tuple[Path, ...]: - """Require every loaded safe target source to forbid unsafe code.""" + """Require every loaded source to forbid unsafe code. + + Returns + ------- + tuple[Path, ...] + Validated crate-root source paths. + + Raises + ------ + ValueError + If no target root exists or a root lacks the required prohibition. + """ if not sources: msg = "safe crate has no discoverable Cargo target roots" raise ValueError(msg) @@ -134,14 +145,7 @@ def evaluate_safe_target_policy(sources: cabc.Mapping[Path, str]) -> tuple[Path, return tuple(sources) -def _load_safe_manifest(workspace: Path) -> tuple[Path, cabc.Mapping[str, object]]: - """Load the safe crate manifest before evaluating its target policy.""" - crate = workspace / "cuprum-streams" - return crate, _load_toml(crate / "Cargo.toml") - - def _load_toml(path: Path) -> cabc.Mapping[str, object]: - """Read and parse one manifest while retaining the affected path on failure.""" text = _read_text(path) try: manifest = tomllib.loads(text) @@ -152,7 +156,6 @@ def _load_toml(path: Path) -> cabc.Mapping[str, object]: def _read_text(path: Path) -> str: - """Read UTF-8 text, failing closed when the source cannot be inspected.""" try: return path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError) as error: @@ -163,36 +166,27 @@ def _read_text(path: Path) -> str: def _load_automatic_target_roots( crate: Path, package: cabc.Mapping[str, object] ) -> tuple[Path, ...]: - """Load the existing Cargo auto-target roots selected by package switches.""" direct = ( crate / path for switch, path in AUTOMATIC_TARGET_ROOTS - if _automatic_target_is_enabled(package, switch) + if package.get(switch) is not False ) directories = ( crate / directory for switch, directory in AUTOMATIC_TARGET_DIRECTORIES - if _automatic_target_is_enabled(package, switch) + if package.get(switch) is not False ) candidates = itertools.chain( direct, itertools.chain.from_iterable( _load_directory_target_roots(path) for path in directories ), - _load_default_build_script_root(crate, package), + (crate / "build.rs",) if package.get("build", True) is True else (), ) return tuple(path for path in candidates if _is_regular_file(path)) -def _automatic_target_is_enabled( - package: cabc.Mapping[str, object], switch: str -) -> bool: - """Return whether Cargo's default-enabled automatic target switch is active.""" - return package.get(switch) is not False - - def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: - """Load Cargo's flat and one-level-nested conventional target roots.""" for entry in _directory_entries(directory): if entry.suffix == ".rs": yield entry @@ -201,12 +195,9 @@ def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: def _directory_entries(directory: Path) -> tuple[Path, ...]: - """List one optional automatic-target directory without masking I/O errors.""" try: return tuple(directory.iterdir()) - except FileNotFoundError: - return () - except NotADirectoryError: + except (FileNotFoundError, NotADirectoryError): return () except OSError as error: msg = f"cannot inspect automatic target directory {directory}: {error}" @@ -214,7 +205,6 @@ def _directory_entries(directory: Path) -> tuple[Path, ...]: def _is_regular_file(path: Path) -> bool: - """Check a candidate root while treating inspection failures as contract errors.""" try: return stat.S_ISREG(path.stat().st_mode) except FileNotFoundError: @@ -225,7 +215,6 @@ def _is_regular_file(path: Path) -> bool: def _is_directory(path: Path) -> bool: - """Check a nested target directory without masking an access failure.""" try: return stat.S_ISDIR(path.stat().st_mode) except FileNotFoundError: @@ -235,12 +224,25 @@ def _is_directory(path: Path) -> bool: raise ValueError(msg) from error +def _automatic_roots_overridden_by_explicit_targets( + crate: Path, + manifest: cabc.Mapping[str, object], + package: cabc.Mapping[str, object], + automatic: cabc.Iterable[Path], +) -> frozenset[Path]: + candidates = itertools.chain.from_iterable( + _inferred_target_roots(crate, target_type, target, package) + for target_type in EXPLICIT_TARGET_TYPES + for target in _target_definitions(manifest.get(target_type)) + ) + return frozenset(automatic).intersection(candidates) + + def _explicit_target_roots( crate: Path, manifest: cabc.Mapping[str, object], package: cabc.Mapping[str, object], ) -> cabc.Iterator[Path]: - """Yield crate roots declared through Cargo target or build-script fields.""" target_roots = itertools.chain.from_iterable( ( (crate / path,) @@ -261,7 +263,6 @@ def _inferred_target_roots( target: cabc.Mapping[str, object], package: cabc.Mapping[str, object], ) -> tuple[Path, ...]: - """Return existing conventional paths Cargo may infer for a target table.""" match target_type: case "lib": candidates = (crate / "src/lib.rs",) @@ -269,7 +270,12 @@ def _inferred_target_roots( name = target.get("name") if not isinstance(name, str): return () - directory = INFERRED_TARGET_DIRECTORIES[target_type] + directory = { + "bin": "src/bin", + "example": "examples", + "test": "tests", + "bench": "benches", + }[target_type] candidates = ( crate / directory / f"{name}.rs", crate / directory / name / "main.rs", @@ -282,7 +288,6 @@ def _inferred_target_roots( def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: - """Normalize a Cargo target table into a sequence of target definitions.""" match value: case dict() as target: return (target,) @@ -292,23 +297,9 @@ def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: return () -def _package_table(manifest: cabc.Mapping[str, object]) -> cabc.Mapping[str, object]: - """Return the package table or an empty table for malformed optional input.""" - package = manifest.get("package") - return package if isinstance(package, dict) else {} - - -def _load_default_build_script_root( - crate: Path, package: cabc.Mapping[str, object] -) -> tuple[Path, ...]: - """Return Cargo's default build script unless the manifest disables it.""" - return (crate / "build.rs",) if package.get("build", True) is True else () - - def _configured_build_script_target_root( crate: Path, package: cabc.Mapping[str, object] ) -> tuple[Path, ...]: - """Return a non-default package build-script target, if one is declared.""" build_script = package.get("build") return (crate / build_script,) if isinstance(build_script, str) else () @@ -367,7 +358,6 @@ def main() -> None: def _unsafe_was_forbidden(code: int, output: str) -> bool: - """Require compilation failure with an explicit unsafe-forbid diagnostic.""" markers = ("forbid(unsafe_code)", "-F unsafe-code") return code != 0 and any(marker in output for marker in markers) @@ -383,12 +373,17 @@ def _probe_appended(path: Path, probe: str) -> cabc.Iterator[None]: path.write_text(original, encoding="utf-8") +def _probe_log_name(workspace: Path, path: Path, index: int) -> str: + relative = path.relative_to(workspace).as_posix() + encoded = base64.urlsafe_b64encode(relative.encode()).decode().rstrip("=") + return f"{path.stem}-{encoded}-unsafe-{index}.log" + + def _check_target(workspace: Path, path: Path, logs: Path) -> None: - """Probe one actual target and restore it even if compiler checking fails.""" for index, probe in enumerate(PROBES): with _probe_appended(path, probe): code, output = _compile(workspace) - name = f"{path.stem}-unsafe-{index}.log" + name = _probe_log_name(workspace, path, index) (logs / name).write_text(output, encoding="utf-8") if not _unsafe_was_forbidden(code, output): msg = f"safe target did not reject unsafe probe: {name}" diff --git a/scripts/tests/test_boundary_contract.py b/scripts/tests/test_boundary_contract.py index 735ab943f..3428ae6cc 100644 --- a/scripts/tests/test_boundary_contract.py +++ b/scripts/tests/test_boundary_contract.py @@ -7,6 +7,7 @@ from __future__ import annotations +import base64 from pathlib import Path import pytest @@ -200,12 +201,15 @@ def test_main_probes_every_target_with_every_unsafe_form( assert sum(probe in text for text in compiler.probes) == len(TARGETS), ( f"the {probe!r} probe must be compiled against every target" ) + workspace = root / ".cache/boundary-contract/workspace" logs = root / "rust/target/boundary-verification" assert (logs / "safe-positive.log").read_text(encoding="utf-8") == ( "finished checking" ), "the baseline result must be archived" expected = [ - f"{Path(target).stem}-unsafe-{index}.log" + contract._probe_log_name( + workspace, workspace / "cuprum-streams" / target, index + ) for target in TARGETS for index in range(3) ] @@ -215,6 +219,45 @@ def test_main_probes_every_target_with_every_unsafe_form( ]), "every probe result must be archived under its target and form" +def test_same_basename_target_logs_are_unique_and_traceable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Separate Cargo targets retain each unsafe-form result without overwriting.""" + workspace = tmp_path / "workspace" + logs = tmp_path / "logs" + logs.mkdir() + targets = ( + workspace / "cuprum-streams/examples/client.rs", + workspace / "cuprum-streams/tests/client.rs", + ) + for target in targets: + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("#![forbid(unsafe_code)]" + chr(10), encoding="utf-8") + monkeypatch.setattr( + contract, + "_compile", + lambda _workspace: (1, "error: forbid(unsafe_code)"), + ) + + for target in targets: + contract._check_target(workspace, target, logs) + + expected = { + contract._probe_log_name(workspace, target, index) + for target in targets + for index in range(len(contract.PROBES)) + } + assert {path.name for path in logs.glob("*.log")} == expected + assert len(expected) == len(targets) * len(contract.PROBES) + for target in targets: + name = contract._probe_log_name(workspace, target, 0) + encoded = name.removeprefix("client-").removesuffix("-unsafe-0.log") + padding = "=" * (-len(encoded) % 4) + assert base64.urlsafe_b64decode(encoded + padding).decode() == ( + target.relative_to(workspace).as_posix() + ) + + def test_every_probed_target_is_restored_afterwards( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index 2e5a0bb4c..38d1d2afb 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -2,6 +2,9 @@ from __future__ import annotations +import json +import shutil +import subprocess # ruff: ignore[suspicious-subprocess-import] - fixed Cargo metadata argv. from pathlib import Path import pytest @@ -35,6 +38,14 @@ "tools/explicit.rs", "tools/build.rs", ) +OVERRIDING_EXPLICIT_TARGETS = ( + ("lib", None, "src/lib.rs", "tools/custom_lib.rs"), + ("bin", "cuprum-streams", "src/main.rs", "tools/custom_main.rs"), + ("bin", "client", "src/bin/client.rs", "tools/custom_client.rs"), + ("example", "client", "examples/client.rs", "tools/custom_example.rs"), + ("test", "client", "tests/client.rs", "tools/custom_test.rs"), + ("bench", "client", "benches/client.rs", "tools/custom_bench.rs"), +) PATHLESS_EXPLICIT_TARGETS = ( ("lib", None, "src/lib.rs"), ("bin", "cuprum-streams", "src/main.rs"), @@ -68,6 +79,45 @@ def _set_package_field(manifest: Path, field: str) -> None: ) +def _append_explicit_target( + manifest: Path, target_type: str, name: str | None, source: str +) -> None: + """Declare one named Cargo target table with a custom source path.""" + table = "[lib]" if target_type == "lib" else f"[[{target_type}]]" + name_entry = "" if name is None else chr(10) + f'name = "{name}"' + manifest.write_text( + manifest.read_text(encoding="utf-8") + + chr(10) + + table + + name_entry + + chr(10) + + f'path = "{source}"' + + chr(10), + encoding="utf-8", + ) + + +def _metadata_target_roots(crate: Path) -> tuple[Path, ...]: + """Return Cargo's effective source roots for the safe package.""" + cargo = shutil.which("cargo") + assert cargo is not None, "Cargo is required for the metadata contract" + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] + [cargo, "metadata", "--no-deps", "--format-version=1"], + capture_output=True, + check=False, + cwd=crate, + text=True, + ) + assert completed.returncode == 0, completed.stderr + metadata = json.loads(completed.stdout) + package = next( + package + for package in metadata["packages"] + if package["name"] == "cuprum-streams" + ) + return tuple(sorted(Path(target["src_path"]) for target in package["targets"])) + + def _append_pathless_target(manifest: Path, target_type: str, name: str | None) -> None: """Declare a target table whose source path Cargo must infer.""" table = "[lib]" if target_type == "lib" else f"[[{target_type}]]" @@ -127,6 +177,34 @@ def test_explicit_target_without_unsafe_prohibition_is_rejected(tmp_path: Path) check_safe_policy(root) +@pytest.mark.parametrize( + ("target_type", "name", "dormant", "custom"), OVERRIDING_EXPLICIT_TARGETS +) +def test_explicit_target_replaces_its_dormant_automatic_root( + target_type: str, + name: str | None, + dormant: str, + custom: str, + tmp_path: Path, +) -> None: + """Cargo metadata and the safe policy must agree on explicit overrides.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + manifest = crate / "Cargo.toml" + dormant_path = _write_target( + crate, dormant, "//! Dormant automatic root." + chr(10) + ) + custom_path = _write_target(crate, custom) + _append_explicit_target(manifest, target_type, name, custom) + + roots = safe_target_roots(root) + + assert custom_path in roots + assert dormant_path not in roots + assert roots == _metadata_target_roots(crate) + check_safe_policy(root) + + @pytest.mark.parametrize(("target_type", "name", "target"), PATHLESS_EXPLICIT_TARGETS) def test_pathless_explicit_target_is_discovered_and_checked( target_type: str, name: str | None, target: str, tmp_path: Path From 1a62a50760ed94bdf4efef05660349aa4457eb09 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 20:25:24 +0200 Subject: [PATCH 11/21] Group Cargo override test inputs Keep CodeScene's test-function argument budget while retaining every metadata-backed target override case. --- scripts/tests/test_boundary_target_discovery.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index 38d1d2afb..4776193e8 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -177,17 +177,12 @@ def test_explicit_target_without_unsafe_prohibition_is_rejected(tmp_path: Path) check_safe_policy(root) -@pytest.mark.parametrize( - ("target_type", "name", "dormant", "custom"), OVERRIDING_EXPLICIT_TARGETS -) +@pytest.mark.parametrize("case", OVERRIDING_EXPLICIT_TARGETS) def test_explicit_target_replaces_its_dormant_automatic_root( - target_type: str, - name: str | None, - dormant: str, - custom: str, - tmp_path: Path, + case: tuple[str, str | None, str, str], tmp_path: Path ) -> None: """Cargo metadata and the safe policy must agree on explicit overrides.""" + target_type, name, dormant, custom = case root = copy_boundary_repository(tmp_path) / "rust" crate = root / "cuprum-streams" manifest = crate / "Cargo.toml" From 73a9e93e990a15c7ae06f5bc06d6f91fd4a3c5c8 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 20:31:53 +0200 Subject: [PATCH 12/21] Document boundary contract helpers Restore complete documentation coverage for the boundary verifier while consolidating equivalent target-path inspection and build-script handling. --- scripts/check_boundary_contract.py | 55 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index cd7aa2321..ebfee19e3 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -51,6 +51,7 @@ def check_members(manifest: str) -> None: def _check_members_manifest(manifest: cabc.Mapping[str, object]) -> None: + """Validate audited workspace membership from parsed manifest data.""" workspace = manifest.get("workspace") members = workspace.get("members") if isinstance(workspace, dict) else None if not isinstance(members, list) or not all( @@ -146,6 +147,7 @@ def evaluate_safe_target_policy(sources: cabc.Mapping[Path, str]) -> tuple[Path, def _load_toml(path: Path) -> cabc.Mapping[str, object]: + """Read and parse a manifest while retaining its path on failure.""" text = _read_text(path) try: manifest = tomllib.loads(text) @@ -156,6 +158,7 @@ def _load_toml(path: Path) -> cabc.Mapping[str, object]: def _read_text(path: Path) -> str: + """Read UTF-8 text and fail closed when inspection is impossible.""" try: return path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError) as error: @@ -166,6 +169,7 @@ def _read_text(path: Path) -> str: def _load_automatic_target_roots( crate: Path, package: cabc.Mapping[str, object] ) -> tuple[Path, ...]: + """Load existing automatic Cargo target roots enabled by package switches.""" direct = ( crate / path for switch, path in AUTOMATIC_TARGET_ROOTS @@ -183,18 +187,22 @@ def _load_automatic_target_roots( ), (crate / "build.rs",) if package.get("build", True) is True else (), ) - return tuple(path for path in candidates if _is_regular_file(path)) + return tuple( + path for path in candidates if _path_matches(path, stat.S_ISREG, "root") + ) def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: + """Yield flat and nested conventional Cargo target roots.""" for entry in _directory_entries(directory): if entry.suffix == ".rs": yield entry - if _is_directory(entry): + elif _path_matches(entry, stat.S_ISDIR, "directory"): yield entry / "main.rs" def _directory_entries(directory: Path) -> tuple[Path, ...]: + """List an automatic-target directory without masking access failures.""" try: return tuple(directory.iterdir()) except (FileNotFoundError, NotADirectoryError): @@ -204,23 +212,16 @@ def _directory_entries(directory: Path) -> tuple[Path, ...]: raise ValueError(msg) from error -def _is_regular_file(path: Path) -> bool: - try: - return stat.S_ISREG(path.stat().st_mode) - except FileNotFoundError: - return False - except OSError as error: - msg = f"cannot inspect target root {path}: {error}" - raise ValueError(msg) from error - - -def _is_directory(path: Path) -> bool: +def _path_matches( + path: Path, predicate: cabc.Callable[[int], bool], target: str +) -> bool: + """Inspect one target path with a stat-mode predicate.""" try: - return stat.S_ISDIR(path.stat().st_mode) + return predicate(path.stat().st_mode) except FileNotFoundError: return False except OSError as error: - msg = f"cannot inspect target directory {path}: {error}" + msg = f"cannot inspect target {target} {path}: {error}" raise ValueError(msg) from error @@ -230,6 +231,7 @@ def _automatic_roots_overridden_by_explicit_targets( package: cabc.Mapping[str, object], automatic: cabc.Iterable[Path], ) -> frozenset[Path]: + """Identify automatic roots replaced by explicit Cargo target tables.""" candidates = itertools.chain.from_iterable( _inferred_target_roots(crate, target_type, target, package) for target_type in EXPLICIT_TARGET_TYPES @@ -243,6 +245,7 @@ def _explicit_target_roots( manifest: cabc.Mapping[str, object], package: cabc.Mapping[str, object], ) -> cabc.Iterator[Path]: + """Yield source roots from explicit Cargo tables and build configuration.""" target_roots = itertools.chain.from_iterable( ( (crate / path,) @@ -252,9 +255,9 @@ def _explicit_target_roots( for target_type in EXPLICIT_TARGET_TYPES for target in _target_definitions(manifest.get(target_type)) ) - return itertools.chain( - target_roots, _configured_build_script_target_root(crate, package) - ) + build_script = package.get("build") + configured_build = (crate / build_script,) if isinstance(build_script, str) else () + return itertools.chain(target_roots, configured_build) def _inferred_target_roots( @@ -263,6 +266,7 @@ def _inferred_target_roots( target: cabc.Mapping[str, object], package: cabc.Mapping[str, object], ) -> tuple[Path, ...]: + """Return existing conventional paths Cargo infers for one target table.""" match target_type: case "lib": candidates = (crate / "src/lib.rs",) @@ -284,10 +288,13 @@ def _inferred_target_roots( candidates = (crate / "src/main.rs", *candidates) case _: return () - return tuple(path for path in candidates if _is_regular_file(path)) + return tuple( + path for path in candidates if _path_matches(path, stat.S_ISREG, "root") + ) def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: + """Normalize a Cargo target section into target definitions.""" match value: case dict() as target: return (target,) @@ -297,13 +304,6 @@ def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: return () -def _configured_build_script_target_root( - crate: Path, package: cabc.Mapping[str, object] -) -> tuple[Path, ...]: - build_script = package.get("build") - return (crate / build_script,) if isinstance(build_script, str) else () - - def check_safe_policy(workspace: Path) -> tuple[Path, ...]: """Require every effective safe target root to forbid unsafe source code.""" targets = safe_target_roots(workspace) @@ -358,6 +358,7 @@ def main() -> None: def _unsafe_was_forbidden(code: int, output: str) -> bool: + """Return whether a compiler result names the required unsafe prohibition.""" markers = ("forbid(unsafe_code)", "-F unsafe-code") return code != 0 and any(marker in output for marker in markers) @@ -374,12 +375,14 @@ def _probe_appended(path: Path, probe: str) -> cabc.Iterator[None]: def _probe_log_name(workspace: Path, path: Path, index: int) -> str: + """Return a filesystem-safe, reversible workspace-relative probe log name.""" relative = path.relative_to(workspace).as_posix() encoded = base64.urlsafe_b64encode(relative.encode()).decode().rstrip("=") return f"{path.stem}-{encoded}-unsafe-{index}.log" def _check_target(workspace: Path, path: Path, logs: Path) -> None: + """Probe one target and archive each compiler result.""" for index, probe in enumerate(PROBES): with _probe_appended(path, probe): code, output = _compile(workspace) From 4773181f7e44f7ca51b314db4ed96c78fc7f39d7 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 20:36:54 +0200 Subject: [PATCH 13/21] Explain boundary contract test failures Add evidence-bearing assertion diagnostics and document why the fixed Cargo metadata command uses the scoped security-rule exception. --- scripts/tests/test_boundary_contract.py | 10 +++++++--- scripts/tests/test_boundary_target_discovery.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/scripts/tests/test_boundary_contract.py b/scripts/tests/test_boundary_contract.py index 3428ae6cc..b45a9735a 100644 --- a/scripts/tests/test_boundary_contract.py +++ b/scripts/tests/test_boundary_contract.py @@ -247,15 +247,19 @@ def test_same_basename_target_logs_are_unique_and_traceable( for target in targets for index in range(len(contract.PROBES)) } - assert {path.name for path in logs.glob("*.log")} == expected - assert len(expected) == len(targets) * len(contract.PROBES) + assert {path.name for path in logs.glob("*.log")} == expected, ( + "each same-basename target and unsafe form must retain its own log" + ) + assert len(expected) == len(targets) * len(contract.PROBES), ( + "the expected log names must be distinct for every target and probe" + ) for target in targets: name = contract._probe_log_name(workspace, target, 0) encoded = name.removeprefix("client-").removesuffix("-unsafe-0.log") padding = "=" * (-len(encoded) % 4) assert base64.urlsafe_b64decode(encoded + padding).decode() == ( target.relative_to(workspace).as_posix() - ) + ), "the encoded log name must identify its workspace-relative target" def test_every_probed_target_is_restored_afterwards( diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index 4776193e8..87aff64fc 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -101,7 +101,7 @@ def _metadata_target_roots(crate: Path) -> tuple[Path, ...]: """Return Cargo's effective source roots for the safe package.""" cargo = shutil.which("cargo") assert cargo is not None, "Cargo is required for the metadata contract" - completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Cargo metadata argv runs in the isolated fixture. [cargo, "metadata", "--no-deps", "--format-version=1"], capture_output=True, check=False, @@ -194,9 +194,13 @@ def test_explicit_target_replaces_its_dormant_automatic_root( roots = safe_target_roots(root) - assert custom_path in roots - assert dormant_path not in roots - assert roots == _metadata_target_roots(crate) + assert custom_path in roots, "the explicit target path must remain audited" + assert dormant_path not in roots, ( + "the explicit target must replace its dormant conventional root" + ) + assert roots == _metadata_target_roots(crate), ( + "the scanner and Cargo metadata must resolve identical target roots" + ) check_safe_policy(root) From 69c940f288a2ef9ba9fe2d9efd8ac0a318811a5e Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 20:55:50 +0200 Subject: [PATCH 14/21] Align boundary discovery with Cargo Ignore hidden automatic target entries and document the actual Rust test and formatter toolchain routes. --- docs/developers-guide.md | 19 ++++++++------- scripts/check_boundary_contract.py | 2 ++ .../tests/test_boundary_target_discovery.py | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 6872c17b8..7d1bd869f 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -3343,10 +3343,12 @@ Run the normal project test gate from the repository root: make test ``` -`make test` runs the Python pytest batches before the crate tests through -`cargo nextest`, including proptest cases compiled under `#[cfg(test)]`. It -then runs Cargo doctests separately because Nextest does not execute them. Run -the complete Rust lint and formatting gates before committing Rust changes: +`make test` runs the Python pytest batches before the crate tests. The +`test-rust` target uses `cargo nextest run` when `cargo-nextest` is available +and otherwise falls back to `cargo test`, including proptest cases compiled +under `#[cfg(test)]`. It then runs Cargo doctests separately because Nextest +does not execute them. Run the complete Rust lint and formatting gates before +committing Rust changes: ```bash make check-fmt @@ -3375,10 +3377,11 @@ keeps its explicit crate-level prohibition, which the boundary contract checks. The [ADR-011 addendum](adr-011-audited-rust-boundaries.md#addendum-2026-09-20-lint-baseline-and-target-discovery) records why that source-level contract complements the shared lint tables. The -Rust 1.85.0 maintenance toolchain provides `rustfmt`, `clippy`, and -`rust-analyzer`; Linux debug Rustdoc, Clippy, and doctests use the separately -pinned dev-fast configuration, while MSRV, coverage, release, verification, and -Whitaker retain their prescribed routes. +The Rust 1.85.0 maintenance toolchain provides `clippy` and `rust-analyzer`; +`RUSTFMT_TOOLCHAIN=nightly-2026-05-28` provides `rustfmt`. Linux debug Rustdoc, +Clippy, and doctests use the separately pinned dev-fast configuration, while +MSRV, coverage, release, verification, and Whitaker retain their prescribed +routes. ### Rustfmt fixture compatibility diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index ebfee19e3..fa10d33c0 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -195,6 +195,8 @@ def _load_automatic_target_roots( def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: """Yield flat and nested conventional Cargo target roots.""" for entry in _directory_entries(directory): + if entry.name.startswith("."): + continue if entry.suffix == ".rs": yield entry elif _path_matches(entry, stat.S_ISDIR, "directory"): diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index 87aff64fc..ff40ddca0 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -20,6 +20,10 @@ ) from scripts.tests.boundary_harness_support import copy_boundary_repository +HIDDEN_AUTOMATIC_TARGETS = ( + ("src/bin/.scratch.rs", "src/bin/visible.rs"), + ("src/bin/.scratch/main.rs", "src/bin/visible/main.rs"), +) AUTO_DISABLED_TARGETS = ( ("autolib", "src/lib.rs"), ("autobins", "src/main.rs"), @@ -128,6 +132,26 @@ def _append_pathless_target(manifest: Path, target_type: str, name: str | None) ) +@pytest.mark.parametrize(("hidden", "visible"), HIDDEN_AUTOMATIC_TARGETS) +def test_hidden_automatic_targets_are_ignored( + hidden: str, visible: str, tmp_path: Path +) -> None: + """Cargo ignores hidden automatic targets but retains their visible siblings.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + hidden_path = _write_target(crate, hidden, "//! Hidden automatic root." + chr(10)) + visible_path = _write_target(crate, visible) + + roots = safe_target_roots(root) + + assert hidden_path not in roots, "Cargo must ignore a hidden automatic target" + assert visible_path in roots, "Cargo must retain a visible automatic target" + assert roots == _metadata_target_roots(crate), ( + "the scanner and Cargo metadata must agree about hidden target entries" + ) + check_safe_policy(root) + + @pytest.mark.parametrize(("switch", "target"), AUTO_DISABLED_TARGETS) def test_disabled_automatic_targets_are_not_checked( switch: str, target: str, tmp_path: Path From 1887e81c5755ae3e24cf4a605ff7089d0193a48b Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 21:09:06 +0200 Subject: [PATCH 15/21] Require nested Cargo target roots Ignore visible helper directories without a regular main.rs while preserving fail-closed inspection and enforcement for real nested targets. --- scripts/check_boundary_contract.py | 12 ++++----- .../tests/test_boundary_target_discovery.py | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index fa10d33c0..d102d9909 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -200,7 +200,9 @@ def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: if entry.suffix == ".rs": yield entry elif _path_matches(entry, stat.S_ISDIR, "directory"): - yield entry / "main.rs" + candidate = entry / "main.rs" + if _path_matches(candidate, stat.S_ISREG, "root"): + yield candidate def _directory_entries(directory: Path) -> tuple[Path, ...]: @@ -214,16 +216,14 @@ def _directory_entries(directory: Path) -> tuple[Path, ...]: raise ValueError(msg) from error -def _path_matches( - path: Path, predicate: cabc.Callable[[int], bool], target: str -) -> bool: +def _path_matches(path: Path, check: cabc.Callable[[int], bool], kind: str) -> bool: """Inspect one target path with a stat-mode predicate.""" try: - return predicate(path.stat().st_mode) + return check(path.stat().st_mode) except FileNotFoundError: return False except OSError as error: - msg = f"cannot inspect target {target} {path}: {error}" + msg = f"cannot inspect target {kind} {path}: {error}" raise ValueError(msg) from error diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index ff40ddca0..ddf9108b1 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -152,6 +152,32 @@ def test_hidden_automatic_targets_are_ignored( check_safe_policy(root) +def test_visible_support_directory_without_main_is_not_a_target(tmp_path: Path) -> None: + """Only a visible nested main.rs is an automatic Cargo target.""" + root = copy_boundary_repository(tmp_path) / "rust" + crate = root / "cuprum-streams" + support = crate / "tests/support" + support.mkdir(parents=True) + + roots = safe_target_roots(root) + + assert support / "main.rs" not in roots, "support without main.rs is not a target" + assert roots == _metadata_target_roots(crate), ( + "the scanner and Cargo metadata must ignore the support directory" + ) + check_safe_policy(root) + + nested = _write_target( + crate, "tests/behaviour/main.rs", "//! Missing safety." + chr(10) + ) + + assert nested in safe_target_roots(root), ( + "a visible nested main.rs must remain an automatic target" + ) + with pytest.raises(ValueError, match="safe target must forbid unsafe code"): + check_safe_policy(root) + + @pytest.mark.parametrize(("switch", "target"), AUTO_DISABLED_TARGETS) def test_disabled_automatic_targets_are_not_checked( switch: str, target: str, tmp_path: Path From 86d378346202a8257079b038f35bc179953fcf19 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 21:10:40 +0200 Subject: [PATCH 16/21] Simplify nested target discovery Keep regular main.rs checks fail-closed without nested conditionals in the automatic Cargo target scan. --- scripts/check_boundary_contract.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index d102d9909..51f4fa1eb 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -199,10 +199,10 @@ def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: continue if entry.suffix == ".rs": yield entry - elif _path_matches(entry, stat.S_ISDIR, "directory"): - candidate = entry / "main.rs" - if _path_matches(candidate, stat.S_ISREG, "root"): - yield candidate + continue + candidate = entry / "main.rs" + if _path_matches(candidate, stat.S_ISREG, "root"): + yield candidate def _directory_entries(directory: Path) -> tuple[Path, ...]: From e6bc20d90d0c824cd102b71ec897c820f117062b Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 21:27:01 +0200 Subject: [PATCH 17/21] Match Cargo automatic target discovery Exclude automatic directory symlinks while retaining direct source links. Recognize directory-form targets whose names end in .rs so every effective Cargo target remains protected by the unsafe-code contract. --- scripts/check_boundary_contract.py | 22 +++---- .../tests/test_boundary_target_discovery.py | 65 ++++++++++--------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 51f4fa1eb..7260845fe 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -187,9 +187,7 @@ def _load_automatic_target_roots( ), (crate / "build.rs",) if package.get("build", True) is True else (), ) - return tuple( - path for path in candidates if _path_matches(path, stat.S_ISREG, "root") - ) + return tuple(path for path in candidates if _is_mode(path, stat.S_ISREG)) def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: @@ -197,11 +195,13 @@ def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: for entry in _directory_entries(directory): if entry.name.startswith("."): continue - if entry.suffix == ".rs": + if entry.suffix == ".rs" and _is_mode(entry, stat.S_ISREG): yield entry continue + if _is_mode(entry, stat.S_ISLNK, follow=False): + continue candidate = entry / "main.rs" - if _path_matches(candidate, stat.S_ISREG, "root"): + if _is_mode(candidate, stat.S_ISREG): yield candidate @@ -216,14 +216,16 @@ def _directory_entries(directory: Path) -> tuple[Path, ...]: raise ValueError(msg) from error -def _path_matches(path: Path, check: cabc.Callable[[int], bool], kind: str) -> bool: +def _is_mode( + path: Path, check: cabc.Callable[[int], bool], *, follow: bool = True +) -> bool: """Inspect one target path with a stat-mode predicate.""" try: - return check(path.stat().st_mode) + return check(path.stat(follow_symlinks=follow).st_mode) except FileNotFoundError: return False except OSError as error: - msg = f"cannot inspect target {kind} {path}: {error}" + msg = f"cannot inspect target path {path}: {error}" raise ValueError(msg) from error @@ -290,9 +292,7 @@ def _inferred_target_roots( candidates = (crate / "src/main.rs", *candidates) case _: return () - return tuple( - path for path in candidates if _path_matches(path, stat.S_ISREG, "root") - ) + return tuple(path for path in candidates if _is_mode(path, stat.S_ISREG)) def _target_definitions(value: object) -> tuple[cabc.Mapping[str, object], ...]: diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index ddf9108b1..c8464dfb3 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -33,15 +33,8 @@ ("autotests", "tests/integration.rs"), ("autobenches", "benches/benchmark.rs"), ) -AUTOMATIC_PROPERTY_TARGETS = ( - "src/lib.rs", - "src/bin/nested/main.rs", -) -EXPLICIT_PROPERTY_TARGETS = ( - "src/lib.rs", - "tools/explicit.rs", - "tools/build.rs", -) +AUTOMATIC_PROPERTY_TARGETS = ("src/lib.rs", "src/bin/nested/main.rs") +EXPLICIT_PROPERTY_TARGETS = ("src/lib.rs", "tools/explicit.rs", "tools/build.rs") OVERRIDING_EXPLICIT_TARGETS = ( ("lib", None, "src/lib.rs", "tools/custom_lib.rs"), ("bin", "cuprum-streams", "src/main.rs", "tools/custom_main.rs"), @@ -89,14 +82,9 @@ def _append_explicit_target( """Declare one named Cargo target table with a custom source path.""" table = "[lib]" if target_type == "lib" else f"[[{target_type}]]" name_entry = "" if name is None else chr(10) + f'name = "{name}"' + content = manifest.read_text(encoding="utf-8") manifest.write_text( - manifest.read_text(encoding="utf-8") - + chr(10) - + table - + name_entry - + chr(10) - + f'path = "{source}"' - + chr(10), + f'{content}{chr(10)}{table}{name_entry}{chr(10)}path = "{source}"{chr(10)}', encoding="utf-8", ) @@ -141,7 +129,6 @@ def test_hidden_automatic_targets_are_ignored( crate = root / "cuprum-streams" hidden_path = _write_target(crate, hidden, "//! Hidden automatic root." + chr(10)) visible_path = _write_target(crate, visible) - roots = safe_target_roots(root) assert hidden_path not in roots, "Cargo must ignore a hidden automatic target" @@ -152,28 +139,42 @@ def test_hidden_automatic_targets_are_ignored( check_safe_policy(root) -def test_visible_support_directory_without_main_is_not_a_target(tmp_path: Path) -> None: +@pytest.mark.parametrize( + "nested_target", + [ + "src/bin/behaviour.rs/main.rs", + "examples/behaviour.rs/main.rs", + "tests/behaviour.rs/main.rs", + "benches/behaviour.rs/main.rs", + ], +) +def test_visible_support_directory_without_main_is_not_a_target( + nested_target: str, tmp_path: Path +) -> None: """Only a visible nested main.rs is an automatic Cargo target.""" root = copy_boundary_repository(tmp_path) / "rust" crate = root / "cuprum-streams" - support = crate / "tests/support" - support.mkdir(parents=True) - + (crate / "tests/support").mkdir(parents=True) + _write_target(crate, "shared/main.rs") + try: + (crate / "tests/shared").symlink_to("../shared", target_is_directory=True) + (crate / "tests/shared.rs").symlink_to("../shared/main.rs") + except OSError as error: + pytest.skip(f"the platform cannot create the symlink fixture: {error}") roots = safe_target_roots(root) - - assert support / "main.rs" not in roots, "support without main.rs is not a target" - assert roots == _metadata_target_roots(crate), ( - "the scanner and Cargo metadata must ignore the support directory" + assert crate / "tests/support/main.rs" not in roots, ( + "support/main.rs is not a target" ) - check_safe_policy(root) - - nested = _write_target( - crate, "tests/behaviour/main.rs", "//! Missing safety." + chr(10) + assert crate / "tests/shared/main.rs" not in roots, ( + "directory symlink is not a target" ) - - assert nested in safe_target_roots(root), ( - "a visible nested main.rs must remain an automatic target" + assert crate / "tests/shared.rs" in roots, ( + "a direct source symlink must retain Cargo's target behaviour" ) + assert roots == _metadata_target_roots(crate), "scanner must match Cargo metadata" + check_safe_policy(root) + nested = _write_target(crate, nested_target, "//! Missing." + chr(10)) + assert nested in safe_target_roots(root), "nested main.rs remains a target" with pytest.raises(ValueError, match="safe target must forbid unsafe code"): check_safe_policy(root) From f1ba45da88f588fff3a1625493bd78e3a182e6a2 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 21:47:27 +0200 Subject: [PATCH 18/21] Preserve boundary workspace symlinks Retain Cargo's automatic target topology in the isolated boundary probe so metadata, baseline compilation, and unsafe probes cover the same sources. --- scripts/check_boundary_contract.py | 3 +- scripts/tests/boundary_harness_support.py | 31 ++++++++++-- scripts/tests/test_boundary_contract.py | 48 ++++++++++++++++++- .../tests/test_boundary_target_discovery.py | 35 +++----------- 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 7260845fe..2de6c5c11 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -43,6 +43,7 @@ ("autobenches", "benches"), ) EXPLICIT_TARGET_TYPES = ("lib", "bin", "example", "test", "bench") +IGNORE_BUILD_OUTPUT = shutil.ignore_patterns("target") def check_members(manifest: str) -> None: @@ -343,7 +344,7 @@ def main() -> None: workspace = ROOT / ".cache/boundary-contract/workspace" if workspace.exists(): shutil.rmtree(workspace) - shutil.copytree(ROOT / "rust", workspace, ignore=shutil.ignore_patterns("target")) + shutil.copytree(ROOT / "rust", workspace, ignore=IGNORE_BUILD_OUTPUT, symlinks=True) logs = ROOT / "rust/target/boundary-verification" logs.mkdir(parents=True, exist_ok=True) code, output = _compile(workspace) diff --git a/scripts/tests/boundary_harness_support.py b/scripts/tests/boundary_harness_support.py index 6c30e683e..7709fa184 100644 --- a/scripts/tests/boundary_harness_support.py +++ b/scripts/tests/boundary_harness_support.py @@ -8,14 +8,13 @@ from __future__ import annotations +import json import shutil -import typing as typ +import subprocess # ruff: ignore[suspicious-subprocess-import] - fixed Cargo metadata argv. +from pathlib import Path from scripts import check_boundary_contract -if typ.TYPE_CHECKING: - from pathlib import Path - # Build output is the only part of the workspace a harness must never copy. IGNORED = shutil.ignore_patterns("target") @@ -33,3 +32,27 @@ def copy_boundary_repository(tmp_path: Path) -> Path: ) shutil.copytree(check_boundary_contract.ROOT / "tools", tmp_path / "tools") return tmp_path + + +def cargo_target_roots(crate: Path) -> tuple[Path, ...]: + """Return Cargo metadata source roots for the synthetic safe package.""" + cargo = shutil.which("cargo") + if cargo is None: + msg = "Cargo is required for the metadata contract" + raise RuntimeError(msg) + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Cargo metadata argv runs in the isolated fixture. + [cargo, "metadata", "--no-deps", "--format-version=1"], + capture_output=True, + check=False, + cwd=crate, + text=True, + ) + if completed.returncode != 0: + raise RuntimeError(completed.stderr) + metadata = json.loads(completed.stdout) + package = next( + package + for package in metadata["packages"] + if package["name"] == "cuprum-streams" + ) + return tuple(sorted(Path(target["src_path"]) for target in package["targets"])) diff --git a/scripts/tests/test_boundary_contract.py b/scripts/tests/test_boundary_contract.py index b45a9735a..81eb2165e 100644 --- a/scripts/tests/test_boundary_contract.py +++ b/scripts/tests/test_boundary_contract.py @@ -19,7 +19,10 @@ check_safe_policy, safe_target_roots, ) -from scripts.tests.boundary_harness_support import copy_boundary_repository +from scripts.tests.boundary_harness_support import ( + cargo_target_roots, + copy_boundary_repository, +) # The one diagnostic string that distinguishes a rejected probe from a compile # that failed for some unrelated reason. @@ -219,6 +222,49 @@ def test_main_probes_every_target_with_every_unsafe_form( ]), "every probe result must be archived under its target and form" +def test_main_preserves_automatic_directory_symlinks( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The isolated probe workspace retains Cargo's directory-symlink topology.""" + root = copy_boundary_repository(tmp_path) + crate = root / "rust/cuprum-streams" + shared = crate / "shared/main.rs" + shared.parent.mkdir() + shared.write_text("//! Non-target source.\n", encoding="utf-8") + try: + (crate / "tests/shared").symlink_to("../shared", target_is_directory=True) + (crate / "tests/broken").symlink_to("../missing", target_is_directory=True) + except OSError as error: + pytest.skip(f"the platform cannot create the symlink fixture: {error}") + before = cargo_target_roots(crate) + assert {path.relative_to(crate) for path in before} == { + Path(path) for path in TARGETS + }, "Cargo must omit directory symlink targets before the copy" + compiler = _RecordedCompile() + monkeypatch.setattr(contract, "ROOT", root) + monkeypatch.setattr(contract, "_compile", compiler) + + contract.main() + + copied_crate = root / ".cache/boundary-contract/workspace/cuprum-streams" + after = cargo_target_roots(copied_crate) + assert {path.relative_to(copied_crate) for path in after} == { + Path(path) for path in TARGETS + }, "Cargo must omit directory symlink targets after the copy" + assert all((copied_crate / target).is_file() for target in TARGETS), ( + "the copied workspace must retain each intended target" + ) + assert (copied_crate / "tests/shared").is_symlink(), ( + "the copied workspace must preserve valid directory symlinks" + ) + assert (copied_crate / "tests/broken").is_symlink(), ( + "the copied workspace must preserve broken directory symlinks" + ) + assert len(compiler.probes) == 1 + len(TARGETS) * len(contract.PROBES), ( + "the copied topology must leave the probe target count unchanged" + ) + + def test_same_basename_target_logs_are_unique_and_traceable( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index c8464dfb3..78ffe057f 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -2,9 +2,6 @@ from __future__ import annotations -import json -import shutil -import subprocess # ruff: ignore[suspicious-subprocess-import] - fixed Cargo metadata argv. from pathlib import Path import pytest @@ -18,7 +15,10 @@ evaluate_safe_target_policy, safe_target_roots, ) -from scripts.tests.boundary_harness_support import copy_boundary_repository +from scripts.tests.boundary_harness_support import ( + cargo_target_roots, + copy_boundary_repository, +) HIDDEN_AUTOMATIC_TARGETS = ( ("src/bin/.scratch.rs", "src/bin/visible.rs"), @@ -89,27 +89,6 @@ def _append_explicit_target( ) -def _metadata_target_roots(crate: Path) -> tuple[Path, ...]: - """Return Cargo's effective source roots for the safe package.""" - cargo = shutil.which("cargo") - assert cargo is not None, "Cargo is required for the metadata contract" - completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - fixed Cargo metadata argv runs in the isolated fixture. - [cargo, "metadata", "--no-deps", "--format-version=1"], - capture_output=True, - check=False, - cwd=crate, - text=True, - ) - assert completed.returncode == 0, completed.stderr - metadata = json.loads(completed.stdout) - package = next( - package - for package in metadata["packages"] - if package["name"] == "cuprum-streams" - ) - return tuple(sorted(Path(target["src_path"]) for target in package["targets"])) - - def _append_pathless_target(manifest: Path, target_type: str, name: str | None) -> None: """Declare a target table whose source path Cargo must infer.""" table = "[lib]" if target_type == "lib" else f"[[{target_type}]]" @@ -133,7 +112,7 @@ def test_hidden_automatic_targets_are_ignored( assert hidden_path not in roots, "Cargo must ignore a hidden automatic target" assert visible_path in roots, "Cargo must retain a visible automatic target" - assert roots == _metadata_target_roots(crate), ( + assert roots == cargo_target_roots(crate), ( "the scanner and Cargo metadata must agree about hidden target entries" ) check_safe_policy(root) @@ -171,7 +150,7 @@ def test_visible_support_directory_without_main_is_not_a_target( assert crate / "tests/shared.rs" in roots, ( "a direct source symlink must retain Cargo's target behaviour" ) - assert roots == _metadata_target_roots(crate), "scanner must match Cargo metadata" + assert roots == cargo_target_roots(crate), "scanner must match Cargo metadata" check_safe_policy(root) nested = _write_target(crate, nested_target, "//! Missing." + chr(10)) assert nested in safe_target_roots(root), "nested main.rs remains a target" @@ -249,7 +228,7 @@ def test_explicit_target_replaces_its_dormant_automatic_root( assert dormant_path not in roots, ( "the explicit target must replace its dormant conventional root" ) - assert roots == _metadata_target_roots(crate), ( + assert roots == cargo_target_roots(crate), ( "the scanner and Cargo metadata must resolve identical target roots" ) check_safe_policy(root) From 43805fca22f8f2c2a060672f5ea67c7bc9e3aaaf Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 22:15:47 +0200 Subject: [PATCH 19/21] Isolate boundary probe source copies Preserve Cargo's directory topology while materializing writable source-file links in the copied workspace. Reject target paths that could route probes through an escaped or symlinked directory, and restore source bytes after a failed probe write. --- scripts/boundary_workspace.py | 85 +++++++++ scripts/check_boundary_contract.py | 16 +- scripts/tests/boundary_harness_support.py | 8 +- scripts/tests/test_boundary_workspace.py | 204 ++++++++++++++++++++++ 4 files changed, 298 insertions(+), 15 deletions(-) create mode 100644 scripts/boundary_workspace.py create mode 100644 scripts/tests/test_boundary_workspace.py diff --git a/scripts/boundary_workspace.py b/scripts/boundary_workspace.py new file mode 100644 index 000000000..f897749d7 --- /dev/null +++ b/scripts/boundary_workspace.py @@ -0,0 +1,85 @@ +"""Copy the Rust boundary workspace without changing Cargo target topology.""" + +from __future__ import annotations + +import os +import shutil +import stat +import typing as typ +from pathlib import Path + +if typ.TYPE_CHECKING: + import collections.abc as cabc + + +def copy_workspace( + source: Path, destination: Path, source_targets: cabc.Iterable[Path] +) -> None: + """Copy one workspace, materializing safe source links before mutation.""" + source = _lexical_absolute(source) + targets = tuple(source_targets) + copied_targets = tuple( + _copied_target_path(source, destination, target) for target in targets + ) + copy_source_tree(source, destination) + for target, copied_target in zip(targets, copied_targets, strict=True): + if _is_symlink(target): + copied_target.unlink() + shutil.copyfile(target, copied_target) + _make_writable(copied_target) + + +def copy_source_tree(source: Path, destination: Path) -> None: + """Copy a workspace tree while ignoring only root Cargo build output.""" + source = _lexical_absolute(source) + + def ignore_root_build_output(directory: str, _names: list[str]) -> set[str]: + """Ignore Cargo build output only when copying the workspace root.""" + return {"target"} if Path(directory) == source else set() + + shutil.copytree(source, destination, ignore=ignore_root_build_output, symlinks=True) + + +def _lexical_absolute(path: Path) -> Path: + """Normalize a path without resolving its symlink topology.""" + return Path(os.path.normpath(Path.cwd() / path)) + + +def _copied_target_path(source: Path, destination: Path, target: Path) -> Path: + """Map a target into the copy only when no directory link can escape it.""" + lexical_target = _lexical_absolute(target) + try: + relative = lexical_target.relative_to(source) + except ValueError as error: + msg = f"source target escapes the Rust workspace: {target}" + raise ValueError(msg) from error + _reject_symlinked_parents(source, relative, target) + return destination / relative + + +def _reject_symlinked_parents(source: Path, relative: Path, target: Path) -> None: + """Reject target paths whose copied parent would still point outside the copy.""" + parent = source + for component in relative.parts[:-1]: + parent /= component + if _is_symlink(parent): + msg = f"source target has a symlinked parent directory: {target}" + raise ValueError(msg) + + +def _is_symlink(path: Path) -> bool: + """Inspect a source target without turning access errors into omissions.""" + try: + return stat.S_ISLNK(path.lstat().st_mode) + except OSError as error: + msg = f"cannot inspect source target {path}: {error}" + raise ValueError(msg) from error + + +def _make_writable(path: Path) -> None: + """Give the copied probe target an owner write bit without altering its source.""" + try: + path.chmod(stat.S_IMODE(path.stat().st_mode) | stat.S_IWUSR) + except OSError as error: + msg = f"cannot make copied source target writable {path}: {error}" + raise ValueError(msg) from error diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 2de6c5c11..3811a43e3 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -5,9 +5,7 @@ # /// """Check audited workspace lint inheritance and safe-crate unsafe boundaries. -The workspace copy avoids touching a developer's sources while proving that -the real library and integration-test targets reject unsafe code. Cargo keeps -its shared package cache. Only build output uses a separate target directory. +The isolated copy preserves Cargo target topology and leaves source untouched. """ from __future__ import annotations @@ -26,6 +24,7 @@ import collections.abc as cabc from cuprum import Program, ProgramCatalogue, ProjectSettings, ScopeConfig, scoped, sh +from scripts.boundary_workspace import copy_workspace ROOT = Path(__file__).resolve().parent.parent BOUNDARIES = frozenset({"cuprum-rust", "cuprum-native-io"}) @@ -43,7 +42,6 @@ ("autobenches", "benches"), ) EXPLICIT_TARGET_TYPES = ("lib", "bin", "example", "test", "bench") -IGNORE_BUILD_OUTPUT = shutil.ignore_patterns("target") def check_members(manifest: str) -> None: @@ -344,7 +342,7 @@ def main() -> None: workspace = ROOT / ".cache/boundary-contract/workspace" if workspace.exists(): shutil.rmtree(workspace) - shutil.copytree(ROOT / "rust", workspace, ignore=IGNORE_BUILD_OUTPUT, symlinks=True) + copy_workspace(ROOT / "rust", workspace, safe_targets) logs = ROOT / "rust/target/boundary-verification" logs.mkdir(parents=True, exist_ok=True) code, output = _compile(workspace) @@ -368,13 +366,13 @@ def _unsafe_was_forbidden(code: int, output: str) -> bool: @contextlib.contextmanager def _probe_appended(path: Path, probe: str) -> cabc.Iterator[None]: - """Temporarily append a probe to a source file, then restore it.""" - original = path.read_text(encoding="utf-8") - path.write_text(original + "\n" + probe + "\n", encoding="utf-8") + """Temporarily append a byte-preserving probe, then restore the source.""" + original = path.read_bytes() try: + path.write_bytes(original + b"\n" + probe.encode() + b"\n") yield finally: - path.write_text(original, encoding="utf-8") + path.write_bytes(original) def _probe_log_name(workspace: Path, path: Path, index: int) -> str: diff --git a/scripts/tests/boundary_harness_support.py b/scripts/tests/boundary_harness_support.py index 7709fa184..5606951d5 100644 --- a/scripts/tests/boundary_harness_support.py +++ b/scripts/tests/boundary_harness_support.py @@ -14,9 +14,7 @@ from pathlib import Path from scripts import check_boundary_contract - -# Build output is the only part of the workspace a harness must never copy. -IGNORED = shutil.ignore_patterns("target") +from scripts.boundary_workspace import copy_source_tree def copy_boundary_repository(tmp_path: Path) -> Path: @@ -27,9 +25,7 @@ def copy_boundary_repository(tmp_path: Path) -> Path: Path Temporary root laid out like the repository, minus any build output. """ - shutil.copytree( - check_boundary_contract.ROOT / "rust", tmp_path / "rust", ignore=IGNORED - ) + copy_source_tree(check_boundary_contract.ROOT / "rust", tmp_path / "rust") shutil.copytree(check_boundary_contract.ROOT / "tools", tmp_path / "tools") return tmp_path diff --git a/scripts/tests/test_boundary_workspace.py b/scripts/tests/test_boundary_workspace.py new file mode 100644 index 000000000..d67f88879 --- /dev/null +++ b/scripts/tests/test_boundary_workspace.py @@ -0,0 +1,204 @@ +"""Exercise isolated boundary workspace copying and probe topology.""" + +from __future__ import annotations + +import stat +import typing as typ + +import pytest + +from scripts import check_boundary_contract as contract +from scripts.boundary_workspace import copy_workspace +from scripts.check_boundary_contract import safe_target_roots + +if typ.TYPE_CHECKING: + from pathlib import Path + +from scripts.tests.boundary_harness_support import ( + cargo_target_roots, + copy_boundary_repository, +) + +SAFE_SOURCE = "//! Safe target.\n#![forbid(unsafe_code)]\n" + + +def test_main_keeps_nested_target_sources_and_omits_root_build_output( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A nested target directory is source, while root target is Cargo output.""" + root = copy_boundary_repository(tmp_path) + rust = root / "rust" + nested = rust / "cuprum-streams/src/bin/target/main.rs" + nested.parent.mkdir(parents=True) + nested.write_text(SAFE_SOURCE, encoding="utf-8") + nested.chmod(stat.S_IRUSR) + build_output = rust / "target/ignored.txt" + build_output.parent.mkdir() + build_output.write_text("build output", encoding="utf-8") + source_targets = safe_target_roots(rust) + workspace = root / ".cache/boundary-contract/workspace" + + copy_workspace(rust, workspace, source_targets) + + copied_nested = workspace / nested.relative_to(rust) + assert copied_nested in cargo_target_roots(workspace / "cuprum-streams"), ( + "Cargo metadata must retain the nested source directory named target" + ) + assert copied_nested.is_file(), "the nested source target must be copied" + assert copied_nested.stat().st_mode & stat.S_IWUSR, ( + "the copied target must be writable for temporary probes" + ) + assert not nested.stat().st_mode & stat.S_IWUSR, ( + "the source target mode must remain unchanged" + ) + assert not (workspace / "target").exists(), ( + "only the workspace-root Cargo build output must be omitted" + ) + scheduled: list[Path] = [] + monkeypatch.setattr(contract, "ROOT", root) + monkeypatch.setattr(contract, "_compile", lambda _workspace: (0, "checked")) + monkeypatch.setattr( + contract, + "_check_target", + lambda _workspace, target, _logs: scheduled.append(target), + ) + + contract.main() + + assert copied_nested in scheduled, ( + "the copied nested target must be scheduled for unsafe probes" + ) + + +def test_main_materializes_external_source_file_symlinks( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """External source links become copied files before the unsafe probes run.""" + root = copy_boundary_repository(tmp_path) + crate = root / "rust/cuprum-streams" + links = ( + ( + crate / "tests/relative.rs", + root / "relative-source.rs", + "../../../relative-source.rs", + ), + (crate / "tests/absolute.rs", tmp_path / "absolute-source.rs", None), + ) + for _link, source, _target in links: + source.write_text(SAFE_SOURCE, encoding="utf-8") + try: + for link, source, target in links: + link.symlink_to(source if target is None else target) + except OSError as error: + pytest.skip(f"the platform cannot create the symlink fixture: {error}") + before = cargo_target_roots(crate) + assert links[0][0] in before, "Cargo must recognize the relative source-file link" + assert links[1][0] in before, "Cargo must recognize the absolute source-file link" + observed_sources: list[str] = [] + + def compile_workspace(workspace: Path) -> tuple[int, str]: + """Reject only the probes applied to the isolated copied sources.""" + source = "\n".join( + path.read_text(encoding="utf-8") + for path in sorted((workspace / "cuprum-streams").rglob("*.rs")) + ) + observed_sources.append(source) + if any(probe in source for probe in contract.PROBES): + return 1, "error: forbid(unsafe_code)" + return 0, "checked" + + boundary_compile = contract._compile + monkeypatch.setattr(contract, "ROOT", root) + monkeypatch.setattr(contract, "_compile", compile_workspace) + + contract.main() + + copied_crate = root / ".cache/boundary-contract/workspace/cuprum-streams" + code, output = boundary_compile(copied_crate.parent) + assert code == 0, output + copied_links = tuple( + copied_crate / link.relative_to(crate) for link, _source, _target in links + ) + assert cargo_target_roots(copied_crate) == tuple( + copied_crate / path.relative_to(crate) for path in before + ), "Cargo metadata must retain the copied source-file targets" + assert all(path.is_file() and not path.is_symlink() for path in copied_links), ( + "external source-file links must be materialized inside the copied workspace" + ) + assert all( + source.read_text(encoding="utf-8") == SAFE_SOURCE + for _link, source, _target in links + ), "external source bytes must survive the copied probes" + assert all( + any(probe in source for source in observed_sources) for probe in contract.PROBES + ), "each unsafe probe must reach the isolated copied sources" + + +@pytest.mark.parametrize( + ("target_path", "expected_error"), + [ + ("../../outside/target.rs", "escapes the Rust workspace"), + ("/unused", "escapes the Rust workspace"), + ("tools/target.rs", "symlinked parent directory"), + ], +) +def test_main_rejects_explicit_targets_that_escape_the_copy( + target_path: str, + expected_error: str, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Explicit paths cannot reach original sources through escapes or directories.""" + root = copy_boundary_repository(tmp_path) + crate = root / "rust/cuprum-streams" + external = root / "outside/target.rs" + external.parent.mkdir() + external.write_text(SAFE_SOURCE, encoding="utf-8") + if target_path == "/unused": + target_path = str(external) + if target_path.startswith("tools/"): + (crate / "tools").symlink_to("../../outside", target_is_directory=True) + manifest = crate / "Cargo.toml" + manifest.write_text( + manifest.read_text(encoding="utf-8") + + f'\n[[bin]]\nname = "escaped-target"\npath = "{target_path}"\n', + encoding="utf-8", + ) + monkeypatch.setattr(contract, "ROOT", root) + + with pytest.raises(ValueError, match=expected_error): + contract.main() + + +def test_probe_appended_preserves_crlf_bytes_after_a_write_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Probe restoration preserves raw bytes even when its first write fails.""" + target = tmp_path / "target.rs" + original = b"//! CRLF source.\r\n#![forbid(unsafe_code)]\r\n" + target.write_bytes(original) + probe = "pub fn unsafe_probe() { unsafe {} }" + with contract._probe_appended(target, probe): + assert target.read_bytes() == original + b"\n" + probe.encode() + b"\n", ( + "the probe must preserve the original CRLF bytes" + ) + assert target.read_bytes() == original, "the normal probe path must restore bytes" + original_write = type(target).write_bytes + + def write_bytes(path: Path, data: bytes) -> int: + """Simulate a partial first write while allowing the restoration write.""" + if path == target and data != original: + original_write(path, data[:1]) + msg = "injected probe write failure" + raise OSError(msg) + return original_write(path, data) + + monkeypatch.setattr(type(target), "write_bytes", write_bytes) + with ( + pytest.raises(OSError, match="injected probe write failure"), + contract._probe_appended(target, probe), + ): + pass + assert target.read_bytes() == original, ( + "the failed first probe write must still restore the original raw bytes" + ) From 67abfb813dd26815741d60358614a2d76f5ccd90 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 22:18:11 +0200 Subject: [PATCH 20/21] Simplify boundary workspace link tests Separate fixture creation, probe recording, and copy assertions so the external-link regression remains readable and within the CodeScene threshold. --- scripts/tests/test_boundary_workspace.py | 64 +++++++++++++++++------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/scripts/tests/test_boundary_workspace.py b/scripts/tests/test_boundary_workspace.py index d67f88879..d8823767b 100644 --- a/scripts/tests/test_boundary_workspace.py +++ b/scripts/tests/test_boundary_workspace.py @@ -12,6 +12,7 @@ from scripts.check_boundary_contract import safe_target_roots if typ.TYPE_CHECKING: + import collections.abc as cabc from pathlib import Path from scripts.tests.boundary_harness_support import ( @@ -70,12 +71,10 @@ def test_main_keeps_nested_target_sources_and_omits_root_build_output( ) -def test_main_materializes_external_source_file_symlinks( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """External source links become copied files before the unsafe probes run.""" - root = copy_boundary_repository(tmp_path) - crate = root / "rust/cuprum-streams" +def _external_source_links( + root: Path, crate: Path, tmp_path: Path +) -> tuple[tuple[Path, Path, str | None], ...]: + """Create relative and absolute external source-file symlinks.""" links = ( ( crate / "tests/relative.rs", @@ -91,10 +90,13 @@ def test_main_materializes_external_source_file_symlinks( link.symlink_to(source if target is None else target) except OSError as error: pytest.skip(f"the platform cannot create the symlink fixture: {error}") - before = cargo_target_roots(crate) - assert links[0][0] in before, "Cargo must recognize the relative source-file link" - assert links[1][0] in before, "Cargo must recognize the absolute source-file link" - observed_sources: list[str] = [] + return links + + +def _recording_probe_compiler( + observed_sources: list[str], +) -> cabc.Callable[[Path], tuple[int, str]]: + """Return a compiler seam that rejects probes in the copied source tree.""" def compile_workspace(workspace: Path) -> tuple[int, str]: """Reject only the probes applied to the isolated copied sources.""" @@ -107,20 +109,21 @@ def compile_workspace(workspace: Path) -> tuple[int, str]: return 1, "error: forbid(unsafe_code)" return 0, "checked" - boundary_compile = contract._compile - monkeypatch.setattr(contract, "ROOT", root) - monkeypatch.setattr(contract, "_compile", compile_workspace) + return compile_workspace - contract.main() - copied_crate = root / ".cache/boundary-contract/workspace/cuprum-streams" - code, output = boundary_compile(copied_crate.parent) - assert code == 0, output +def _assert_external_links_are_isolated( + crate: Path, + copied_crate: Path, + links: tuple[tuple[Path, Path, str | None], ...], + original_targets: tuple[Path, ...], +) -> None: + """Require metadata parity, materialization, and untouched external bytes.""" copied_links = tuple( copied_crate / link.relative_to(crate) for link, _source, _target in links ) assert cargo_target_roots(copied_crate) == tuple( - copied_crate / path.relative_to(crate) for path in before + copied_crate / path.relative_to(crate) for path in original_targets ), "Cargo metadata must retain the copied source-file targets" assert all(path.is_file() and not path.is_symlink() for path in copied_links), ( "external source-file links must be materialized inside the copied workspace" @@ -129,6 +132,31 @@ def compile_workspace(workspace: Path) -> tuple[int, str]: source.read_text(encoding="utf-8") == SAFE_SOURCE for _link, source, _target in links ), "external source bytes must survive the copied probes" + + +def test_main_materializes_external_source_file_symlinks( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """External source links become copied files before the unsafe probes run.""" + root = copy_boundary_repository(tmp_path) + crate = root / "rust/cuprum-streams" + links = _external_source_links(root, crate, tmp_path) + before = cargo_target_roots(crate) + assert links[0][0] in before, "Cargo must recognize the relative source-file link" + assert links[1][0] in before, "Cargo must recognize the absolute source-file link" + observed_sources: list[str] = [] + boundary_compile = contract._compile + monkeypatch.setattr(contract, "ROOT", root) + monkeypatch.setattr( + contract, "_compile", _recording_probe_compiler(observed_sources) + ) + + contract.main() + + copied_crate = root / ".cache/boundary-contract/workspace/cuprum-streams" + code, output = boundary_compile(copied_crate.parent) + assert code == 0, output + _assert_external_links_are_isolated(crate, copied_crate, links, before) assert all( any(probe in source for source in observed_sources) for probe in contract.PROBES ), "each unsafe probe must reach the isolated copied sources" From 6f6e2c6831a2ffea4e4a314d5eb5fbff929d39c7 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 20 Sep 2026 22:35:06 +0200 Subject: [PATCH 21/21] Ignore ordinary Cargo support files Require a visible automatic-target entry to be a real directory before checking for nested main.rs, matching Cargo's treatment of support files. --- scripts/check_boundary_contract.py | 2 ++ scripts/tests/test_boundary_target_discovery.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/scripts/check_boundary_contract.py b/scripts/check_boundary_contract.py index 3811a43e3..7d5245061 100644 --- a/scripts/check_boundary_contract.py +++ b/scripts/check_boundary_contract.py @@ -199,6 +199,8 @@ def _load_directory_target_roots(directory: Path) -> cabc.Iterator[Path]: continue if _is_mode(entry, stat.S_ISLNK, follow=False): continue + if not _is_mode(entry, stat.S_ISDIR): + continue candidate = entry / "main.rs" if _is_mode(candidate, stat.S_ISREG): yield candidate diff --git a/scripts/tests/test_boundary_target_discovery.py b/scripts/tests/test_boundary_target_discovery.py index 78ffe057f..905652a28 100644 --- a/scripts/tests/test_boundary_target_discovery.py +++ b/scripts/tests/test_boundary_target_discovery.py @@ -134,6 +134,8 @@ def test_visible_support_directory_without_main_is_not_a_target( root = copy_boundary_repository(tmp_path) / "rust" crate = root / "cuprum-streams" (crate / "tests/support").mkdir(parents=True) + support_file = crate / "tests/README.md" + support_file.write_text("test support", encoding="utf-8") _write_target(crate, "shared/main.rs") try: (crate / "tests/shared").symlink_to("../shared", target_is_directory=True) @@ -144,6 +146,7 @@ def test_visible_support_directory_without_main_is_not_a_target( assert crate / "tests/support/main.rs" not in roots, ( "support/main.rs is not a target" ) + assert support_file not in roots, "an ordinary support file is not a target" assert crate / "tests/shared/main.rs" not in roots, ( "directory symlink is not a target" )