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:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        task:
          - check-whole-repo-tests
          - lint
          - check-format
          - check-coverage
          - check-conjecture-coverage
          - check-py39-cover
          - check-pypy39-cover
          - 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-py314-cover
          # - check-py314-nocover
          # - check-py314-niche
          # - check-py314t-cover
          # - check-py314t-nocover
          # - check-py314t-niche
          - check-quality
          ## Skip all the (inactive/old) Rust and Ruby tests pending fixes
          # - lint-ruby
          # - check-ruby-tests
          # - check-rust-in-ruby-format
          # - lint-rust-in-ruby
          # - audit-rust-in-ruby
          # - check-conjecture-rust-format
          # - check-rust-tests
          # - audit-conjecture-rust
          # - lint-conjecture-rust
          - check-py39-nose
          - check-py39-pytest46
          - check-py39-pytest54
          - check-pytest62
          - check-django51
          - check-django50
          - check-django42
          - check-pandas22
          - check-pandas21
          - check-pandas20
          - check-pandas15
          - check-pandas14
          - check-pandas13
          ## FIXME: actions update means Python builds without eg _bz2, which was required
          # - check-py39-pandas12
          # - check-py39-pandas11
          ## `-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-nocover
          # - check-crosshair-niche
          - check-py39-oldestnumpy
          - check-numpy-nightly
      fail-fast: false
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: Set up Python 3.10
      uses: actions/setup-python@v4
      with:
        python-version: "3.10"
    - name: Restore cache
      uses: actions/cache@v3
      with:
        path: |
          ~/.cache
          ~/wheelhouse
          ~/.local
          vendor/bundle
          .tox/
        key: deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}-${{ matrix.task }}
        restore-keys: |
          deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}
          deps-${{ runner.os }}
    - name: Install dotnet6 for Pyjion
      if: ${{ endsWith(matrix.task, '-pyjion') }}
      run: |
        wget https://packages.microsoft.com/config/ubuntu/21.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
        sudo dpkg -i packages-microsoft-prod.deb
        rm packages-microsoft-prod.deb
        sudo apt-get update
        sudo apt-get install -y apt-transport-https && \
          sudo apt-get update && \
          sudo apt-get install -y dotnet-sdk-6.0
    - 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)
        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
        path: |
          hypothesis-python/.coverage*
          !hypothesis-python/.coveragerc
          hypothesis-python/branch-check*

  test-win:
    runs-on: windows-latest
    strategy:
      matrix:
        python:
          - version: "3.11"
          - version: "3.11"
            architecture: "x86"
        whichtests:
          - nocover
          - cover+rest
      fail-fast: false
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: Set up Python ${{ matrix.python.version }} ${{ matrix.python.architecture }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python.version }}
        architecture: ${{ matrix.python.architecture }}
    - name: Restore cache
      uses: actions/cache@v3
      with:
        path: |
          ~\appdata\local\pip\cache
          vendor\bundle
          .tox
        key: deps-${{ runner.os }}-${{ matrix.python.architecture }}-${{ hashFiles('requirements/*.txt') }}-${{ matrix.python.version }}
        restore-keys: |
          deps-${{ runner.os }}-${{ matrix.python.architecture }}-${{ hashFiles('requirements/*.txt') }}
          deps-${{ runner.os }}-${{ matrix.python.architecture }}
    - name: Use old pandas on win32
      if: matrix.python.architecture
      # See https://github.com/pandas-dev/pandas/issues/54979
      run: |
        (Get-Content .\requirements\coverage.txt) -replace 'numpy==[0-9.]+', 'numpy==1.26.4' | Out-File .\requirements\coverage.txt
        (Get-Content .\requirements\coverage.txt) -replace 'pandas==[0-9.]+', 'pandas==2.0.3' | Out-File .\requirements\coverage.txt
        (Get-Content .\requirements\coverage.txt) -replace 'pyarrow==[0-9.]+', '# omitted' | Out-File .\requirements\coverage.txt
    - name: Install dependencies
      run: |
        pip install --upgrade setuptools pip wheel
        pip install -r requirements/coverage.txt
        pip install hypothesis-python/[all]
    - name: Run tests
      run: python -m pytest --numprocesses auto ${{ matrix.whichtests == 'nocover' && 'hypothesis-python/tests/nocover' || 'hypothesis-python/tests/ --ignore=hypothesis-python/tests/nocover/ --ignore=hypothesis-python/tests/quality/ --ignore=hypothesis-python/tests/ghostwriter/ --ignore=hypothesis-python/tests/patching/' }}

  test-osx:
    runs-on: macos-latest
    strategy:
      matrix:
        task:
          - check-py310-cover
          - check-py310-nocover
          - check-py310-niche
          - check-py311-cover
      fail-fast: false
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: Set up Python 3.10
      uses: actions/setup-python@v4
      with:
        python-version: "3.10"
    - name: Restore cache
      uses: actions/cache@v3
      with:
        path: |
          ~/.cache
          ~/Library/Caches/pip
          .tox/
        key: deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}-${{ matrix.task }}
    - name: Run tests
      run: TASK=${{ matrix.task }} ./build.sh

  # See https://pyodide.org/en/stable/development/building-and-testing-packages.html#testing-packages-against-pyodide
  # and https://github.com/numpy/numpy/blob/9a650391651c8486d8cb8b27b0e75aed5d36033e/.github/workflows/emscripten.yml
  test-pyodide:
    runs-on: ubuntu-latest
    env:
      NODE_VERSION: 18
      # Note that the versions below must be updated in sync; we've automated
      # that with `update_pyodide_versions()` in our weekly cronjob.
      PYODIDE_VERSION: 0.27.3
      PYTHON_VERSION: 3.12.7
      EMSCRIPTEN_VERSION: 3.1.58
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      - name: Set up Node
        uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
        with:
          node-version: ${{ env.NODE_VERSION }}
      - name: Set up Emscripten
        uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14
        with:
          version: ${{ env.EMSCRIPTEN_VERSION }}
      - name: Build
        run: |
          pip install pyodide-build==$PYODIDE_VERSION
          cd hypothesis-python/
          CFLAGS=-g2 LDFLAGS=-g2 pyodide build
      - name: Set up Pyodide venv and install dependencies
        run: |
          pip install --upgrade setuptools pip wheel
          python hypothesis-python/setup.py bdist_wheel
          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=$(python -m pytest -p no:cacheprovider --setup-plan hypothesis-python/tests/cover | grep ^hypothesis-python/ | cut -d " " -f 1)
          parallel --max-procs 100% --max-args 20 --keep-order --line-buffer \
            python -m pytest -p no:cacheprovider <<< $TEST_FILES

  deploy:
    if: "github.event_name == 'push' && github.repository == 'HypothesisWorks/hypothesis'"
    runs-on: ubuntu-latest
    needs: [test, test-win, test-osx]
    strategy:
      matrix:
        task:
          - deploy
      fail-fast: false
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
        token: ${{ secrets.GH_TOKEN }}
    - name: Set up Python 3.10
      uses: actions/setup-python@v4
      with:
        python-version: "3.10"
    - name: Restore cache
      uses: actions/cache@v3
      with:
        path: |
          ~/.cache
          ~/wheelhouse
          ~/.local
          vendor/bundle
        key: deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}-${{ matrix.task }}
        restore-keys: |
          deps-${{ runner.os }}-${{ hashFiles('requirements/*.txt') }}
          deps-${{ runner.os }}
    - name: Deploy package
      env:
        GH_TOKEN: ${{ secrets.GH_TOKEN }}
        TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
        GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_TOKEN }}
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: TASK=${{ matrix.task }} ./build.sh
