name: Unit Tests

on:
  push:
    branches:
      - main
      - 3.0.x
  pull_request:
    branches:
      - main
      - 3.0.x
    paths-ignore:
      - "doc/**"
      - "web/**"

permissions: {}

defaults:
  run:
    shell: bash -el {0}

jobs:
  ubuntu:
    runs-on: ${{ matrix.platform }}
    permissions:
      contents: read
    timeout-minutes: 90
    strategy:
      matrix:
        platform: [ubuntu-24.04, ubuntu-24.04-arm]
        env_file: [actions-311.yaml, actions-312.yaml, actions-313.yaml, actions-314.yaml]
        # Prevent the include jobs from overriding other jobs
        pattern: [""]
        pandas_future_infer_string: ["1"]
        pandas_future_python_scalars: ["0"]
        include:
          - name: "Downstream Compat"
            env_file: actions-313-downstream_compat.yaml
            pattern: "not slow and not network and not single_cpu"
            pytest_target: "pandas/tests/test_downstream.py"
            platform: ubuntu-24.04
          - name: "Minimum Versions"
            env_file: actions-311-minimum_versions.yaml
            pattern: "not slow and not network and not single_cpu"
            platform: ubuntu-24.04
          - name: "Freethreading"
            env_file: actions-313-freethreading.yaml
            pattern: "not slow and not network and not single_cpu"
            platform: ubuntu-24.04
          - name: "Without PyArrow"
            env_file: actions-312.yaml
            pattern: "not slow and not network and not single_cpu"
            platform: ubuntu-24.04
          - name: "Locale: it_IT"
            env_file: actions-313.yaml
            pattern: "not slow and not network and not single_cpu"
            extra_apt: "language-pack-it"
            # Use the utf8 version as the default, it has no bad side-effect.
            lang: "it_IT.utf8"
            lc_all: "it_IT.utf8"
            # Also install it_IT (its encoding is ISO8859-1) but do not activate it.
            # It will be temporarily activated during tests with locale.setlocale
            extra_loc: "it_IT"
            platform: ubuntu-24.04
          - name: "Locale: zh_CN"
            env_file: actions-313.yaml
            pattern: "not slow and not network and not single_cpu"
            extra_apt: "language-pack-zh-hans"
            # Use the utf8 version as the default, it has no bad side-effect.
            lang: "zh_CN.utf8"
            lc_all: "zh_CN.utf8"
            # Also install zh_CN (its encoding is gb2312) but do not activate it.
            # It will be temporarily activated during tests with locale.setlocale
            extra_loc: "zh_CN"
            platform: ubuntu-24.04
          - name: "PANDAS_FUTURE_INFER_STRING=0"
            env_file: actions-313.yaml
            pandas_future_infer_string: "0"
            platform: ubuntu-24.04
          - name: "PANDAS_FUTURE_PYTHON_SCALARS=1"
            env_file: actions-313.yaml
            pandas_future_python_scalars: "1"
            platform: ubuntu-24.04
          - name: "Numpy Dev"
            env_file: actions-313-numpydev.yaml
            pattern: "not slow and not network and not single_cpu"
            test_args: "-W error::DeprecationWarning -W error::FutureWarning"
            platform: ubuntu-24.04
          - name: "Pyarrow Nightly"
            env_file: actions-313-pyarrownightly.yaml
            pattern: "not slow and not network and not single_cpu"
            platform: ubuntu-24.04
      fail-fast: false
    name: ${{ matrix.name || format('{0} {1}', matrix.platform, matrix.env_file) }}
    env:
      PATTERN: ${{ matrix.pattern }}
      LANG: ${{ matrix.lang || 'C.UTF-8' }}
      LC_ALL: ${{ matrix.lc_all || '' }}
      PANDAS_CI: '1'
      PANDAS_FUTURE_INFER_STRING: ${{ matrix.pandas_future_infer_string || '1' }}
      PANDAS_FUTURE_PYTHON_SCALARS: ${{ matrix.pandas_future_python_scalars || '0' }}
      TEST_ARGS: ${{ matrix.test_args || '' }}
      PYTEST_WORKERS: 'auto'
      PYTEST_TARGET: ${{ matrix.pytest_target || 'pandas' }}
      # Clipboard tests
      QT_QPA_PLATFORM: offscreen
      REMOVE_PYARROW: ${{ matrix.name == 'Without PyArrow' && '1' || '0' }}
    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.env_file }}-${{ matrix.pattern }}-${{ matrix.extra_apt || '' }}-${{ matrix.pandas_future_infer_string }}-${{ matrix.platform }}
      cancel-in-progress: true

    services:
      mysql:
        image: mysql:9
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          MYSQL_DATABASE: pandas
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 3306:3306

      postgres:
        image: postgres:18
        env:
          PGUSER: postgres
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: pandas
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      moto:
        image: motoserver/moto:5.1.18
        ports:
          - 5000:5000

    steps:
    - name: Checkout
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      with:
        fetch-depth: 0

    - name: Extra installs
      # https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions-azure-pipelines-travis-ci-and-gitlab-ci-cd
      run: sudo apt-get update && sudo apt-get install -y libegl1 libopengl0 ${{ matrix.extra_apt || ''}}

    - name: Generate extra locales
      # These extra locales will be available for locale.setlocale() calls in tests
      run: sudo locale-gen ${{ matrix.extra_loc }}
      if: ${{ matrix.extra_loc }}

    - name: Set up Conda
      uses: ./.github/actions/setup-conda
      with:
        environment-file: ci/deps/${{ matrix.env_file }}

    - name: Build Pandas
      id: build
      uses: ./.github/actions/build_pandas
      with:
        # python3.14t/cpython/pyatomic_gcc.h:63:10:
        #   error: ‘__atomic_fetch_add_8’
        #   writing 8 bytes into a region of size 0 overflows the destination [-Werror=stringop-overflow=]
        werror: ${{ matrix.name != 'Freethreading' }}

    - name: Test (not single_cpu)
      uses: ./.github/actions/run-tests
      env:
        # Set pattern to not single_cpu if not already set
        PATTERN: ${{ env.PATTERN == '' && 'not single_cpu' || matrix.pattern }}

    - name: Test (single_cpu)
      uses: ./.github/actions/run-tests
      env:
        PATTERN: 'single_cpu'
        PYTEST_WORKERS: 0
      if: ${{ matrix.pattern == '' && (always() && steps.build.outcome == 'success')}}

  macos-windows:
    permissions:
      contents: read
    timeout-minutes: 90
    strategy:
      matrix:
        os: [macos-15-intel, macos-15, windows-2025]
        env_file: [actions-311.yaml, actions-312.yaml, actions-313.yaml, actions-314.yaml]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    name: ${{ format('{0} {1}', matrix.os, matrix.env_file) }}
    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.env_file }}-${{ matrix.os }}
      cancel-in-progress: true
    env:
      PANDAS_CI: 1
      PYTEST_TARGET: pandas
      PATTERN: "not slow and not db and not network and not single_cpu"
      PYTEST_WORKERS: 'auto'

    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0

      - name: Set up Conda
        uses: ./.github/actions/setup-conda
        with:
          environment-file: ci/deps/${{ matrix.env_file }}

      - name: Build Pandas
        uses: ./.github/actions/build_pandas

      - name: Test
        uses: ./.github/actions/run-tests

  Linux-32-bit:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    container:
      image: quay.io/pypa/manylinux_2_28_i686
      options: --platform linux/386
    steps:
      - name: Checkout pandas Repo
        # actions/checkout does not work since it requires node
        run: |
          git config --global --add safe.directory $PWD

          if [ $GITHUB_EVENT_NAME != pull_request ]; then
              git clone --recursive --branch=$GITHUB_REF_NAME https://github.com/${GITHUB_REPOSITORY}.git $GITHUB_WORKSPACE
              git reset --hard $GITHUB_SHA
          else
              git clone --recursive https://github.com/${GITHUB_REPOSITORY}.git $GITHUB_WORKSPACE
              git fetch origin $GITHUB_REF:my_ref_name
              git checkout $GITHUB_BASE_REF
              git -c user.email="you@example.com" merge --no-commit my_ref_name
          fi
      - name: Build environment and Run Tests
        # https://github.com/numpy/numpy/issues/24703#issuecomment-1722379388
        run: |
          /opt/python/cp313-cp313/bin/python -m venv ~/virtualenvs/pandas-dev
          . ~/virtualenvs/pandas-dev/bin/activate
          python -m pip install --no-cache-dir -U pip wheel setuptools meson[ninja]==1.2.1 meson-python==0.13.1
          python -m pip install numpy -Csetup-args="-Dallow-noblas=true"
          python -m pip install --no-cache-dir versioneer[toml] cython python-dateutil "pytest>=8.3.4,<9.1" pytest-xdist>=3.6.1 hypothesis>=6.116.0
          python -m pip install --no-cache-dir --no-build-isolation -e . -Csetup-args="--werror"
          python -m pip list --no-cache-dir
          PANDAS_CI=1 python -m pytest -m 'not slow and not network and not clipboard and not single_cpu' pandas --junitxml=test-data.xml
    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-32bit
      cancel-in-progress: true

  Linux-Musl:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    container:
      image: quay.io/pypa/musllinux_1_2_x86_64
    steps:
      - name: Checkout pandas Repo
        # actions/checkout does not work since it requires node
        run: |
          git config --global --add safe.directory $PWD

          if [ $GITHUB_EVENT_NAME != pull_request ]; then
              git clone --recursive --branch=$GITHUB_REF_NAME https://github.com/${GITHUB_REPOSITORY}.git $GITHUB_WORKSPACE
              git reset --hard $GITHUB_SHA
          else
              git clone --recursive https://github.com/${GITHUB_REPOSITORY}.git $GITHUB_WORKSPACE
              git fetch origin $GITHUB_REF:my_ref_name
              git checkout $GITHUB_BASE_REF
              git -c user.email="you@example.com" merge --no-commit my_ref_name
          fi
      - name: Configure System Packages
        run: |
          apk update
          apk add musl-locales
      - name: Build environment
        run: |
          /opt/python/cp313-cp313/bin/python -m venv ~/virtualenvs/pandas-dev
          . ~/virtualenvs/pandas-dev/bin/activate
          python -m pip install --no-cache-dir -U pip wheel setuptools meson-python==0.13.1 meson[ninja]==1.2.1
          python -m pip install --no-cache-dir versioneer[toml] cython numpy python-dateutil "pytest>=8.3.4,<9.1" pytest-xdist>=3.6.1 hypothesis>=6.116.0
          python -m pip install --no-cache-dir --no-build-isolation -e . -Csetup-args="--werror"
          python -m pip list --no-cache-dir

      - name: Run Tests
        run: |
          . ~/virtualenvs/pandas-dev/bin/activate
          PANDAS_CI=1 python -m pytest -m 'not slow and not network and not clipboard and not single_cpu' pandas --junitxml=test-data.xml
    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-musl
      cancel-in-progress: true

  python-dev:
    # This job may or may not run depending on the state of the next
    # unreleased Python version. DO NOT DELETE IT.
    #
    # In general, this will remain frozen(present, but not running) until:
    #    - The next unreleased Python version has released beta 1
    #      - This version should be available on GitHub Actions.
    #    - Our required build/runtime dependencies(numpy, Cython, python-dateutil)
    #      support that unreleased Python version.
    #    To unfreeze, comment out the ``if: false`` condition, and make sure you update
    #    the name of the workflow and Python version in actions/setup-python ``python-version:``
    #
    # After it has been unfrozen, this file should remain unfrozen(present, and running) until:
    #    - The next Python version has been officially released.
    #    OR
    #    - Most/All of our optional dependencies support the next Python version AND
    #    - The next Python version has released a rc(we are guaranteed a stable ABI).
    #    To freeze this file, uncomment out the ``if: false`` condition, and migrate the jobs
    #    to the corresponding posix/windows-macos/sdist etc. workflows.
    # Feel free to modify this comment as necessary.
    if: false
    permissions:
      contents: read
    defaults:
      run:
        shell: bash -eou pipefail {0}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-24.04, macos-15-intel, macos-15, windows-2025]

    timeout-minutes: 90

    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.os }}-python-dev
      cancel-in-progress: true

    env:
      PYTEST_WORKERS: "auto"
      PANDAS_CI: 1
      PATTERN: "not slow and not network and not clipboard and not single_cpu"
      PYTEST_TARGET: pandas

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0

      - name: Set up Python Dev Version
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: '3.15-dev'

      - name: Build Environment
        run: |
          python --version
          python -m pip install --upgrade pip setuptools wheel meson[ninja]==1.2.1 meson-python==0.13.1
          python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy
          python -m pip install versioneer[toml] python-dateutil tzdata cython hypothesis>=6.116.0 "pytest>=8.3.4,<9.1" pytest-xdist>=3.6.1 pytest-cov
          python -m pip install -ve . --no-build-isolation --no-index --no-deps -Csetup-args="--werror"
          python -m pip list

      - name: Run Tests
        uses: ./.github/actions/run-tests

  # NOTE: this job must be kept in sync with the Pyodide build job in wheels.yml
  emscripten:
    # Note: the Python version, Emscripten toolchain version are determined
    # by the Pyodide version. The appropriate versions can be found in the
    # Pyodide repodata.json "info" field, or in the Makefile.envs file:
    # https://github.com/pyodide/pyodide/blob/stable/Makefile.envs#L2
    # The Node.js version can be determined via Pyodide:
    # https://pyodide.org/en/stable/usage/index.html#node-js
    name: Pyodide build
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-wasm
      cancel-in-progress: true
    steps:
      - name: Checkout pandas Repo
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0

      - name: Set up Python for pyodide-build
        id: setup-python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: '3.12'

      - name: Set up Emscripten toolchain
        uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14
        with:
          version: '3.1.58'
          actions-cache-folder: emsdk-cache

      - name: Install pyodide-build
        # TODO: switch back to a released `pyodide-build` once a version
        # containing the fix from pyodide/pyodide-build#341 is published.
        # Pinned to the merge commit to keep the build reproducible. Without
        # this fix, `pyodide venv` produces a pip wrapper that fails to import
        # because pip>=26.1 vendors a urllib3 whose Emscripten contrib imports
        # `js` eagerly when sys.platform is patched to "emscripten".
        # See pyodide/pyodide-build#339.
        run: pip install "pyodide-build @ git+https://github.com/pyodide/pyodide-build.git@471889be97c53d160909dd7e5ef8aace17e30637"

      - name: Build pandas for Pyodide
        run: |
          pyodide build

      - name: Set up Node.js
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
        with:
          node-version: '20'

      - name: Set up Pyodide virtual environment
        env:
          pyodide-version: '0.27.1'
        run: |
          pyodide xbuildenv install ${{ env.pyodide-version }}
          pyodide venv .venv-pyodide
          source .venv-pyodide/bin/activate
          pip install dist/*.whl

      - name: Test pandas for Pyodide
        env:
          PANDAS_CI: 1
        run: |
          source .venv-pyodide/bin/activate
          pip install "pytest>=8.3.4,<9.1" hypothesis
          # do not import pandas from the checked out repo
          cd ..
          python -c 'import pandas as pd; pd.test(extra_args=["-m not clipboard and not single_cpu and not slow and not network and not db"])'
