diff --git a/.github/workflows/ci-preflight-rust.yml b/.github/workflows/ci-preflight-rust.yml index a7cac2ba..6a45a2d2 100644 --- a/.github/workflows/ci-preflight-rust.yml +++ b/.github/workflows/ci-preflight-rust.yml @@ -16,7 +16,7 @@ name: CI Preflight Rust-only (Reusable) # C-build-script crates (rusqlite/bundled, blake3 asm, etc.). # - `zackees/setup-soldr@v0` provisions the toolchain from # rust-toolchain.toml and soldr's zccache-backed build cache -# (CACHE-025, zackees/ci.yml#209); `soldr rustup target add` adds +# (CACHE-025, zackees/ci.yml#209); `ci/install_rust_target.sh` adds # the rust-std for the musl target. # - `soldr cargo build/clippy/nextest --target ` builds # against the musl target. The platform-default linker is kept: @@ -95,7 +95,7 @@ jobs: linker: platform-default - name: Install ${{ inputs.target }} standard library - run: soldr rustup target add ${{ inputs.target }} --toolchain 1.95.0 + run: bash ci/install_rust_target.sh ${{ inputs.target }} 1.95.0 - name: cargo build --target ${{ inputs.target }} run: soldr cargo build --workspace --exclude running-process-py --all-targets --features ${{ inputs.features }} --target ${{ inputs.target }} @@ -121,7 +121,7 @@ jobs: prebuild-deps: none linker: platform-default - name: Install ${{ inputs.target }} standard library - run: soldr rustup target add ${{ inputs.target }} --toolchain 1.95.0 + run: bash ci/install_rust_target.sh ${{ inputs.target }} 1.95.0 - name: cargo clippy --target ${{ inputs.target }} run: soldr cargo clippy --workspace --exclude running-process-py --all-targets --features ${{ inputs.features }} --target ${{ inputs.target }} -- -D warnings @@ -147,7 +147,7 @@ jobs: prebuild-deps: none linker: platform-default - name: Install ${{ inputs.target }} standard library - run: soldr rustup target add ${{ inputs.target }} --toolchain 1.95.0 + run: bash ci/install_rust_target.sh ${{ inputs.target }} 1.95.0 - name: Install cargo-nextest uses: taiki-e/install-action@v2 with: @@ -201,7 +201,7 @@ jobs: prebuild-deps: none linker: platform-default - name: Install ${{ inputs.target }} standard library - run: soldr rustup target add ${{ inputs.target }} --toolchain 1.95.0 + run: bash ci/install_rust_target.sh ${{ inputs.target }} 1.95.0 - name: Install cargo-nextest uses: taiki-e/install-action@v2 with: diff --git a/.github/workflows/ci-preflight.yml b/.github/workflows/ci-preflight.yml index 6df8ea31..f237eb51 100644 --- a/.github/workflows/ci-preflight.yml +++ b/.github/workflows/ci-preflight.yml @@ -346,7 +346,11 @@ jobs: name: dylint (${{ inputs.label }}) if: ${{ inputs.dylint }} runs-on: ${{ inputs.os }} - timeout-minutes: 30 + # Windows x86 with a cold compiler-unit cache spent 20-30 min in each of + # the two workspace passes (2026-10-26, PR #1337: 50 min once, over 60 + # the next time), and that cache is saved only from main, so a PR lane is + # always cold. 30 and then 60 cancelled the second pass. + timeout-minutes: 120 env: SOLDR_DYLINT_DRIVER_FALLBACK: "off" # The lint crates select dylint-link through cfg(all()). Soldr's diff --git a/Cargo.lock b/Cargo.lock index 9f2308b5..295733c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,13 +4,13 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "e567177890eb1617b1f774005b66b26b2377afd138a2ca37aae7d8f0c81429d4" dependencies = [ - "gimli", + "gimli 0.34.0", "memmap2", - "object", + "object 0.40.0", "typed-arena", ] @@ -663,7 +663,7 @@ dependencies = [ "arrayvec", "cfg-if", "fallible-iterator 0.3.0", - "gimli", + "gimli 0.31.1", "macho-unwind-info", "pe-unwind-info", ] @@ -786,6 +786,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -1348,9 +1357,9 @@ dependencies = [ [[package]] name = "mimalloc-pprof" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d30b36d87ca14bd04fec54438531e7ef88ee0f28b34d75a37ddc98ac3fa93d" +checksum = "9143eb24178e618a9671d22074e2e64badd61454a1ae5c042325daf8238f5407" dependencies = [ "cc", ] @@ -1442,6 +1451,15 @@ name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" dependencies = [ "flate2", "memchr", @@ -2137,7 +2155,7 @@ dependencies = [ "getrandom 0.4.2", "libc", "mach2", - "object", + "object 0.36.7", "proptest", "prost", "prost-build", @@ -2213,7 +2231,7 @@ version = "4.10.17" dependencies = [ "addr2line", "lzma-rs", - "object", + "object 0.40.0", "pdb", "pdb-addr2line", "running-process-platform-internal", @@ -2297,9 +2315,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "log", "once_cell", @@ -2321,9 +2339,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -2350,9 +2368,9 @@ dependencies = [ [[package]] name = "ruzstd" -version = "0.7.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" +checksum = "69e4ce4a039f42bf25920b95667e4d789e98749ab11bbb8d2c2936f788421bb5" dependencies = [ "twox-hash", ] @@ -2574,12 +2592,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -2678,7 +2690,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2771,9 +2783,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -3072,13 +3084,9 @@ dependencies = [ [[package]] name = "twox-hash" -version = "1.6.3" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "static_assertions", -] +checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2" [[package]] name = "typed-arena" @@ -3316,14 +3324,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.11", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" dependencies = [ "rustls-pki-types", ] @@ -3356,7 +3364,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3733,27 +3741,27 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" -version = "0.13.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.4" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.2+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" dependencies = [ "cc", "pkg-config", diff --git a/ci/install_rust_target.sh b/ci/install_rust_target.sh new file mode 100755 index 00000000..ca849cc1 --- /dev/null +++ b/ci/install_rust_target.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Install a Rust target's standard library, and reinstall it when the +# toolchain claims it but its files are missing. +# +# Usage: ci/install_rust_target.sh [toolchain] +# +# setup-soldr restores the toolchain from a solo-toolchain cache whose key does +# not include the added targets. A payload saved from a job that had added a +# target can come back with rustup's component manifest listing +# `rust-std-` but without the library files. `rustup target add` then +# reports "up to date" and changes nothing, and soldr refuses the build +# ("lists rust-std ... but the library files are missing"). Removing the +# component first makes the add a real install. `rustup component remove` +# itself rolls back when a file listed in the component's manifest is gone, +# so if it fails the component is dropped from the toolchain's installed list +# (`components`) and its file list (`manifest-*`), and the removal retried: +# rustup then only unregisters it from the channel config. +set -euo pipefail + +target="${1:?usage: install_rust_target.sh [toolchain]}" +toolchain="${2:-1.95.0}" + +soldr rustup target add "$target" --toolchain "$toolchain" + +rustc="$(soldr rustup which rustc --toolchain "$toolchain")" +rustlib="$(dirname "$(dirname "$rustc")")/lib/rustlib" +libdir="$rustlib/$target/lib" +if compgen -G "$libdir/libstd-*.rlib" > /dev/null; then + exit 0 +fi + +echo "rust-std for $target is registered but $libdir has no libstd; reinstalling" >&2 +if ! soldr rustup component remove "rust-std-$target" --toolchain "$toolchain"; then + sed -i "/^rust-std-$target\$/d" "$rustlib/components" + rm -f "$rustlib/manifest-rust-std-$target" + soldr rustup component remove "rust-std-$target" --toolchain "$toolchain" +fi +soldr rustup target add "$target" --toolchain "$toolchain" +if ! compgen -G "$libdir/libstd-*.rlib" > /dev/null; then + echo "rust-std for $target is still missing from $libdir after reinstall" >&2 + exit 1 +fi diff --git a/ci/local_gate.py b/ci/local_gate.py index 4ed3fea3..8d41321d 100644 --- a/ci/local_gate.py +++ b/ci/local_gate.py @@ -17,7 +17,7 @@ import sys import tempfile from dataclasses import dataclass -from pathlib import Path +from pathlib import Path, PurePath from typing import TypeAlias JsonValue: TypeAlias = str | int | float | bool | list["JsonValue"] | dict[str, "JsonValue"] | None @@ -185,10 +185,23 @@ def _jobs_error(jobs: tuple[Job, ...]) -> str | None: return None +def _is_rooted(path: PurePath) -> bool: + """Whether a receipt's workspace names a location rather than a relative path. + + Bosn executes the gate inside a Linux container, so its receipt reports a + POSIX workspace such as ``/work/repo``. ``Path.is_absolute()`` is false for + that on Windows, where an absolute path also needs a drive, so the check + rejected every genuine receipt there. A root is what separates a located + path from ``relative/repo`` (or a drive-relative ``C:repo``) on every host; + the ``resolve()`` comparison that follows still pins the exact workspace. + """ + return bool(path.root) + + def proof_error(output: str, *, workspace: Path, head_sha: str) -> str | None: try: proof = _parse_proof(output) - if not proof.workspace.is_absolute() or proof.workspace.resolve() != workspace.resolve(): + if not _is_rooted(proof.workspace) or proof.workspace.resolve() != workspace.resolve(): return "Bosn executed another workspace" if proof.sha != head_sha: return "Bosn executed another commit" diff --git a/crates/running-process-platform-internal/Cargo.toml b/crates/running-process-platform-internal/Cargo.toml index a73e34ea..f0bb745c 100644 --- a/crates/running-process-platform-internal/Cargo.toml +++ b/crates/running-process-platform-internal/Cargo.toml @@ -80,7 +80,7 @@ png = { version = "0.17", optional = true } [target.'cfg(windows)'.dependencies] sha2 = { version = "0.10", optional = true } ureq = { version = "2", default-features = false, features = ["tls"], optional = true } -zstd = { version = "0.13", default-features = false, optional = true } +zstd = { version = "0.14", default-features = false, optional = true } tar = { version = "0.4", default-features = false, optional = true } windows-sys = { version = "0.61", features = [ "Wdk_System_Threading", diff --git a/crates/running-process-platform-internal/src/platform_linux_session_relay.rs b/crates/running-process-platform-internal/src/platform_linux_session_relay.rs index 13e0ccc3..574c0db9 100644 --- a/crates/running-process-platform-internal/src/platform_linux_session_relay.rs +++ b/crates/running-process-platform-internal/src/platform_linux_session_relay.rs @@ -35,8 +35,8 @@ impl Direction { destination: RawFd, committed: Arc, ) -> io::Result { - let source = AsyncFd::new(duplicate(source)?)?; - let destination = AsyncFd::new(duplicate(destination)?)?; + let source = register(duplicate(source)?)?; + let destination = register(duplicate(destination)?)?; let (pipe_read, pipe_write) = pipe()?; Ok(Self { source, @@ -115,6 +115,18 @@ fn injected_splice_error() -> io::Error { io::Error::other("injected SESSION splice failure") } +/// Register an owned descriptor with the reactor. +/// +/// tokio 1.54 deprecates the safe `AsyncFd::new` as unsound +/// (RUSTSEC-2026-0386): it trusted `as_raw_fd()` to stay valid and stable. +fn register(fd: OwnedFd) -> io::Result> { + // SAFETY: `OwnedFd` holds an open descriptor, returns that same descriptor + // from every `as_raw_fd()` call, and closes it only when dropped. The + // `AsyncFd` takes ownership of it, so it stays open for the `AsyncFd`'s + // whole lifetime. + unsafe { AsyncFd::register(fd) }.map_err(io::Error::from) +} + fn duplicate(fd: RawFd) -> io::Result { // SAFETY: fd is borrowed from a live socket half. fcntl either returns // a new independently owned descriptor or a negative error sentinel. diff --git a/crates/running-process-probe-worker/Cargo.toml b/crates/running-process-probe-worker/Cargo.toml index a41c9e51..45d17460 100644 --- a/crates/running-process-probe-worker/Cargo.toml +++ b/crates/running-process-probe-worker/Cargo.toml @@ -45,11 +45,11 @@ pdb = "0.8" # own. Same isolation argument as every other parser here: it lives in the # disposable worker, never in the daemon. pdb-addr2line = "0.12" -object = { version = "0.36", default-features = false, features = ["read", "std"] } +object = { version = "0.40", default-features = false, features = ["read", "std"] } [target.'cfg(not(target_os = "windows"))'.dependencies] lzma-rs = "0.3" -object = { version = "0.36", default-features = false, features = ["read", "std"] } +object = { version = "0.40", default-features = false, features = ["read", "std"] } # DWARF line programs, for resolving a frame to file:line (#803). # # The `loader` feature is what provides `addr2line::Loader`, which owns its @@ -60,7 +60,7 @@ object = { version = "0.36", default-features = false, features = ["read", "std" # Unix-only, alongside the rest of the ELF/Mach-O parsing. The Windows half is # blocked separately: `pdb` 0.8 panics iterating line programs on PDBs rustc # produces, so that side needs a different crate (see #803). -addr2line = { version = "0.24", default-features = false, features = [ +addr2line = { version = "0.27", default-features = false, features = [ "std", "loader", ] } diff --git a/crates/running-process-probe-worker/src/pdb_symbols.rs b/crates/running-process-probe-worker/src/pdb_symbols.rs index 9ea1b5e6..34a062ce 100644 --- a/crates/running-process-probe-worker/src/pdb_symbols.rs +++ b/crates/running-process-probe-worker/src/pdb_symbols.rs @@ -767,9 +767,9 @@ mod tests { let ranges: Vec<(u64, u64)> = file .sections() .filter(|s| match s.flags() { - // IMAGE_SCN_MEM_EXECUTE + // object 0.40 types the characteristics as `pe::SectionFlags`. object::SectionFlags::Coff { characteristics } => { - characteristics & 0x2000_0000 != 0 + characteristics.0 & object::pe::IMAGE_SCN_MEM_EXECUTE.0 != 0 } _ => false, }) diff --git a/crates/running-process/tests/broker/lifecycle_process_conformance.rs b/crates/running-process/tests/broker/lifecycle_process_conformance.rs index 2bdad754..be50056c 100644 --- a/crates/running-process/tests/broker/lifecycle_process_conformance.rs +++ b/crates/running-process/tests/broker/lifecycle_process_conformance.rs @@ -255,8 +255,14 @@ fn round_trip( Err(BrokerClientError::Refused { code: ErrorCode::ErrorBackendSpawnFailed, retry_after_ms, - .. + reason, }) if retry_after_ms > 0 => { + // Printed so a lane where the transition never settles shows why + // the broker could not launch the backend, not just that it + // kept asking for a retry. + eprintln!( + "lifecycle {service}: backend spawn failed, retry in {retry_after_ms} ms: {reason}" + ); return ReconnectOutcome::BackendTransition(Duration::from_millis(retry_after_ms)); } Err(BrokerClientError::Refused { diff --git a/crates/running-process/tests/daemon_integration/daemon_tui_repaint_test.rs b/crates/running-process/tests/daemon_integration/daemon_tui_repaint_test.rs index f88ff6d4..3995c07b 100644 --- a/crates/running-process/tests/daemon_integration/daemon_tui_repaint_test.rs +++ b/crates/running-process/tests/daemon_integration/daemon_tui_repaint_test.rs @@ -100,13 +100,33 @@ fn start_server(scope: &str) -> (tokio::task::JoinHandle<()>, String) { /// Drain the attachment, concatenating every data frame into a single /// Vec for byte-exact assertions. Stops when the deadline passes /// or a frame other than `Data` arrives (e.g. SessionExited). +/// Answer every cursor-position query (`ESC[6n`) seen so far, as the real +/// terminal behind an attachment would. +/// +/// Windows ConPTY (`PSEUDOCONSOLE_INHERIT_CURSOR`) opens each session with +/// that query and forwards none of the child's output until it is answered, +/// so a client that never answers sees a backlog of just `ESC[6n` (#1337). +/// Unix PTYs never ask, so this is a no-op there. +fn answer_cursor_queries(att: &mut PtyAttachment, out: &[u8], answered: &mut usize) { + let asked = out.windows(4).filter(|w| *w == b"\x1b[6n").count(); + while *answered < asked { + let _ = att.send_input(b"\x1b[1;1R"); + *answered += 1; + } +} + fn drain_attachment(att: &mut PtyAttachment, deadline: Instant) -> Vec { let mut out = att.initial_backlog.clone(); + let mut answered = 0; + answer_cursor_queries(att, &out, &mut answered); while Instant::now() < deadline { let remaining = deadline.saturating_duration_since(Instant::now()); match att.recv_frame_with_timeout(remaining) { Ok(Some(frame)) => match frame.frame { - Some(StreamOneof::Output(bytes)) => out.extend_from_slice(&bytes), + Some(StreamOneof::Output(bytes)) => { + out.extend_from_slice(&bytes); + answer_cursor_queries(att, &out, &mut answered); + } // `MissedBytes` reports that the session ring dropped output // because this attachment fell behind. The daemon marks it // non-final (`encode_outbound`), so the stream continues and diff --git a/lints/running-process-env-literal/src/lib.rs b/lints/running-process-env-literal/src/lib.rs index 3bdd5d46..84816bcb 100644 --- a/lints/running-process-env-literal/src/lib.rs +++ b/lints/running-process-env-literal/src/lib.rs @@ -202,5 +202,12 @@ fn inside_env_module(cx: &LateContext<'_>, module: DefId) -> bool { #[test] fn ui() { + // dylint_testing builds this library with a nested `cargo build` and then + // loads it from `/debug/`. On Windows `soldr cargo` exports + // CARGO_BUILD_TARGET (its MSVC-default policy), which the nested build + // inherits and which moves the library to `//debug/`, + // so the fixture failed with "could not read ...dll". The lint library is + // a host cdylib, so building it for the host is always correct. + std::env::remove_var("CARGO_BUILD_TARGET"); dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui"); } diff --git a/lints/running-process-platform-boundary/src/lib.rs b/lints/running-process-platform-boundary/src/lib.rs index 293e22ab..7d35fa6e 100644 --- a/lints/running-process-platform-boundary/src/lib.rs +++ b/lints/running-process-platform-boundary/src/lib.rs @@ -639,5 +639,12 @@ impl EarlyLintPass for EnforcePlatformBoundary { #[test] fn ui() { + // dylint_testing builds this library with a nested `cargo build` and then + // loads it from `/debug/`. On Windows `soldr cargo` exports + // CARGO_BUILD_TARGET (its MSVC-default policy), which the nested build + // inherits and which moves the library to `//debug/`, + // so the fixture failed with "could not read ...dll". The lint library is + // a host cdylib, so building it for the host is always correct. + std::env::remove_var("CARGO_BUILD_TARGET"); dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui"); } diff --git a/tests/test_ci_local_gate.py b/tests/test_ci_local_gate.py index 09b493b3..6b1e6994 100644 --- a/tests/test_ci_local_gate.py +++ b/tests/test_ci_local_gate.py @@ -5,7 +5,7 @@ import json import unittest from dataclasses import dataclass -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath from unittest.mock import patch from ci import local_gate @@ -48,6 +48,17 @@ def test_local_execution_uses_quick_dispatch_before_attesting(self) -> None: self.document["event"] = "workflow_dispatch" self.assertIsNone(self.error()) + def test_posix_receipt_workspace_counts_as_located_on_every_host(self) -> None: + # The receipt comes from a Linux container; Windows pathlib reports + # `/work/repo` as not absolute (no drive), which failed every real + # receipt on the Windows preflight lanes. + for flavour in (PurePosixPath, PureWindowsPath): + with self.subTest(flavour=flavour.__name__): + self.assertTrue(local_gate._is_rooted(flavour("/work/repo"))) + self.assertFalse(local_gate._is_rooted(flavour("relative/repo"))) + self.assertFalse(local_gate._is_rooted(PureWindowsPath("C:relative"))) + self.assertTrue(local_gate._is_rooted(PureWindowsPath("C:\\work\\repo"))) + def test_integration_label_preserves_additional_remote_coverage(self) -> None: trust = (local_gate.ROOT / "local-gate.toml").read_text().split("[gate.trust]", 1)[1] labels = next(line for line in trust.splitlines() if line.startswith("full-labels =")) diff --git a/tests/test_service.py b/tests/test_service.py index 20ea2554..d581c63a 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -44,9 +44,10 @@ class _Runtime(unittest.TestCase): def setUp(self) -> None: self._tmp = tempfile.TemporaryDirectory(prefix="rpsvc-") self.runtime = Path(self._tmp.name) - - def tearDown(self) -> None: - self._tmp.cleanup() + # Cleanups run last-registered-first, after tearDown. Registering the + # directory first makes it the very last thing removed: a client + # closed in its own cleanup can no longer respawn a service into it. + self.addCleanup(self._tmp.cleanup) def spec(self, name: str = "svc-test", **kwargs): from running_process_service import ServiceSpec @@ -255,14 +256,17 @@ def setUp(self) -> None: self.script = self.runtime / "service.py" self.script.write_text(SERVICE_SCRIPT, encoding="utf-8") self.pids: list[int] = [] + # Before the directory goes, after every client is closed (clients + # register their close later, so it runs first): otherwise a live + # supervisor sees the kill and respawns a service mid-rmtree. + self.addCleanup(self._kill_services) - def tearDown(self) -> None: + def _kill_services(self) -> None: for pid in self.pids: try: os.kill(pid, 9) except OSError: pass - super().tearDown() def spawns(self) -> list[int]: log = self.runtime / "spawns.log"