|
1 | | -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions |
2 | | -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python |
3 | | - |
4 | | -name: Tests for Aragog |
| 1 | +name: Aragog CI Test Suite |
5 | 2 |
|
6 | 3 | on: |
7 | 4 | push: |
|
17 | 14 | - ready_for_review |
18 | 15 | workflow_dispatch: |
19 | 16 |
|
| 17 | +# Push and PR CI: unit tests only, on ubuntu + macos x py 3.12 / 3.13. |
| 18 | +# Targets <5 min wall. Slow tests (full multi-Myr CVODE solves) live in |
| 19 | +# the scheduled nightly workflow `nightly.yml` instead, alongside coverage |
| 20 | +# enforcement against the 95% floor. Running the slow suite or coverage |
| 21 | +# instrumentation on every push burns ~10 min CI time per commit and gives |
| 22 | +# no bug-finding signal that the unit suite doesn't already cover. |
| 23 | +# |
| 24 | +# Trigger scoping: push fires only on main (release-merge sanity check), |
| 25 | +# pull_request fires on PRs into main (feature-branch validation). With |
| 26 | +# both, every commit on a PR runs exactly one matrix sweep instead of two |
| 27 | +# (push + pull_request would otherwise both fire on a feature-branch |
| 28 | +# push that has an open PR). |
| 29 | +# |
| 30 | +# Uses micromamba + conda-forge for the env so SUNDIALS, gfortran, and the |
| 31 | +# scikits-odes binary stack come prebuilt instead of being compiled from |
| 32 | +# source against stock apt/brew (which lag the SUNDIALS major version |
| 33 | +# scikits-odes-sundials 3.x requires). |
| 34 | + |
20 | 35 | permissions: |
21 | | - actions: write |
22 | | - contents: write |
| 36 | + contents: read |
23 | 37 |
|
24 | | -jobs: |
25 | | - build: |
| 38 | +# Cancel superseded runs on the same ref so a stack of feature-branch |
| 39 | +# pushes does not pile up macOS runners (free-tier concurrency is tight). |
| 40 | +# The base ref keeps push to main and pull_request to main in separate |
| 41 | +# groups so the latter cannot cancel a release sanity check. |
| 42 | +concurrency: |
| 43 | + group: ci-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} |
| 44 | + cancel-in-progress: true |
26 | 45 |
|
| 46 | +jobs: |
| 47 | + test-fast: |
| 48 | + name: Aragog-check (${{ matrix.os }}, py${{ matrix.python-version }}) |
27 | 49 | strategy: |
28 | 50 | fail-fast: false |
29 | 51 | matrix: |
30 | | - python-version: ["3.11", "3.12","3.13"] |
31 | 52 | os: [ubuntu-latest, macos-latest] |
| 53 | + python-version: ["3.12", "3.13"] |
32 | 54 |
|
33 | 55 | runs-on: ${{ matrix.os }} |
34 | 56 |
|
| 57 | + defaults: |
| 58 | + run: |
| 59 | + shell: bash -el {0} |
| 60 | + |
35 | 61 | steps: |
36 | | - - uses: actions/checkout@v5 |
37 | | - |
38 | | - - name: Set up Python ${{ matrix.python-version }} |
39 | | - uses: actions/setup-python@v5 |
40 | | - with: |
41 | | - python-version: ${{ matrix.python-version }} |
42 | | - |
43 | | - - name: Install dependencies |
44 | | - run: | |
45 | | - python -m pip install --upgrade pip |
46 | | - python -m pip install flake8 pytest pytest-cov pytest-dependency |
47 | | - if [ -f pyproject.toml ]; then pip install .; fi |
48 | | -
|
49 | | - - name: Lint with flake8 |
50 | | - run: | |
51 | | - # stop the build if there are Python syntax errors or undefined names |
52 | | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics |
53 | | - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide |
54 | | - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics |
55 | | -
|
56 | | - - name: Test with pytest |
57 | | - run: pytest --cov-report term --cov-report markdown-append:$GITHUB_STEP_SUMMARY --cov=aragog tests/ |
| 62 | + - uses: actions/checkout@v5 |
| 63 | + with: |
| 64 | + fetch-depth: 0 |
| 65 | + |
| 66 | + - name: Set up conda env (sundials from conda-forge) |
| 67 | + uses: mamba-org/setup-micromamba@v2 |
| 68 | + with: |
| 69 | + environment-name: aragog-test |
| 70 | + create-args: >- |
| 71 | + python=${{ matrix.python-version }} |
| 72 | + sundials |
| 73 | + condarc: | |
| 74 | + channels: |
| 75 | + - conda-forge |
| 76 | +
|
| 77 | + - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) |
| 78 | + run: | |
| 79 | + pip install pytest pytest-xdist pytest-dependency ruff |
| 80 | + # Install scikits-odes-sundials directly (skip the scikits-odes |
| 81 | + # metapackage). This avoids scikits-odes-daepack, which is a |
| 82 | + # transitive Fortran dep with f2py incompatibility with numpy |
| 83 | + # 2.3+ and needs gfortran on macOS. aragog imports CVODE |
| 84 | + # directly from scikits_odes_sundials.cvode and has no use for |
| 85 | + # the DAE solvers in the metapackage. |
| 86 | + pip install scikits-odes-sundials jax equinox |
| 87 | +
|
| 88 | + - name: Install aragog |
| 89 | + run: | |
| 90 | + pip install -e . |
| 91 | +
|
| 92 | + - name: Fetch SPIDER EOS test data |
| 93 | + run: | |
| 94 | + mkdir -p /tmp/aragog-test-data |
| 95 | + curl -sL -o /tmp/spider_eos.tar.gz \ |
| 96 | + https://github.com/FormingWorlds/aragog/releases/download/test-data-v1/spider_eos_test_data.tar.gz |
| 97 | + tar xzf /tmp/spider_eos.tar.gz -C /tmp/aragog-test-data/ |
| 98 | + echo "ARAGOG_TEST_EOS_DIR=/tmp/aragog-test-data/spider_eos" >> $GITHUB_ENV |
| 99 | +
|
| 100 | + - name: Validate test markers |
| 101 | + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' }} |
| 102 | + run: bash tools/validate_test_structure.sh |
| 103 | + |
| 104 | + - name: Lint with Ruff |
| 105 | + run: | |
| 106 | + ruff check src/ tests/ |
| 107 | + ruff format --check src/ tests/ |
| 108 | +
|
| 109 | + - name: Pre-flight fail_under ratchet check |
| 110 | + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' }} |
| 111 | + env: |
| 112 | + BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }} |
| 113 | + run: | |
| 114 | + # Reject PRs that lower [tool.coverage.report] fail_under below |
| 115 | + # the value on the base branch. The 95% ceiling is the maximum |
| 116 | + # ratchet per ecosystem policy; this check enforces the |
| 117 | + # one-way-only direction (down forbidden) so the gate cannot be |
| 118 | + # weakened to land an under-tested change. |
| 119 | + git fetch --no-tags --depth=1 origin "$BASE_REF" || true |
| 120 | + python - <<'PY' |
| 121 | + import os |
| 122 | + import pathlib |
| 123 | + import subprocess |
| 124 | + import sys |
| 125 | + import tomllib |
| 126 | +
|
| 127 | + base_ref = os.environ.get('BASE_REF', 'main') |
| 128 | + out = subprocess.run( |
| 129 | + ['git', 'show', f'origin/{base_ref}:pyproject.toml'], |
| 130 | + capture_output=True, |
| 131 | + text=True, |
| 132 | + check=False, |
| 133 | + ) |
| 134 | + if out.returncode != 0: |
| 135 | + print(f'Could not read pyproject.toml from origin/{base_ref}; skipping check.') |
| 136 | + sys.exit(0) |
| 137 | +
|
| 138 | + base = tomllib.loads(out.stdout) |
| 139 | + head = tomllib.loads(pathlib.Path('pyproject.toml').read_text()) |
| 140 | +
|
| 141 | + def fail_under(d): |
| 142 | + try: |
| 143 | + return float(d['tool']['coverage']['report']['fail_under']) |
| 144 | + except KeyError: |
| 145 | + return None |
| 146 | +
|
| 147 | + base_v = fail_under(base) |
| 148 | + head_v = fail_under(head) |
| 149 | + print(f'fail_under: base={base_v} head={head_v}') |
| 150 | +
|
| 151 | + # Base has no [tool.coverage.report].fail_under: nothing to |
| 152 | + # ratchet against. The first PR that introduces the gate is |
| 153 | + # allowed regardless of head_v. |
| 154 | + if base_v is None: |
| 155 | + print( |
| 156 | + f'origin/{base_ref} has no [tool.coverage.report].fail_under; ' |
| 157 | + 'no ratchet to enforce. PASS.' |
| 158 | + ) |
| 159 | + sys.exit(0) |
| 160 | +
|
| 161 | + # Head removed the gate that base had: that's a decrease. |
| 162 | + if head_v is None: |
| 163 | + print( |
| 164 | + f'ERROR: head removed [tool.coverage.report].fail_under ' |
| 165 | + f'(base had {base_v}). The gate is one-way only.' |
| 166 | + ) |
| 167 | + sys.exit(1) |
| 168 | +
|
| 169 | + if head_v < base_v: |
| 170 | + print( |
| 171 | + f'ERROR: fail_under decreased ({head_v} < {base_v}). ' |
| 172 | + 'The coverage gate is one-way only; ask a maintainer if a ' |
| 173 | + 'downward adjustment is required.' |
| 174 | + ) |
| 175 | + sys.exit(1) |
| 176 | + print('OK: fail_under is non-decreasing.') |
| 177 | + PY |
| 178 | +
|
| 179 | + - name: Run unit tests |
| 180 | + run: | |
| 181 | + pytest -m "unit and not slow" -n auto -o "addopts=" \ |
| 182 | + -o "junit_family=legacy" --junitxml=junit.xml |
| 183 | +
|
| 184 | + - name: Upload test results to Codecov |
| 185 | + if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' }} |
| 186 | + uses: codecov/test-results-action@v1 |
| 187 | + with: |
| 188 | + token: ${{ secrets.CODECOV_TOKEN }} |
| 189 | + slug: FormingWorlds/Aragog |
| 190 | + files: junit.xml |
0 commit comments