From 6c3f292cf96a7fc5337ea94599f1c6a7f98b2fd3 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Thu, 3 Sep 2026 23:32:28 -0600 Subject: [PATCH 1/2] Run the PR gate's workspace Rust tests through cargo-nextest --- .github/workflows/pr-core-gate.yml | 9 +- Justfile | 19 +++- crates/pecos-cli/src/cli.rs | 7 ++ crates/pecos-cli/src/cli/rust_cmd.rs | 131 ++++++++++++++++++++++++--- scripts/ci/ensure-nextest.sh | 38 ++++++++ 5 files changed, 188 insertions(+), 16 deletions(-) create mode 100755 scripts/ci/ensure-nextest.sh diff --git a/.github/workflows/pr-core-gate.yml b/.github/workflows/pr-core-gate.yml index 615718327..7ff1d4834 100644 --- a/.github/workflows/pr-core-gate.yml +++ b/.github/workflows/pr-core-gate.yml @@ -228,6 +228,10 @@ jobs: export PATH="$HOME/.cargo/bin:$PATH" rustup show + - name: Install cargo-nextest + if: steps.detect.outputs.run == 'true' + run: bash scripts/ci/ensure-nextest.sh + - name: Verify cmake is available (MWPF decoder build) if: steps.detect.outputs.run == 'true' run: cmake --version @@ -262,8 +266,9 @@ jobs: - name: Run core Rust tests if: steps.detect.outputs.run == 'true' # debug profile keeps this sentinel fast; the full release matrix runs - # post-merge on push via rust-test.yml. - run: just rstest debug + # post-merge on push via rust-test.yml. nextest runs the ~320 workspace + # test binaries in parallel (cargo test runs them one after another). + run: just rstest debug nextest - name: Core Rust gate passed (no core-relevant changes) if: steps.detect.outputs.run != 'true' diff --git a/Justfile b/Justfile index cf9e55687..5eec2c061 100644 --- a/Justfile +++ b/Justfile @@ -363,13 +363,26 @@ pytest-zluppy: python-ci-smoke profile="debug": (validate-profile "python-ci-smoke" profile) (python-ci-build profile) uv run --frozen python -c "from importlib.metadata import version; import pecos, pecos_rslib, pecos_rslib_llvm; print({'pecos': pecos.__version__, 'pecos_rslib': pecos_rslib.__version__, 'pecos_rslib_llvm': version('pecos-rslib-llvm')})" -# Run Rust tests (CUDA-aware; mode: dev/debug, release, native) +# Run Rust tests (CUDA-aware; mode: dev/debug, release, native; runner: cargo +# or nextest -- nextest runs the workspace test binaries in parallel and needs +# cargo-nextest on PATH, see scripts/ci/ensure-nextest.sh) [group('test')] -rstest mode="release": _msvc-bootstrap (validate-test-mode "rstest" mode) +rstest mode="release" runner="cargo": _msvc-bootstrap (validate-test-mode "rstest" mode) #!/usr/bin/env bash set -euo pipefail MODE="{{mode}}" - {{pecos}} rust test --profile "$MODE" + case "{{runner}}" in + cargo) + {{pecos}} rust test --profile "$MODE" + ;; + nextest) + {{pecos}} rust test --profile "$MODE" --nextest + ;; + *) + echo "Invalid runner: {{runner}} (expected cargo or nextest)" >&2 + exit 2 + ;; + esac # Run all tests (Rust + Python + Julia if available; mode: dev/debug, release, native) [group('test')] diff --git a/crates/pecos-cli/src/cli.rs b/crates/pecos-cli/src/cli.rs index cd99fa6ed..d18a279b8 100644 --- a/crates/pecos-cli/src/cli.rs +++ b/crates/pecos-cli/src/cli.rs @@ -65,6 +65,13 @@ pub enum RustCommands { /// --include-ffi`. #[arg(long)] include_ffi: bool, + + /// Run the workspace test binaries with cargo-nextest, which runs + /// them in parallel instead of one binary after another. Doctests + /// still run through `cargo test --doc` with the same selection. + /// Requires cargo-nextest on PATH (CI: scripts/ci/ensure-nextest.sh). + #[arg(long)] + nextest: bool, }, } diff --git a/crates/pecos-cli/src/cli/rust_cmd.rs b/crates/pecos-cli/src/cli/rust_cmd.rs index 7979eb738..53a5b6a6f 100644 --- a/crates/pecos-cli/src/cli/rust_cmd.rs +++ b/crates/pecos-cli/src/cli/rust_cmd.rs @@ -139,7 +139,8 @@ pub fn run(command: &super::RustCommands) -> Result<()> { super::RustCommands::Test { profile, include_ffi, - } => run_test(*profile, *include_ffi), + nextest, + } => run_test(*profile, *include_ffi, *nextest), } } @@ -452,8 +453,57 @@ fn run_clippy(include_ffi: bool, fix: bool) -> Result<()> { Ok(()) } +/// The cargo invocations for the workspace test phase. +/// +/// With `cargo test` this is one command. With nextest it is two: `cargo +/// nextest run` for the test binaries (nextest schedules tests across all +/// binaries at once, where `cargo test` runs one binary after another) and +/// `cargo test --doc` for the doctests, which nextest does not run. Both use +/// the same package selection and features. nextest's own `--profile` selects +/// a nextest profile, so the cargo profile goes through `--release` / +/// `--cargo-profile`. +fn workspace_test_commands<'a>( + nextest: bool, + profile: super::BuildProfile, + excludes: &[&'a str], +) -> Vec> { + let selection = ["--workspace", "--features=runtime,hugr,neo"]; + let cargo_profile_args: &[&str] = match profile { + super::BuildProfile::Dev | super::BuildProfile::Debug => &[], + super::BuildProfile::Release => &["--release"], + super::BuildProfile::Native => &["--profile", "native"], + }; + let nextest_profile_args: &[&str] = match profile { + super::BuildProfile::Dev | super::BuildProfile::Debug => &[], + super::BuildProfile::Release => &["--release"], + super::BuildProfile::Native => &["--cargo-profile", "native"], + }; + let mut commands = Vec::new(); + if nextest { + // `--locked` is placed explicitly: run_cargo_command_with_rustflags + // only inserts it for the plain cargo subcommands. + let mut run_args = vec!["nextest", "run", "--locked"]; + run_args.extend(selection); + run_args.extend(excludes); + run_args.extend(nextest_profile_args); + commands.push(run_args); + let mut doc_args = vec!["test", "--doc"]; + doc_args.extend(selection); + doc_args.extend(excludes); + doc_args.extend(cargo_profile_args); + commands.push(doc_args); + } else { + let mut args = vec!["test"]; + args.extend(selection); + args.extend(excludes); + args.extend(cargo_profile_args); + commands.push(args); + } + commands +} + /// Run cargo test with GPU-aware feature handling -fn run_test(profile: super::BuildProfile, include_ffi: bool) -> Result<()> { +fn run_test(profile: super::BuildProfile, include_ffi: bool, nextest: bool) -> Result<()> { // Warn about any C++ dependency version differences across crates check_dep_consistency(); @@ -495,14 +545,12 @@ fn run_test(profile: super::BuildProfile, include_ffi: bool) -> Result<()> { // neo = sim() routing to the pecos-neo stack (contract tests) // pecos-cli is excluded here and tested separately below with --features=runtime // to ensure the pecos binary has PHIR/QIS support for integration tests. - let mut args: Vec<&str> = vec!["test", "--workspace", "--features=runtime,hugr,neo"]; - + let mut excludes: Vec<&str> = Vec::new(); for crate_name in FFI_CRATES.iter().chain(PYO3_CDYLIB_TEST_EXCLUDES) { - args.push("--exclude"); - args.push(*crate_name); + excludes.push("--exclude"); + excludes.push(*crate_name); } - - args.extend(&[ + excludes.extend(&[ "--exclude", "pecos-cuquantum", // Requires cuQuantum SDK, test separately if available "--exclude", @@ -513,11 +561,15 @@ fn run_test(profile: super::BuildProfile, include_ffi: bool) -> Result<()> { "pecos-gpu-sims", // Always exclude from workspace test, test separately if GPU available ]); - args.extend(profile_args); reject_static_llvm_workspace_test()?; - if !run(&args) { - return Err(Error::Config("cargo test (workspace) failed".to_string())); + for args in workspace_test_commands(nextest, profile, &excludes) { + if !run(&args) { + return Err(Error::Config(format!( + "cargo {} (workspace) failed", + args.first().copied().unwrap_or("test") + ))); + } } // Test pecos-cli separately with --features=runtime. @@ -605,6 +657,63 @@ fn run_test(profile: super::BuildProfile, include_ffi: bool) -> Result<()> { mod tests { use super::*; + #[test] + fn workspace_test_commands_cargo_test_is_one_command_with_excludes() { + let excludes = ["--exclude", "pecos-cli"]; + let commands = + workspace_test_commands(false, super::super::BuildProfile::Native, &excludes); + assert_eq!(commands.len(), 1); + assert_eq!( + commands[0], + vec![ + "test", + "--workspace", + "--features=runtime,hugr,neo", + "--exclude", + "pecos-cli", + "--profile", + "native" + ] + ); + } + + #[test] + fn workspace_test_commands_nextest_adds_doctests_and_maps_the_cargo_profile() { + let excludes = ["--exclude", "pecos-cli"]; + let commands = workspace_test_commands(true, super::super::BuildProfile::Native, &excludes); + assert_eq!(commands.len(), 2); + assert_eq!( + commands[0], + vec![ + "nextest", + "run", + "--locked", + "--workspace", + "--features=runtime,hugr,neo", + "--exclude", + "pecos-cli", + "--cargo-profile", + "native" + ] + ); + assert_eq!( + commands[1], + vec![ + "test", + "--doc", + "--workspace", + "--features=runtime,hugr,neo", + "--exclude", + "pecos-cli", + "--profile", + "native" + ] + ); + let debug = workspace_test_commands(true, super::super::BuildProfile::Debug, &excludes); + assert!(!debug[0].contains(&"--cargo-profile")); + assert!(!debug[1].contains(&"--profile")); + } + #[test] fn llvm_link_mode_parses_llvm_config_output() { assert_eq!( diff --git a/scripts/ci/ensure-nextest.sh b/scripts/ci/ensure-nextest.sh new file mode 100755 index 000000000..69c6a5e57 --- /dev/null +++ b/scripts/ci/ensure-nextest.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Install the pinned cargo-nextest release into the cargo bin directory so +# `pecos rust test --nextest` (the PR gate's `just rstest debug nextest`) can +# run the workspace test binaries in parallel instead of one after another. +# Prebuilt release, verified against the sha256 nextest publishes next to it; +# a `cargo install` would spend minutes compiling on every run. +# +# Usage: scripts/ci/ensure-nextest.sh +set -euo pipefail + +version="0.9.143" +target="x86_64-unknown-linux-gnu" +sha256="66786b9abe23920d022a182d1416b1bbc8130dd4872a9553d76985a1708dcd1e" + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) ;; + *) + echo "ensure-nextest: only ${target} is pinned; got $(uname -s)-$(uname -m)" >&2 + exit 1 + ;; +esac + +dest="${CARGO_HOME:-$HOME/.cargo}/bin" +if [[ -x "$dest/cargo-nextest" ]] && "$dest/cargo-nextest" --version | grep -q "^cargo-nextest ${version} "; then + echo "cargo-nextest ${version} already installed at $dest" + exit 0 +fi + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +archive="cargo-nextest-${version}-${target}.tar.gz" +curl --proto '=https' --tlsv1.2 -fsSL --retry 3 --retry-all-errors \ + -o "$tmp_dir/$archive" \ + "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-${version}/${archive}" +echo "${sha256} ${tmp_dir}/${archive}" | sha256sum -c - +mkdir -p "$dest" +tar -xzf "$tmp_dir/$archive" -C "$dest" cargo-nextest +"$dest/cargo-nextest" --version From 7d87e986e5a18508285995950357982666c80745 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Thu, 3 Sep 2026 23:35:58 -0600 Subject: [PATCH 2/2] Expose the cargo-nextest install directory to later workflow steps --- scripts/ci/ensure-nextest.sh | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/scripts/ci/ensure-nextest.sh b/scripts/ci/ensure-nextest.sh index 69c6a5e57..9b7f65bd7 100755 --- a/scripts/ci/ensure-nextest.sh +++ b/scripts/ci/ensure-nextest.sh @@ -23,16 +23,22 @@ esac dest="${CARGO_HOME:-$HOME/.cargo}/bin" if [[ -x "$dest/cargo-nextest" ]] && "$dest/cargo-nextest" --version | grep -q "^cargo-nextest ${version} "; then echo "cargo-nextest ${version} already installed at $dest" - exit 0 +else + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + archive="cargo-nextest-${version}-${target}.tar.gz" + curl --proto '=https' --tlsv1.2 -fsSL --retry 3 --retry-all-errors \ + -o "$tmp_dir/$archive" \ + "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-${version}/${archive}" + echo "${sha256} ${tmp_dir}/${archive}" | sha256sum -c - + mkdir -p "$dest" + tar -xzf "$tmp_dir/$archive" -C "$dest" cargo-nextest fi - -tmp_dir="$(mktemp -d)" -trap 'rm -rf "$tmp_dir"' EXIT -archive="cargo-nextest-${version}-${target}.tar.gz" -curl --proto '=https' --tlsv1.2 -fsSL --retry 3 --retry-all-errors \ - -o "$tmp_dir/$archive" \ - "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-${version}/${archive}" -echo "${sha256} ${tmp_dir}/${archive}" | sha256sum -c - -mkdir -p "$dest" -tar -xzf "$tmp_dir/$archive" -C "$dest" cargo-nextest "$dest/cargo-nextest" --version + +# `cargo nextest` is resolved through PATH by cargo, so expose the install +# directory to later workflow steps (a custom CARGO_HOME is not the directory +# ensure-rust.sh adds). +if [[ -n "${GITHUB_PATH:-}" ]]; then + echo "$dest" >>"$GITHUB_PATH" +fi