name: Unit tests

env:
  # enable colored output
  # https://github.com/pytest-dev/pytest/issues/7443
  PY_COLORS: 1

on:
  pull_request:
    paths:
      - .github/workflows/python-tests.yaml
      - "src/prefect/**/*.py"
      - "tests/**/*.py"
      - pyproject.toml
      - Dockerfile
      - scripts/entrypoint.sh
  push:
    branches:
      - main
    paths:
      - .github/workflows/python-tests.yaml
      - "src/prefect/**/*.py"
      - "tests/**/*.py"
      - pyproject.toml
      - Dockerfile
      - scripts/entrypoint.sh
permissions:
  contents: read
  actions: write

# Limit concurrency by workflow/branch combination.
#
# For pull request builds, pushing additional changes to the
# branch will cancel prior in-progress and pending builds.
#
# For builds triggered on a branch push, additional changes
# will wait for prior builds to complete before starting.
#
# https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  run-tests:
    runs-on: ubuntu-latest
    name: ${{ matrix.test-type.name }} - python:${{ matrix.python-version }}, ${{ matrix.database }}
    strategy:
      fail-fast: false
      matrix:
        test-type:
          - name: Server Tests
            modules: tests/server/ tests/events/server --ignore=tests/server/database/ --ignore=tests/server/orchestration/
          - name: Database and Orchestration Tests
            modules: tests/server/database/ tests/server/orchestration/
          - name: Client Tests
            modules: >-
              tests/
              --ignore=tests/typesafety
              --ignore=tests/server/
              --ignore=tests/events/server
              --ignore=tests/test_task_runners.py
              --ignore=tests/runner
              --ignore=tests/workers
              --ignore=tests/cli/
              --ignore=tests/test_settings.py
              --ignore=tests/input/
          - name: Runner, Worker, Settings, Input, and CLI Tests
            modules: tests/test_task_runners.py tests/runner tests/workers tests/cli/ tests/test_settings.py tests/input/
        database:
          - "postgres:14"
          - "sqlite"
        python-version:
          - "3.10"
          - "3.11"
          - "3.12"
          - "3.13"
          - "3.14"
        exclude:
          - database: "sqlite"
            test-type:
              name: Client Tests
              modules: >-
                tests/
                --ignore=tests/typesafety
                --ignore=tests/server/
                --ignore=tests/events/server
                --ignore=tests/test_task_runners.py
                --ignore=tests/runner
                --ignore=tests/workers
                --ignore=tests/cli/
                --ignore=tests/test_settings.py
                --ignore=tests/input/
          - database: "sqlite"
            test-type:
              name: Runner, Worker, Settings, Input, and CLI Tests
              modules: tests/test_task_runners.py tests/runner tests/workers tests/cli/ tests/test_settings.py tests/input/

    timeout-minutes: 15

    steps:
      - name: Display current test matrix
        run: echo '${{ toJSON(matrix) }}'

      - uses: actions/checkout@v6
        with:
          persist-credentials: false
          fetch-depth: 0

      - name: Set up uv and Python ${{ matrix.python-version }}
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true
          python-version: ${{ matrix.python-version }}
          cache-dependency-glob: "pyproject.toml"

      - name: Install packages
        run: uv sync --locked

      - name: Start database container
        if: ${{ startsWith(matrix.database, 'postgres') }}
        run: >
          docker run
          --name "postgres"
          --detach
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
          --publish 5432:5432
          --tmpfs /var/lib/postgresql/data
          --env POSTGRES_USER="prefect"
          --env POSTGRES_PASSWORD="prefect"
          --env POSTGRES_DB="prefect"
          --env LANG="C.UTF-8"
          --env LANGUAGE="C.UTF-8"
          --env LC_ALL="C.UTF-8"
          --env LC_COLLATE="C.UTF-8"
          --env LC_CTYPE="C.UTF-8"
          ${{ matrix.database }}
          -c max_connections=250

          ./scripts/wait-for-healthy-container.sh postgres 30

          echo "PREFECT_SERVER_DATABASE_CONNECTION_URL=postgresql+asyncpg://prefect:prefect@localhost/prefect" >> $GITHUB_ENV

      - name: Start redis
        run: >
          docker run
          --name "redis"
          --detach
          --publish 6379:6379
          redis:latest

      - name: Run tests
        run: |
          uv run pytest ${{ matrix.test-type.modules }} \
          --numprocesses auto \
          --maxprocesses 6 \
          --dist worksteal \
          --disable-docker-image-builds \
          --exclude-service kubernetes \
          --exclude-service docker \
          --durations 26 \

      - name: Check database container
        # Only applicable for Postgres, but we want this to run even when tests fail
        if: always()
        run: |
          docker container inspect postgres \
          && docker container logs postgres \
          || echo "Ignoring bad exit code"

  run-docker-tests:
    runs-on: ubuntu-latest
    name: docker, python:${{ matrix.python-version }}
    strategy:
      matrix:
        database:
          - "postgres:14"
        python-version:
          - "3.10"
          - "3.11"
          - "3.12"
          - "3.13"
          - "3.14"

      fail-fast: false

    timeout-minutes: 45

    steps:
      - name: Display current test matrix
        run: echo '${{ toJSON(matrix) }}'

      - uses: actions/checkout@v6
        with:
          persist-credentials: false
          fetch-depth: 0

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Set up uv and Python ${{ matrix.python-version }}
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true
          python-version: ${{ matrix.python-version }}
          cache-dependency-glob: "pyproject.toml"

      - name: Get image tag
        id: get_image_tag
        run: |
          SHORT_SHA=$(git rev-parse --short=7 HEAD)
          tmp="sha-$SHORT_SHA-python${{ matrix.python-version }}"
          echo "image_tag=${tmp}" >> $GITHUB_OUTPUT

      - name: Get node version
        id: get_node_version
        run: |
          echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT

      - name: Login to DockerHub
        uses: docker/login-action@v3
        if: github.event.pull_request.head.repo.full_name == github.repository
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build test image
        uses: docker/build-push-action@v6
        env:
          DOCKER_BUILD_SUMMARY: false
          DOCKER_RECORD_UPLOAD: false
        with:
          context: .
          build-args: |
            PYTHON_VERSION=${{ matrix.python-version }}
            PREFECT_EXTRAS=[dev]
            NODE_VERSION=${{ steps.get_node_version.outputs.NODE_VERSION }}
          tags: prefecthq/prefect-dev:${{ steps.get_image_tag.outputs.image_tag }}
          outputs: type=docker,dest=/tmp/image.tar
          cache-from: type=gha,scope=test-${{ matrix.python-version }},ignore-error=true
          cache-to: type=gha,mode=max,scope=test-${{ matrix.python-version }},ignore-error=true

      - name: Test Docker image
        run: |
          docker load --input /tmp/image.tar
          docker run --rm prefecthq/prefect-dev:${{ steps.get_image_tag.outputs.image_tag }} prefect version

      - name: Install packages
        run: uv sync --locked

      - name: Start database container
        if: ${{ startsWith(matrix.database, 'postgres') }}
        run: >
          docker run
          --name "postgres"
          --detach
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
          --publish 5432:5432
          --tmpfs /var/lib/postgresql/data
          --env POSTGRES_USER="prefect"
          --env POSTGRES_PASSWORD="prefect"
          --env POSTGRES_DB="prefect"
          --env LANG="C.UTF-8"
          --env LANGUAGE="C.UTF-8"
          --env LC_ALL="C.UTF-8"
          --env LC_COLLATE="C.UTF-8"
          --env LC_CTYPE="C.UTF-8"
          ${{ matrix.database }}
          -c max_connections=250

          ./scripts/wait-for-healthy-container.sh postgres 30

          echo "PREFECT_SERVER_DATABASE_CONNECTION_URL=postgresql+asyncpg://prefect:prefect@localhost/prefect" >> $GITHUB_ENV

      - name: Start docker registry
        run: >
          docker run
          --name "prefect-test-registry"
          --detach
          --publish 5555:5000
          registry:2

      - name: Start redis
        run: >
          docker run
          --name "redis"
          --detach
          --publish 6379:6379
          redis:latest

      - name: Check git diff
        run: |
          git diff --exit-code

      - name: Run tests
        run: |
          uv run pytest tests --ignore=tests/typesafety \
          --numprocesses auto \
          --maxprocesses 6 \
          --dist worksteal \
          --disable-docker-image-builds \
          --only-service docker \
          --durations 26

      - name: Check database container
        # Only applicable for Postgres, but we want this to run even when tests fail
        if: always()
        run: |
          docker container inspect postgres \
          && docker container logs postgres \
          || echo "Ignoring bad exit code"

  run-typesafety-test:
    name: typesafety
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
          fetch-depth: 0

      - name: Set up uv
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true
          python-version: "3.12"
          cache-dependency-glob: "pyproject.toml"

      - name: Install packages
        run: uv sync --locked

      - name: Run tests
        run: |
          uv run pytest tests/typesafety \
          --disable-docker-image-builds
