Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/pr-core-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
19 changes: 16 additions & 3 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Expand Down
7 changes: 7 additions & 0 deletions crates/pecos-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}

Expand Down
131 changes: 120 additions & 11 deletions crates/pecos-cli/src/cli/rust_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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<Vec<&'a str>> {
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();

Expand Down Expand Up @@ -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",
Expand All @@ -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.
Expand Down Expand Up @@ -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!(
Expand Down
44 changes: 44 additions & 0 deletions scripts/ci/ensure-nextest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/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"
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
"$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
Loading