name: Test on: push: branches: - master pull_request: schedule: # cron every week on monday - cron: "0 0 * * 1" permissions: {} env: UV_NO_SYNC: true INLINE_SNAPSHOT_DEFAULT_FLAGS: review jobs: changes: runs-on: ubuntu-latest # Required permissions permissions: pull-requests: read # Set job outputs to values from filter step timeout-minutes: 5 outputs: src: ${{ steps.filter.outputs.src }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # For pull requests it's not necessary to checkout the code but for the main branch it is - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 id: filter with: filters: | src: - .github/workflows/test.yml - docs_src/** - fastapi/** - scripts/** - tests/** - .python-version - pyproject.toml - uv.lock test: needs: - changes if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master' timeout-minutes: 10 strategy: matrix: os: [ windows-latest, macos-latest ] python-version: [ "3.14", "3.14t" ] deprecated-tests: [ "no-deprecation" ] uv-resolution: - highest starlette-src: - starlette-pypi - starlette-git include: - os: macos-latest python-version: "3.10" coverage: coverage uv-resolution: lowest-direct deprecated-tests: "no-deprecation" - os: windows-latest python-version: "3.12" coverage: coverage uv-resolution: lowest-direct deprecated-tests: "no-deprecation" - os: ubuntu-latest python-version: "3.13" coverage: coverage uv-resolution: highest deprecated-tests: "no-deprecation" - os: ubuntu-latest python-version: "3.13" uv-resolution: highest codspeed: codspeed deprecated-tests: "no-deprecation" - os: ubuntu-latest python-version: "3.13" uv-resolution: highest deprecated-tests: "no-deprecation" without-httpx2: true - os: ubuntu-latest python-version: "3.14" coverage: coverage uv-resolution: highest starlette-src: starlette-git deprecated-tests: "test-deprecation" - os: ubuntu-latest python-version: "3.14t" coverage: coverage uv-resolution: highest deprecated-tests: "no-deprecation" fail-fast: false runs-on: ${{ matrix.os }} env: UV_PYTHON: ${{ matrix.python-version }} UV_RESOLUTION: ${{ matrix.uv-resolution }} STARLETTE_SRC: ${{ matrix.starlette-src }} steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python-version }} - name: Setup uv uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 version: "0.11.18" enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Install Dependencies run: uv sync --no-dev --group tests --extra all - name: Ensure that we have the lowest supported Pydantic version if: matrix.uv-resolution == 'lowest-direct' run: uv pip install "pydantic==2.9.0" - name: Install Starlette from source if: matrix.starlette-src == 'starlette-git' run: uv pip install "git+https://github.com/Kludex/starlette@main" - name: Install deprecated libraries just for testing if: matrix.deprecated-tests == 'test-deprecation' run: uv pip install orjson ujson - name: Uninstall httpx2 to run tests with httpx if: matrix.without-httpx2 == 'true' run: uv pip uninstall httpx2 - name: Reinstall SQLAlchemy without Cython extensions if: matrix.python-version == '3.14t' && matrix.os == 'ubuntu-latest' run: "DISABLE_SQLALCHEMY_CEXT=1 uv pip install --force-reinstall --no-binary :all: sqlalchemy" - run: mkdir coverage - name: Test run: uv run --no-sync bash scripts/test-cov.sh $PYTEST_OPTIONS env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.deprecated-tests}} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.deprecated-tests}} PYTEST_OPTIONS: ${{ (matrix.without-httpx2 == 'true') && '-W ignore::UserWarning' || '' }} # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow - name: Store coverage files if: matrix.coverage == 'coverage' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.deprecated-tests}}-${{ hashFiles('**/coverage/.coverage.*') }} path: coverage include-hidden-files: true benchmark: needs: - changes if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master' runs-on: ubuntu-latest timeout-minutes: 5 env: UV_PYTHON: "3.13" UV_RESOLUTION: highest steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.13" - name: Setup uv uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 version: "0.11.18" enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Install Dependencies run: uv sync --no-dev --group tests --extra all - name: CodSpeed benchmarks uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1 with: mode: simulation run: uv run --no-sync pytest tests/benchmarks --codspeed - name: CodSpeed memory benchmark uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1 with: mode: memory run: uv run --no-sync pytest tests/memory_benchmarks --codspeed regression-test: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check out the pull request if: github.event_name == 'pull_request' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.sha }} path: pr persist-credentials: false fetch-depth: 0 - name: Find changed tests if: github.event_name == 'pull_request' id: changed-tests working-directory: pr env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | git diff --name-only --diff-filter=AM -z "$BASE_SHA" "$HEAD_SHA" -- tests \ | while IFS= read -r -d '' file; do case "$(basename "$file")" in test_*.py) printf '%s\0' "$file" ;; esac done > "$RUNNER_TEMP/changed-tests" if [ -s "$RUNNER_TEMP/changed-tests" ]; then echo "found=true" >> "$GITHUB_OUTPUT" git diff --binary "$BASE_SHA" "$HEAD_SHA" -- tests \ > "$RUNNER_TEMP/tests.patch" else echo "found=false" >> "$GITHUB_OUTPUT" echo "No added or modified test files; regression proof is not applicable." fi - name: Check out the base revision if: steps.changed-tests.outputs.found == 'true' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.base.sha }} path: base persist-credentials: false - name: Set up Python if: steps.changed-tests.outputs.found == 'true' uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version-file: "base/.python-version" - name: Setup uv if: steps.changed-tests.outputs.found == 'true' uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 version: "0.11.18" enable-cache: true - name: Run the changed tests against the base code if: steps.changed-tests.outputs.found == 'true' working-directory: base run: | git apply "$RUNNER_TEMP/tests.patch" uv sync --locked --no-dev --group tests --extra all set +e xargs -0 uv run --no-sync pytest -- < "$RUNNER_TEMP/changed-tests" status=$? set -e if [ "$status" -eq 0 ]; then echo "::warning::The changed tests already pass on the base revision. Check whether the fix is still needed." echo "### Regression proof: base already passes :warning:" >> "$GITHUB_STEP_SUMMARY" echo "The changed tests pass without the pull request's code changes." >> "$GITHUB_STEP_SUMMARY" else echo "The changed tests fail on the base revision as expected (pytest exit code $status)." echo "### Regression proof: base fails as expected :white_check_mark:" >> "$GITHUB_STEP_SUMMARY" fi coverage-combine: needs: - test runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version-file: ".python-version" - name: Setup uv uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 with: # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837 version: "0.11.18" enable-cache: true cache-dependency-glob: | pyproject.toml uv.lock - name: Install Dependencies run: uv sync --locked --no-dev --group tests --extra all - name: Get coverage files uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: coverage-* path: coverage merge-multiple: true - run: ls -la coverage - run: uv run coverage combine coverage - run: uv run coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-html path: htmlcov include-hidden-files: true - run: uv run coverage report --fail-under=100 # https://github.com/marketplace/actions/alls-green#why test-alls-green: # This job does nothing and is only used for the branch protection if: always() needs: - test - coverage-combine - benchmark - regression-test runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 with: jobs: ${{ toJSON(needs) }} allowed-skips: coverage-combine,test,benchmark