Skip to content
Latchkey

Tests workflow (PyWavelets/pywt)

The Tests workflow from PyWavelets/pywt, explained and optimized by Latchkey.

C

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.

Grade your own workflow free or run it on Latchkey →
Source: PyWavelets/pywt.github/workflows/tests.ymlLicense MITView source

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

workflow (.yml)
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

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:

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.

Actions used in this workflow