name: Rust CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: rust-ci-${{ github.ref }}
cancel-in-progress: true
permissions:
actions: read
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Install Rust 1.93.0
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
components: rustfmt
- name: Cache cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Install Rust 1.93.0
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
components: clippy
- name: Cache cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Lint
run: cargo clippy --all-targets --all-features -- -A warnings -D clippy::correctness -D clippy::suspicious
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Install Rust 1.93.0
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
- name: Cache cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Run tests
shell: bash
run: |
set -uo pipefail
# Capture cargo test's exit status without aborting, so the
# forbidden-substring grep below can act as a diagnostic
# classifier on failure rather than a separate gate that trips
# on log output from passing tests that deliberately exercise
# these error paths.
cargo test --all-features -- --nocapture 2>&1 | tee /tmp/cargo-test.log
test_status=${PIPESTATUS[0]}
# The patterns here are literal/ERE substrings that match the
# exact log::warn!/log::error!/Display strings emitted at runtime
# when the solver hits specific degenerate states. Many library
# tests deliberately drive the solver into those states — e.g.
# perfect-separation coverage, seed-exhaustion coverage,
# PIRLS-non-convergence coverage. On a passing run those
# emissions are *expected* test output, not regressions. The
# grep only fires when cargo test itself returned non-zero,
# where the pattern match helps classify the failure mode.
#
# Patterns verified against src/ to match non-test runtime log
# output:
# - "P-IRLS INNER LOOP FAILED" — solver/reml/runtime.rs
# log::warn! emitted by the outer REML cost path when PIRLS
# returns an error.
# - "P-IRLS failed convergence check" — runtime.rs log::error!
# when the inner solve hits max iterations with gradient
# norm still above the acceptable KKT band.
# - "P-IRLS separation/non-convergence at current rho" —
# runtime.rs log::warn! used when PerfectSeparationDetected
# or PirlsDidNotConverge gets converted into +inf/infeasible
# outer evaluations.
# - "Perfect or quasi-perfect separation detected" — Display
# for EstimationError::PerfectSeparationDetected.
# - "seed candidates failed" — aggregate seed-loop error from
# solver/outer_strategy.rs.
# - "no candidate seeds passed outer startup validation" —
# aggregate seed-loop error when started_seeds == 0.
forbidden='P-IRLS INNER LOOP FAILED|P-IRLS failed convergence check|P-IRLS separation/non-convergence at current rho|Perfect or quasi-perfect separation detected|seed candidates failed|no candidate seeds passed outer startup validation'
if [ "$test_status" -ne 0 ]; then
if grep -nE "$forbidden" /tmp/cargo-test.log; then
echo "::error::Forbidden runtime error signatures were found alongside a cargo test failure."
fi
exit "$test_status"
fi
python-tests:
name: Python API tests
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Install Rust 1.93.0
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
- name: Cache cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install maturin
run: python -m pip install --upgrade pip "maturin>=1.7,<2"
- name: Build and install gam extension (maturin build + wheel install)
shell: bash
run: |
set -euo pipefail
rm -rf dist
maturin build --release -i python3 -o dist
python -m pip install --force-reinstall dist/*.whl
- name: Install Python test extras
run: |
python -m pip install \
'pytest>=8' 'numpy>=1.26' 'pandas>=2.0' 'pyarrow>=14' \
'matplotlib>=3.8' 'scikit-learn>=1.4'
- name: Run Python API + pipeline smoke tests
run: python -m pytest tests/test_python_api.py -v --tb=short
- name: Run Python tables tests
run: python -m pytest tests/test_tables.py -v --tb=short
- name: Run benchmark configuration / mapping tests
run: |
python -m pytest \
tests/bench_run_suite_mapping_test.py \
tests/bench_run_suite_optional_imports_test.py \
tests/bench_fuzz_vs_mgcv_test.py \
tests/bench_biobank_scale_runner_test.py \
-v --tb=short
- name: Build gam release binary
run: cargo build --release --bin gam
- name: Run CLI integration scripts (numeric contract checks)
run: |
python tests/integration_marginal_slope.py
python tests/integration_pit_pipeline.py