name: Unit Testing and Deployment

# zizmor ignore note: All caching for pushes to main should be disabled with the `USE_CACHE` env var
on: # zizmor: ignore[cache-poisoning]
  pull_request:
  merge_group:
  workflow_dispatch:
  schedule:
    - cron: "0 4 * * *"
  push:
    tags:
      - "*"
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  ALLOW_PLOTTING: true
  SHELLOPTS: "errexit:pipefail"
  PYTEST_ADDOPTS: "--color=yes"
  USE_CACHE: ${{
    (
    (github.event_name == 'workflow_dispatch' && github.event.inputs.cache == 'true') ||
    (github.event_name == 'pull_request') ||
    (github.event_name == 'push')
    ) &&
    !startsWith(github.ref, 'refs/tags/v') &&
    !startsWith(github.ref, 'refs/heads/release/') &&
    !startsWith(github.ref, 'refs/heads/main')
    }}
  CACHE_FOLDER_NAME: vtk-data
  PYVISTA_USERDATA_PATH: ${{ github.workspace }}/vtk-data
  LINUX_PLOTTING_ARGS: >
    tests/plotting -v --cov=pyvista --cov-append --cov-report=xml --cov-branch --disallow_unused_cache

permissions:
  id-token: none

jobs:
  cache-vtk-data:
    name: Cache vtk-data Repo
    runs-on: ubuntu-22.04
    outputs:
      key: ${{ steps.restore-cache-vtk-data.outputs.cache-primary-key }}
    steps:
      - name: Get vtk-data commit
        run: echo VTK_DATA_COMMIT=$(git ls-remote https://github.com/pyvista/vtk-data HEAD | awk '{ print $1}') >> $GITHUB_ENV

      - name: Test if the example data cache exists
        id: restore-cache-vtk-data
        uses: actions/cache/restore@v4
        with:
          path: ${{ env.CACHE_FOLDER_NAME }}
          key: vtk-data-${{ env.VTK_DATA_COMMIT }}
          lookup-only: true

      - name: Clone vtk-data if the cache does not exists # zizmor: ignore[artipacked]
        if: steps.restore-cache-vtk-data.outputs.cache-hit != 'true'
        uses: actions/checkout@v4
        with:
          repository: pyvista/vtk-data
          path: ${{ env.CACHE_FOLDER_NAME }}

      - name: Create the example data cache
        if: steps.restore-cache-vtk-data.outputs.cache-hit != 'true'
        id: save-cache-vtk-data
        uses: actions/cache/save@v4
        with:
          path: ${{ env.CACHE_FOLDER_NAME }}
          key: vtk-data-${{ env.VTK_DATA_COMMIT }}
          enableCrossOsArchive: true

  macOS:
    name: Mac OS Unit Testing
    strategy:
      fail-fast: false
      matrix:
        config:
          - { os: macos-13, python-version: "3.9" }
          - { os: macos-14, python-version: "3.10" }
          - { os: macos-latest, python-version: "3.11" }
          - { os: macos-latest, python-version: "3.12" }
          - { os: macos-latest, python-version: "3.13" }

    runs-on: ${{ matrix.config.os }}
    needs: cache-vtk-data

    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Set up Python ${{ matrix.config.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.config.python-version }}
          cache: ${{ env.USE_CACHE == 'true' && 'pip' || '' }}

      - name: Restore example data cache
        uses: actions/cache/restore@v4
        with:
          path: ${{ env.CACHE_FOLDER_NAME }}
          key: ${{ needs.cache-vtk-data.outputs.key }}
          enableCrossOsArchive: true

      - name: Install PyVista test dependencies
        run: |
          pip install --upgrade pip
          pip install . --group test

      - name: Report
        run: python -c "import pyvista;print(pyvista.Report(gpu=False));from pyvista import examples;print('User data path:', examples.USER_DATA_PATH)"

      - name: Test Core API
        run: pytest -v --ignore=tests/plotting --test_downloads

      - name: Test Plotting
        if: always()
        run: pytest -v tests/plotting --disallow_unused_cache

      - name: Upload Images for Failed Tests
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: failed_test_images-${{ github.job }}-${{ join(matrix.config.* , '-') }}
          path: _failed_test_images

      - name: Upload Generated Images
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: generated_test_images-${{ github.job }}-${{ join(matrix.config.* , '-') }}
          path: _generated_test_images

  Linux:
    name: Linux Unit Testing
    runs-on: ubuntu-22.04
    needs: cache-vtk-data
    strategy:
      fail-fast: false

      # see discussion at https://github.com/pyvista/pyvista/issues/2867
      matrix:
        include:
          # numeric numpy versions are ~= conditions, e.g. "1.23" means "numpy~=1.23.0"
          - python-version: "3.9"
            vtk-version: "9.0.3" # Requires numpy~=1.23
            numpy-version: "1.23"
          - python-version: "3.9"
            vtk-version: "9.1"
            numpy-version: "1.26" # Test numpy 1.26
          - python-version: "3.10"
            vtk-version: "9.2.2"
            numpy-version: "latest"
          - python-version: "3.11"
            vtk-version: "9.2.6"
            numpy-version: "latest"
          - python-version: "3.12"
            vtk-version: "9.3.1"
            numpy-version: "latest"
          - python-version: "3.13"
            vtk-version: "9.4.2"
            numpy-version: "latest"
          - python-version: "3.13"
            vtk-version: "latest"
            numpy-version: "nightly"

    environment:
      name: pypi
      url: https://pypi.org/p/pyvista
    permissions:
      id-token: write # this permission is mandatory for trusted publishing

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
          persist-credentials: false

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"

      - name: Cache example data
        uses: actions/cache/restore@v4
        with:
          path: ${{ env.CACHE_FOLDER_NAME }}
          key: ${{ needs.cache-vtk-data.outputs.key }}
          enableCrossOsArchive: true

      - name: Build wheel and install pyvista
        run: |
          pip install build
          python -m build --wheel
          pip install dist/pyvista*.whl

      - name: Set up vtk
        if: ${{ matrix.vtk-version != 'latest' }}
        run: pip install vtk==${{ matrix.vtk-version }}

      # Make sure PyVista does not break from non-core dependencies
      - name: Software Report (Core Dependencies)
        run: python -c "import pyvista; print(pyvista.Report());"

      - name: Install Testing Requirements
        run: |
          pip install --upgrade pip
          pip install . --group test

      - name: Set up numpy
        if: ${{ matrix.numpy-version != 'latest' && matrix.numpy-version != 'nightly' }}
        run: pip install 'numpy~=${{ matrix.numpy-version }}.0'

      - name: Limit Matplotlib for VTK<9.2.2
        if: ${{ matrix.vtk-version == '9.1' || matrix.vtk-version == '9.0.3' }}
        run: pip install 'matplotlib<3.6'

      - name: Install latest numpy 2.0 beta/rc
        if: ${{ matrix.numpy-version == 'nightly' }}
        run: |
          pip uninstall numpy matplotlib -y
          pip install --pre --no-deps -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple matplotlib
          pip install --pre --no-deps -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy

      - name: Core Testing (no GL)
        run: python -m pytest --cov=pyvista --cov-branch -v --ignore=tests/plotting --test_downloads

      - uses: awalsh128/cache-apt-pkgs-action@4c82c3ccdc1344ee11e9775dbdbdf43aa8a5614e
        if: always()
        with:
          packages: xvfb
          version: 3.0

      - name: Software Report (Plotting Dependencies)
        if: always()
        run: xvfb-run -a python -c "import pyvista; print(pyvista.Report());"

      - name: Plotting Testing (uses GL)
        if: always()
        run: |
          xvfb-run python -m pytest $LINUX_PLOTTING_ARGS

      - name: Upload Images for Failed Tests
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: failed_test_images-${{ github.job }}-${{ join(matrix.* , '-') }}
          path: _failed_test_images

      - name: Upload Generated Images
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: generated_test_images-${{ github.job }}-${{ join(matrix.* , '-') }}
          path: _generated_test_images

      - name: Software Report
        if: always()
        run: |
          xvfb-run -a python -c "import pyvista; print(pyvista.Report()); from pyvista import examples; print('User data path:', examples.USER_DATA_PATH)"
          which python
          pip list

      - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24
        name: "Upload coverage to CodeCov"
        env:
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

      - name: Check package
        run: |
          pip install build twine
          python -m build
          twine check --strict dist/*

      - name: Upload to PyPi
        if: matrix.python-version == '3.13' && startsWith(github.ref, 'refs/tags/v')
        uses: pypa/gh-action-pypi-publish@ab69e431e9c9f48a3310be0a56527c679f56e04d

  VTK-dev:
    # For PRs, only run this job if the 'vtk-dev-testing' label is applied
    if: |
      (github.event_name != 'pull_request' && github.event_name != 'push') ||
      contains(github.event.pull_request.labels.*.name, 'vtk-dev-testing')
    name: Linux VTK Dev Testing
    permissions:
      contents: read
    runs-on: ubuntu-22.04
    needs: cache-vtk-data
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.13"]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
          persist-credentials: false

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"

      - name: Cache example data
        uses: actions/cache/restore@v4
        with:
          path: ${{ env.CACHE_FOLDER_NAME }}
          key: ${{ needs.cache-vtk-data.outputs.key }}
          enableCrossOsArchive: true

      - name: Install Testing Requirements
        run: |
          pip install --upgrade pip
          pip install . --group test

      - name: Install VTK dev wheel
        run: |
          pip install --upgrade vtk --pre --no-cache --extra-index-url https://wheels.vtk.org

      - name: Core Testing (no GL)
        run: python -m pytest --cov=pyvista --cov-branch -v --ignore=tests/plotting --test_downloads

      - uses: awalsh128/cache-apt-pkgs-action@4c82c3ccdc1344ee11e9775dbdbdf43aa8a5614e
        if: always()
        with:
          packages: xvfb
          version: 3.0

      - name: Software Report (Plotting Dependencies)
        if: always()
        run: xvfb-run -a python -c "import pyvista; print(pyvista.Report());"

      - name: Plotting Testing (uses GL)
        if: always()
        run: |
          xvfb-run python -m pytest $LINUX_PLOTTING_ARGS

      - name: Upload Images for Failed Tests
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: failed_test_images-${{ github.job }}-${{ join(matrix.* , '-') }}
          path: _failed_test_images

      - name: Upload Generated Images
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: generated_test_images-${{ github.job }}-${{ join(matrix.* , '-') }}
          path: _generated_test_images

      - name: Software Report
        if: always()
        run: |
          xvfb-run -a python -c "import pyvista; print(pyvista.Report()); from pyvista import examples; print('User data path:', examples.USER_DATA_PATH)"
          which python
          pip list
          pip show vtk

  windows:
    name: Windows Unit Testing
    runs-on: windows-latest
    needs: cache-vtk-data
    env:
      CI_WINDOWS: true
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"

      - name: Cache example data
        uses: actions/cache/restore@v4
        with:
          path: ${{ env.CACHE_FOLDER_NAME }}
          key: ${{ needs.cache-vtk-data.outputs.key }}
          enableCrossOsArchive: true

      - name: Set up headless display
        uses: pyvista/setup-headless-display-action@7d84ae825e6d9297a8e99bdbbae20d1b919a0b19

      - name: Install PyVista test dependencies
        run: |
          python -m pip install --upgrade pip
          pip install . --group test

      - name: Report
        run: python -c "import pyvista; print(pyvista.Report(gpu=False)); from pyvista import examples; print('User data path:', examples.USER_DATA_PATH)"

      - name: Test Core API
        run: python -m pytest -v --ignore=tests/plotting --test_downloads

      - name: Test Plotting
        if: always()
        run: python -m pytest -v tests/plotting --disallow_unused_cache

      - name: Upload Images for Failed Tests
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: failed_test_images-${{ github.job }}-${{ join(matrix.* , '-') }}
          path: _failed_test_images

      - name: Upload Generated Images
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: generated_test_images-${{ github.job }}-${{ join(matrix.* , '-') }}
          path: _generated_test_images
