name: Hypothesis CI

env:
  # Tell pytest and other tools to produce coloured terminal output.
  # Make sure this is also in the "passenv" section of the tox config.
  PY_COLORS: 1

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]
  workflow_dispatch:

# Cancel in-progress PR builds if another commit is pushed.
# On non-PR builds, fall back to the globally-unique run_id and don't cancel.
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  basic:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        task:
          - tox-cover
          - tox-nocover
          - tox-rest
          - check-whole-repo-tests
          - check-types-hypothesis
          - lint
          - check-format
      fail-fast: false
    env:
      PYTHON_VERSION: "3.14"
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: "Install base for Python ${{ env.PYTHON_VERSION }}"
      uses: ./.github/actions/install-base
      with:
        python-version: ${{ env.PYTHON_VERSION }}
        task: ${{ matrix.task }}
    - name: Install tox
      if: ${{ startsWith(matrix.task, 'tox-') }}
      run: |
        pip install --upgrade setuptools pip wheel
        pip install tox
    - name: Run tests
      run: |
        export TASK=${{ matrix.task }}
        if [[ $TASK == tox-* ]]; then
           TOX_TASK="${TASK#tox-}"
           cd hypothesis-python
           tox -e $TOX_TASK
        else
          ./build.sh
        fi

  req:
    needs: [basic]
    runs-on: ubuntu-latest
    strategy:
      matrix:
        task:
          - check-documentation
          - check-types-api
          - check-coverage
          - check-conjecture-coverage
          - check-py310-cover
          - check-py310-nocover
          - check-py310-niche
          - check-pypy310-cover
          # - check-py310-pyjion  # see notes in tox.ini
          - check-py311-cover
          - check-py311-nocover
          - check-py311-niche
          - check-pypy311-cover
          - check-py312-cover
          - check-py312-nocover
          - check-py312-niche
          - check-py313-cover
          - check-py313-nocover
          - check-py313-niche
          - check-py313t-cover
          - check-py313t-nocover
          - check-py313t-niche
          - check-quality
          - check-pytest9
          - check-pytest84
          - check-pytest74
          - check-py311-pytest62
          - check-django60
          - check-django42
          - check-py313-pandas22
          - check-py312-pandas21
          - check-py311-pandas20
          - check-py311-pandas15
          - check-py310-pandas14
          - check-py310-pandas13
          ## FIXME: actions update means Python builds without eg _bz2, which was required
          # - check-py310-pandas12
          # - check-py310-pandas11
      fail-fast: false
    env:
      PYTHON_VERSION: "3.14"
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: "Install base for Python ${{ env.PYTHON_VERSION }}"
      uses: ./.github/actions/install-base
      with:
        python-version: ${{ env.PYTHON_VERSION }}
        task: ${{ matrix.task }}
    - name: Run tests
      run: |
        export TASK=${{ matrix.task }}
        if [[ $TASK == check-crosshair-custom-* ]]; then
          GROUP="${TASK#check-crosshair-custom-}"
          ./build.sh check-crosshair-custom -- -n auto $(cd hypothesis-python && echo tests/$GROUP | xargs -n1 echo | grep -Ev "_py312|_py314" | xargs)
        else
          ./build.sh
        fi
    - name: Upload coverage data
      uses: actions/upload-artifact@v4
      # Invoke the magic `always` function to run on both success and failure.
      if: ${{ always() && endsWith(matrix.task, '-coverage') }}
      with:
        name: ${{ matrix.task }}-data
        include-hidden-files: true
        path: |
          hypothesis-python/.coverage*
          !hypothesis-python/.coveragerc
          hypothesis-python/branch-check*

  nonreq:
    needs: [basic]
    runs-on: ubuntu-latest
    strategy:
      matrix:
        task:
          - check-py314-cover
          - check-py314-nocover
          # Blocked by a 3.14rc1 bug. see
          # https://github.com/HypothesisWorks/hypothesis/pull/4490#issuecomment-3144989862.
          # Can revisit in 3.14rc2.
          # - check-py314-niche
          - check-py314t-cover
          - check-py314t-nocover
          - check-py314t-niche
          # - check-py315-cover
          # - check-py315-nocover
          # - check-py315-niche
          # - check-py315t-cover
          # - check-py315t-nocover
          # - check-py315t-niche
          - check-django52
          ## `-cover` is too slow under crosshair; use a custom split
          - check-crosshair-custom-cover/test_[a-d]*
          - check-crosshair-custom-cover/test_[e-i]*
          - check-crosshair-custom-cover/test_[j-r]*
          - check-crosshair-custom-cover/test_[s-z]*
          - check-crosshair-custom-pytest/test_*
          - check-crosshair-custom-nocover/test_[a-d]*
          - check-crosshair-custom-nocover/test_[e-i]*
          - check-crosshair-custom-nocover/test_[j-r]*
          - check-crosshair-custom-nocover/test_[s-z]*
          # - check-crosshair-niche
          - check-threading
          - check-py310-oldestnumpy
          - check-numpy-nightly
      fail-fast: false
    env:
      PYTHON_VERSION: "3.14"
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: "Install base for Python ${{ env.PYTHON_VERSION }}"
      uses: ./.github/actions/install-base
      with:
        python-version: ${{ env.PYTHON_VERSION }}
        task: ${{ matrix.task }}
    - name: Run tests
      run: |
        export TASK=${{ matrix.task }}
        if [[ $TASK == check-crosshair-custom-* ]]; then
          GROUP="${TASK#check-crosshair-custom-}"
          ./build.sh check-crosshair-custom -- -n auto $(cd hypothesis-python && echo tests/$GROUP | xargs -n1 echo | grep -Ev "_py312|_py314" | xargs)
        else
          ./build.sh
        fi

  cross:
    needs: [basic]
    strategy:
      matrix:
        os:
          - windows-latest
          - macos-latest
        python-version:
          - "3.11"
          - "3.14"
        python-architecture:
          - null
          - "x86"
        task:
          - cover
          - nocover
          - rest
          - alt-nocover
          - alt-rest
        exclude:
          - { os: macos-latest, python-architecture: "x86" }
          - { python-version: "3.14", python-architecture: "x86" }
          - { python-version: "3.11", task: nocover }
          - { python-version: "3.11", task: rest }
          - { python-version: "3.14", task: alt-nocover }
          - { python-version: "3.14", task: alt-rest }
      fail-fast: false
    runs-on: ${{ matrix.os }}
    env:
      # Override default from tox.ini
      PYTHONWARNDEFAULTENCODING: ""
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: "Install base for Python ${{ matrix.python-version }} ${{ matrix.python-architecture }}"
      uses: ./.github/actions/install-base
      with:
        python-version: ${{ matrix.python-version }}
        python-architecture: ${{ matrix.python-architecture }}
        task: ${{ matrix.task }}
    - name: Install tox
      run: |
        pip install --upgrade setuptools pip wheel
        pip install tox
    - name: Run tests
      working-directory: ./hypothesis-python
      run: |
        tox -e ${{ matrix.task }}

  # See https://pyodide.org/en/stable/usage/building-and-testing-packages.html
  # and https://github.com/numpy/numpy/blob/9a650391651c8486d8cb8b27b0e75aed5d36033e/.github/workflows/emscripten.yml
  test-pyodide:
    needs: [basic]
    runs-on: ubuntu-latest
    env:
      NODE_VERSION: 22
      # Note that the versions below are updated by `update_pyodide_versions()` in our weekly cronjob.
      # The versions of pyodide-build and the Pyodide runtime may differ.
      PYODIDE_VERSION: 0.29.1
      PYODIDE_BUILD_VERSION: 0.31.1
      # pyodide 0.29.0 (latest at time of writing) doesn't yet support 3.14
      PYTHON_VERSION: 3.13.2
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: "Install base for Python ${{ env.PYTHON_VERSION }}"
      uses: ./.github/actions/install-base
      with:
        python-version: ${{ env.PYTHON_VERSION }}
        task: pyodide
    - name: Set up Node
      uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
      with:
        node-version: ${{ env.NODE_VERSION }}
    - name: Install pyodide-build and Pyodide cross-build environment
      run: |
        pip install pyodide-build==${{ env.PYODIDE_BUILD_VERSION }}
        pyodide xbuildenv install ${{ env.PYODIDE_VERSION }}
    - name: Set up Pyodide venv and install dependencies
      run: |
        pip install --upgrade setuptools pip wheel build
        python -m build --wheel hypothesis-python --outdir dist/
        pip download --dest=dist/ hypothesis-python/ pytest tzdata  # fetch all the wheels
         rm dist/packaging-*.whl  # fails with `invalid metadata entry 'name'`
         pyodide venv .venv-pyodide
        source .venv-pyodide/bin/activate
        pip install dist/*.whl
    - name: Run tests
      run: |
        source .venv-pyodide/bin/activate
        # pyodide can't run multiple processes internally, so parallelize explicitly over
        # discovered test files instead (20 at a time)
        TEST_FILES=$(ls hypothesis-python/tests/cover/test*.py | grep -v "_py314")
        echo "test files: $TEST_FILES"
        parallel --max-procs 100% --max-args 20 --keep-order --line-buffer \
          python -m pytest -p no:cacheprovider <<< $TEST_FILES

  check-required:
    if: always()
    needs: [basic, req, cross]
    runs-on: ubuntu-latest
    steps:
    - name: Check required jobs
      uses: re-actors/alls-green@release/v1
      with:
        jobs: ${{ toJSON(needs) }}

  deploy:
    if: "github.event_name == 'push' && github.repository == 'HypothesisWorks/hypothesis'"
    runs-on: ubuntu-latest
    needs: [check-required]
    strategy:
      matrix:
        task:
          - deploy
      fail-fast: false
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
        token: ${{ secrets.GH_TOKEN }}
    - uses: ./.github/actions/install-base
      with:
        python-version: "3.14"
    - name: Deploy package
      env:
        GH_TOKEN: ${{ secrets.GH_TOKEN }}
        TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
      run: |
        TASK=${{ matrix.task }} ./build.sh
