tests: add Python 3.15 testing #17061
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| - preview | |
| # Trigger workflow on GitHub merge queue events | |
| # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#merge_group | |
| merge_group: | |
| types: [checks_requested] | |
| name: unittest | |
| permissions: | |
| contents: read | |
| # Configurable global environment variables for batching | |
| env: | |
| TEST_ALL_PACKAGES: "false" # Set to "false" to only run tests for packages with a git diff | |
| ALL_PYTHON: "['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15']" | |
| jobs: | |
| # Dynamic package discovery job to calculate required matrix size automatically | |
| python_config: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| all_python: ${{ steps.export.outputs.python_list }} | |
| steps: | |
| - id: export | |
| run: echo "python_list=${{ env.ALL_PYTHON }}" >> "$GITHUB_OUTPUT" | |
| discover-packages: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| batch-indices: ${{ steps.set-matrix.outputs.indices }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 | |
| with: | |
| python-version: "3.14" | |
| - name: Generate Batch Indices | |
| id: set-matrix | |
| run: | | |
| INDICES=$(python -c 'import sys; sys.path.append("ci"); import get_batches; print(get_batches.get_batch_indices())') | |
| echo "indices=${INDICES}" >> "$GITHUB_OUTPUT" | |
| unit: | |
| name: "unit-run (${{ matrix.python }}, Batch ${{ matrix.batch-index }})" | |
| runs-on: ubuntu-22.04 | |
| needs: [python_config, discover-packages] | |
| strategy: | |
| matrix: | |
| python: ${{ fromJSON(needs.python_config.outputs.all_python) }} | |
| batch-index: ${{ fromJson(needs.discover-packages.outputs.batch-indices) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| # Use a fetch-depth of 2 to avoid error `fatal: origin/main...HEAD: no merge base` | |
| # See https://github.com/googleapis/google-cloud-python/issues/12013 | |
| # and https://github.com/actions/checkout#checkout-head. | |
| with: | |
| fetch-depth: 2 | |
| persist-credentials: false | |
| - name: Setup Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 | |
| with: | |
| python-version: ${{ matrix.python }} | |
| allow-prereleases: true | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: 'packages/**/testing/constraints*.txt' | |
| - name: Install nox | |
| run: | | |
| uv pip install --system nox nox-uv | |
| # Caches compiled wheels locally to prevent building heavy libraries | |
| # such as grpcio, which we build from scratch on every run for Python 3.15+. | |
| # Follow https://github.com/grpc/grpc/issues/41010 for updates. | |
| - name: Cache Built Python 3.15 Wheels | |
| if: matrix.python == '3.15' | |
| uses: actions/cache@v4 | |
| with: | |
| path: .uv-wheel-cache | |
| key: ${{ runner.os }}-uv-wheels-3.15-${{ hashFiles('packages/**/testing/constraints*.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-uv-wheels-3.15- | |
| - name: Run unit tests | |
| id: run-tests | |
| env: | |
| COVERAGE_FILE: ${{ github.workspace }}/.coverage-${{ matrix.python }}-${{ matrix.batch-index }} | |
| # Dynamically set BUILD_TYPE to an empty string to skip the diff calculation if TEST_ALL_PACKAGES is true | |
| BUILD_TYPE: ${{ env.TEST_ALL_PACKAGES == 'true' && '' || 'presubmit' }} | |
| TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} | |
| TEST_TYPE: unit | |
| PY_VERSION: ${{ matrix.python }} | |
| # Workaround: Allows libcst to compile on Python 3.15+ while PyO3 catches up | |
| # Can be removed once libcst releases a version with native Python 3.15 wheels | |
| # Follow https://github.com/Instagram/LibCST/issues/1445 for updates. | |
| PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1" | |
| run: | | |
| UNIQUE_BATCH_PACKAGES=$(python -c 'import sys; sys.path.append("ci"); import get_batches; print(get_batches.get_batch_slice(${{ matrix.batch-index }}))') | |
| if [ -z "$UNIQUE_BATCH_PACKAGES" ]; then | |
| echo "No structural units allocated to this matrix slice." | |
| exit 0 | |
| fi | |
| echo "Running tests for packages in this weighted batch: ${UNIQUE_BATCH_PACKAGES}" | |
| # Only inject overrides if we are strictly on the 3.15 pre-release | |
| # This is needed to speed up builds | |
| if [ "${{ matrix.python }}" = "3.15" ]; then | |
| # Tell uv it is allowed to use the pre-release 3.15 toolchain | |
| export UV_PRERELEASE="allow" | |
| # Route uv's wheel and environment cache to our persistent workspace directory | |
| export UV_CACHE_DIR="${{ github.workspace }}/.uv-wheel-cache" | |
| fi | |
| ci/run_conditional_tests.sh ${UNIQUE_BATCH_PACKAGES} | |
| - name: Save Status Footprint | |
| run: | | |
| mkdir -p footprints | |
| echo "${{ steps.run-tests.outcome }}" > footprints/status.txt | |
| - name: Upload Status Footprint | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: footprint-${{ matrix.python }}-${{ matrix.batch-index }} | |
| path: footprints/ | |
| - name: Upload coverage results | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| # Appended batch-index to separate parallel coverage uploads cleanly | |
| name: coverage-artifact-${{ matrix.python }}-${{ matrix.batch-index }} | |
| path: .coverage-${{ matrix.python }}-${{ matrix.batch-index }} | |
| include-hidden-files: true | |
| cover: | |
| runs-on: ubuntu-latest | |
| needs: [unit] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| # Use a fetch-depth of 2 to avoid error `fatal: origin/main...HEAD: no merge base` | |
| # See https://github.com/googleapis/google-cloud-python/issues/12013 | |
| # and https://github.com/actions/checkout#checkout-head. | |
| with: | |
| fetch-depth: 2 | |
| persist-credentials: false | |
| - name: Setup Python | |
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.10" | |
| - name: Set number of files changes in packages directory | |
| id: packages | |
| run: | | |
| git diff HEAD~1 -- packages > /dev/null | |
| num_files_changed=$(git diff HEAD~1 -- packages | wc -l | tr -d ' ') | |
| echo "num_files_changed=${num_files_changed}" >> "$GITHUB_OUTPUT" | |
| - name: Install coverage | |
| if: ${{ steps.packages.outputs.num_files_changed > 0 }} | |
| run: | | |
| python -m pip install --upgrade setuptools pip wheel | |
| python -m pip install coverage | |
| - name: Download coverage results | |
| if: ${{ steps.packages.outputs.num_files_changed > 0 }} | |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 | |
| with: | |
| path: .coverage-results/ | |
| - name: Report coverage results | |
| if: ${{ steps.packages.outputs.num_files_changed > 0 }} | |
| env: | |
| # TODO: default to 100% coverage after next gapic-generator release | |
| # https://github.com/googleapis/google-cloud-python/issues/17459 | |
| DEFAULT_FAIL_UNDER: 99 | |
| run: | | |
| if [ -d .coverage-results ]; then | |
| # Unzip any zipped coverage results | |
| find .coverage-results -type f -name '*.zip' -exec unzip -o {} \; | |
| # Find all coverage files and combine them. | |
| # We find files starting with .coverage (excluding .coveragerc files and templates) | |
| coverage_files=$(find .coverage-results . -type f -name '.coverage*' ! -name '.coveragerc*') | |
| if [ -n "${coverage_files}" ]; then | |
| coverage combine ${coverage_files} | |
| else | |
| echo "Error: No coverage files found to combine." | |
| exit 1 | |
| fi | |
| # Find all modified packages | |
| modified_packages=$(git diff --name-only HEAD~1 -- packages | cut -d/ -f1,2 | sort -u) | |
| failed_packages=() | |
| passed_packages=() | |
| for pkg in ${modified_packages}; do | |
| if [ -d "${pkg}" ]; then | |
| echo "============================================================" | |
| echo "Evaluating coverage for package: ${pkg}" | |
| echo "============================================================" | |
| set +e | |
| pushd "${pkg}" > /dev/null | |
| if [ -f ".coveragerc" ]; then | |
| echo "Using package-specific configuration: ${pkg}/.coveragerc" | |
| # If fail_under is specified in the package-specific .coveragerc, coverage report | |
| # will automatically enforce it. Otherwise, we enforce the default. | |
| if grep -q "fail_under" ".coveragerc"; then | |
| COVERAGE_FILE=../../.coverage coverage report --include="$PWD/**" | |
| else | |
| echo "No fail_under specified in ${pkg}/.coveragerc, enforcing default" | |
| COVERAGE_FILE=../../.coverage coverage report --include="$PWD/**" --fail-under="${DEFAULT_FAIL_UNDER}" | |
| fi | |
| else | |
| echo "No .coveragerc found for ${pkg}, enforcing default" | |
| COVERAGE_FILE=../../.coverage coverage report --include="$PWD/**" --fail-under="${DEFAULT_FAIL_UNDER}" | |
| fi | |
| status=$? | |
| popd > /dev/null | |
| set -e | |
| if [ ${status} -ne 0 ]; then | |
| failed_packages+=("${pkg}") | |
| else | |
| passed_packages+=("${pkg}") | |
| fi | |
| fi | |
| done | |
| echo "============================================================" | |
| echo "Coverage Evaluation Summary" | |
| echo "============================================================" | |
| if [ ${#passed_packages[@]} -gt 0 ]; then | |
| echo "Passed packages:" | |
| for pkg in "${passed_packages[@]}"; do | |
| echo " - ${pkg}" | |
| done | |
| fi | |
| if [ ${#failed_packages[@]} -gt 0 ]; then | |
| echo "Failed packages:" | |
| for pkg in "${failed_packages[@]}"; do | |
| echo " - ${pkg}" | |
| done | |
| exit 1 | |
| fi | |
| else | |
| echo "Error: No coverage results were downloaded from the unit test jobs." | |
| echo "This usually means the unit tests did not run or failed to upload their coverage files." | |
| exit 1 | |
| fi | |
| unittest-runtime-result: | |
| name: "unit (${{ matrix.python }})" | |
| needs: [python_config, discover-packages, unit] | |
| if: always() | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python: ${{ fromJSON(needs.python_config.outputs.all_python) }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download all status footprints for this runtime | |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 | |
| with: | |
| pattern: footprint-${{ matrix.python }}-* | |
| path: footprint-results/ | |
| - name: Validate complete batch execution footprint status | |
| run: | | |
| EXPECTED_BATCHES=$(echo '${{ needs.discover-packages.outputs.batch-indices }}' | jq '. | length') | |
| if [ -d "footprint-results" ]; then | |
| ACTUAL_BATCHES=$(find footprint-results -type f -name 'status.txt' | wc -l | tr -d ' ') | |
| else | |
| ACTUAL_BATCHES=0 | |
| fi | |
| echo "Validation metrics for Python ${{ matrix.python }}:" | |
| echo " -> Expected footprint files: $EXPECTED_BATCHES" | |
| echo " -> Downloaded footprint files: $ACTUAL_BATCHES" | |
| if [ "$ACTUAL_BATCHES" -ne "$EXPECTED_BATCHES" ]; then | |
| echo "Error: Footprint count mismatch! Expected $EXPECTED_BATCHES files, but found $ACTUAL_BATCHES." | |
| exit 1 | |
| fi | |
| FAILED_RUNS=0 | |
| while read -r STATUS_FILE; do | |
| RUN_STATUS=$(cat "$STATUS_FILE" | tr -d '[:space:]') | |
| if [[ "$RUN_STATUS" != "success" ]]; then | |
| echo "Failure detected in batch profile path: $STATUS_FILE (Status: $RUN_STATUS)" | |
| FAILED_RUNS=$((FAILED_RUNS + 1)) | |
| fi | |
| done < <(find footprint-results -type f -name 'status.txt' 2>/dev/null) | |
| if [ "$FAILED_RUNS" -gt 0 ]; then | |
| echo "Error: Validation failed. Found $FAILED_RUNS failing test batches." | |
| exit 1 | |
| fi |