Tests workflow (PyWavelets/pywt)
The Tests workflow from PyWavelets/pywt, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get caching, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Tests workflow from the PyWavelets/pywt repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Tests
on:
push:
branches:
- v1.**
pull_request:
branches:
- main
- v1.**
concurrency:
# avoid duplicate runs on both pushes and PRs
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# Coloured output for GitHub Actions
FORCE_COLOR: 3
# Some common environment variables for both GNU/Linux and macOS jobs
MPLBACKEND: Agg
CYTHON_TRACE: 1
CYTHONSPEC: cython
NUMPY_MIN: numpy==1.26.4
CYTHON_MIN: cython==3.1.3
jobs:
test_pywavelets_linux:
name: ${{ matrix.runs-on }}-cp${{ matrix.python-version }}-${{ matrix.OPTIONS_NAME }}
runs-on: ${{ matrix.runs-on }}
strategy:
# Ensure that a wheel builder finishes even if another fails
fail-fast: false
matrix:
runs-on: [ubuntu-latest] # Arm runner tested separately, see below
python-version: ["3.12", "3.14"]
MINIMUM_REQUIREMENTS: [0]
USE_SDIST: [0]
REFGUIDE_CHECK: [1]
PIP_FLAGS: [""]
OPTIONS_NAME: ["default"]
include:
# Linux arm64
- runs-on: ubuntu-22.04-arm
python-version: "3.14"
# Linux amd64
- runs-on: ubuntu-latest
python-version: "3.12"
MINIMUM_REQUIREMENTS: 1
OPTIONS_NAME: "minimum-req"
- runs-on: ubuntu-latest
python-version: "3.12"
- runs-on: ubuntu-latest
python-version: "3.14"
USE_SDIST: 1
OPTIONS_NAME: "install-from-sdist"
- runs-on: ubuntu-latest
python-version: "3.14"
PIP_FLAGS: "--pre"
OPTIONS_NAME: "pre-releases"
- runs-on: ubuntu-latest
python-version: "3.14"
OPTIONS_NAME: "editable-install"
steps:
- uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.2
- uses: actions/setup-python@2e3e4b15a884dc73a63f962bff250a855150a234 # v5.5.0
with:
python-version: ${{ matrix.python-version}}
allow-prereleases: true
- name: Build package
env:
VERSION: ${{ matrix.python-version }}
MINIMUM_REQUIREMENTS: ${{ matrix.MINIMUM_REQUIREMENTS }}
PIP_FLAGS: ${{ matrix.PIP_FLAGS }}
USE_SDIST: ${{ matrix.USE_SDIST }}
REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }}
OPTIONS_NAME: ${{ matrix.OPTIONS_NAME }}
run: |
uname -a
df -h
ulimit -a
# ccache -s
which python
python --version
# sudo apt-get install libatlas-base-dev
pip install --upgrade pip build
# Set numpy version first, other packages link against it
if [ "${MINIMUM_REQUIREMENTS}" == "1" ]; then
pip install ${CYTHON_MIN}
pip install ${NUMPY_MIN}
else
pip install ${PIP_FLAGS} cython
pip install ${PIP_FLAGS} numpy
fi
pip install ${PIP_FLAGS} matplotlib pytest
set -o pipefail
if [ "${USE_SDIST}" == "1" ]; then
python -m build --sdist
pip install dist/pyw*.tar.gz -v
elif [ "${REFGUIDE_CHECK}" == "1" ]; then
pip install sphinx numpydoc scipy-doctest
pip install . -v
else
pip install . -v
fi
if [ "${OPTIONS_NAME}" == "editable-install" ]; then
pip install meson-python>=0.16.0 matplotlib pytest ninja
pip install -e . --no-build-isolation
fi
- name: Run tests
env:
USE_SDIST: ${{ matrix.USE_SDIST }}
REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }}
OPTIONS_NAME: ${{ matrix.OPTIONS_NAME }}
run: |
set -o pipefail
# Move out of source directory to avoid finding local pywt
pushd demo
if [ "${USE_SDIST}" == "1" ]; then
pytest --pyargs pywt
python ../pywt/tests/test_doc.py
elif [ "${REFGUIDE_CHECK}" == "1" ]; then
# doctest docstrings
pytest --doctest-modules --pyargs pywt -v --doctest-collect=api
# Run Sphinx HTML docs builder, converting warnings to errors
# Move back out of demo/ to root directory
cd ..
pip install -r util/readthedocs/requirements.txt
sphinx-build -b html -W --keep-going -d _build/doctrees doc/source doc/build
else
pytest --pyargs pywt
fi
popd
test_pywavelets_linux_free_threaded:
name: linux-cp${{ matrix.python-version }}-default
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13t", "3.14t"]
steps:
- uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.2
- uses: actions/setup-python@2e3e4b15a884dc73a63f962bff250a855150a234 # v5.5.0
with:
python-version: ${{ matrix.python-version}}
- name: Build package
run: |
pip install --upgrade pip build
pip install cython numpy meson-python ninja pytest
pip install . -v --no-build-isolation
- name: Run tests
run: |
# Move out of source directory to avoid finding local pywt
cd demo
pytest --pyargs pywt
test_pywavelets_macos:
name: macos-cp${{ matrix.python-version }}-${{ matrix.OPTIONS_NAME }}
runs-on: macos-latest
strategy:
# Ensure that a wheel builder finishes even if another fails
fail-fast: false
matrix:
python-version: ["3.12", "3.14"]
MINIMUM_REQUIREMENTS: [0]
USE_SDIST: [0]
REFGUIDE_CHECK: [0]
PIP_FLAGS: [""]
OPTIONS_NAME: ["default"]
include:
- python-version: "3.12"
MINIMUM_REQUIREMENTS: 1
OPTIONS_NAME: "osx-minimum-req"
- python-version: "3.14"
PIP_FLAGS: "--pre"
OPTIONS_NAME: "pre-releases"
steps:
- uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.2
- uses: actions/setup-python@2e3e4b15a884dc73a63f962bff250a855150a234 # v5.5.0
with:
python-version: ${{ matrix.python-version}}
allow-prereleases: true
- name: Build package
env:
VERSION: ${{ matrix.python-version }}
MINIMUM_REQUIREMENTS: ${{ matrix.MINIMUM_REQUIREMENTS }}
PIP_FLAGS: ${{ matrix.PIP_FLAGS }}
USE_SDIST: ${{ matrix.USE_SDIST }}
REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }}
CC: /usr/bin/clang
CXX: /usr/bin/clang++
run: |
uname -a
df -h
ulimit -a
which python
python --version
pip install --upgrade pip build
# Set numpy version first, other packages link against it
if [ "${MINIMUM_REQUIREMENTS}" == "1" ]; then
pip install ${CYTHON_MIN} ${NUMPY_MIN}
else
pip install ${PIP_FLAGS} cython numpy
fi
pip install ${PIP_FLAGS} matplotlib pytest
set -o pipefail
if [ "${USE_SDIST}" == "1" ]; then
python -m build --sdist
pip install pywavelets* -v
elif [ "${REFGUIDE_CHECK}" == "1" ]; then
pip install sphinx numpydoc scipy-doctest
pip install . -v
else
pip install . -v
fi
- name: Run tests
env:
PIP_FLAGS: ${{ matrix.PIP_FLAGS }}
USE_SDIST: ${{ matrix.USE_SDIST }}
REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }}
run: |
# Move out of source directory to avoid finding local pywt
pushd demo
if [ "${USE_SDIST}" == "1" ]; then
pytest --pyargs pywt
python ../pywt/tests/test_doc.py
elif [ "${REFGUIDE_CHECK}" == "1" ]; then
# doctests docstrings
pytest --doctest-modules --doctest-only-doctests=true --pyargs pywt -v --doctest-collect=api
pytest --doctest-modules --doctest-only-doctests=true --pyargs pywt.data -v
else
pytest --pyargs pywt
fi
popd
clang_ASan_UBSan:
name: Test under ASan and UBSan
runs-on: macos-latest
steps:
- uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.2.2
with:
submodules: recursive
fetch-tags: true
persist-credentials: false
- name: Set up pyenv
run: |
git clone https://github.com/pyenv/pyenv.git "$HOME/.pyenv"
PYENV_ROOT="$HOME/.pyenv"
PYENV_BIN="$PYENV_ROOT/bin"
PYENV_SHIMS="$PYENV_ROOT/shims"
echo "$PYENV_BIN" >> $GITHUB_PATH
echo "$PYENV_SHIMS" >> $GITHUB_PATH
echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV
- name: Set up LLVM
run: |
brew install llvm@19
LLVM_PREFIX=$(brew --prefix llvm@19)
echo CC="$LLVM_PREFIX/bin/clang" >> $GITHUB_ENV
echo CXX="$LLVM_PREFIX/bin/clang++" >> $GITHUB_ENV
echo LDFLAGS="-L$LLVM_PREFIX/lib" >> $GITHUB_ENV
echo CPPFLAGS="-I$LLVM_PREFIX/include" >> $GITHUB_ENV
- name: Build Python with AddressSanitizer
run: |
CONFIGURE_OPTS="--with-address-sanitizer" pyenv install 3.14
pyenv global 3.14
- name: Install NumPy dependencies from PyPI
run: |
pip install meson-python ninja cython
- name: Build NumPy with ASan
run: |
pip install numpy --no-binary numpy --no-build-isolation -Csetup-args="-Db_sanitize=address" -v
- name: Install dependencies from PyPI
run: |
pip install spin pytest pytest-timeout
- name: Build PyWavelets with ASan and UBSan
run: |
export CFLAGS=-fno-sanitize=function # suppressed upstream, see cython#7437
spin build -- -Db_sanitize=address,undefined -Db_lundef=false
- name: Test
run: |
# pass -s to pytest to see ASAN errors and warnings, otherwise pytest captures them
ASAN_OPTIONS=detect_leaks=0:symbolize=1:strict_init_order=true:allocator_may_return_null=1:use_sigaltstack=0 \
UBSAN_OPTIONS=halt_on_error=1 \
spin test -- -v -s --timeout=600 --durations=10
The same workflow, on Latchkey
Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.
name: Tests on: push: branches: - v1.** pull_request: branches: - main - v1.** concurrency: # avoid duplicate runs on both pushes and PRs group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: # Coloured output for GitHub Actions FORCE_COLOR: 3 # Some common environment variables for both GNU/Linux and macOS jobs MPLBACKEND: Agg CYTHON_TRACE: 1 CYTHONSPEC: cython NUMPY_MIN: numpy==1.26.4 CYTHON_MIN: cython==3.1.3 jobs: test_pywavelets_linux: timeout-minutes: 30 name: ${{ matrix.runs-on }}-cp${{ matrix.python-version }}-${{ matrix.OPTIONS_NAME }} runs-on: ${{ matrix.runs-on }} strategy: # Ensure that a wheel builder finishes even if another fails fail-fast: false matrix: runs-on: [ubuntu-latest] # Arm runner tested separately, see below python-version: ["3.12", "3.14"] MINIMUM_REQUIREMENTS: [0] USE_SDIST: [0] REFGUIDE_CHECK: [1] PIP_FLAGS: [""] OPTIONS_NAME: ["default"] include: # Linux arm64 - runs-on: latchkey-small-arm python-version: "3.14" # Linux amd64 - runs-on: latchkey-small python-version: "3.12" MINIMUM_REQUIREMENTS: 1 OPTIONS_NAME: "minimum-req" - runs-on: latchkey-small python-version: "3.12" - runs-on: latchkey-small python-version: "3.14" USE_SDIST: 1 OPTIONS_NAME: "install-from-sdist" - runs-on: latchkey-small python-version: "3.14" PIP_FLAGS: "--pre" OPTIONS_NAME: "pre-releases" - runs-on: latchkey-small python-version: "3.14" OPTIONS_NAME: "editable-install" steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.2 - uses: actions/setup-python@2e3e4b15a884dc73a63f962bff250a855150a234 # v5.5.0 with: cache: 'pip' python-version: ${{ matrix.python-version}} allow-prereleases: true - name: Build package env: VERSION: ${{ matrix.python-version }} MINIMUM_REQUIREMENTS: ${{ matrix.MINIMUM_REQUIREMENTS }} PIP_FLAGS: ${{ matrix.PIP_FLAGS }} USE_SDIST: ${{ matrix.USE_SDIST }} REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }} OPTIONS_NAME: ${{ matrix.OPTIONS_NAME }} run: | uname -a df -h ulimit -a # ccache -s which python python --version # sudo apt-get install libatlas-base-dev pip install --upgrade pip build # Set numpy version first, other packages link against it if [ "${MINIMUM_REQUIREMENTS}" == "1" ]; then pip install ${CYTHON_MIN} pip install ${NUMPY_MIN} else pip install ${PIP_FLAGS} cython pip install ${PIP_FLAGS} numpy fi pip install ${PIP_FLAGS} matplotlib pytest set -o pipefail if [ "${USE_SDIST}" == "1" ]; then python -m build --sdist pip install dist/pyw*.tar.gz -v elif [ "${REFGUIDE_CHECK}" == "1" ]; then pip install sphinx numpydoc scipy-doctest pip install . -v else pip install . -v fi if [ "${OPTIONS_NAME}" == "editable-install" ]; then pip install meson-python>=0.16.0 matplotlib pytest ninja pip install -e . --no-build-isolation fi - name: Run tests env: USE_SDIST: ${{ matrix.USE_SDIST }} REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }} OPTIONS_NAME: ${{ matrix.OPTIONS_NAME }} run: | set -o pipefail # Move out of source directory to avoid finding local pywt pushd demo if [ "${USE_SDIST}" == "1" ]; then pytest --pyargs pywt python ../pywt/tests/test_doc.py elif [ "${REFGUIDE_CHECK}" == "1" ]; then # doctest docstrings pytest --doctest-modules --pyargs pywt -v --doctest-collect=api # Run Sphinx HTML docs builder, converting warnings to errors # Move back out of demo/ to root directory cd .. pip install -r util/readthedocs/requirements.txt sphinx-build -b html -W --keep-going -d _build/doctrees doc/source doc/build else pytest --pyargs pywt fi popd test_pywavelets_linux_free_threaded: timeout-minutes: 30 name: linux-cp${{ matrix.python-version }}-default runs-on: latchkey-small strategy: fail-fast: false matrix: python-version: ["3.13t", "3.14t"] steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.2 - uses: actions/setup-python@2e3e4b15a884dc73a63f962bff250a855150a234 # v5.5.0 with: cache: 'pip' python-version: ${{ matrix.python-version}} - name: Build package run: | pip install --upgrade pip build pip install cython numpy meson-python ninja pytest pip install . -v --no-build-isolation - name: Run tests run: | # Move out of source directory to avoid finding local pywt cd demo pytest --pyargs pywt test_pywavelets_macos: timeout-minutes: 30 name: macos-cp${{ matrix.python-version }}-${{ matrix.OPTIONS_NAME }} runs-on: macos-latest strategy: # Ensure that a wheel builder finishes even if another fails fail-fast: false matrix: python-version: ["3.12", "3.14"] MINIMUM_REQUIREMENTS: [0] USE_SDIST: [0] REFGUIDE_CHECK: [0] PIP_FLAGS: [""] OPTIONS_NAME: ["default"] include: - python-version: "3.12" MINIMUM_REQUIREMENTS: 1 OPTIONS_NAME: "osx-minimum-req" - python-version: "3.14" PIP_FLAGS: "--pre" OPTIONS_NAME: "pre-releases" steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.2 - uses: actions/setup-python@2e3e4b15a884dc73a63f962bff250a855150a234 # v5.5.0 with: cache: 'pip' python-version: ${{ matrix.python-version}} allow-prereleases: true - name: Build package env: VERSION: ${{ matrix.python-version }} MINIMUM_REQUIREMENTS: ${{ matrix.MINIMUM_REQUIREMENTS }} PIP_FLAGS: ${{ matrix.PIP_FLAGS }} USE_SDIST: ${{ matrix.USE_SDIST }} REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }} CC: /usr/bin/clang CXX: /usr/bin/clang++ run: | uname -a df -h ulimit -a which python python --version pip install --upgrade pip build # Set numpy version first, other packages link against it if [ "${MINIMUM_REQUIREMENTS}" == "1" ]; then pip install ${CYTHON_MIN} ${NUMPY_MIN} else pip install ${PIP_FLAGS} cython numpy fi pip install ${PIP_FLAGS} matplotlib pytest set -o pipefail if [ "${USE_SDIST}" == "1" ]; then python -m build --sdist pip install pywavelets* -v elif [ "${REFGUIDE_CHECK}" == "1" ]; then pip install sphinx numpydoc scipy-doctest pip install . -v else pip install . -v fi - name: Run tests env: PIP_FLAGS: ${{ matrix.PIP_FLAGS }} USE_SDIST: ${{ matrix.USE_SDIST }} REFGUIDE_CHECK: ${{ matrix.REFGUIDE_CHECK }} run: | # Move out of source directory to avoid finding local pywt pushd demo if [ "${USE_SDIST}" == "1" ]; then pytest --pyargs pywt python ../pywt/tests/test_doc.py elif [ "${REFGUIDE_CHECK}" == "1" ]; then # doctests docstrings pytest --doctest-modules --doctest-only-doctests=true --pyargs pywt -v --doctest-collect=api pytest --doctest-modules --doctest-only-doctests=true --pyargs pywt.data -v else pytest --pyargs pywt fi popd clang_ASan_UBSan: timeout-minutes: 30 name: Test under ASan and UBSan runs-on: macos-latest steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.2.2 with: submodules: recursive fetch-tags: true persist-credentials: false - name: Set up pyenv run: | git clone https://github.com/pyenv/pyenv.git "$HOME/.pyenv" PYENV_ROOT="$HOME/.pyenv" PYENV_BIN="$PYENV_ROOT/bin" PYENV_SHIMS="$PYENV_ROOT/shims" echo "$PYENV_BIN" >> $GITHUB_PATH echo "$PYENV_SHIMS" >> $GITHUB_PATH echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV - name: Set up LLVM run: | brew install llvm@19 LLVM_PREFIX=$(brew --prefix llvm@19) echo CC="$LLVM_PREFIX/bin/clang" >> $GITHUB_ENV echo CXX="$LLVM_PREFIX/bin/clang++" >> $GITHUB_ENV echo LDFLAGS="-L$LLVM_PREFIX/lib" >> $GITHUB_ENV echo CPPFLAGS="-I$LLVM_PREFIX/include" >> $GITHUB_ENV - name: Build Python with AddressSanitizer run: | CONFIGURE_OPTS="--with-address-sanitizer" pyenv install 3.14 pyenv global 3.14 - name: Install NumPy dependencies from PyPI run: | pip install meson-python ninja cython - name: Build NumPy with ASan run: | pip install numpy --no-binary numpy --no-build-isolation -Csetup-args="-Db_sanitize=address" -v - name: Install dependencies from PyPI run: | pip install spin pytest pytest-timeout - name: Build PyWavelets with ASan and UBSan run: | export CFLAGS=-fno-sanitize=function # suppressed upstream, see cython#7437 spin build -- -Db_sanitize=address,undefined -Db_lundef=false - name: Test run: | # pass -s to pytest to see ASAN errors and warnings, otherwise pytest captures them ASAN_OPTIONS=detect_leaks=0:symbolize=1:strict_init_order=true:allocator_may_return_null=1:use_sigaltstack=0 \ UBSAN_OPTIONS=halt_on_error=1 \ spin test -- -v -s --timeout=600 --durations=10
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cache dependency installs on the setup step so they are served from cache.
- Add a job timeout so a hung step cannot burn hours of runner time.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 4 jobs (7 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.