From 3ea278648cdb7c3dfa0f70493997cb3ce825cfbc Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 19 Jul 2026 10:06:41 +0800 Subject: [PATCH 1/2] ci: cut test wall time without losing coverage The slowest matrix cells (windows-latest, macos-15-intel) spend ~12 of their ~20 minutes in nextest, and the log timings show most of the tail is serialized: - The slowest tests (pgo, conda, uniffi, uv multi-python) were scheduled last, so the run drains into a single-test tail. Schedule them first via nextest priority overrides (nextest >= 0.9.91). - threads-required = num-cpus made every uniffi test run exclusively, ~4 min of serialized wall time per job. Relax it to 2 threads. - test_integration_conda created five conda envs (3.10-3.14, 152s on windows); oldest + newest keeps the coverage for half the cost. - Enable the existing faster-tests feature in CI so test wheels are stored uncompressed and stripped; the deflate path stays covered by unit tests and the release-binary jobs (auditwheel, docker, cross). - Disable debuginfo on all platforms instead of only Windows: smaller artifacts mean faster links, wheel packing and pip installs. - Run the zig cross-compile step once per host OS instead of twice (it was running for both 3.9 and pypy); zig mostly exercises host-OS-specific plumbing. macos-latest keeps its pypy run since it has no 3.9 cell. - Skip miniconda setup on the -dev cells; the conda tests create their own envs independent of the matrix python, so once per OS is enough and the -dev cells skip the ~2 min setup. See #2189 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NNNLg7XZnxM8Aiqrzo6c5q --- .config/nextest.toml | 14 ++++++++++++-- .github/workflows/test.yml | 20 +++++++++++++------- tests/common/integration.rs | 5 +++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index eaab0374a..2109ca833 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -3,6 +3,16 @@ slow-timeout = { period = "60s", terminate-after = 30 } [[profile.default.overrides]] -# See https://nexte.st/book/threads-required.html +# Schedule the slowest tests first so they don't serialize at the tail of the +# run: pgo tests build everything twice, conda tests create conda environments, +# uniffi tests build a large dependency tree and uv_multi_python tests build +# for two interpreters. +filter = 'test(/pgo_|_conda|_uniffi_|uv_multi_python/)' +priority = 100 + +[[profile.default.overrides]] +# uniffi builds are heavy, but claiming the whole machine serialized all uniffi +# tests; two threads keeps them from crowding out other tests while still +# allowing overlap. See https://nexte.st/book/threads-required.html filter = 'test(/_uniffi_/)' -threads-required = "num-cpus" +threads-required = 2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8787bbf98..c36bcc55f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,8 +88,11 @@ jobs: uv tool install virtualenv uv tool install twine uv tool install uniffi-bindgen==0.28.0 + # The conda tests create their own conda environments independent of the + # matrix python, so exercising them once per OS (on the non-dev cells) is + # enough; skipping the slow miniconda setup on the -dev cells saves time. - uses: conda-incubator/setup-miniconda@fc2d68f6413eb2d87b895e92f8584b5b94a10167 # v3 - if: ${{ matrix.os != 'windows-11-arm' }} + if: ${{ matrix.os != 'windows-11-arm' && !contains(matrix.python-version, '-dev') }} with: auto-activate-base: "false" activate-environment: "" @@ -130,17 +133,20 @@ jobs: - name: Set MATURIN_TEST_PYTHON shell: bash run: echo "MATURIN_TEST_PYTHON=${{ steps.setup-python.outputs.python-path }}" >> "${GITHUB_ENV}" - # To save cache disk space - - name: Disable debuginfo on Windows - if: startsWith(matrix.os, 'windows') + # Saves cache disk space and speeds up linking and packing/installing the + # test wheels; the test suite doesn't need debuginfo in the artifacts. + - name: Disable debuginfo shell: bash run: echo "RUSTFLAGS=-C debuginfo=0" >> "${GITHUB_ENV}" - name: Install llvm-tools run: rustup component add llvm-tools-preview - name: cargo test - run: cargo nextest run --features password-storage + run: cargo nextest run --features password-storage,faster-tests + # Zig cross compilation mainly exercises host-OS-specific plumbing, so one + # python per OS is enough; macos-latest and windows-11-arm have no 3.9 + # cell, so macos-latest falls back to pypy to keep coverage on arm macs. - name: test cross compiling with zig - if: ${{ !contains(matrix.python-version, '-dev') }} + if: ${{ matrix.python-version == '3.9' || (matrix.os == 'macos-latest' && matrix.python-version == 'pypy3.11') }} shell: bash run: | set -ex @@ -326,7 +332,7 @@ jobs: - name: cargo test run: | # unset GITHUB_ACTIONS env var to disable zig related tests - env -u GITHUB_ACTIONS cargo nextest run --features password-storage + env -u GITHUB_ACTIONS cargo nextest run --features password-storage,faster-tests test-auditwheel: name: Test Auditwheel diff --git a/tests/common/integration.rs b/tests/common/integration.rs index fea785cb2..330349be8 100644 --- a/tests/common/integration.rs +++ b/tests/common/integration.rs @@ -471,9 +471,10 @@ pub fn test_integration_conda( let package_string = package.as_ref().join("Cargo.toml").display().to_string(); // Create environments to build against, prepended with "A" to ensure that integration - // tests are executed with these environments + // tests are executed with these environments. Conda env creation is slow, so only cover + // the oldest and newest supported versions instead of every minor release in between. let mut interpreters = Vec::new(); - for minor in 10..=14 { + for minor in [10, 14] { let (_, venv_python) = create_conda_env(&format!("maturin-{case_id}-3{minor}"), 3, minor)?; interpreters.push(venv_python); } From 23d12a50e9aa73d980e6e792e1851367266fac29 Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 19 Jul 2026 10:15:23 +0800 Subject: [PATCH 2/2] test: never strip symbols in test builds The faster-tests feature was coupled to strip=true in the test harness, and enabling it in CI broke uniffi proc-macro tests: bindings are generated by reading uniffi metadata from the compiled cdylib, which -C strip=symbols removes, producing an empty Python module. pyo3 stub generation introspects artifacts the same way (nextest's fail-fast just cancelled the run before those could fail). Decouple strip from faster-tests: tests always build with strip disabled, matching what CI did before faster-tests was enabled, and faster-tests keeps only its uncompressed-wheel behavior which is where the speedup comes from. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NNNLg7XZnxM8Aiqrzo6c5q --- tests/common/errors.rs | 10 +++++----- tests/common/integration.rs | 6 +++--- tests/common/mod.rs | 5 +++++ tests/common/other.rs | 22 +++++++++++----------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/tests/common/errors.rs b/tests/common/errors.rs index 38694c716..962b5adb3 100644 --- a/tests/common/errors.rs +++ b/tests/common/errors.rs @@ -26,7 +26,7 @@ pub fn pyo3_no_extension_module() -> Result<()> { let options = BuildOptions::try_parse_from(cli)?; let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; let result = BuildOrchestrator::new(&build_context).build_wheels(); @@ -63,7 +63,7 @@ pub fn locked_doesnt_build_without_cargo_lock() -> Result<()> { let options = BuildOptions::try_parse_from(cli)?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); if let Err(err) = result { @@ -100,7 +100,7 @@ pub fn invalid_manylinux_does_not_panic() -> Result<()> { let options: BuildOptions = BuildOptions::try_parse_from(cli)?; let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; let result = BuildOrchestrator::new(&build_context).build_wheels(); @@ -170,7 +170,7 @@ pub fn pypi_compatibility_unsupported_target() -> Result<()> { let options: BuildOptions = BuildOptions::try_parse_from(cli)?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); @@ -207,7 +207,7 @@ pub fn pypi_compatibility_linux_tag() -> Result<()> { let options: BuildOptions = BuildOptions::try_parse_from(cli)?; let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; let result = BuildOrchestrator::new(&build_context).build_wheels(); diff --git a/tests/common/integration.rs b/tests/common/integration.rs index 330349be8..15471c274 100644 --- a/tests/common/integration.rs +++ b/tests/common/integration.rs @@ -317,7 +317,7 @@ pub fn test_integration(case: &IntegrationCase<'_>) -> Result<()> { let options: BuildOptions = BuildOptions::try_parse_from(cli)?; let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .pgo(case.pgo) .build()?; @@ -434,7 +434,7 @@ pub fn test_integration_uv_multi_python(case: &IntegrationCase<'_>) -> Result<() let options = BuildOptions::try_parse_from(cli)?; let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .pgo(case.pgo) .build()?; @@ -505,7 +505,7 @@ pub fn test_integration_conda( let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; let wheels = BuildOrchestrator::new(&build_context).build_wheels()?; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d5d97bece..45748d269 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -85,6 +85,11 @@ pub const CFFI_MIXED_SRC_COPY: TestPackageCopy<'static> = TestPackageCopy { prune_copy_paths: &["test-crates/cffi-mixed-src/src/cffi_mixed_src/cffi_mixed_src"], }; +/// Test builds must not strip symbols: uniffi proc-macro bindings and pyo3 +/// stub generation read metadata from the compiled artifact, which +/// `-C strip=symbols` would remove. +pub const TEST_STRIP: Option = Some(false); + pub fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } diff --git a/tests/common/other.rs b/tests/common/other.rs index ec1e39941..e016b396a 100644 --- a/tests/common/other.rs +++ b/tests/common/other.rs @@ -104,7 +104,7 @@ pub fn test_musl() -> Result { let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; let built_lib = @@ -403,7 +403,7 @@ pub fn abi3_python_interpreter_args() -> Result<()> { ])?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); assert!(result.is_ok()); @@ -419,7 +419,7 @@ pub fn abi3_python_interpreter_args() -> Result<()> { ])?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); assert!(result.is_ok()); @@ -439,7 +439,7 @@ pub fn abi3_python_interpreter_args() -> Result<()> { ])?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); assert!(result.is_err()); @@ -455,7 +455,7 @@ pub fn abi3_python_interpreter_args() -> Result<()> { ])?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); assert!(result.is_err()); @@ -481,7 +481,7 @@ pub fn abi3_without_version() -> Result<()> { let options = BuildOptions::try_parse_from(cli)?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); assert!(result.is_ok()); @@ -513,7 +513,7 @@ pub fn pyo3_cffi_build_script() -> Result<()> { let options = BuildOptions::try_parse_from(cli)?; let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; // Actually build the wheel so the crate's build.rs runs. @@ -540,7 +540,7 @@ pub fn abi3t_without_version() -> Result<()> { let options = BuildOptions::try_parse_from(cli)?; let result = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build(); assert!(result.is_ok()); @@ -595,7 +595,7 @@ pub fn combined_stable_abi_wheel_selection(unique_name: &str, features: &[&str]) }; let build_context = options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; let python_interpreter = build_context @@ -694,7 +694,7 @@ pub fn test_unreadable_dir() -> Result<()> { let wheel_context = wheel_options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .build()?; let wheel_result = BuildOrchestrator::new(&wheel_context).build_wheels(); @@ -762,7 +762,7 @@ pub fn test_build_wheels_from_sdist(package: impl AsRef, unique_name: &str }; let wheel_context = wheel_options .into_build_context() - .strip(Some(cfg!(feature = "faster-tests"))) + .strip(crate::common::TEST_STRIP) .editable(false) .pyproject_toml_path(Some(pyproject_toml)) .build()?;