---
name: Tests
concurrency:
  group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }}
  cancel-in-progress: true

on:
  push:
    branches-ignore:
      - auto-backport-of-pr-[0-9]+
      - v[0-9]+.[0-9]+.[0-9x]+-doc
      - dependabot/**
  pull_request:
    branches-ignore:
      - v[0-9]+.[0-9]+.[0-9x]+-doc
    paths-ignore:
      # Skip running tests if changes are only in documentation directories
      - 'doc/**'
      - 'galleries/**'
  schedule:
    # 5:47 UTC on Saturdays
    - cron: "47 5 * * 6"
  workflow_dispatch:

env:
  NO_AT_BRIDGE: 1  # Necessary for GTK3 interactive test.
  OPENBLAS_NUM_THREADS: 1
  PYTHONFAULTHANDLER: 1

jobs:
  test:
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (
        github.repository == 'matplotlib/matplotlib' &&
        !contains(github.event.head_commit.message, '[ci skip]') &&
        !contains(github.event.head_commit.message, '[skip ci]') &&
        !contains(github.event.head_commit.message, '[skip github]') &&
        !contains(github.event.head_commit.message, '[ci doc]')
      )
    permissions:
      contents: read
    name: "Python ${{ matrix.python-version }} on ${{ matrix.os }} ${{ matrix.name-suffix }}"
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - name-suffix: "(Minimum Versions)"
            os: ubuntu-20.04
            python-version: '3.10'
            extra-requirements: '-c requirements/testing/minver.txt'
            delete-font-cache: true
            # Oldest versions with Py3.10 wheels.
            pyqt5-ver: '==5.15.5 sip==6.3.0'
            pyqt6-ver: '==6.2.0 PyQt6-Qt6==6.2.0'
            pyside2-ver: '==5.15.2.1'
            pyside6-ver: '==6.2.0'
          - os: ubuntu-20.04
            python-version: '3.10'
            # One CI run tests ipython/matplotlib-inline before backend mapping moved to mpl
            extra-requirements:
              -r requirements/testing/extra.txt
              "ipython==7.29.0"
              "ipykernel==5.5.6"
              "matplotlib-inline<0.1.7"
            CFLAGS: "-fno-lto"  # Ensure that disabling LTO works.
            # https://github.com/matplotlib/matplotlib/pull/26052#issuecomment-1574595954
            # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html
            pyqt6-ver: '!=6.5.1,!=6.6.0,!=6.7.1'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'
          - os: ubuntu-22.04
            python-version: '3.11'
            # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html
            pyqt6-ver: '!=6.6.0'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'
            extra-requirements: '-r requirements/testing/extra.txt'
          - os: ubuntu-22.04
            python-version: '3.12'
            # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html
            pyqt6-ver: '!=6.6.0'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'
          - os: ubuntu-22.04
            python-version: '3.13'
            # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html
            pyqt6-ver: '!=6.6.0'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'
          - name-suffix: "Free-threaded"
            os: ubuntu-22.04
            python-version: '3.13t'
            # https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html
            pyqt6-ver: '!=6.6.0'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'
          - os: macos-13  # This runner is on Intel chips.
            python-version: '3.10'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'
          - os: macos-14  # This runner is on M1 (arm64) chips.
            python-version: '3.12'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'
          - os: macos-14  # This runner is on M1 (arm64) chips.
            python-version: '3.13'
            # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
            pyside6-ver: '!=6.5.1'

    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b  # v5.3.0
        if: matrix.python-version != '3.13t'
        with:
          python-version: ${{ matrix.python-version }}
          allow-prereleases: true

      - name: Set up Python ${{ matrix.python-version }}
        uses: Quansight-Labs/setup-python@b9ab292c751a42bcd2bb465b7fa202ea2c3f5796  # v5.3.1
        if: matrix.python-version == '3.13t'
        with:
          python-version: ${{ matrix.python-version }}
          allow-prereleases: true

      - name: Install OS dependencies
        run: |
          case "${{ runner.os }}" in
          Linux)
            echo 'Acquire::Retries "3";' | sudo tee /etc/apt/apt.conf.d/80-retries
            sudo apt-get update -yy
            sudo apt-get install -yy --no-install-recommends \
              ccache \
              cm-super \
              dvipng \
              ffmpeg \
              fonts-freefont-otf \
              fonts-noto-cjk \
              fonts-wqy-zenhei \
              gdb \
              gir1.2-gtk-3.0 \
              graphviz \
              inkscape \
              language-pack-de \
              lcov \
              libcairo2 \
              libcairo2-dev \
              libffi-dev \
              libgeos-dev \
              libgirepository1.0-dev \
              libsdl2-2.0-0 \
              libxkbcommon-x11-0 \
              libxcb-cursor0 \
              libxcb-icccm4 \
              libxcb-image0 \
              libxcb-keysyms1 \
              libxcb-randr0 \
              libxcb-render-util0 \
              libxcb-xinerama0 \
              lmodern \
              ninja-build \
              pkg-config \
              qtbase5-dev \
              texlive-fonts-recommended \
              texlive-latex-base \
              texlive-latex-extra \
              texlive-latex-recommended \
              texlive-luatex \
              texlive-pictures \
              texlive-xetex
            if [[ "${{ matrix.os }}" = ubuntu-20.04 ]]; then
              sudo apt-get install -yy --no-install-recommends libopengl0
            else  # ubuntu-22.04
              sudo apt-get install -yy --no-install-recommends \
                gir1.2-gtk-4.0 libnotify4
            fi
            ;;
          macOS)
            brew update
            # Periodically, Homebrew updates Python and fails to overwrite the
            # existing not-managed-by-Homebrew copy without explicitly being told
            # to do so. GitHub/Azure continues to avoid fixing their runner images:
            # https://github.com/actions/runner-images/issues/9966
            # so force an overwrite even if there are no Python updates.
            # We don't even care about Homebrew's Python because we use the one
            # from actions/setup-python.
            for python_package in $(brew list | grep python@); do
              brew unlink ${python_package}
              brew link --overwrite ${python_package}
            done
            # Workaround for https://github.com/actions/runner-images/issues/10984
            brew uninstall --ignore-dependencies --force pkg-config@0.29.2
            brew install ccache ghostscript gobject-introspection gtk4 ninja
            brew install --cask font-noto-sans-cjk inkscape
            ;;
          esac

      - name: Cache pip
        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57  # v4.2.0
        if: startsWith(runner.os, 'Linux')
        with:
          path: ~/.cache/pip
          key: ${{ matrix.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('requirements/*/*.txt') }}
          restore-keys: |
            ${{ matrix.os }}-py${{ matrix.python-version }}-pip-
      - name: Cache pip
        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57  # v4.2.0
        if: startsWith(runner.os, 'macOS')
        with:
          path: ~/Library/Caches/pip
          key: ${{ matrix.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('requirements/*/*.txt') }}
          restore-keys: |
            ${{ matrix.os }}-py${{ matrix.python-version }}-pip-
      - name: Cache ccache
        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57  # v4.2.0
        with:
          path: |
            ~/.ccache
          key: ${{ matrix.os }}-py${{ matrix.python-version }}-ccache-${{ hashFiles('src/*') }}
          restore-keys: |
            ${{ matrix.os }}-py${{ matrix.python-version }}-ccache-
      - name: Cache Matplotlib
        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57  # v4.2.0
        with:
          path: |
            ~/.cache/matplotlib
            !~/.cache/matplotlib/tex.cache
            !~/.cache/matplotlib/test_cache
          key: 4-${{ runner.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-${{ github.sha }}
          restore-keys: |
            4-${{ runner.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-
            4-${{ runner.os }}-py${{ matrix.python-version }}-mpl-

      - name: Install Python dependencies
        run: |
          # Upgrade pip and setuptools and wheel to get as clean an install as
          # possible.
          python -m pip install --upgrade pip setuptools wheel

          # Install pre-release versions during our weekly upcoming dependency tests.
          if [[ "${{ github.event_name }}" == 'schedule'
                && "${{ matrix.name-suffix }}" != '(Minimum Versions)' ]]; then
            PRE="--pre"
          fi

          # Install dependencies from PyPI.
          # Preinstall build requirements to enable no-build-isolation builds.
          python -m pip install --upgrade $PRE \
            'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \
            packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \
            'meson-python>=0.13.1' 'pybind11>=2.13.2' \
            -r requirements/testing/all.txt \
            ${{ matrix.extra-requirements }}

          # Install optional dependencies from PyPI.
          # Sphinx is needed to run sphinxext tests
          python -m pip install --upgrade sphinx!=6.1.2

          if [[ "${{ matrix.python-version }}" != '3.13t' ]]; then
          # GUI toolkits are pip-installable only for some versions of Python
          # so don't fail if we can't install them.  Make it easier to check
          # whether the install was successful by trying to import the toolkit
          # (sometimes, the install appears to be successful but shared
          # libraries cannot be loaded at runtime, so an actual import is a
          # better check).
          python -m pip install --upgrade pycairo 'cairocffi>=0.8' PyGObject &&
            (
              python -c 'import gi; gi.require_version("Gtk", "4.0"); from gi.repository import Gtk' &&
              echo 'PyGObject 4 is available' || echo 'PyGObject 4 is not available'
            ) && (
              python -c 'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk' &&
              echo 'PyGObject 3 is available' || echo 'PyGObject 3 is not available'
            )

          python -mpip install --upgrade pyqt5${{ matrix.pyqt5-ver }} &&
            python -c 'import PyQt5.QtCore' &&
            echo 'PyQt5 is available' ||
            echo 'PyQt5 is not available'
          # Even though PySide2 wheels can be installed on Python 3.12+, they are broken and since PySide2 is
          # deprecated, they are unlikely to be fixed. For the same deprecation reason, there are no wheels
          # on M1 macOS, so don't bother there either.
          if [[ "${{ matrix.os }}" != 'macos-14'
                && "${{ matrix.python-version }}" != '3.12' && "${{ matrix.python-version }}" != '3.13' ]]; then
            python -mpip install --upgrade pyside2${{ matrix.pyside2-ver }} &&
              python -c 'import PySide2.QtCore' &&
              echo 'PySide2 is available' ||
              echo 'PySide2 is not available'
          fi
          python -mpip install --upgrade pyqt6${{ matrix.pyqt6-ver }} &&
            python -c 'import PyQt6.QtCore' &&
            echo 'PyQt6 is available' ||
            echo 'PyQt6 is not available'
          python -mpip install --upgrade pyside6${{ matrix.pyside6-ver }} &&
            python -c 'import PySide6.QtCore' &&
            echo 'PySide6 is available' ||
            echo 'PySide6 is not available'

          python -mpip install --upgrade \
            -f "https://extras.wxpython.org/wxPython4/extras/linux/gtk3/${{ matrix.os }}" \
            wxPython &&
            python -c 'import wx' &&
            echo 'wxPython is available' ||
            echo 'wxPython is not available'

          fi  # Skip backends on Python 3.13t.

      - name: Install the nightly dependencies
        # Only install the nightly dependencies during the scheduled event
        if: github.event_name == 'schedule' && matrix.name-suffix != '(Minimum Versions)'
        run: |
          python -m pip install pytz tzdata  # Must be installed for Pandas.
          python -m pip install \
            --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
            --upgrade --only-binary=:all: numpy pandas

      - name: Install Matplotlib
        run: |
          ccache -s
          git describe

          # Set flag in a delayed manner to avoid issues with installing other
          # packages
          if [[ "${{ runner.os }}" == 'macOS' ]]; then
            export CPPFLAGS='-fprofile-instr-generate=default.%m.profraw'
            export CPPFLAGS="$CPPFLAGS -fcoverage-mapping"
          else
            export CPPFLAGS='--coverage -fprofile-abs-path'
          fi

          python -m pip install --no-deps --no-build-isolation --verbose \
            --config-settings=setup-args="-DrcParams-backend=Agg" \
            --editable .[dev]

          if [[ "${{ runner.os }}" != 'macOS' ]]; then
            unset CPPFLAGS
          fi

      - name: Clear font cache
        run: |
          rm -rf ~/.cache/matplotlib
        if: matrix.delete-font-cache

      - name: Run pytest
        run: |
          if [[ "${{ matrix.python-version }}" == '3.13t' ]]; then
            export PYTHON_GIL=0
          fi
          pytest -rfEsXR -n auto \
            --maxfail=50 --timeout=300 --durations=25 \
            --cov-report=xml --cov=lib --log-level=DEBUG --color=yes

      - name: Cleanup non-failed image files
        if: failure()
        run: |
          function remove_files() {
              local extension=$1
              find ./result_images -type f -name "*-expected*.$extension" | while read file; do
                  if [[ $file == *"-expected_pdf"* ]]; then
                      base=${file%-expected_pdf.$extension}_pdf
                  elif [[ $file == *"-expected_eps"* ]]; then
                      base=${file%-expected_eps.$extension}_eps
                  elif [[ $file == *"-expected_svg"* ]]; then
                      base=${file%-expected_svg.$extension}_svg
                  else
                      base=${file%-expected.$extension}
                  fi
                  if [[ ! -e "${base}-failed-diff.$extension" ]]; then
                      if [[ -e "$file" ]]; then
                          rm "$file"
                          echo "Removed $file"
                      fi
                      if [[ -e "${base}.$extension" ]]; then
                          rm "${base}.$extension"
                          echo " Removed ${base}.$extension"
                      fi
                  fi
              done
          }

          remove_files "png"; remove_files "svg"; remove_files "pdf"; remove_files "eps";

          if [ "$(find ./result_images -mindepth 1 -type d)" ]; then
              find ./result_images/* -type d -empty -delete
          fi

      - name: Filter C coverage
        if: ${{ !cancelled() && github.event_name != 'schedule' }}
        run: |
          if [[ "${{ runner.os }}" != 'macOS' ]]; then
            lcov --rc lcov_branch_coverage=1 --capture --directory . \
              --output-file coverage.info
            lcov --rc lcov_branch_coverage=1 --output-file coverage.info \
              --extract coverage.info $PWD/src/'*' $PWD/lib/'*'
            lcov --rc lcov_branch_coverage=1 --list coverage.info
            find . -name '*.gc*' -delete
          else
            xcrun llvm-profdata merge -sparse default.*.profraw \
              -o default.profdata
            xcrun llvm-cov export -format="lcov" build/*/src/*.so \
              -instr-profile default.profdata > info.lcov
          fi
      - name: Upload code coverage
        if: ${{ !cancelled() && github.event_name != 'schedule' }}
        uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e  # v5.1.1
        with:
          name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }}"
          token: ${{ secrets.CODECOV_TOKEN }}

      - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882  # v4.4.3
        if: failure()
        with:
          name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }} result images"
          path: ./result_images

  # Separate dependent job to only upload one issue from the matrix of jobs
  create-issue:
    if: ${{ failure() && github.event_name == 'schedule' }}
    needs: [test]
    permissions:
      issues: write
    runs-on: ubuntu-latest
    name: "Create issue on failure"

    steps:
      - name: Create issue on failure
        uses: imjohnbo/issue-bot@572eed14422c4d6ca37e870f97e7da209422f5bd  # v3.4.4
        with:
          title: "[TST] Upcoming dependency test failures"
          body: |
            The weekly build with nightly wheels from numpy and pandas
            has failed. Check the logs for any updates that need to be
            made in matplotlib.
            https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}

          pinned: false
          close-previous: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
