Skip to content

Commit e352b6a

Browse files
committed
Use cargo-nextest for Rust tests
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 5e7d9f6 commit e352b6a

18 files changed

Lines changed: 284 additions & 44 deletions

File tree

.cargo/mutants.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
test_tool = "nextest"
4+
additional_cargo_test_args = ["--profile=mutants"]
5+
cap_lints = false

.config/nextest.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
nextest-version = "0.9.133"
4+
5+
[profile.ci]
6+
fail-fast = false
7+
failure-output = "immediate-final"
8+
9+
[profile.mutants]
10+
fail-fast = true
11+
failure-output = "final"

.github/workflows/ci-rust-python-package.yaml

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ on:
77
- "Makefile"
88
- "Cargo.toml"
99
- "Cargo.lock"
10+
- ".cargo/**"
11+
- ".config/nextest.toml"
1012
- "deny.toml"
1113
- "crates/**"
1214
- "README.md"
@@ -23,6 +25,8 @@ on:
2325
- "Makefile"
2426
- "Cargo.toml"
2527
- "Cargo.lock"
28+
- ".cargo/**"
29+
- ".config/nextest.toml"
2630
- "deny.toml"
2731
- "crates/**"
2832
- "README.md"
@@ -115,6 +119,11 @@ jobs:
115119
rustc --version
116120
cargo --version
117121
122+
- name: Install cargo-nextest
123+
run: |
124+
cargo install cargo-nextest --version 0.9.133 --locked
125+
cargo nextest --version
126+
118127
- name: Install uv
119128
run: python -m pip install uv==0.9.30 maturin==1.12.6
120129

@@ -125,11 +134,15 @@ jobs:
125134
- name: Plugin CI build verification
126135
if: matrix.os == 'ubuntu-latest'
127136
working-directory: plugins/rust/python-package/${{ matrix.plugin }}
137+
env:
138+
NEXTEST_PROFILE: ci
128139
run: make ci-build
129140

130141
- name: Plugin CI verification
131142
if: matrix.os != 'ubuntu-latest'
132143
working-directory: plugins/rust/python-package/${{ matrix.plugin }}
144+
env:
145+
NEXTEST_PROFILE: ci
133146
run: make ci
134147

135148
security-policy:
@@ -150,6 +163,50 @@ jobs:
150163
- name: Run cargo deny
151164
run: cargo deny check --config deny.toml
152165

166+
mutation-testing:
167+
needs: validate-and-detect
168+
if: github.event_name == 'pull_request' && needs.validate-and-detect.outputs.has_plugins == 'true'
169+
strategy:
170+
fail-fast: false
171+
matrix:
172+
cargo_package: ${{ fromJson(needs.validate-and-detect.outputs.cargo_packages) }}
173+
runs-on: ubuntu-latest
174+
defaults:
175+
run:
176+
shell: bash
177+
steps:
178+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
179+
with:
180+
fetch-depth: 0
181+
182+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
183+
with:
184+
python-version: "3.12"
185+
186+
- name: Verify Rust toolchain
187+
run: |
188+
rustc --version
189+
cargo --version
190+
191+
- name: Install Rust mutation testing tooling
192+
run: |
193+
cargo install cargo-nextest --version 0.9.133 --locked
194+
cargo nextest --version
195+
cargo install cargo-mutants --version 27.0.0 --locked
196+
cargo mutants --version
197+
198+
- name: Create mutation diff
199+
env:
200+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
201+
HEAD_SHA: ${{ github.sha }}
202+
run: git diff "${BASE_SHA}..${HEAD_SHA}" -- '*.rs' > cargo-mutants.diff
203+
204+
- name: Run cargo-mutants with nextest
205+
env:
206+
CARGO_PACKAGE: ${{ matrix.cargo_package }}
207+
PYO3_PYTHON: python
208+
run: cargo mutants -p "${CARGO_PACKAGE}" --in-diff cargo-mutants.diff
209+
153210
coverage:
154211
needs: validate-and-detect
155212
if: needs.validate-and-detect.outputs.has_plugins == 'true'
@@ -168,13 +225,16 @@ jobs:
168225
run: |
169226
rustup component add llvm-tools-preview
170227
cargo install cargo-llvm-cov --version 0.8.4 --locked
228+
cargo install cargo-nextest --version 0.9.133 --locked
229+
cargo nextest --version
171230
172231
- name: Install Python build tooling
173232
run: python -m pip install uv==0.9.30 maturin==1.12.6
174233

175234
- name: Generate Rust coverage report
176235
env:
177236
CARGO_PACKAGES: ${{ needs.validate-and-detect.outputs.cargo_packages }}
237+
NEXTEST_PROFILE: ci
178238
PLUGINS: ${{ needs.validate-and-detect.outputs.plugins }}
179239
PYO3_PYTHON: python
180240
run: |
@@ -186,6 +246,8 @@ jobs:
186246
cargo_args+=("-p" "${package}")
187247
done
188248
cargo llvm-cov clean --workspace
249+
mkdir -p coverage
250+
cargo llvm-cov nextest --no-report "${cargo_args[@]}" -P "${NEXTEST_PROFILE}"
189251
eval "$(cargo llvm-cov show-env --sh)"
190252
export CARGO_TARGET_DIR="${CARGO_LLVM_COV_TARGET_DIR}/llvm-cov-target"
191253
export CARGO_LLVM_COV_BUILD_DIR="${CARGO_TARGET_DIR}"
@@ -194,7 +256,6 @@ jobs:
194256
for plugin in "${plugins[@]}"; do
195257
(cd "plugins/rust/python-package/${plugin}" && make sync && uv run maturin develop)
196258
done
197-
cargo test "${cargo_args[@]}"
198259
for plugin in "${plugins[@]}"; do
199260
(cd "plugins/rust/python-package/${plugin}" && make test-integration)
200261
done

.github/workflows/ci-scaffold-generator.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ jobs:
2727
test:
2828
runs-on: ubuntu-latest
2929
steps:
30-
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
30+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
3131

32-
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
32+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
3333
with:
3434
python-version: "3.12"
3535

@@ -52,6 +52,11 @@ jobs:
5252
rustc --version
5353
cargo --version
5454
55+
- name: Install cargo-nextest
56+
run: |
57+
cargo install cargo-nextest --version 0.9.133 --locked
58+
cargo nextest --version
59+
5560
- name: Generate default plugin (tool_pre_invoke)
5661
run: |
5762
python3 tools/scaffold_plugin.py --non-interactive \

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
.PHONY: help plugins-list plugins-validate plugin-test plugin-scaffold plugin-scaffold-help
1+
.PHONY: help plugins-list plugins-validate plugin-test plugin-mutants plugin-mutants-list plugin-scaffold plugin-scaffold-help
22

33
help:
4-
@printf "plugins-list\nplugins-validate\nplugin-test PLUGIN=<slug>\nplugin-scaffold\nplugin-scaffold-help\n"
4+
@printf "plugins-list\nplugins-validate\nplugin-test PLUGIN=<slug>\nplugin-mutants PLUGIN=<slug>\nplugin-mutants-list PLUGIN=<slug>\nplugin-scaffold\nplugin-scaffold-help\n"
55

66
plugins-list:
77
@python3 tools/plugin_catalog.py list .
@@ -14,6 +14,14 @@ plugin-test:
1414
@test -n "$(PLUGIN)" || (echo "Set PLUGIN=<slug>" && exit 1)
1515
@cd plugins/rust/python-package/$(PLUGIN) && make sync && make ci
1616

17+
plugin-mutants:
18+
@test -n "$(PLUGIN)" || (echo "Set PLUGIN=<slug>" && exit 1)
19+
cargo mutants -p "$(PLUGIN)"
20+
21+
plugin-mutants-list:
22+
@test -n "$(PLUGIN)" || (echo "Set PLUGIN=<slug>" && exit 1)
23+
cargo mutants --list -p "$(PLUGIN)"
24+
1725
plugin-scaffold:
1826
@python3 -m pip install --quiet jinja2 2>/dev/null || pip install --quiet jinja2 2>/dev/null || true
1927
@python3 tools/scaffold_plugin.py

TESTING.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ Each plugin has its own Rust and Python test suite.
3030
```bash
3131
cd plugins/rust/python-package/rate_limiter
3232
uv sync --dev
33+
cargo install cargo-nextest --version 0.9.133 --locked
3334
make install
3435
make test-all
3536
```
3637

38+
Set `NEXTEST_PROFILE=ci` to use the repository CI profile locally. The CI profile is defined in `.config/nextest.toml`; it disables fail-fast so all Rust test failures are reported in one run.
39+
3740
Equivalent repo-level helper:
3841

3942
```bash
@@ -51,6 +54,7 @@ To run the same coverage check locally for all managed Rust plugins:
5154
```bash
5255
rustup component add llvm-tools-preview
5356
cargo install cargo-llvm-cov --version 0.8.4 --locked
57+
cargo install cargo-nextest --version 0.9.133 --locked
5458
mkdir -p coverage
5559
CARGO_PACKAGES="$(python3 tools/plugin_catalog.py ci-selection-field . all '' '' cargo_packages)"
5660
PLUGINS="$(python3 tools/plugin_catalog.py ci-selection-field . all '' '' plugins)"
@@ -61,6 +65,7 @@ for package in "${cargo_packages[@]}"; do
6165
cargo_args+=("-p" "${package}")
6266
done
6367
cargo llvm-cov clean --workspace
68+
cargo llvm-cov nextest --no-report "${cargo_args[@]}" -P ci
6469
eval "$(cargo llvm-cov show-env --sh)"
6570
export CARGO_TARGET_DIR="${CARGO_LLVM_COV_TARGET_DIR}/llvm-cov-target"
6671
export CARGO_LLVM_COV_BUILD_DIR="${CARGO_TARGET_DIR}"
@@ -69,14 +74,29 @@ mkdir -p "${CARGO_TARGET_DIR}"
6974
for plugin in "${plugins[@]}"; do
7075
(cd "plugins/rust/python-package/${plugin}" && make sync && uv run maturin develop)
7176
done
72-
cargo test "${cargo_args[@]}"
7377
for plugin in "${plugins[@]}"; do
7478
(cd "plugins/rust/python-package/${plugin}" && make test-integration)
7579
done
7680
env -u CARGO_TARGET_DIR -u CARGO_LLVM_COV_BUILD_DIR -u CARGO_LLVM_COV_TARGET_DIR -u LLVM_PROFILE_FILE cargo llvm-cov report "${cargo_args[@]}" --cobertura --output-path coverage/cobertura.xml
7781
python3 tools/plugin_catalog.py coverage-check . coverage/cobertura.xml 90.00 "${PLUGINS}"
7882
```
7983

84+
Rust unit tests use `cargo nextest run`. Coverage uses `cargo llvm-cov nextest --no-report` for the Rust test phase, then runs pytest before generating the final report so PyO3 paths stay covered. CI uses the `ci` nextest profile, which disables fail-fast and prints failure output immediately and again at the end. Nextest does not run Rust doctests; this repo currently has no Rust doctest code blocks, so there is no separate doctest step.
85+
86+
Criterion benchmarks are verified in CI with `cargo nextest run --benches`, which runs benchmarks in test mode rather than collecting noisy performance measurements on shared CI runners.
87+
88+
## 4. Mutation Testing
89+
90+
Mutation testing runs in PR CI on Ubuntu for Rust code touched by the pull request diff. It is also available locally through cargo-mutants and runs Rust tests with nextest.
91+
92+
```bash
93+
cargo install cargo-mutants --version 27.0.0 --locked
94+
make plugin-mutants-list PLUGIN=retry_with_backoff
95+
make plugin-mutants PLUGIN=retry_with_backoff
96+
```
97+
98+
`.cargo/mutants.toml` sets `test_tool = "nextest"`, selects the `mutants` nextest profile, and keeps `cap_lints = false` so Rust warnings are not downgraded during mutant builds. The `mutants` profile keeps fail-fast enabled because cargo-mutants only needs one failing test to mark a mutant as caught. CI installs `cargo-mutants` with `cargo install cargo-mutants --version 27.0.0 --locked` and runs `cargo mutants -p "${CARGO_PACKAGE}" --in-diff cargo-mutants.diff`.
99+
80100
## CI Behavior
81101

82102
Repo contract tests run in their own CI workflow. The Rust plugin CI workflow uses the same plugin catalog to select affected plugin build, integration, and coverage jobs.

plugins/rust/python-package/encoded_exfil_detection/Makefile

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ help:
55
PACKAGE_NAME := cpex-encoded-exfil-detection
66
WHEEL_PREFIX := cpex_encoded_exfil_detection
77
CARGO := cargo
8+
CARGO_PACKAGE := encoded_exfil_detection
9+
NEXTEST_PROFILE ?= default
810
STUB_FILES := cpex_encoded_exfil_detection/__init__.pyi cpex_encoded_exfil_detection/encoded_exfil_detection_rust/__init__.pyi
911
WHEEL_DIR := ../../../../target/wheels
1012

@@ -38,7 +40,7 @@ sync:
3840

3941
test-unit:
4042
@echo "$(GREEN)Running encoded_exfil_detection Rust tests...$(NC)"
41-
$(CARGO) test
43+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE)
4244

4345
test: test-unit test-integration
4446

@@ -85,16 +87,16 @@ uninstall:
8587
@uv pip uninstall -y $(PACKAGE_NAME) 2>/dev/null || true
8688

8789
# help: bench - Run Criterion benchmarks
88-
# help: bench-no-run - Compile Criterion benchmarks without running them
90+
# help: bench-no-run - Run Criterion benchmarks in nextest test mode
8991
.PHONY: bench bench-no-run
9092

9193
bench:
9294
@echo "$(GREEN)Running benchmarks...$(NC)"
9395
$(CARGO) bench
9496

9597
bench-no-run:
96-
@echo "$(GREEN)Compiling benchmarks without running them...$(NC)"
97-
$(CARGO) bench --no-run
98+
@echo "$(GREEN)Running Criterion benchmarks in nextest test mode...$(NC)"
99+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --benches
98100

99101
.PHONY: clean clean-all
100102

plugins/rust/python-package/pii_filter/Makefile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ help:
55
PACKAGE_NAME := cpex-pii-filter
66
WHEEL_PREFIX := cpex_pii_filter
77
CARGO := cargo
8+
CARGO_PACKAGE := pii_filter
9+
NEXTEST_PROFILE ?= default
810
STUB_FILES := cpex_pii_filter/__init__.pyi cpex_pii_filter/pii_filter_rust/__init__.pyi
911
WHEEL_DIR := ../../../../target/wheels
1012

@@ -39,13 +41,13 @@ sync:
3941

4042
test-unit:
4143
@echo "$(GREEN)Running pii_filter Rust tests...$(NC)"
42-
$(CARGO) test
44+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE)
4345

4446
test: test-unit test-integration
4547

4648
test-verbose:
4749
@echo "$(GREEN)Running pii_filter Rust tests (verbose)...$(NC)"
48-
$(CARGO) test -- --nocapture
50+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --no-capture
4951

5052
test-python:
5153
$(MAKE) test-integration
@@ -90,7 +92,7 @@ uninstall:
9092
@uv pip uninstall -y $(PACKAGE_NAME) 2>/dev/null || true
9193

9294
# help: bench - Run Criterion benchmarks
93-
# help: bench-no-run - Compile Criterion benchmarks without running them
95+
# help: bench-no-run - Run Criterion benchmarks in nextest test mode
9496
# help: bench-compare - Compare against saved baseline
9597
.PHONY: bench bench-no-run bench-baseline bench-compare
9698

@@ -99,8 +101,8 @@ bench:
99101
$(CARGO) bench
100102

101103
bench-no-run:
102-
@echo "$(GREEN)Compiling benchmarks without running them...$(NC)"
103-
$(CARGO) bench --no-run
104+
@echo "$(GREEN)Running Criterion benchmarks in nextest test mode...$(NC)"
105+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --benches
104106

105107
bench-baseline:
106108
$(CARGO) bench --bench pii_filter -- --save-baseline main

plugins/rust/python-package/rate_limiter/Makefile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ help:
1313
PACKAGE_NAME := cpex-rate-limiter
1414
WHEEL_PREFIX := cpex_rate_limiter
1515
CARGO := cargo
16+
CARGO_PACKAGE := rate_limiter
17+
NEXTEST_PROFILE ?= default
1618
STUB_FILES := cpex_rate_limiter/__init__.pyi cpex_rate_limiter/rate_limiter_rust/__init__.pyi
1719
WHEEL_DIR := ../../../../target/wheels
1820

@@ -53,13 +55,13 @@ sync:
5355

5456
test-unit:
5557
@echo "$(GREEN)Running rate_limiter Rust tests...$(NC)"
56-
$(CARGO) test
58+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE)
5759

5860
test: test-unit test-integration
5961

6062
test-verbose:
6163
@echo "$(GREEN)Running rate_limiter Rust tests (verbose)...$(NC)"
62-
$(CARGO) test -- --nocapture
64+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --no-capture
6365

6466
test-python:
6567
$(MAKE) test-integration
@@ -110,7 +112,7 @@ uninstall:
110112
# 📊 BENCHMARKS
111113
# =============================================================================
112114
# help: bench - Run Criterion benchmarks
113-
# help: bench-no-run - Compile Criterion benchmarks without running them
115+
# help: bench-no-run - Run Criterion benchmarks in nextest test mode
114116
# help: bench-compare - Compare against saved baseline
115117
.PHONY: bench bench-no-run bench-baseline bench-compare
116118

@@ -119,8 +121,8 @@ bench:
119121
$(CARGO) bench
120122

121123
bench-no-run:
122-
@echo "$(GREEN)Compiling benchmarks without running them...$(NC)"
123-
$(CARGO) bench --no-run
124+
@echo "$(GREEN)Running Criterion benchmarks in nextest test mode...$(NC)"
125+
$(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --benches
124126

125127
bench-baseline:
126128
$(CARGO) bench --bench rate_limiter -- --save-baseline main

plugins/rust/python-package/rate_limiter/src/plugin.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ mod tests {
523523

524524
#[test]
525525
fn await_async_tuple_parses_successful_result() -> PyResult<()> {
526+
Python::initialize();
526527
Python::attach(|py| -> PyResult<()> {
527528
let sys = py.import("sys")?;
528529
let asyncio = py.import("asyncio")?;

0 commit comments

Comments
 (0)