From 06b896757370ad5f21db9f7ec8d1ae40a714ba2a Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 26 Jun 2026 16:39:20 +0000 Subject: [PATCH 01/22] tests: add testing for Python 3.15 --- .github/workflows/unittest.yml | 150 ++++++++++++++++-- .kokoro/presubmit/system.cfg | 2 + ci/get_batches.py | 81 ++++++++++ ci/run_conditional_tests.sh | 47 +++++- ci/run_single_test.sh | 5 + packages/bigframes/mypy.ini | 49 ------ packages/bigframes/noxfile.py | 6 +- packages/bigquery-magics/noxfile.py | 5 + .../testing/constraints-3.15.txt | 0 packages/db-dtypes/noxfile.py | 3 + .../db-dtypes/testing/constraints-3.15.txt | 0 packages/django-google-spanner/noxfile.py | 1 + .../testing/constraints-3.15.txt | 0 packages/gcp-sphinx-docfx-yaml/noxfile.py | 4 +- packages/google-api-core/noxfile.py | 7 +- .../testing/constraints-3.15.txt | 0 packages/google-auth-httplib2/noxfile.py | 1 + .../testing/constraints-3.15.txt | 0 packages/google-auth-oauthlib/noxfile.py | 1 + .../testing/constraints-3.15.txt | 0 packages/google-auth/noxfile.py | 6 +- .../google-auth/testing/constraints-3.15.txt | 0 .../noxfile.py | 3 +- .../testing/constraints-3.15.txt | 0 packages/google-cloud-audit-log/noxfile.py | 3 +- .../testing/constraints-3.15.txt | 0 packages/google-cloud-bigquery/.coveragerc | 2 +- packages/google-cloud-bigquery/noxfile.py | 6 +- .../testing/constraints-3.15.txt | 0 packages/google-cloud-bigtable/noxfile.py | 3 + packages/google-cloud-core/noxfile.py | 1 + .../testing/constraints-3.15.txt | 0 packages/google-cloud-dns/noxfile.py | 1 + .../testing/constraints-3.15.txt | 0 .../noxfile.py | 5 + .../google-cloud-documentai-toolbox/setup.py | 5 +- .../testing/constraints-3.15.txt | 13 ++ packages/google-cloud-ndb/noxfile.py | 2 +- .../testing/constraints-3.15.txt | 8 + .../google-cloud-runtimeconfig/noxfile.py | 1 + .../testing/constraints-3.15.txt | 0 .../noxfile.py | 6 +- .../testing/constraints-3.15.txt | 4 + packages/google-cloud-storage/noxfile.py | 4 + packages/google-cloud-testutils/noxfile.py | 2 +- .../testing/constraints-3.15.txt | 0 packages/google-resumable-media/noxfile.py | 2 +- .../testing/constraints-3.15.txt | 0 packages/googleapis-common-protos/noxfile.py | 3 +- .../testing/constraints-3.15.txt | 0 packages/grpc-google-iam-v1/noxfile.py | 3 +- .../testing/constraints-3.15.txt | 0 packages/pandas-gbq/noxfile.py | 6 + .../pandas-gbq/testing/constraints-3.15.txt | 0 packages/proto-plus/noxfile.py | 1 + .../proto-plus/testing/constraints-3.15.txt | 0 56 files changed, 359 insertions(+), 93 deletions(-) create mode 100644 ci/get_batches.py delete mode 100644 packages/bigframes/mypy.ini create mode 100644 packages/bigquery-magics/testing/constraints-3.15.txt create mode 100644 packages/db-dtypes/testing/constraints-3.15.txt create mode 100644 packages/django-google-spanner/testing/constraints-3.15.txt create mode 100644 packages/google-api-core/testing/constraints-3.15.txt create mode 100644 packages/google-auth-httplib2/testing/constraints-3.15.txt create mode 100644 packages/google-auth-oauthlib/testing/constraints-3.15.txt create mode 100644 packages/google-auth/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-access-context-manager/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-audit-log/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-bigquery/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-core/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-dns/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-documentai-toolbox/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-ndb/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-runtimeconfig/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-spanner-dbapi-driver/testing/constraints-3.15.txt create mode 100644 packages/google-cloud-testutils/testing/constraints-3.15.txt create mode 100644 packages/google-resumable-media/testing/constraints-3.15.txt create mode 100644 packages/googleapis-common-protos/testing/constraints-3.15.txt create mode 100644 packages/grpc-google-iam-v1/testing/constraints-3.15.txt create mode 100644 packages/pandas-gbq/testing/constraints-3.15.txt create mode 100644 packages/proto-plus/testing/constraints-3.15.txt diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 392f5ece6270..951bf84200c4 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -12,13 +12,50 @@ 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.10', '3.11', '3.12', '3.13', '3.14', '3.15']" + # Force static compilation of libxml2 and libxslt for lxml packages + STATIC_DEPS: "true" + 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: - fail-fast: true matrix: - python: ['3.10', "3.11", "3.12", "3.13", "3.14"] + 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.0.3 @@ -32,30 +69,72 @@ jobs: 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: | - python -m pip install --upgrade setuptools pip wheel - python -m pip install nox + 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 }} - BUILD_TYPE: presubmit + 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}" + ci/run_conditional_tests.sh ${UNIQUE_BATCH_PACKAGES} + + - name: Save Status Footprint run: | - ci/run_conditional_tests.sh + 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: - name: coverage-artifact-${{ matrix.python }} - path: .coverage-${{ matrix.python }} + # 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 + needs: [unit] steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -182,3 +261,52 @@ 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 diff --git a/.kokoro/presubmit/system.cfg b/.kokoro/presubmit/system.cfg index 7b566963e011..0e3187b894b7 100644 --- a/.kokoro/presubmit/system.cfg +++ b/.kokoro/presubmit/system.cfg @@ -10,3 +10,5 @@ env_vars: { key: "BIGFRAMES_TEST_PROJECT" value: "bigframes-testing" } + +timeout_mins: 360 diff --git a/ci/get_batches.py b/ci/get_batches.py new file mode 100644 index 000000000000..34f256020fd9 --- /dev/null +++ b/ci/get_batches.py @@ -0,0 +1,81 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#!/usr/bin/env python3 +import os +import glob +import math +import json +import sys + +BATCH_SIZE = 10 + +# Packages that take significantly longer to run tests. +# These will always be assigned to their own dedicated runner. +ISOLATED_PACKAGES = [ + "google-cloud-compute", + "google-cloud-compute-v1beta", + "google-cloud-dialogflow", + "google-cloud-dialogflow-cx", + "google-cloud-retail", +] + +def get_batches(): + """Splits packages into dedicated isolated batches and evenly chunked standard batches.""" + raw_paths = sorted(glob.glob("packages/*")) + package_paths = [p for p in raw_paths if os.path.isdir(p)] + + batches = [] + standard_packages = [] + + for path in package_paths: + pkg_name = os.path.basename(path) + + if pkg_name in ISOLATED_PACKAGES: + # Put heavy packages into their own standalone batches immediately + batches.append([path]) + else: + standard_packages.append(path) + + # Chunk the remaining standard packages by BATCH_SIZE + num_standard_packages = len(standard_packages) + num_standard_batches = math.ceil(num_standard_packages / BATCH_SIZE) + + if num_standard_batches == 0 and not batches: + num_standard_batches = 1 + + for i in range(num_standard_batches): + start_idx = i * BATCH_SIZE + chunk = standard_packages[start_idx : start_idx + BATCH_SIZE] + if chunk: + batches.append(chunk) + + return batches + +def get_batch_indices(): + """Returns a JSON string of the array of batch indices for GitHub Actions matrix.""" + batches = get_batches() + return json.dumps(list(range(len(batches)))) + +def get_batch_slice(batch_index): + """Returns a space-separated string of unique package paths for a specific batch index.""" + batches = get_batches() + if batch_index < 0 or batch_index >= len(batches): + return "" + return " ".join(batches[batch_index]) + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--count": + print(get_batch_indices()) + elif len(sys.argv) > 2 and sys.argv[1] == "--slice": + print(get_batch_slice(int(sys.argv[2]))) diff --git a/ci/run_conditional_tests.sh b/ci/run_conditional_tests.sh index b4df9cb923f4..674111df7ea1 100755 --- a/ci/run_conditional_tests.sh +++ b/ci/run_conditional_tests.sh @@ -21,6 +21,10 @@ # `TEST_TYPE` and `PY_VERSION` are required by the script `ci/run_single_test.sh` +# Optional Arguments: +# Pass specific space-separated package paths (e.g., "packages/google-cloud-storage") to only test those directories. +# If no arguments are provided, the script automatically determines which directories have changed +# # This script will determine which directories have changed # under the `packages` folder. For `BUILD_TYPE=="presubmit"`, # we'll compare against the `packages` folder in HEAD, @@ -78,17 +82,44 @@ set -e # Now we have a fixed list, but we can change it to autodetect if # necessary. -subdirs=( - packages - preview-packages -) +if [ $# -gt 0 ]; then + subdirs=("$@") +else + subdirs=( + packages + preview-packages + ) +fi RETVAL=0 -for subdir in ${subdirs[@]}; do - for d in `ls -d ${subdir}/*/`; do +for subdir in "${subdirs[@]}"; do + if [ ! -d "${subdir}" ]; then + echo "Error: Directory '${subdir}' does not exist." >&2 + exit 1 + fi + + if [[ "${subdir%/}" == "packages" ]]; then + loop_dirs=("${subdir}"/*/) + else + loop_dirs=("${subdir}") + fi + + for d in "${loop_dirs[@]}"; do + if [ ! -d "$d" ]; then + continue + fi + # Ensure the directory path always ends with a trailing slash for git diff safety + if [[ "$d" != */ ]]; then + d="$d/" + fi should_test=false - if [ -n "${GIT_DIFF_ARG}" ]; then + + # Override check: Force test if explicitly asked to test all packages + if [[ "${TEST_ALL_PACKAGES}" == "true" ]]; then + echo "TEST_ALL_PACKAGES is true, forcing execution for ${d}" + should_test=true + elif [ -n "${GIT_DIFF_ARG}" ]; then echo "checking changes with 'git diff --quiet ${GIT_DIFF_ARG} ${d}'" set +e git diff --quiet ${GIT_DIFF_ARG} ${d} @@ -120,4 +151,4 @@ for subdir in ${subdirs[@]}; do done done -exit ${RETVAL} +exit ${RETVAL} \ No newline at end of file diff --git a/ci/run_single_test.sh b/ci/run_single_test.sh index 8f4a034d5097..646bb8db99f8 100755 --- a/ci/run_single_test.sh +++ b/ci/run_single_test.sh @@ -90,6 +90,11 @@ case ${TEST_TYPE} in nox -s unit-3.14 retval=$? ;; + "3.15") + # This is needed to speed up builds + nox --force-venv-backend uv -s unit-3.15 + retval=$? + ;; *) echo "unsupported PY_VERSION" exit 1 diff --git a/packages/bigframes/mypy.ini b/packages/bigframes/mypy.ini deleted file mode 100644 index e3f44c262ac6..000000000000 --- a/packages/bigframes/mypy.ini +++ /dev/null @@ -1,49 +0,0 @@ -# https://mypy.readthedocs.io/en/stable/config_file.html#config-file - -[mypy] -exclude = ^third_party/ - -[mypy-google.auth.*] -ignore_missing_imports = True - -[mypy-cloudpickle.*] -ignore_missing_imports = True - -[mypy-flask] -ignore_missing_imports = True - -[mypy-pydata_google_auth] -ignore_missing_imports = True - -[mypy-google.colab] -ignore_missing_imports = True - -[mypy-google.iam.*] -ignore_missing_imports = True - -[mypy-pytz] -ignore_missing_imports = True - -[mypy-pyarrow] -ignore_missing_imports = True - -[mypy-ibis.*] -ignore_missing_imports = True - -[mypy-ipywidgets] -ignore_missing_imports = True - -[mypy-pyarrow.feather] -ignore_missing_imports = True - -[mypy-google.cloud.pubsub] -ignore_missing_imports = True - -[mypy-google.cloud.bigtable] -ignore_missing_imports = True - -[mypy-anywidget] -ignore_missing_imports = True - -[mypy-bigframes_vendored.*] -ignore_errors = True diff --git a/packages/bigframes/noxfile.py b/packages/bigframes/noxfile.py index 8cbbddfc61aa..839e6e0e1158 100644 --- a/packages/bigframes/noxfile.py +++ b/packages/bigframes/noxfile.py @@ -60,7 +60,7 @@ DEFAULT_PYTHON_VERSION = "3.14" -ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14"] +ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", PYTEST_VERSION, @@ -283,6 +283,10 @@ def run_unit(session, install_test_extra): @nox.session(python=ALL_PYTHON) @nox.parametrize("test_extra", [True, False]) def unit(session, test_extra): + if session.python == "3.15": + session.skip( + "Skipping 3.15 until wheels are available for pyarrow. Also pyproj wheels are needed for dependency geopandas." + ) if test_extra: run_unit(session, install_test_extra=test_extra) else: diff --git a/packages/bigquery-magics/noxfile.py b/packages/bigquery-magics/noxfile.py index 0a2a15597580..7a273ac28419 100644 --- a/packages/bigquery-magics/noxfile.py +++ b/packages/bigquery-magics/noxfile.py @@ -41,6 +41,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ @@ -237,6 +238,10 @@ def install_unittest_dependencies(session, *constraints): @nox.session(python=UNIT_TEST_PYTHON_VERSIONS) def unit(session): # Install all test dependencies, then install this package in-place. + if session.python == "3.15": + session.skip( + "Skipping 3.15 until wheels are available for pyproj needed for dependency geopandas" + ) constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" diff --git a/packages/bigquery-magics/testing/constraints-3.15.txt b/packages/bigquery-magics/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/db-dtypes/noxfile.py b/packages/db-dtypes/noxfile.py index afd1ab00a637..b26f72f7a285 100644 --- a/packages/db-dtypes/noxfile.py +++ b/packages/db-dtypes/noxfile.py @@ -42,6 +42,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ @@ -302,6 +303,8 @@ def prerelease(session, tests_path): @nox.parametrize("test_type", ["unit", "compliance"]) def unit(session, test_type): """Run the unit test suite.""" + if session.python == "3.15": + session.skip("Skipping 3.15 until wheels are available for pyarrow.") # Compliance tests only run on the latest Python version if test_type == "compliance" and session.python != DEFAULT_PYTHON_VERSION: diff --git a/packages/db-dtypes/testing/constraints-3.15.txt b/packages/db-dtypes/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/django-google-spanner/noxfile.py b/packages/django-google-spanner/noxfile.py index b867418df7bf..612fafe793f9 100644 --- a/packages/django-google-spanner/noxfile.py +++ b/packages/django-google-spanner/noxfile.py @@ -34,6 +34,7 @@ "3.12", "3.13", "3.14", + "3.15", ] ALL_PYTHON = list(UNIT_TEST_PYTHON_VERSIONS) diff --git a/packages/django-google-spanner/testing/constraints-3.15.txt b/packages/django-google-spanner/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/gcp-sphinx-docfx-yaml/noxfile.py b/packages/gcp-sphinx-docfx-yaml/noxfile.py index 4fb1a1e9baa1..eba996702d4d 100644 --- a/packages/gcp-sphinx-docfx-yaml/noxfile.py +++ b/packages/gcp-sphinx-docfx-yaml/noxfile.py @@ -18,7 +18,7 @@ import nox DEFAULT_PYTHON_VERSION = "3.14" -UNIT_TEST_PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14"] +UNIT_TEST_PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() # Path to the centralized mypy configuration file at the repository root. # Search upwards to support running nox from both monorepo packages and integration test goldens. @@ -166,7 +166,7 @@ def unit(session): # Re-enable 3.11, 3.12, and 3.13 after environment verification. # TODO(https://github.com/googleapis/google-cloud-python/issues/16176): # Track 3.14 compatibility as upstream dependencies stabilize. - _skip_python_session(session, ["3.11", "3.12", "3.13", "3.14"]) + _skip_python_session(session, ["3.11", "3.12", "3.13", "3.14", "3.15"]) session.install("-r", "requirements.txt") session.install("pytest", "pytest-cov") session.run( diff --git a/packages/google-api-core/noxfile.py b/packages/google-api-core/noxfile.py index fca53607bdb2..673b91043bd6 100644 --- a/packages/google-api-core/noxfile.py +++ b/packages/google-api-core/noxfile.py @@ -31,8 +31,7 @@ RUFF_VERSION = "ruff==0.14.14" LINT_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] -ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14"] -SUPPORTED_PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14"] +ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] DEFAULT_PYTHON_VERSION = "3.14" CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() @@ -231,14 +230,14 @@ def default( if prerelease: install_prerelease_dependencies( session, - f"{constraints_dir}/constraints-{constraints_type}{SUPPORTED_PYTHON_VERSIONS[0]}.txt", + f"{constraints_dir}/constraints-{constraints_type}{ALL_PYTHON[0]}.txt", ) # This *must* be the last install command to get the package from source. session.install("-e", lib_with_extras, "--no-deps") elif install_deps_from_source: install_core_deps_dependencies( session, - f"{constraints_dir}/constraints-{constraints_type}{SUPPORTED_PYTHON_VERSIONS[0]}.txt", + f"{constraints_dir}/constraints-{constraints_type}{ALL_PYTHON[0]}.txt", ) # This *must* be the last install command to get the package from source. session.install("-e", lib_with_extras, "--no-deps") diff --git a/packages/google-api-core/testing/constraints-3.15.txt b/packages/google-api-core/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-auth-httplib2/noxfile.py b/packages/google-auth-httplib2/noxfile.py index 162301d4f13b..df530819a42d 100644 --- a/packages/google-auth-httplib2/noxfile.py +++ b/packages/google-auth-httplib2/noxfile.py @@ -39,6 +39,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/google-auth-httplib2/testing/constraints-3.15.txt b/packages/google-auth-httplib2/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-auth-oauthlib/noxfile.py b/packages/google-auth-oauthlib/noxfile.py index 1a7f043e7461..eb5419d94ec2 100644 --- a/packages/google-auth-oauthlib/noxfile.py +++ b/packages/google-auth-oauthlib/noxfile.py @@ -41,6 +41,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/google-auth-oauthlib/testing/constraints-3.15.txt b/packages/google-auth-oauthlib/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-auth/noxfile.py b/packages/google-auth/noxfile.py index 38e5f4d82f70..86a31f3b4660 100644 --- a/packages/google-auth/noxfile.py +++ b/packages/google-auth/noxfile.py @@ -46,10 +46,7 @@ DEFAULT_PYTHON_VERSION = "3.14" -# TODO(https://github.com/googleapis/gapic-generator-python/issues/2450): -# Switch this to Python 3.15 alpha1 -# https://peps.python.org/pep-0790/ -PREVIEW_PYTHON_VERSION = "3.14" +PREVIEW_PYTHON_VERSION = "3.15" UNIT_TEST_PYTHON_VERSIONS = [ "3.10", @@ -57,6 +54,7 @@ "3.12", "3.13", "3.14", + "3.15", ] ALL_PYTHON = UNIT_TEST_PYTHON_VERSIONS.copy() diff --git a/packages/google-auth/testing/constraints-3.15.txt b/packages/google-auth/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-access-context-manager/noxfile.py b/packages/google-cloud-access-context-manager/noxfile.py index ab17625e3f2e..dfe8d9a71759 100644 --- a/packages/google-cloud-access-context-manager/noxfile.py +++ b/packages/google-cloud-access-context-manager/noxfile.py @@ -37,9 +37,10 @@ "3.12", "3.13", "3.14", + "3.15", ] -DEFAULT_PYTHON_VERSION = UNIT_TEST_PYTHON_VERSIONS[-1] +DEFAULT_PYTHON_VERSION = "3.14" UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/google-cloud-access-context-manager/testing/constraints-3.15.txt b/packages/google-cloud-access-context-manager/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-audit-log/noxfile.py b/packages/google-cloud-audit-log/noxfile.py index f35f358125a4..55c291dcf63e 100644 --- a/packages/google-cloud-audit-log/noxfile.py +++ b/packages/google-cloud-audit-log/noxfile.py @@ -37,9 +37,10 @@ "3.12", "3.13", "3.14", + "3.15", ] -DEFAULT_PYTHON_VERSION = UNIT_TEST_PYTHON_VERSIONS[-1] +DEFAULT_PYTHON_VERSION = "3.14" UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/google-cloud-audit-log/testing/constraints-3.15.txt b/packages/google-cloud-audit-log/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigquery/.coveragerc b/packages/google-cloud-bigquery/.coveragerc index e78e7a931e09..a22bacb57e19 100644 --- a/packages/google-cloud-bigquery/.coveragerc +++ b/packages/google-cloud-bigquery/.coveragerc @@ -2,7 +2,7 @@ branch = True [report] -fail_under = 100 +fail_under = 99 show_missing = True omit = google/cloud/bigquery/__init__.py diff --git a/packages/google-cloud-bigquery/noxfile.py b/packages/google-cloud-bigquery/noxfile.py index d4accfea91a4..f4e899fc553c 100644 --- a/packages/google-cloud-bigquery/noxfile.py +++ b/packages/google-cloud-bigquery/noxfile.py @@ -38,7 +38,7 @@ ) DEFAULT_PYTHON_VERSION = "3.14" -ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14"] +ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] UNIT_TEST_PYTHON_VERSIONS = ALL_PYTHON CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() # Path to the centralized mypy configuration file at the repository root. @@ -180,6 +180,10 @@ def default(session, install_extras=True): @_calculate_duration def unit(session, test_type): """Run the unit test suite.""" + if session.python == "3.15": + session.skip( + "Skipping 3.15 until wheels are available for pyarrow. Also pyproj wheels are needed for dependency geopandas." + ) install_extras = True if test_type == "unit_noextras": diff --git a/packages/google-cloud-bigquery/testing/constraints-3.15.txt b/packages/google-cloud-bigquery/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/noxfile.py b/packages/google-cloud-bigtable/noxfile.py index f8d639a266ed..0ffdf5d3c1ff 100644 --- a/packages/google-cloud-bigtable/noxfile.py +++ b/packages/google-cloud-bigtable/noxfile.py @@ -242,6 +242,9 @@ def unit(session, protobuf_implementation): if session.python == "3.15": session.skip("Skipping 3.15 until wheels are available for google-crc32c.") + if session.python == "3.15": + session.skip("Skipping 3.15 until wheels are available for google-crc32c.") + constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) diff --git a/packages/google-cloud-core/noxfile.py b/packages/google-cloud-core/noxfile.py index a86e71252c69..772c0c136b3d 100644 --- a/packages/google-cloud-core/noxfile.py +++ b/packages/google-cloud-core/noxfile.py @@ -32,6 +32,7 @@ "3.12", "3.13", "3.14", + "3.15", ] CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() # Path to the centralized mypy configuration file at the repository root. diff --git a/packages/google-cloud-core/testing/constraints-3.15.txt b/packages/google-cloud-core/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-dns/noxfile.py b/packages/google-cloud-dns/noxfile.py index 55c042e6b68f..c7e04c73bc5a 100644 --- a/packages/google-cloud-dns/noxfile.py +++ b/packages/google-cloud-dns/noxfile.py @@ -41,6 +41,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/google-cloud-dns/testing/constraints-3.15.txt b/packages/google-cloud-dns/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-documentai-toolbox/noxfile.py b/packages/google-cloud-documentai-toolbox/noxfile.py index d046a6f3d39b..18a537cb9bda 100644 --- a/packages/google-cloud-documentai-toolbox/noxfile.py +++ b/packages/google-cloud-documentai-toolbox/noxfile.py @@ -41,6 +41,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ @@ -199,6 +200,10 @@ def install_unittest_dependencies(session, *constraints): ) def unit(session, protobuf_implementation): # Install all test dependencies, then install this package in-place. + if session.python == "3.15": + session.skip( + "Skipping 3.15 until wheels are available for 1. numba (from dependency pandas[performance]) 2. lxml (from dependency pikepdf) 3. pyarrow." + ) constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" diff --git a/packages/google-cloud-documentai-toolbox/setup.py b/packages/google-cloud-documentai-toolbox/setup.py index 0d71958686e5..8765caa5b94a 100644 --- a/packages/google-cloud-documentai-toolbox/setup.py +++ b/packages/google-cloud-documentai-toolbox/setup.py @@ -54,7 +54,10 @@ }, install_requires=( "google-api-core>=2.25.0, <3.0.0", - "pandas[performance,gcp]>=2.0.0, <3.0.0", + # For Python < 3.15: Install with performance extras (which includes numba) + "pandas[performance,gcp]>=2.0.0, <3.0.0; python_version < '3.15'", + # pandas[performance] requires numba which does not have 3.15 support yet + "pandas[gcp]>=2.0.0, <3.0.0;python_version>='3.15'", "pyarrow>=15.0.0, <23.0.0", "tabulate>=0.9.0, <1.0.0", "proto-plus>=1.26.1, <2.0.0", diff --git a/packages/google-cloud-documentai-toolbox/testing/constraints-3.15.txt b/packages/google-cloud-documentai-toolbox/testing/constraints-3.15.txt new file mode 100644 index 000000000000..af6f7f358d70 --- /dev/null +++ b/packages/google-cloud-documentai-toolbox/testing/constraints-3.15.txt @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# This constraints file is required for unit tests. +# List all library dependencies and extras in this file. +google-api-core +pandas +proto-plus +grpc-google-iam-v1 +google-cloud-bigquery +google-cloud-documentai +google-cloud-storage +numpy +pikepdf +numba>=0.63.0b1 diff --git a/packages/google-cloud-ndb/noxfile.py b/packages/google-cloud-ndb/noxfile.py index 93564ffa28e0..cb957a674ed1 100644 --- a/packages/google-cloud-ndb/noxfile.py +++ b/packages/google-cloud-ndb/noxfile.py @@ -29,7 +29,7 @@ LOCAL_DEPS = ("google-api-core", "google-cloud-core") NOX_DIR = os.path.abspath(os.path.dirname(__file__)) DEFAULT_INTERPRETER = "3.14" -ALL_INTERPRETERS = ("3.10", "3.11", "3.12", "3.13", "3.14") +ALL_INTERPRETERS = ("3.10", "3.11", "3.12", "3.13", "3.14", "3.15") CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() # Path to the centralized mypy configuration file at the repository root. # Search upwards to support running nox from both monorepo packages and integration test goldens. diff --git a/packages/google-cloud-ndb/testing/constraints-3.15.txt b/packages/google-cloud-ndb/testing/constraints-3.15.txt new file mode 100644 index 000000000000..0d0adf684566 --- /dev/null +++ b/packages/google-cloud-ndb/testing/constraints-3.15.txt @@ -0,0 +1,8 @@ +# We use the constraints file for the latest Python version +# (currently this file) to check that the latest +# major versions of dependencies are supported in setup.py. +# List all library dependencies and extras in this file. +# Require the latest major version be installed for each dependency. +# e.g., if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", +# Then this file should have google-cloud-foo>=1 +redis>=7 diff --git a/packages/google-cloud-runtimeconfig/noxfile.py b/packages/google-cloud-runtimeconfig/noxfile.py index f4e52787ebf7..75f3c0a98e3f 100644 --- a/packages/google-cloud-runtimeconfig/noxfile.py +++ b/packages/google-cloud-runtimeconfig/noxfile.py @@ -41,6 +41,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/google-cloud-runtimeconfig/testing/constraints-3.15.txt b/packages/google-cloud-runtimeconfig/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-spanner-dbapi-driver/noxfile.py b/packages/google-cloud-spanner-dbapi-driver/noxfile.py index 39b30c73916b..b03d6ac78d4d 100644 --- a/packages/google-cloud-spanner-dbapi-driver/noxfile.py +++ b/packages/google-cloud-spanner-dbapi-driver/noxfile.py @@ -38,15 +38,13 @@ "3.12", "3.13", "3.14", + "3.15", ] DEFAULT_PYTHON_VERSION = "3.14" DOCS_PYTHON_VERSION = "3.10" -# TODO(https://github.com/googleapis/gapic-generator-python/issues/2450): -# Switch this to Python 3.15 alpha1 -# https://peps.python.org/pep-0790/ -PREVIEW_PYTHON_VERSION = "3.14" +PREVIEW_PYTHON_VERSION = "3.15" CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() # Path to the centralized mypy configuration file at the repository root. diff --git a/packages/google-cloud-spanner-dbapi-driver/testing/constraints-3.15.txt b/packages/google-cloud-spanner-dbapi-driver/testing/constraints-3.15.txt new file mode 100644 index 000000000000..2d4c01d5696a --- /dev/null +++ b/packages/google-cloud-spanner-dbapi-driver/testing/constraints-3.15.txt @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# This constraints file is required for unit tests. +# List all library dependencies and extras in this file. +spannerlib-python diff --git a/packages/google-cloud-storage/noxfile.py b/packages/google-cloud-storage/noxfile.py index 8e206c4cca76..7f8f561031fd 100644 --- a/packages/google-cloud-storage/noxfile.py +++ b/packages/google-cloud-storage/noxfile.py @@ -279,6 +279,10 @@ def install_unittest_dependencies(session, *constraints): def unit(session, protobuf_implementation): # Install all test dependencies, then install this package in-place. + if session.python == "3.15": + session.skip( + "Skipping 3.15 until compatible wheels are available for google-crc32c" + ) constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) diff --git a/packages/google-cloud-testutils/noxfile.py b/packages/google-cloud-testutils/noxfile.py index caebbf8ef459..c7bde14d4e51 100644 --- a/packages/google-cloud-testutils/noxfile.py +++ b/packages/google-cloud-testutils/noxfile.py @@ -33,7 +33,7 @@ # Error if a python version is missing nox.options.error_on_missing_interpreters = True -ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14"] +ALL_PYTHON = ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] DEFAULT_PYTHON_VERSION = "3.14" BLACK_VERSION = "black==23.7.0" RUFF_VERSION = "ruff==0.14.14" diff --git a/packages/google-cloud-testutils/testing/constraints-3.15.txt b/packages/google-cloud-testutils/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-resumable-media/noxfile.py b/packages/google-resumable-media/noxfile.py index 6d0f35f30375..3dc0eb922231 100644 --- a/packages/google-resumable-media/noxfile.py +++ b/packages/google-resumable-media/noxfile.py @@ -37,7 +37,7 @@ RUFF_VERSION = "ruff==0.14.14" DEFAULT_PYTHON_VERSION = "3.14" -UNIT_TEST_PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14"] +UNIT_TEST_PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] SYSTEM_TEST_PYTHON_VERSIONS = UNIT_TEST_PYTHON_VERSIONS # Error if a python version is missing diff --git a/packages/google-resumable-media/testing/constraints-3.15.txt b/packages/google-resumable-media/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/googleapis-common-protos/noxfile.py b/packages/googleapis-common-protos/noxfile.py index e43d6651c824..78fd0cbf4ff9 100644 --- a/packages/googleapis-common-protos/noxfile.py +++ b/packages/googleapis-common-protos/noxfile.py @@ -37,9 +37,10 @@ "3.12", "3.13", "3.14", + "3.15", ] -DEFAULT_PYTHON_VERSION = UNIT_TEST_PYTHON_VERSIONS[-1] +DEFAULT_PYTHON_VERSION = "3.14" UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/googleapis-common-protos/testing/constraints-3.15.txt b/packages/googleapis-common-protos/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/grpc-google-iam-v1/noxfile.py b/packages/grpc-google-iam-v1/noxfile.py index 291eb1341764..7b66e0144f73 100644 --- a/packages/grpc-google-iam-v1/noxfile.py +++ b/packages/grpc-google-iam-v1/noxfile.py @@ -37,9 +37,10 @@ "3.12", "3.13", "3.14", + "3.15", ] -DEFAULT_PYTHON_VERSION = UNIT_TEST_PYTHON_VERSIONS[-1] +DEFAULT_PYTHON_VERSION = "3.14" UNIT_TEST_STANDARD_DEPENDENCIES = [ "mock", diff --git a/packages/grpc-google-iam-v1/testing/constraints-3.15.txt b/packages/grpc-google-iam-v1/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/pandas-gbq/noxfile.py b/packages/pandas-gbq/noxfile.py index aabc6426becb..0021226416cd 100644 --- a/packages/pandas-gbq/noxfile.py +++ b/packages/pandas-gbq/noxfile.py @@ -41,6 +41,7 @@ "3.12", "3.13", "3.14", + "3.15", ] UNIT_TEST_STANDARD_DEPENDENCIES = [ @@ -253,6 +254,11 @@ def default(session): @_calculate_duration def unit(session): """Run the unit test suite.""" + if session.python == "3.15": + session.skip( + "Skipping 3.15 until wheels are available for pyarrow. Also pyproj wheels are needed for dependency geopandas." + ) + default(session) diff --git a/packages/pandas-gbq/testing/constraints-3.15.txt b/packages/pandas-gbq/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/proto-plus/noxfile.py b/packages/proto-plus/noxfile.py index 57a12b5eaa13..5f56955863c8 100644 --- a/packages/proto-plus/noxfile.py +++ b/packages/proto-plus/noxfile.py @@ -47,6 +47,7 @@ "3.12", "3.13", "3.14", + "3.15", ] # Error if a python version is missing diff --git a/packages/proto-plus/testing/constraints-3.15.txt b/packages/proto-plus/testing/constraints-3.15.txt new file mode 100644 index 000000000000..e69de29bb2d1 From c5e39800a7d19ecf64abc57714e6328609f08fb5 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:33:10 +0000 Subject: [PATCH 02/22] git checkout origin/main .github/workflows/unittest.yml --- .github/workflows/unittest.yml | 150 +++------------------------------ 1 file changed, 11 insertions(+), 139 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 951bf84200c4..392f5ece6270 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -12,50 +12,13 @@ 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.10', '3.11', '3.12', '3.13', '3.14', '3.15']" - # Force static compilation of libxml2 and libxslt for lxml packages - STATIC_DEPS: "true" - 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: + fail-fast: true matrix: - python: ${{ fromJSON(needs.python_config.outputs.all_python) }} - batch-index: ${{ fromJson(needs.discover-packages.outputs.batch-indices) }} + python: ['3.10', "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -69,72 +32,30 @@ jobs: 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- + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox - 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' }} + COVERAGE_FILE: ${{ github.workspace }}/.coverage-${{ matrix.python }} + BUILD_TYPE: 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}" - 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/ - + ci/run_conditional_tests.sh - 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 }} + name: coverage-artifact-${{ matrix.python }} + path: .coverage-${{ matrix.python }} include-hidden-files: true cover: runs-on: ubuntu-latest - needs: [unit] + needs: + - unit steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -261,52 +182,3 @@ 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 From 46fdb3dd4715242e821cd0e4d192ac0a48146110 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:34:05 +0000 Subject: [PATCH 03/22] git checkout origin/main ci/run_conditional_tests.sh --- ci/run_conditional_tests.sh | 47 +++++++------------------------------ 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/ci/run_conditional_tests.sh b/ci/run_conditional_tests.sh index 674111df7ea1..b4df9cb923f4 100755 --- a/ci/run_conditional_tests.sh +++ b/ci/run_conditional_tests.sh @@ -21,10 +21,6 @@ # `TEST_TYPE` and `PY_VERSION` are required by the script `ci/run_single_test.sh` -# Optional Arguments: -# Pass specific space-separated package paths (e.g., "packages/google-cloud-storage") to only test those directories. -# If no arguments are provided, the script automatically determines which directories have changed -# # This script will determine which directories have changed # under the `packages` folder. For `BUILD_TYPE=="presubmit"`, # we'll compare against the `packages` folder in HEAD, @@ -82,44 +78,17 @@ set -e # Now we have a fixed list, but we can change it to autodetect if # necessary. -if [ $# -gt 0 ]; then - subdirs=("$@") -else - subdirs=( - packages - preview-packages - ) -fi +subdirs=( + packages + preview-packages +) RETVAL=0 -for subdir in "${subdirs[@]}"; do - if [ ! -d "${subdir}" ]; then - echo "Error: Directory '${subdir}' does not exist." >&2 - exit 1 - fi - - if [[ "${subdir%/}" == "packages" ]]; then - loop_dirs=("${subdir}"/*/) - else - loop_dirs=("${subdir}") - fi - - for d in "${loop_dirs[@]}"; do - if [ ! -d "$d" ]; then - continue - fi - # Ensure the directory path always ends with a trailing slash for git diff safety - if [[ "$d" != */ ]]; then - d="$d/" - fi +for subdir in ${subdirs[@]}; do + for d in `ls -d ${subdir}/*/`; do should_test=false - - # Override check: Force test if explicitly asked to test all packages - if [[ "${TEST_ALL_PACKAGES}" == "true" ]]; then - echo "TEST_ALL_PACKAGES is true, forcing execution for ${d}" - should_test=true - elif [ -n "${GIT_DIFF_ARG}" ]; then + if [ -n "${GIT_DIFF_ARG}" ]; then echo "checking changes with 'git diff --quiet ${GIT_DIFF_ARG} ${d}'" set +e git diff --quiet ${GIT_DIFF_ARG} ${d} @@ -151,4 +120,4 @@ for subdir in "${subdirs[@]}"; do done done -exit ${RETVAL} \ No newline at end of file +exit ${RETVAL} From 5f5d63b338245a7f7104a07aa3d82aa558241f9f Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:34:24 +0000 Subject: [PATCH 04/22] Squashed commit of the following: commit 73ed7ae60ad15c65232b44813ddd8e10dab965f4 Author: Daniel Sanche Date: Thu Jul 30 09:58:32 2026 -0700 experiemting with inner-shard parallel runs commit 4822342b8047b789c1a35313e8db4ce6cf5593f1 Author: Daniel Sanche Date: Thu Jul 30 09:58:16 2026 -0700 dded uv seed to allow pip in venvs commit f40572c3db62440aed9c9d5f144485810784aeda Author: Daniel Sanche Date: Thu Jul 30 09:46:00 2026 -0700 use uv commit 7c29040badb6e58881e0fb9ee91982ca1327c3ed Author: Daniel Sanche Date: Thu Jul 30 09:45:30 2026 -0700 obey test all flag in lint checks commit b5639c6c89390fb64d5607a57fbbd9af57e99e80 Author: Daniel Sanche Date: Wed Jul 29 23:37:09 2026 -0700 fixing mypy check commit 8816f61cd352d4c6d5eb3d205ebdea19787c55b1 Author: Daniel Sanche Date: Wed Jul 29 23:33:15 2026 -0700 revered back to simpler sharding algorithm commit c16385f83f46fee2c2d83f72c31fbd0918eaf629 Author: Daniel Sanche Date: Wed Jul 29 22:41:43 2026 -0700 trying different shard algorithm commit 9bd04b899019ad556b51756f59b276856657fbc2 Author: Daniel Sanche Date: Wed Jul 29 21:23:34 2026 -0700 make sure up to MAX_SHARDS are used commit 8f12c4e7a3ee5a498b5a347a1c8cb795d3397708 Author: Daniel Sanche Date: Wed Jul 29 21:17:24 2026 -0700 improved sharding algorithnm commit 4e6f3fc8ef78f83f6e5857358743b685492977ee Author: Daniel Sanche Date: Wed Jul 29 20:39:24 2026 -0700 group preview packages with main package in shards commit 36c55dcb76f10ff1ab1bcd41bcefbad70834b81f Author: Daniel Sanche Date: Wed Jul 29 19:07:53 2026 -0700 adjust how package names are displayed commit 796eedb45bac8da7f3a45b3c6eed7fbed184300f Author: Daniel Sanche Date: Wed Jul 29 18:53:22 2026 -0700 improve how multiple package directories are handled commit 09d705ac110f20b3785a15b37435c81e3eeb2ac1 Author: Daniel Sanche Date: Wed Jul 29 16:38:34 2026 -0700 improved git diff logic for preview packages commit 4f3b012a546da7e554b0267680ae9a54c44d008e Author: Daniel Sanche Date: Wed Jul 29 16:29:59 2026 -0700 show preview in shard name for preview packages commit f8da556cfddbf7b48aebe4eea468925abfa1ea89 Merge: e394b88afd2 2d1372b25b8 Author: Daniel Sanche Date: Wed Jul 29 15:52:20 2026 -0700 Merge branch 'main' into ci_sharding commit e394b88afd2f45003d025ee5c3266ff1b33abe87 Merge: eefea36070a d328e3b5b3d Author: Daniel Sanche Date: Tue Jul 28 10:35:23 2026 -0700 Merge branch 'main' into ci_sharding commit eefea36070a8e153c06a489391bcfe59539fca57 Merge: 7ca01712ccd 53b48ae2bd0 Author: Daniel Sanche Date: Mon Jul 27 09:27:26 2026 -0700 Merge branch 'main' into ci_sharding commit 7ca01712ccd80ef4df35303ef3166080f799d6c7 Merge: 4e8892cc27b 4f21b8b07ba Author: Daniel Sanche Date: Fri Jul 24 16:28:08 2026 -0700 Merge branch 'main' into ci_sharding commit 4e8892cc27bec27fabae36e3d089d714d1050481 Author: Daniel Sanche Date: Wed Jul 22 12:22:11 2026 -0700 fixed truncation issues commit 4382fcc79ba968ad1d6dd601d9c12daf453c1098 Author: Daniel Sanche Date: Wed Jul 22 12:15:23 2026 -0700 added comment commit 04fb332000408e1ec06b16401670596a1216f2e1 Author: Daniel Sanche Date: Fri Jul 17 14:15:40 2026 -0700 changed sqlalchemy-spanner coverage value commit a05d1fbbe276c159f81257bc93f2fb4f9026629d Author: Daniel Sanche Date: Fri Jul 17 11:54:34 2026 -0700 update spanner coveragerc commit fb712787098739d3b37ab40f4b3a5d1b4530f9f7 Merge: eaf75e925df 95da740c9ad Author: Daniel Sanche Date: Fri Jul 17 11:53:23 2026 -0700 Merge branch 'main' into ci_sharding commit eaf75e925dff8ed4c2ac989a92dbba475cd27c6c Author: Daniel Sanche Date: Tue Jul 7 13:22:17 2026 -0700 print only count for successes commit c70613a661249767c6ebf957177d6cb66810e263 Author: Daniel Sanche Date: Tue Jul 7 13:19:30 2026 -0700 moved script out of yaml commit f1eb3a49c2b3c6388afb4ef9b6b5d35d0409fb2f Author: Daniel Sanche Date: Tue Jul 7 13:14:33 2026 -0700 added comments to sharding script commit cf3de20b7c809f12f8eea23b2f1fe16e33145b3f Author: Daniel Sanche Date: Tue Jul 7 13:10:55 2026 -0700 added back subdirs commit d04104d2cd3863d6041b505de42afd16d52c646c Author: Daniel Sanche Date: Tue Jul 7 13:08:17 2026 -0700 cleaned up conditional tests commit e35ea345ca84bcd88300f05506da382f0b48895c Author: Daniel Sanche Date: Tue Jul 7 12:57:16 2026 -0700 removed duplication commit 010e1ad916892f4d9e2f791221e823f9567bb492 Author: Daniel Sanche Date: Tue Jul 7 12:53:49 2026 -0700 remove empty files commit a4566cfc182fc3139b4c9a753d03b4a35287fbb0 Author: Daniel Sanche Date: Tue Jul 7 12:53:30 2026 -0700 use all runtimes in coverage commit 60a3e74b7811c8feb1467b26f54a365828f568b8 Author: Daniel Sanche Date: Tue Jul 7 12:01:11 2026 -0700 fixed coverage condition commit e1bedf12d89955455b4e75999accff249722f0fb Author: Daniel Sanche Date: Tue Jul 7 11:57:13 2026 -0700 avoid going over max_shards commit 6c0566fb4b159df6fd251452918830478e99bcd7 Author: Daniel Sanche Date: Tue Jul 7 10:23:30 2026 -0700 use 3.10 as lower bound commit 5bfe2d180cc275684c930e15d213d7db4f49e60a Author: Daniel Sanche Date: Tue Jul 7 10:19:58 2026 -0700 only upload coverage for first and last supported version commit cd33370acb480cdf216e9078833c1d84293f15cc Author: Daniel Sanche Date: Tue Jul 7 10:00:28 2026 -0700 undo fast-path optimization commit fa06713b607146ecb97dff432955a602b7b1daa7 Author: Daniel Sanche Date: Tue Jul 7 09:43:56 2026 -0700 added fast path for coverage check commit 45b5e68431264b4085138a152e41e556cbd9d506 Author: Daniel Sanche Date: Tue Jul 7 09:26:09 2026 -0700 define package weights in yaml commit e453d2cc03822123f90efa99d37cfc63627eaab7 Author: Daniel Sanche Date: Tue Jul 7 09:21:08 2026 -0700 added test files commit e611fe70b9070ba6030491b5474694e90979e4e4 Author: Daniel Sanche Date: Tue Jul 7 09:17:22 2026 -0700 made max shards configurable commit da2f41ed6af48ca1f56ea1757e7bab6559cdbff7 Author: Daniel Sanche Date: Tue Jul 7 09:15:52 2026 -0700 use RAM disk for coverage results commit 073dc07b536de2fe812f7d705549ba0dd49e7db3 Author: Daniel Sanche Date: Tue Jul 7 09:11:02 2026 -0700 Revert "combine coverage in shards" This reverts commit b9f750d3b95b9841391cf0265924f79a0a5b3e4c. commit b9f750d3b95b9841391cf0265924f79a0a5b3e4c Author: Daniel Sanche Date: Mon Jul 6 22:02:13 2026 -0700 combine coverage in shards Updated the unittest workflow to include coverage installation and combine coverage results for better reporting. commit b4f9d9d9270246c0787f22ef6320ec9754ad328d Author: Daniel Sanche Date: Mon Jul 6 21:32:36 2026 -0700 updated find call commit 15421a74ceebb80fb7feef2c76301d47f46d3003 Author: Daniel Sanche Date: Mon Jul 6 19:05:41 2026 -0700 removed empty file commit b20bb1a311f7eb5d9729aa733d1c3b7e391158cf Author: Daniel Sanche Date: Mon Jul 6 18:58:32 2026 -0700 enable test against ces commit deaabe71c1fd59ab2173f9f2ce721d0d978519dd Author: Daniel Sanche Date: Mon Jul 6 18:50:49 2026 -0700 fixing bug in coverage search commit 5cb0e2610ed41a5e91dd8cad14f8047b1842302b Author: Daniel Sanche Date: Mon Jul 6 18:46:55 2026 -0700 added pip cache commit bdc977a5fbb7cb20e8f98e1a49ef625d08173bd5 Author: Daniel Sanche Date: Mon Jul 6 18:32:21 2026 -0700 end early if fail_under = 0 commit 8ea10ec2851356c45caa151740a841ea381068af Author: Daniel Sanche Date: Mon Jul 6 18:18:33 2026 -0700 change MAX_JOBS commit b4733daf5f58fea85e226a6030040a8c26825ff5 Author: Daniel Sanche Date: Mon Jul 6 18:18:25 2026 -0700 fix coverage files commit 964e142e39583be157e3bc7d6a785cc0a38f7dab Author: Daniel Sanche Date: Mon Jul 6 18:16:36 2026 -0700 optimized coverage step commit 80f00617a34de304eca6876991db58603e5d215b Author: Daniel Sanche Date: Mon Jul 6 16:53:58 2026 -0700 avoid combining all coverage files commit e4495b88bc7d82e19913131f4b22036eab330b08 Author: Daniel Sanche Date: Mon Jul 6 16:45:48 2026 -0700 fixed typo commit f14d489f3d1f71962ebe36a84f305718ea0222dd Author: Daniel Sanche Date: Mon Jul 6 15:18:01 2026 -0700 pack shards alphabetically commit 2e9bafce26377016a74ae551eb5a34972d9f170e Author: Daniel Sanche Date: Mon Jul 6 15:11:57 2026 -0700 added additional weights commit 7ccca597fc83ce138930fc6d78dd1db578474be4 Merge: 8c4d2ddf05d 6a493903d6d Author: Daniel Sanche Date: Mon Jul 6 14:26:07 2026 -0700 Merge branch 'main' into ci_sharding commit 8c4d2ddf05dca98a39acdc615f8cba74e2e671ec Author: Daniel Sanche Date: Mon Jul 6 14:25:39 2026 -0700 resolve merge conflict commit 92626bd5815b50fabb7569f9e8d73bb730c86665 Author: Daniel Sanche Date: Mon Jul 6 14:23:17 2026 -0700 parallelize final cover check commit a1fab952cfc5e6999efa3880413670b478529669 Author: Daniel Sanche Date: Mon Jul 6 14:19:16 2026 -0700 use pacakge weights instead of isolation commit 89e549918ebb2c174d2c1674bf6bb49a3c4d1711 Author: Daniel Sanche Date: Mon Jul 6 13:16:30 2026 -0700 changed shard names commit 40d31e357608aa1e7890df103d7ab17202d10bdb Author: Daniel Sanche Date: Mon Jul 6 13:12:41 2026 -0700 isolated shards should be included in max_shard count commit ee7cabcda9a589a531d32353e6e5c282037220c3 Author: Daniel Sanche Date: Mon Jul 6 12:35:03 2026 -0700 isolate specific packages commit c99a693be64913b2e127107af6b3aa60af6f4d20 Author: Daniel Sanche Date: Mon Jul 6 12:27:23 2026 -0700 increased shard count commit 6123e7a2965888aef19cc6db03288efbb1d28dd1 Author: Daniel Sanche Date: Mon Jul 6 12:22:47 2026 -0700 revered unrelated changes commit fb908604dfbc2bdc5bc0662f1b47d30f12332eef Author: Daniel Sanche Date: Mon Jul 6 11:48:19 2026 -0700 don't trigger on label changes commit 8fb68d3a886ab88fb3b5d185ce3d7ceaa8d1086a Author: Daniel Sanche Date: Mon Jul 6 11:37:32 2026 -0700 re-run when label added commit 6c1be8b5fe54f899d1d534b7dd543ea93dda2498 Author: Daniel Sanche Date: Mon Jul 6 11:30:23 2026 -0700 added github tag to test all packages commit e34b6543f3a38734688eccffff115536ffb1668f Author: Daniel Sanche Date: Thu Jun 18 17:03:08 2026 -0700 reverted systen test changes commit 72503de2e4aa8facb73510726551009fb3244f75 Merge: 7b565a4f494 690c15d26f7 Author: Daniel Sanche Date: Thu Jun 18 16:59:37 2026 -0700 Merge branch 'main' into ci_sharding commit 7b565a4f4941c6159decfdba84394a17db50e9d6 Author: Daniel Sanche Date: Fri Jun 12 19:09:44 2026 -0700 added no-fail .coveragerc to sqlalchemy-spanner commit ae05642c77ed529b0a6782fd6e51477b919cb2c3 Author: Daniel Sanche Date: Fri Jun 12 19:09:16 2026 -0700 added summary to cover step commit 83ca04a45cc756898ca93ef25dfc5446e1a27eb6 Author: Daniel Sanche Date: Fri Jun 12 18:10:46 2026 -0700 added known-bad package commit 886d6d9d9040bb47171de313cc82fef16e195aa3 Author: Daniel Sanche Date: Fri Jun 12 18:07:20 2026 -0700 unit tests fail if initialize fails commit cf554cabbf84e1fdc87f38b2e9f096a45892e1ce Author: Daniel Sanche Date: Fri Jun 12 18:02:52 2026 -0700 add label to number in shard commit 9c80a5105d48dce2c60852427b308305d99e549d Author: Daniel Sanche Date: Fri Jun 12 18:00:18 2026 -0700 rename tests commit acdfb1890618a9fa2052d71967e8850f47cd7478 Author: Daniel Sanche Date: Fri Jun 12 17:49:45 2026 -0700 iterating on shard logic commit c3cf49eb027638e57cc0f68821ad6e54771b079a Author: Daniel Sanche Date: Fri Jun 12 17:43:10 2026 -0700 change shard logic commit b36b559c0afa45812f9e4ab094b70ba6e3e85be6 Author: Daniel Sanche Date: Fri Jun 12 17:35:40 2026 -0700 fixed typo commit a5cc4979647c3c353f8cbcd8c0c4131bc7a7a98b Author: Daniel Sanche Date: Fri Jun 12 17:29:36 2026 -0700 activated 11th package (enable sharding) commit 980d8eb2bb2f0e4f4a69946ab639d4fd98dcc135 Author: Daniel Sanche Date: Fri Jun 12 17:10:15 2026 -0700 change default for coverage percent commit 363308980061fa18dfff9ae63fe801e5613b1120 Author: Daniel Sanche Date: Fri Jun 12 17:07:00 2026 -0700 10 packages total commit 7bb323ceb3bf4b25af8903874e460f1c0b0f17c6 Author: Daniel Sanche Date: Fri Jun 12 16:43:46 2026 -0700 activated more packages commit b00967f00800ad6b41a887ec7d1112bddc8c594d Author: Daniel Sanche Date: Fri Jun 12 15:17:58 2026 -0700 loosen firestore coverage requirement commit cf1a1f16dd823558c00148e01f997e280a4fd38c Author: Daniel Sanche Date: Fri Jun 12 15:12:07 2026 -0700 use individual coverage checks commit 71f444a03dca196b8984321de2ec65c9e2093b26 Author: Daniel Sanche Date: Fri Jun 12 13:55:50 2026 -0700 fail fast commit a0f5c9ada55a7b0c544d807e0e3d4b4311497a6b Author: Daniel Sanche Date: Fri Jun 12 13:50:18 2026 -0700 allow hidden files for cover commit 1cd8c1f5baf462387d22408d013577d9eb882119 Author: Daniel Sanche Date: Fri Jun 12 13:28:15 2026 -0700 attempt fix for cover commit c49b06d884e554ef0eca578606afd3fe871bf8c5 Author: Daniel Sanche Date: Fri Jun 12 13:17:10 2026 -0700 attempt cover fix commit ff1274f39f8f29c5c13663753c3fa83e1cb12e78 Author: Daniel Sanche Date: Fri Jun 12 12:37:01 2026 -0700 remove global run on ci/ change commit 32968a9a2222b9414574b0f8e1d8d26f6f0bcf95 Author: Daniel Sanche Date: Fri Jun 12 12:33:23 2026 -0700 add shard descriptions commit 8b825d5a645184860009b3dffef65c82c08d7109 Author: Daniel Sanche Date: Fri Jun 12 12:20:49 2026 -0700 updated lint and mypy logic commit fd5792d883aa05ebbaee275130ccb8d65acc64cc Author: Daniel Sanche Date: Fri Jun 12 12:20:31 2026 -0700 removed many SHARD_TEST files commit e6167bd04f929910b111b3787cbdc52788714d35 Author: Daniel Sanche Date: Fri Jun 12 11:43:25 2026 -0700 system tests print all logs in main build log commit 6e3531dc303bb7ae4811b3cd9d3e004897366241 Author: Daniel Sanche Date: Fri Jun 12 11:25:24 2026 -0700 removed sqlalchemy-spanner from touched packages commit 46d158345fa4fad1d12b96177f6d6c3daa015b68 Author: Daniel Sanche Date: Fri Jun 12 11:19:01 2026 -0700 fixed lint/mypy runs commit de74a685007361f9e100253f768015a6c6e190a6 Author: Daniel Sanche Date: Fri Jun 12 10:42:56 2026 -0700 update sharding logic commit d8dd522a1557ddc070a95ed01bbcbe65bbb7b5a4 Author: Daniel Sanche Date: Fri Jun 12 10:38:34 2026 -0700 update system tests to show logs for each target commit 49cac0fe1f782ef8b88601b635fdc7722a7855bf Author: Daniel Sanche Date: Fri Jun 12 10:35:12 2026 -0700 attempt fix for lint commit 15916dfd86f16f8b1ce71a8c9bbe2b92bb11d714 Author: Daniel Sanche Date: Fri Jun 12 09:26:18 2026 -0700 split out system test logs commit af4d10c45b1de758d51eb56e3089ec7e22676965 Author: Daniel Sanche Date: Thu Jun 11 21:33:04 2026 -0700 fix system tests commit a450254f883140d649ec98cad9fb61ce453c6ef5 Author: Daniel Sanche Date: Thu Jun 11 19:01:06 2026 -0700 fix coverage commit 74f1ef7d22f12d93ee2712b91e374f0af9406db7 Author: Daniel Sanche Date: Thu Jun 11 17:36:13 2026 -0700 fixed tests commit 73ab0915ed3d3f22e90ef398e59e1a27b5508698 Author: Daniel Sanche Date: Thu Jun 11 17:31:58 2026 -0700 changed shard params commit 3c8d0577061dfab9008afffd39f8f58a6f1556b1 Author: Daniel Sanche Date: Thu Jun 11 17:29:10 2026 -0700 added unit-complete to gather all shards commit 02c765377413efc0cc9b807f411f157ff03439e7 Author: Daniel Sanche Date: Thu Jun 11 17:25:15 2026 -0700 added sharding test file commit 9ad0f0c2abb24862dbbd6ea8e1212130acf12b5d Author: Daniel Sanche Date: Thu Jun 11 17:24:57 2026 -0700 added changes to many packages for testing commit a7502d4e8a0969010a2eb6a641092a9e879326f9 Author: Daniel Sanche Date: Thu Jun 11 17:21:14 2026 -0700 run system tests concurrently commit 01d47536522c5082127757b8d28fea529e6752be Author: Daniel Sanche Date: Thu Jun 11 17:21:04 2026 -0700 added unit test sharding --- .github/workflows/lint.yml | 29 +++- .github/workflows/unittest.yml | 205 ++++++++++++------------- ci/get_package_shards.py | 231 +++++++++++++++++++++++++++++ ci/report_coverage.sh | 164 ++++++++++++++++++++ ci/run_conditional_tests.sh | 160 +++++++++++--------- packages/google-crc32c/.coveragerc | 2 +- 6 files changed, 623 insertions(+), 168 deletions(-) create mode 100644 ci/get_package_shards.py create mode 100755 ci/report_coverage.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 129fc766458c..e7fb1fec6a0c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,6 +12,12 @@ name: lint permissions: contents: read +env: + NOX_REUSE_EXISTING_VIRTUALENVS: "true" + NOX_ENVDIR: "/tmp/shared_nox_envs" + NOX_DEFAULT_VENV_BACKEND: "uv" + UV_VENV_SEED: "1" + jobs: lint: runs-on: ubuntu-latest @@ -24,6 +30,14 @@ jobs: with: fetch-depth: 2 persist-credentials: false + - name: Check for unit_test:all_packages label + id: check-label + run: | + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'unit_test:all_packages') }}" == "true" ]]; then + echo "is_full_run=true" >> $GITHUB_OUTPUT + else + echo "is_full_run=false" >> $GITHUB_OUTPUT + fi - name: Setup Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: @@ -31,11 +45,12 @@ jobs: - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel - python -m pip install nox + python -m pip install nox uv - name: Run lint env: BUILD_TYPE: presubmit TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} + TEST_ALL_PACKAGES: ${{ steps.check-label.outputs.is_full_run }} TEST_TYPE: lint # TODO(https://github.com/googleapis/google-cloud-python/issues/13775): Specify `PY_VERSION` rather than relying on the default python version of the nox session. PY_VERSION: "unused" @@ -45,6 +60,7 @@ jobs: env: BUILD_TYPE: presubmit TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} + TEST_ALL_PACKAGES: ${{ steps.check-label.outputs.is_full_run }} TEST_TYPE: lint_setup_py # TODO(https://github.com/googleapis/google-cloud-python/issues/13775): Specify `PY_VERSION` rather than relying on the default python version of the nox session. PY_VERSION: "unused" @@ -61,6 +77,14 @@ jobs: with: fetch-depth: 2 persist-credentials: false + - name: Check for unit_test:all_packages label + id: check-label + run: | + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'unit_test:all_packages') }}" == "true" ]]; then + echo "is_full_run=true" >> $GITHUB_OUTPUT + else + echo "is_full_run=false" >> $GITHUB_OUTPUT + fi - name: Setup Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: @@ -68,11 +92,12 @@ jobs: - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel - python -m pip install nox + python -m pip install nox uv - name: Run mypy env: BUILD_TYPE: presubmit TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} + TEST_ALL_PACKAGES: ${{ steps.check-label.outputs.is_full_run }} TEST_TYPE: mypy # TODO(https://github.com/googleapis/google-cloud-python/issues/13775): Specify `PY_VERSION` rather than relying on the default python version of the nox session. PY_VERSION: "unused" diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 392f5ece6270..196359ddf6e6 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -12,13 +12,76 @@ name: unittest permissions: contents: read +env: + PACKAGE_DIRS: packages preview-packages + NOX_REUSE_EXISTING_VIRTUALENVS: "true" + NOX_ENVDIR: "/tmp/shared_nox_envs" + NOX_DEFAULT_VENV_BACKEND: "uv" + UV_VENV_SEED: "1" + jobs: + initialize: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + is_full_run: ${{ steps.check-label.outputs.is_full_run }} + env: + MAX_SHARDS: 20 + # Define weights for long-running unit tests to balance shard execution time + # Each weight is roughly 1 minute of expected execution time + # Default for unset packages is 1 + PACKAGE_WEIGHTS: | + bigframes: 6 + google-ai-generativelanguage: 4 + google-auth: 5 + google-cloud-compute: 12 + google-cloud-compute-v1beta: 12 + google-cloud-dialogflow: 6 + google-cloud-dialogflow-cx: 6 + google-cloud-discoveryengine: 8 + google-cloud-retail: 5 + google-shopping-merchant-accounts: 4 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Check for unit_test:all_packages label + id: check-label + run: | + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'unit_test:all_packages') }}" == "true" ]]; then + echo "is_full_run=true" >> $GITHUB_OUTPUT + else + echo "is_full_run=false" >> $GITHUB_OUTPUT + fi + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Get package shards + id: set-matrix + env: + BUILD_TYPE: presubmit + TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} + TEST_ALL_PACKAGES: ${{ steps.check-label.outputs.is_full_run }} + MAX_SHARDS: ${{ env.MAX_SHARDS }} + PACKAGE_WEIGHTS: ${{ env.PACKAGE_WEIGHTS }} + run: | + if [ -n "$TARGET_BRANCH" ]; then + git fetch origin "$TARGET_BRANCH" --depth=1 || true + fi + python3 ci/get_package_shards.py + unit: + needs: initialize + if: needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != '' runs-on: ubuntu-22.04 strategy: fail-fast: true matrix: python: ['3.10', "3.11", "3.12", "3.13", "3.14"] + package_shard: ${{ fromJson(needs.initialize.outputs.matrix) }} + name: ${{ matrix.package_shard.is_sharded && format('unit ({0}, {1})', matrix.python, matrix.package_shard.name) || format('unit ({0})', matrix.python) }} steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -32,30 +95,51 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: ${{ matrix.python }} + cache: 'pip' - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel - python -m pip install nox - - name: Run unit tests + python -m pip install nox uv + - name: Run unit tests for ${{ matrix.package_shard.description }} env: - COVERAGE_FILE: ${{ github.workspace }}/.coverage-${{ matrix.python }} BUILD_TYPE: presubmit TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} TEST_TYPE: unit PY_VERSION: ${{ matrix.python }} + PACKAGE_LIST: ${{ matrix.package_shard.packages }} run: | ci/run_conditional_tests.sh - name: Upload coverage results uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: coverage-artifact-${{ matrix.python }} - path: .coverage-${{ matrix.python }} + name: coverage-artifact-${{ matrix.python }}-${{ matrix.package_shard.index }} + path: .coverage.${{ matrix.python }}.* include-hidden-files: true + all-tests: + needs: [initialize, unit] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check unit test results + run: | + # 1. Check initialize job + if [[ "${{ needs.initialize.result }}" != "success" ]]; then + echo "Error: The initialize job status was: ${{ needs.initialize.result }}" + exit 1 + fi + # 2. Check unit test shards + if [[ "${{ needs.unit.result }}" != "success" && "${{ needs.unit.result }}" != "skipped" ]]; then + echo "Unit tests failed" + exit 1 + fi + echo "All unit tests passed or were skipped" + cover: runs-on: ubuntu-latest needs: - unit + - initialize steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -71,10 +155,18 @@ jobs: python-version: "3.10" - name: Set number of files changes in packages and preview-packages directories id: packages + env: + TEST_ALL_PACKAGES: ${{ needs.initialize.outputs.is_full_run }} + TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} run: | - git diff HEAD~1 -- packages preview-packages > /dev/null - num_files_changed=$(git diff HEAD~1 -- packages preview-packages | wc -l | tr -d ' ') - echo "num_files_changed=${num_files_changed}" >> "$GITHUB_OUTPUT" + if [[ "${TEST_ALL_PACKAGES}" == "true" ]]; then + echo "num_files_changed=1" >> "$GITHUB_OUTPUT" + else + TARGET_BRANCH="${TARGET_BRANCH:-main}" + git fetch origin "$TARGET_BRANCH" --depth=1 || true + num_files_changed=$(git diff --name-only "origin/${TARGET_BRANCH}" -- ${PACKAGE_DIRS} | wc -l | tr -d ' ') + echo "num_files_changed=${num_files_changed}" >> "$GITHUB_OUTPUT" + fi - name: Install coverage if: ${{ steps.packages.outputs.num_files_changed > 0 }} run: | @@ -84,101 +176,16 @@ jobs: if: ${{ steps.packages.outputs.num_files_changed > 0 }} uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 with: - path: .coverage-results/ + path: /dev/shm/.coverage-results/ + merge-multiple: true - 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 + TEST_ALL_PACKAGES: ${{ needs.initialize.outputs.is_full_run }} + TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} + BUILD_TYPE: presubmit run: | - # Find all modified packages - modified_packages=$(git diff --name-only HEAD~1 -- packages preview-packages | cut -d/ -f1,2 | sort -u) - - existing_modified_packages=() - for pkg in ${modified_packages}; do - if [ -d "${pkg}" ]; then - existing_modified_packages+=("${pkg}") - fi - done - - if [ ${#existing_modified_packages[@]} -eq 0 ]; then - echo "All modified packages were deleted. Skipping coverage evaluation." - exit 0 - fi - - 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 preview-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 + ci/report_coverage.sh diff --git a/ci/get_package_shards.py b/ci/get_package_shards.py new file mode 100644 index 000000000000..27c4ea6aa1f0 --- /dev/null +++ b/ci/get_package_shards.py @@ -0,0 +1,231 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Script to group changed packages into balanced shards for CI testing. + +This script identifies which packages have changed compared to a target branch +(or since the last commit) and groups them into a fixed number of shards. +It uses package weights (configured via environment variables) to balance the +execution time across shards while maintaining alphabetical order and +contiguous grouping. All directory variants of a package (e.g. packages/foo and +preview-packages/foo) are kept aligned in the exact same shard. +""" + +import os +import subprocess +import json +import math +import sys +import collections + + +def get_package_directories(): + """Parses package directory roots from the PACKAGE_DIRS environment variable. + + Defaults to ['packages', 'preview-packages'] if not set. + """ + env_dirs = os.environ.get("PACKAGE_DIRS", "") + if env_dirs: + dirs = [d.strip() for d in env_dirs.replace('\n', ' ').split(' ') if d.strip()] + if dirs: + return dirs + return ["packages", "preview-packages"] + + +def get_package_weights(): + """Parses package weights from the PACKAGE_WEIGHTS environment variable. + Package weights represent the relative size of the test, where the weight + approximately represents the minutes the test takes to run. This is used + for distributing work properly across shards. + + The environment variable is expected to be a multiline string where each line + is in the format 'package_name: weight'. Lines starting with '#' are ignored. + + Returns: + dict: A mapping of package names to their integer weights. + """ + weights = {} + env_weights = os.environ.get("PACKAGE_WEIGHTS", "") + for line in env_weights.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if ":" in line: + try: + pkg, weight = line.split(":", 1) + weights[pkg.strip()] = int(weight.strip()) + except ValueError: + continue + return weights + + +def get_packages(): + """Lists all package directory paths in the repository grouped by package name. + + Returns: + dict: A dictionary mapping package_name -> list of relative directory paths. + """ + subdirs = get_package_directories() + packages_map = collections.defaultdict(list) + for subdir in subdirs: + if not os.path.exists(subdir): + continue + for d in os.listdir(subdir): + full_path = os.path.join(subdir, d) + '/' + if os.path.isdir(full_path): + packages_map[d].append(full_path) + return packages_map + + +def get_packages_to_test(): + """Determines the dictionary of package names to directory paths that need to be tested. + + This is based on git diffs against the target branch (presubmit) or the + previous commit (continuous). If TEST_ALL_PACKAGES is set to true, + all packages are returned. + + Returns: + dict: A dictionary mapping package_name -> list of relative directory paths to be tested. + """ + build_type = os.environ.get('BUILD_TYPE', 'presubmit') + target_branch = os.environ.get('TARGET_BRANCH', 'main') + test_all_packages = os.environ.get('TEST_ALL_PACKAGES', 'false').lower() == 'true' + + all_packages = get_packages() + + if test_all_packages: + return all_packages + + if build_type == 'presubmit': + git_diff_arg = f"origin/{target_branch}" + elif build_type == 'continuous': + git_diff_arg = "HEAD~1.." + else: + return all_packages + + try: + res = subprocess.check_output(['git', 'diff', '--name-only', git_diff_arg]).decode('utf-8') + changed_files = res.splitlines() + except subprocess.CalledProcessError: + # If change detection fails, fall back to all packages + return all_packages + + package_dirs = set(get_package_directories()) + to_test_names = set() + for f in changed_files: + parts = f.split('/') + if len(parts) >= 2 and parts[0] in package_dirs: + pkg_name = parts[1] + if pkg_name in all_packages: + to_test_names.add(pkg_name) + + return {name: all_packages[name] for name in to_test_names} + + +def group_packages(packages_map): + """Groups packages by package name into balanced shards. + + All directory variants of a package (e.g. packages/foo and preview-packages/foo) + are kept together in the exact same shard. + + Args: + packages_map (dict): Dictionary mapping package_name -> list of directory paths. + + Returns: + list: A list of dictionaries, each representing a shard with its name, + index, description, and the space-separated list of packages. + """ + if not packages_map: + return [] + + package_weights_map = get_package_weights() + sorted_pkg_names = sorted(packages_map.keys()) + + pkg_items = [] + total_weight = 0 + for name in sorted_pkg_names: + paths = packages_map[name] + # Multiply base weight by number of directory variants (e.g. standard + preview) + weight = package_weights_map.get(name, 1) * len(paths) + pkg_items.append((name, paths, weight)) + total_weight += weight + + # Dynamically determine target weight to balance across max shards. + max_shards = int(os.environ.get("MAX_SHARDS", 16)) + target_weight = max(10, math.ceil(total_weight / max_shards)) + + shards_list = [] + current_shard_items = [] + current_shard_weight = 0 + + # Pack packages alphabetically by package name. + for name, paths, weight in pkg_items: + # If adding this package would exceed target weight AND we haven't reached the + # shard limit, start a new shard. Otherwise, keep "stuffing" the current one. + if current_shard_items and (current_shard_weight + weight > target_weight) and len(shards_list) < max_shards - 1: + shards_list.append(current_shard_items) + current_shard_items = [(name, paths, weight)] + current_shard_weight = weight + else: + current_shard_items.append((name, paths, weight)) + current_shard_weight += weight + + if current_shard_items: + shards_list.append(current_shard_items) + + # Construct the final shards output list + shards = [] + for i, shard_items in enumerate(shards_list): + index = i + 1 + name = f"Shard {index}" + num_in_shard = len(shard_items) + + # Calculate contiguous range description using package names + first_pkg_name = shard_items[0][0] + last_pkg_name = shard_items[-1][0] + if num_in_shard == 1: + desc = first_pkg_name + else: + desc = f"{first_pkg_name}...{last_pkg_name} ({num_in_shard} packages)" + + all_paths = [] + for _, paths, _ in shard_items: + all_paths.extend(paths) + + shards.append({ + "name": name, + "index": index, + "description": desc, + "packages": " ".join(all_paths), + "is_sharded": True + }) + + # Set is_sharded dynamically based on the total number of shards + total_shards = len(shards) + for shard in shards: + shard["is_sharded"] = total_shards > 1 + + return shards + + +if __name__ == "__main__": + packages = get_packages_to_test() + shards = group_packages(packages) + shards_json = json.dumps(shards) + print(shards_json) + + github_output = os.environ.get("GITHUB_OUTPUT") + if github_output: + with open(github_output, "a") as f: + f.write(f"matrix={shards_json}\n") diff --git a/ci/report_coverage.sh b/ci/report_coverage.sh new file mode 100755 index 000000000000..f204bc45cdcc --- /dev/null +++ b/ci/report_coverage.sh @@ -0,0 +1,164 @@ +#!/bin/bash +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eo pipefail + +# This script generates coverage reports for modified packages. +# It assumes coverage databases have been downloaded to RESULTS_DIR. +# It uses /dev/shm for high-performance processing. + +RESULTS_DIR="${RESULTS_DIR:-/dev/shm/.coverage-results}" +LOG_DIR="/dev/shm/coverage-logs" +DEFAULT_FAIL_UNDER="${DEFAULT_FAIL_UNDER:-99}" +MAX_JOBS=$(nproc) + +mkdir -p "${LOG_DIR}" + +if [ ! -d "${RESULTS_DIR}" ]; then + echo "Error: No coverage results found in ${RESULTS_DIR}." + exit 1 +fi + +# Unzip any zipped coverage results +find "$RESULTS_DIR" -type f -name '*.zip' -print0 | xargs -0 -P "${MAX_JOBS}" -I {} unzip -q -o {} -d "$RESULTS_DIR" + +# Identify modified packages +BUILD_TYPE="${BUILD_TYPE:-presubmit}" +TARGET_BRANCH="${TARGET_BRANCH:-main}" + +PACKAGE_DIRS="${PACKAGE_DIRS:-packages preview-packages}" + +if [[ "${TEST_ALL_PACKAGES}" == "true" ]]; then + # Test all packages mode: evaluate coverage for every package in the repository + modified_packages=$(for dir in ${PACKAGE_DIRS}; do ls -d ${dir}/*/ 2>/dev/null; done | cut -d/ -f1,2 | sort -u) +elif [[ "${BUILD_TYPE}" == "presubmit" ]]; then + # Presubmit build: evaluate coverage only for packages modified relative to the target branch + modified_packages=$(git diff --name-only "origin/${TARGET_BRANCH}" -- ${PACKAGE_DIRS} 2>/dev/null | cut -d/ -f1,2 | sort -u) +else + # Continuous build (post-merge on main): evaluate coverage for packages modified in the last commit + modified_packages=$(git diff --name-only HEAD~1 -- ${PACKAGE_DIRS} 2>/dev/null | cut -d/ -f1,2 | sort -u) +fi + +# Function to report coverage for a single package +report_package_coverage() { + local pkg=$1 + local pkg_name_clean=$(echo "${pkg}" | sed 's|/$||' | sed 's|/|_|g') + local pkg_log="${LOG_DIR}/${pkg_name_clean}.log" + local pkg_status="${LOG_DIR}/${pkg_name_clean}.status" + + # Use /dev/shm for the combined coverage file to reduce disk I/O + local pkg_coverage_db="/dev/shm/.coverage.${pkg_name_clean}" + + set +e + ( + # Find coverage databases belonging specifically to this package + shopt -s nullglob + local pkg_files=( + "${RESULTS_DIR}/.coverage."*"."${pkg_name_clean} + "${RESULTS_DIR}/.coverage."*"."${pkg_name_clean}"."* + "${RESULTS_DIR}"/*/".coverage."*"."${pkg_name_clean} + "${RESULTS_DIR}"/*/".coverage."*"."${pkg_name_clean}"."* + ) + shopt -u nullglob + + # Handle explicit skip via .coveragerc + if [ -f "${pkg}/.coveragerc" ] && grep -Eq "fail_under\s*=\s*0" "${pkg}/.coveragerc"; then + echo "Package has fail_under = 0, passing instantly with 100% success (even without coverage files)" > "${pkg_log}" + echo 0 > "${pkg_status}" + return + fi + + if [ ${#pkg_files[@]} -eq 0 ]; then + echo "Warning: No coverage results found for ${pkg}" > "${pkg_log}" + echo 1 > "${pkg_status}" + return + fi + + pushd "${pkg}" > /dev/null + + # Combine databases + COVERAGE_FILE="${pkg_coverage_db}" coverage combine "${pkg_files[@]}" >> "${pkg_log}" 2>&1 + + # Generate report + if [ -f ".coveragerc" ]; then + echo "Using package-specific configuration: ${pkg}/.coveragerc" >> "${pkg_log}" + if grep -q "fail_under" ".coveragerc"; then + COVERAGE_FILE="${pkg_coverage_db}" coverage report --rcfile=".coveragerc" --include="$PWD/**" >> "${pkg_log}" 2>&1 + else + echo "No fail_under specified in ${pkg}/.coveragerc, enforcing default" >> "${pkg_log}" + COVERAGE_FILE="${pkg_coverage_db}" coverage report --rcfile=".coveragerc" --include="$PWD/**" --fail-under="${DEFAULT_FAIL_UNDER}" >> "${pkg_log}" 2>&1 + fi + else + echo "No .coveragerc found for ${pkg}, enforcing default" >> "${pkg_log}" + COVERAGE_FILE="${pkg_coverage_db}" coverage report --include="$PWD/**" --fail-under="${DEFAULT_FAIL_UNDER}" >> "${pkg_log}" 2>&1 + fi + + echo $? > "${pkg_status}" + popd > /dev/null + ) + set -e +} + +# Run coverage reporting in parallel +for pkg in ${modified_packages}; do + if [ -d "${pkg}" ]; then + # Limit concurrent jobs + while [ $(jobs -r | wc -l) -ge "${MAX_JOBS}" ]; do + sleep 0.1 + done + report_package_coverage "${pkg}" & + fi +done + +wait + +# Summary and exit +failed_packages=() +passed_packages=() + +for pkg in ${modified_packages}; do + if [ -d "${pkg}" ]; then + pkg_name_clean=$(echo "${pkg}" | sed 's|/$||' | sed 's|/|_|g') + pkg_log="${LOG_DIR}/${pkg_name_clean}.log" + pkg_status="${LOG_DIR}/${pkg_name_clean}.status" + + echo "============================================================" + echo "Evaluating coverage for package: ${pkg}" + echo "============================================================" + [ -f "${pkg_log}" ] && cat "${pkg_log}" + + status=$(cat "${pkg_status}" 2>/dev/null || echo 1) + 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: ${#passed_packages[@]} packages" +fi + +if [ ${#failed_packages[@]} -gt 0 ]; then + echo "❌ Failed: ${#failed_packages[@]} packages" + for pkg in "${failed_packages[@]}"; do + echo " - ${pkg}" + done + exit 1 +fi diff --git a/ci/run_conditional_tests.sh b/ci/run_conditional_tests.sh index b4df9cb923f4..9901711aee1a 100755 --- a/ci/run_conditional_tests.sh +++ b/ci/run_conditional_tests.sh @@ -47,77 +47,105 @@ git config --global url."${PROJECT_ROOT}".insteadOf "https://github.com/googleap # A script file for running the test in a sub project. test_script="${PROJECT_ROOT}/ci/run_single_test.sh" -if [[ ${BUILD_TYPE} == "presubmit" ]]; then - # For presubmit build, we want to know the difference from the - # common commit in the target branch. - GIT_DIFF_ARG="origin/$TARGET_BRANCH..." - - # Then fetch enough history for finding the common commit. - git fetch origin "$TARGET_BRANCH" --deepen=200 - -elif [[ ${BUILD_TYPE} == "continuous" ]]; then - # For continuous build, we want to know the difference in the last - # commit. This assumes we use squash commit when merging PRs. - GIT_DIFF_ARG="HEAD~.." - - # Then fetch one last commit for getting the diff. - git fetch origin "$TARGET_BRANCH" --deepen=1 - -else - # Run everything. - GIT_DIFF_ARG="" -fi - -# Then detect changes in the test scripts. - -set +e -git diff --quiet ${GIT_DIFF_ARG} ci -changed=$? -set -e - -# Now we have a fixed list, but we can change it to autodetect if -# necessary. - -subdirs=( - packages - preview-packages -) - +# Global exit code tracker RETVAL=0 -for subdir in ${subdirs[@]}; do - for d in `ls -d ${subdir}/*/`; do - should_test=false - if [ -n "${GIT_DIFF_ARG}" ]; then - echo "checking changes with 'git diff --quiet ${GIT_DIFF_ARG} ${d}'" - set +e - git diff --quiet ${GIT_DIFF_ARG} ${d} - changed=$? - set -e - if [[ "${changed}" -eq 0 ]]; then - echo "no change detected in ${d}, skipping" +# Shared test execution logic +run_test_in_dir() { + local d=$1 + local pkg_name_clean=$(echo ${d} | sed 's|/$||' | sed 's|/|_|g') + local log_file="/tmp/test_log_${PY_VERSION}_${pkg_name_clean}.log" + export COVERAGE_FILE="${PROJECT_ROOT}/.coverage.${PY_VERSION}.${pkg_name_clean}" + + pushd ${d} > /dev/null + + # Temporarily allow failure. + set +e + ${test_script} > "${log_file}" 2>&1 + local ret=$? + set -e + + popd > /dev/null + + # Atomically print complete package log block so parallel outputs never interleave + { + echo "============================================================" + echo "Running tests in ${d}" + echo "============================================================" + cat "${log_file}" + rm -f "${log_file}" + } + + if [ ${ret} -ne 0 ]; then + exit ${ret} + fi +} + +export -f run_test_in_dir +export test_script PROJECT_ROOT PY_VERSION TEST_TYPE + +dirs_to_test=() + +if [ -n "${PACKAGE_LIST}" ]; then + echo "Using provided PACKAGE_LIST" + for d in ${PACKAGE_LIST}; do + dirs_to_test+=("${d}") + done +else + if [ "${TEST_ALL_PACKAGES}" = "true" ]; then + GIT_DIFF_ARG="" + + elif [[ ${BUILD_TYPE} == "presubmit" ]]; then + # For presubmit build, we want to know the difference from the target branch. + TARGET_BRANCH="${TARGET_BRANCH:-main}" + if [ -n "${TARGET_BRANCH}" ]; then + git fetch origin "${TARGET_BRANCH}" --depth=1 || true + fi + GIT_DIFF_ARG="origin/${TARGET_BRANCH}" + + elif [[ ${BUILD_TYPE} == "continuous" ]]; then + # For continuous build, we want to know the difference in the last + # commit. This assumes we use squash commit when merging PRs. + GIT_DIFF_ARG="HEAD~1.." + + else + # Run everything. + GIT_DIFF_ARG="" + fi + + subdirs=(${PACKAGE_DIRS:-packages preview-packages}) + + for subdir in ${subdirs[@]}; do + if [ ! -d "${subdir}" ]; then continue; fi + for d in `ls -d ${subdir}/*/ 2>/dev/null`; do + should_test=false + if [ -n "${GIT_DIFF_ARG}" ]; then + set +e + git diff --quiet ${GIT_DIFF_ARG} -- "${d}" + changed=$? + set -e + if [[ "${changed}" -ne 0 ]]; then + echo "change detected in ${d}" + should_test=true + fi else - echo "change detected in ${d}" + # If GIT_DIFF_ARG is empty, run all the tests. should_test=true fi - else - # If GIT_DIFF_ARG is empty, run all the tests. - should_test=true - fi - if [ "${should_test}" = true ]; then - echo "running test in ${d}" - pushd ${d} - # Temporarily allow failure. - set +e - ${test_script} - ret=$? - set -e - if [ ${ret} -ne 0 ]; then - RETVAL=${ret} + if [ "${should_test}" = true ]; then + dirs_to_test+=("${d}") fi - popd - fi + done done -done +fi + +if [ ${#dirs_to_test[@]} -eq 0 ]; then + echo "No packages to test." + exit 0 +fi + +# Run tests across target packages in parallel using available CPU cores +NPROC=$(nproc 2>/dev/null || echo 4) +echo "Running tests across ${#dirs_to_test[@]} package(s) using ${NPROC} parallel workers..." -exit ${RETVAL} +printf "%s\0" "${dirs_to_test[@]}" | xargs -0 -P "${NPROC}" -I {} bash -c 'run_test_in_dir "$@"' _ {} diff --git a/packages/google-crc32c/.coveragerc b/packages/google-crc32c/.coveragerc index 195287608806..ab4a10fdea4b 100644 --- a/packages/google-crc32c/.coveragerc +++ b/packages/google-crc32c/.coveragerc @@ -22,7 +22,7 @@ omit = google/cloud/__init__.py [report] -fail_under = 100 +fail_under = 0 show_missing = True exclude_lines = # Re-enable the standard pragma From b6cbd6272a23eeb756ce0e943961887d59f32754 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:35:27 +0000 Subject: [PATCH 05/22] restore 3.15 --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 196359ddf6e6..56bfce0ab3ba 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -79,7 +79,7 @@ jobs: strategy: fail-fast: true matrix: - python: ['3.10', "3.11", "3.12", "3.13", "3.14"] + python: ['3.10', "3.11", "3.12", "3.13", "3.14", "3.15"] package_shard: ${{ fromJson(needs.initialize.outputs.matrix) }} name: ${{ matrix.package_shard.is_sharded && format('unit ({0}, {1})', matrix.python, matrix.package_shard.name) || format('unit ({0})', matrix.python) }} steps: From 2f8483a0cfb2687b1d36cdae726b2d999811c1ef Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:38:28 +0000 Subject: [PATCH 06/22] restore 3.15 --- .github/workflows/unittest.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 56bfce0ab3ba..91e89d0106ab 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -96,10 +96,28 @@ jobs: with: python-version: ${{ matrix.python }} cache: 'pip' + 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: | python -m pip install --upgrade setuptools pip wheel - python -m pip install nox uv + python -m pip install nox + 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 for ${{ matrix.package_shard.description }} env: BUILD_TYPE: presubmit From ea8c9a6f330261ba52920fa4a4be13cf87c76516 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:43:02 +0000 Subject: [PATCH 07/22] address zizmor --- .github/workflows/unittest.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 91e89d0106ab..6c1ca82d3f55 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -43,9 +43,10 @@ jobs: google-shopping-merchant-accounts: 4 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 2 + persist-credentials: false - name: Check for unit_test:all_packages label id: check-label run: | @@ -55,7 +56,7 @@ jobs: echo "is_full_run=false" >> $GITHUB_OUTPUT fi - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.10" - name: Get package shards @@ -98,7 +99,7 @@ jobs: cache: 'pip' allow-prereleases: true - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 with: enable-cache: true cache-dependency-glob: 'packages/**/testing/constraints*.txt' @@ -112,7 +113,7 @@ jobs: # 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 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .uv-wheel-cache key: ${{ runner.os }}-uv-wheels-3.15-${{ hashFiles('packages/**/testing/constraints*.txt') }} From 6eb430fede20725f222e2d0fe80f588b28429ba6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:49:49 +0000 Subject: [PATCH 08/22] address feedback --- packages/google-cloud-bigtable/noxfile.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/google-cloud-bigtable/noxfile.py b/packages/google-cloud-bigtable/noxfile.py index 0ffdf5d3c1ff..f8d639a266ed 100644 --- a/packages/google-cloud-bigtable/noxfile.py +++ b/packages/google-cloud-bigtable/noxfile.py @@ -242,9 +242,6 @@ def unit(session, protobuf_implementation): if session.python == "3.15": session.skip("Skipping 3.15 until wheels are available for google-crc32c.") - if session.python == "3.15": - session.skip("Skipping 3.15 until wheels are available for google-crc32c.") - constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) From f394a590d216a57a1f6e14dff75692da541d956d Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:52:14 +0000 Subject: [PATCH 09/22] temporarily speed up feedback --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 6c1ca82d3f55..f86e4eae556c 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -26,7 +26,7 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} is_full_run: ${{ steps.check-label.outputs.is_full_run }} env: - MAX_SHARDS: 20 + MAX_SHARDS: 50 # Define weights for long-running unit tests to balance shard execution time # Each weight is roughly 1 minute of expected execution time # Default for unset packages is 1 From 527dcaee5c5e14e59ae4a165c15a0c62fd929561 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 17:59:32 +0000 Subject: [PATCH 10/22] temporarily speed up feedback --- ci/get_package_shards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/get_package_shards.py b/ci/get_package_shards.py index 27c4ea6aa1f0..4ff1ac2283e3 100644 --- a/ci/get_package_shards.py +++ b/ci/get_package_shards.py @@ -163,7 +163,7 @@ def group_packages(packages_map): # Dynamically determine target weight to balance across max shards. max_shards = int(os.environ.get("MAX_SHARDS", 16)) - target_weight = max(10, math.ceil(total_weight / max_shards)) + target_weight = max(1, math.ceil(total_weight / max_shards)) shards_list = [] current_shard_items = [] From 6f01ed428ccff142b54916a2344f132c9c7905c0 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 18:05:04 +0000 Subject: [PATCH 11/22] revert --- packages/google-crc32c/.coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-crc32c/.coveragerc b/packages/google-crc32c/.coveragerc index ab4a10fdea4b..195287608806 100644 --- a/packages/google-crc32c/.coveragerc +++ b/packages/google-crc32c/.coveragerc @@ -22,7 +22,7 @@ omit = google/cloud/__init__.py [report] -fail_under = 0 +fail_under = 100 show_missing = True exclude_lines = # Re-enable the standard pragma From 37f1f282bc3f4aea06c62ae07738a6e5a535ddc4 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 19:15:47 +0000 Subject: [PATCH 12/22] apply fix --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index f86e4eae556c..74862b9d4c85 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -75,7 +75,7 @@ jobs: unit: needs: initialize - if: needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != '' + if: ${{ needs.initialize.outputs.matrix && needs.initialize.outputs.matrix != '[]' }} runs-on: ubuntu-22.04 strategy: fail-fast: true From 34a67f6a3e8fd4f64e46b226e1978c6b1327ccc8 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 19:18:54 +0000 Subject: [PATCH 13/22] apply fix --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 74862b9d4c85..31b77cac9d3d 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -75,7 +75,7 @@ jobs: unit: needs: initialize - if: ${{ needs.initialize.outputs.matrix && needs.initialize.outputs.matrix != '[]' }} + if: ${{ needs.initialize.outputs.matrix && needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != '' }} runs-on: ubuntu-22.04 strategy: fail-fast: true From e3c23c91126a3ddd01e7f0787f0bd5d15c6a2cd6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 19:22:32 +0000 Subject: [PATCH 14/22] Revert "apply fix" This reverts commit 34a67f6a3e8fd4f64e46b226e1978c6b1327ccc8. --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 31b77cac9d3d..74862b9d4c85 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -75,7 +75,7 @@ jobs: unit: needs: initialize - if: ${{ needs.initialize.outputs.matrix && needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != '' }} + if: ${{ needs.initialize.outputs.matrix && needs.initialize.outputs.matrix != '[]' }} runs-on: ubuntu-22.04 strategy: fail-fast: true From 26a4d62783e244b11f144ae5688012e9e41dc16b Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 19:22:40 +0000 Subject: [PATCH 15/22] Revert "apply fix" This reverts commit 37f1f282bc3f4aea06c62ae07738a6e5a535ddc4. --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 74862b9d4c85..f86e4eae556c 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -75,7 +75,7 @@ jobs: unit: needs: initialize - if: ${{ needs.initialize.outputs.matrix && needs.initialize.outputs.matrix != '[]' }} + if: needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != '' runs-on: ubuntu-22.04 strategy: fail-fast: true From d0607e1efbae91eb2ea6fa76507d2170b583ef47 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 19:22:57 +0000 Subject: [PATCH 16/22] reduce shards --- ci/get_package_shards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/get_package_shards.py b/ci/get_package_shards.py index 4ff1ac2283e3..9eda3e440fec 100644 --- a/ci/get_package_shards.py +++ b/ci/get_package_shards.py @@ -163,7 +163,7 @@ def group_packages(packages_map): # Dynamically determine target weight to balance across max shards. max_shards = int(os.environ.get("MAX_SHARDS", 16)) - target_weight = max(1, math.ceil(total_weight / max_shards)) + target_weight = max(4, math.ceil(total_weight / max_shards)) shards_list = [] current_shard_items = [] From bf8bd812ebbbfc0eeb6898a9ac51f78b01967f77 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 19:27:40 +0000 Subject: [PATCH 17/22] reduce shards --- ci/get_package_shards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/get_package_shards.py b/ci/get_package_shards.py index 9eda3e440fec..0b1e18f2c076 100644 --- a/ci/get_package_shards.py +++ b/ci/get_package_shards.py @@ -163,7 +163,7 @@ def group_packages(packages_map): # Dynamically determine target weight to balance across max shards. max_shards = int(os.environ.get("MAX_SHARDS", 16)) - target_weight = max(4, math.ceil(total_weight / max_shards)) + target_weight = max(5, math.ceil(total_weight / max_shards)) shards_list = [] current_shard_items = [] From dcb2ad186d33176ad0fa2f3cd05cbee86e4f54fc Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 19:32:40 +0000 Subject: [PATCH 18/22] reduce shards --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index f86e4eae556c..6c1ca82d3f55 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -26,7 +26,7 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} is_full_run: ${{ steps.check-label.outputs.is_full_run }} env: - MAX_SHARDS: 50 + MAX_SHARDS: 20 # Define weights for long-running unit tests to balance shard execution time # Each weight is roughly 1 minute of expected execution time # Default for unset packages is 1 From 9e6652125033afce28048bc78a966b8646bd1007 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 21:26:54 +0000 Subject: [PATCH 19/22] cache grpcio --- .github/workflows/unittest.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 6c1ca82d3f55..ef483921ec09 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -119,6 +119,13 @@ jobs: key: ${{ runner.os }}-uv-wheels-3.15-${{ hashFiles('packages/**/testing/constraints*.txt') }} restore-keys: | ${{ runner.os }}-uv-wheels-3.15- + # Pre-cache heavy dependencies sequentially before running parallel tests + - name: Pre-cache heavy dependencies (grpcio) + if: matrix.python == '3.15' + run: | + # Download and compile grpcio sequentially so that parallel test workers + # immediately hit the warm cache instead of deadlocking on compilation. + uv pip install --system grpcio --find-links .uv-wheel-cache - name: Run unit tests for ${{ matrix.package_shard.description }} env: BUILD_TYPE: presubmit From e0f000391648b1cd51362d06a86431de7b9a3f1a Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 30 Jul 2026 21:33:29 +0000 Subject: [PATCH 20/22] apply fix --- .github/workflows/unittest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index ef483921ec09..7fff19dfadf6 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -123,6 +123,8 @@ jobs: - name: Pre-cache heavy dependencies (grpcio) if: matrix.python == '3.15' run: | + # Create the directory so uv doesn't crash if there is a `cache miss` + mkdir -p .uv-wheel-cache # Download and compile grpcio sequentially so that parallel test workers # immediately hit the warm cache instead of deadlocking on compilation. uv pip install --system grpcio --find-links .uv-wheel-cache From 3f89358d9172c14d9742b897e98190e6c276e449 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 31 Jul 2026 14:54:27 +0000 Subject: [PATCH 21/22] revert --- .github/workflows/lint.yml | 78 +++++++++++++++++++++++++++++----- .github/workflows/unittest.yml | 55 ++++++++---------------- ci/get_package_shards.py | 12 +++--- ci/run_conditional_tests.sh | 77 ++++++++++++++------------------- 4 files changed, 125 insertions(+), 97 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e7fb1fec6a0c..69402439d08a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,8 +17,50 @@ env: NOX_ENVDIR: "/tmp/shared_nox_envs" NOX_DEFAULT_VENV_BACKEND: "uv" UV_VENV_SEED: "1" + # Run checks in parallel using 4 cores + PARALLEL_WORKERS: "4" jobs: + initialize: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + is_full_run: ${{ steps.check-label.outputs.is_full_run }} + env: + MAX_SHARDS: 4 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + # 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: Check for unit_test:all_packages label + id: check-label + run: | + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'unit_test:all_packages') }}" == "true" ]]; then + echo "is_full_run=true" >> $GITHUB_OUTPUT + else + echo "is_full_run=false" >> $GITHUB_OUTPUT + fi + - name: Setup Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.10" + - name: Get package shards + id: set-matrix + env: + BUILD_TYPE: presubmit + TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} + TEST_ALL_PACKAGES: ${{ steps.check-label.outputs.is_full_run }} + run: | + if [ -n "$TARGET_BRANCH" ]; then + git fetch origin "$TARGET_BRANCH" --depth=1 || true + fi + python3 ci/get_package_shards.py + lint: runs-on: ubuntu-latest steps: @@ -66,8 +108,16 @@ jobs: PY_VERSION: "unused" run: | ci/run_conditional_tests.sh - mypy: + + mypy-shard: + needs: initialize + if: needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != '' runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + package_shard: ${{ fromJson(needs.initialize.outputs.matrix) }} + name: ${{ matrix.package_shard.is_sharded && format('mypy ({0})', matrix.package_shard.name) || format('mypy ({0})', matrix.package_shard.description) }} steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -77,14 +127,6 @@ jobs: with: fetch-depth: 2 persist-credentials: false - - name: Check for unit_test:all_packages label - id: check-label - run: | - if [[ "${{ contains(github.event.pull_request.labels.*.name, 'unit_test:all_packages') }}" == "true" ]]; then - echo "is_full_run=true" >> $GITHUB_OUTPUT - else - echo "is_full_run=false" >> $GITHUB_OUTPUT - fi - name: Setup Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: @@ -93,13 +135,27 @@ jobs: run: | python -m pip install --upgrade setuptools pip wheel python -m pip install nox uv - - name: Run mypy + - name: Run mypy for ${{ matrix.package_shard.description }} env: BUILD_TYPE: presubmit TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} - TEST_ALL_PACKAGES: ${{ steps.check-label.outputs.is_full_run }} TEST_TYPE: mypy + PACKAGE_LIST: ${{ matrix.package_shard.packages }} # TODO(https://github.com/googleapis/google-cloud-python/issues/13775): Specify `PY_VERSION` rather than relying on the default python version of the nox session. PY_VERSION: "unused" run: | ci/run_conditional_tests.sh + + mypy-status: + if: always() + needs: [initialize, mypy-shard] + runs-on: ubuntu-latest + name: mypy-status + steps: + - name: Check mypy job status + run: | + if [[ "${{ needs['mypy-shard'].result }}" != "success" && "${{ needs['mypy-shard'].result }}" != "skipped" ]]; then + echo "mypy failed with result: ${{ needs['mypy-shard'].result }}" + exit 1 + fi + echo "All mypy shards passed successfully!" diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 7fff19dfadf6..c9e8f5239bf1 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -18,6 +18,9 @@ env: NOX_ENVDIR: "/tmp/shared_nox_envs" NOX_DEFAULT_VENV_BACKEND: "uv" UV_VENV_SEED: "1" + # Note: PARALLEL_WORKERS must remain "1" for unit tests because running pytest concurrently + # on the same runner VM can cause socket/IPC/process deadlocks across packages. + PARALLEL_WORKERS: "1" jobs: initialize: @@ -26,7 +29,7 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} is_full_run: ${{ steps.check-label.outputs.is_full_run }} env: - MAX_SHARDS: 20 + MAX_SHARDS: 16 # Define weights for long-running unit tests to balance shard execution time # Each weight is roughly 1 minute of expected execution time # Default for unset packages is 1 @@ -80,7 +83,7 @@ jobs: strategy: fail-fast: true matrix: - python: ['3.10', "3.11", "3.12", "3.13", "3.14", "3.15"] + python: ['3.10', "3.11", "3.12", "3.13", "3.14"] package_shard: ${{ fromJson(needs.initialize.outputs.matrix) }} name: ${{ matrix.package_shard.is_sharded && format('unit ({0}, {1})', matrix.python, matrix.package_shard.name) || format('unit ({0})', matrix.python) }} steps: @@ -97,37 +100,10 @@ jobs: with: python-version: ${{ matrix.python }} cache: 'pip' - allow-prereleases: true - - name: Setup uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 - with: - enable-cache: true - cache-dependency-glob: 'packages/**/testing/constraints*.txt' - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel - python -m pip install nox - 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@0057852bfaa89a56745cba8c7296529d2fc39830 # 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- - # Pre-cache heavy dependencies sequentially before running parallel tests - - name: Pre-cache heavy dependencies (grpcio) - if: matrix.python == '3.15' - run: | - # Create the directory so uv doesn't crash if there is a `cache miss` - mkdir -p .uv-wheel-cache - # Download and compile grpcio sequentially so that parallel test workers - # immediately hit the warm cache instead of deadlocking on compilation. - uv pip install --system grpcio --find-links .uv-wheel-cache + python -m pip install nox uv - name: Run unit tests for ${{ matrix.package_shard.description }} env: BUILD_TYPE: presubmit @@ -164,9 +140,10 @@ jobs: echo "All unit tests passed or were skipped" cover: + if: always() && !cancelled() && needs.all-tests.result == 'success' runs-on: ubuntu-latest needs: - - unit + - all-tests - initialize steps: - name: Checkout @@ -181,33 +158,37 @@ jobs: uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.10" - - name: Set number of files changes in packages and preview-packages directories + - name: Determine if coverage evaluation is required id: packages env: TEST_ALL_PACKAGES: ${{ needs.initialize.outputs.is_full_run }} TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }} run: | if [[ "${TEST_ALL_PACKAGES}" == "true" ]]; then - echo "num_files_changed=1" >> "$GITHUB_OUTPUT" + echo "should_evaluate_coverage=true" >> "$GITHUB_OUTPUT" else TARGET_BRANCH="${TARGET_BRANCH:-main}" git fetch origin "$TARGET_BRANCH" --depth=1 || true num_files_changed=$(git diff --name-only "origin/${TARGET_BRANCH}" -- ${PACKAGE_DIRS} | wc -l | tr -d ' ') - echo "num_files_changed=${num_files_changed}" >> "$GITHUB_OUTPUT" + if [[ "${num_files_changed}" -gt 0 ]]; then + echo "should_evaluate_coverage=true" >> "$GITHUB_OUTPUT" + else + echo "should_evaluate_coverage=false" >> "$GITHUB_OUTPUT" + fi fi - name: Install coverage - if: ${{ steps.packages.outputs.num_files_changed > 0 }} + if: ${{ steps.packages.outputs.should_evaluate_coverage == 'true' }} 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 }} + if: ${{ steps.packages.outputs.should_evaluate_coverage == 'true' }} uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 with: path: /dev/shm/.coverage-results/ merge-multiple: true - name: Report coverage results - if: ${{ steps.packages.outputs.num_files_changed > 0 }} + if: ${{ steps.packages.outputs.should_evaluate_coverage == 'true' }} env: # TODO: default to 100% coverage after next gapic-generator release # https://github.com/googleapis/google-cloud-python/issues/17459 diff --git a/ci/get_package_shards.py b/ci/get_package_shards.py index 0b1e18f2c076..77cc191cc3ac 100644 --- a/ci/get_package_shards.py +++ b/ci/get_package_shards.py @@ -122,15 +122,17 @@ def get_packages_to_test(): return all_packages package_dirs = set(get_package_directories()) - to_test_names = set() + to_test_paths = collections.defaultdict(list) for f in changed_files: parts = f.split('/') if len(parts) >= 2 and parts[0] in package_dirs: pkg_name = parts[1] - if pkg_name in all_packages: - to_test_names.add(pkg_name) + full_path = f"{parts[0]}/{parts[1]}/" + if pkg_name in all_packages and full_path in all_packages[pkg_name]: + if full_path not in to_test_paths[pkg_name]: + to_test_paths[pkg_name].append(full_path) - return {name: all_packages[name] for name in to_test_names} + return dict(to_test_paths) def group_packages(packages_map): @@ -163,7 +165,7 @@ def group_packages(packages_map): # Dynamically determine target weight to balance across max shards. max_shards = int(os.environ.get("MAX_SHARDS", 16)) - target_weight = max(5, math.ceil(total_weight / max_shards)) + target_weight = max(10, math.ceil(total_weight / max_shards)) shards_list = [] current_shard_items = [] diff --git a/ci/run_conditional_tests.sh b/ci/run_conditional_tests.sh index 9901711aee1a..241de51a4f88 100755 --- a/ci/run_conditional_tests.sh +++ b/ci/run_conditional_tests.sh @@ -47,10 +47,27 @@ git config --global url."${PROJECT_ROOT}".insteadOf "https://github.com/googleap # A script file for running the test in a sub project. test_script="${PROJECT_ROOT}/ci/run_single_test.sh" -# Global exit code tracker -RETVAL=0 +if [ "${TEST_ALL_PACKAGES}" = "true" ]; then + GIT_DIFF_ARG="" + +elif [[ ${BUILD_TYPE} == "presubmit" ]]; then + # For presubmit build, we want to know the difference from the + # common commit in the target branch. + if [ -n "${TARGET_BRANCH}" ]; then + git fetch origin "${TARGET_BRANCH}" --depth=1 || true + fi + GIT_DIFF_ARG="origin/${TARGET_BRANCH}" + +elif [[ ${BUILD_TYPE} == "continuous" ]]; then + # For continuous build, we want to know the difference in the last + # commit. This assumes we use squash commit when merging PRs. + GIT_DIFF_ARG="HEAD~1.." + +else + # Run everything. + GIT_DIFF_ARG="" +fi -# Shared test execution logic run_test_in_dir() { local d=$1 local pkg_name_clean=$(echo ${d} | sed 's|/$||' | sed 's|/|_|g') @@ -58,29 +75,22 @@ run_test_in_dir() { export COVERAGE_FILE="${PROJECT_ROOT}/.coverage.${PY_VERSION}.${pkg_name_clean}" pushd ${d} > /dev/null - - # Temporarily allow failure. set +e ${test_script} > "${log_file}" 2>&1 local ret=$? set -e - popd > /dev/null - # Atomically print complete package log block so parallel outputs never interleave - { - echo "============================================================" - echo "Running tests in ${d}" - echo "============================================================" - cat "${log_file}" - rm -f "${log_file}" - } + echo "============================================================" + echo "Running tests in ${d}" + echo "============================================================" + cat "${log_file}" + rm -f "${log_file}" if [ ${ret} -ne 0 ]; then exit ${ret} fi } - export -f run_test_in_dir export test_script PROJECT_ROOT PY_VERSION TEST_TYPE @@ -88,31 +98,8 @@ dirs_to_test=() if [ -n "${PACKAGE_LIST}" ]; then echo "Using provided PACKAGE_LIST" - for d in ${PACKAGE_LIST}; do - dirs_to_test+=("${d}") - done + dirs_to_test=(${PACKAGE_LIST}) else - if [ "${TEST_ALL_PACKAGES}" = "true" ]; then - GIT_DIFF_ARG="" - - elif [[ ${BUILD_TYPE} == "presubmit" ]]; then - # For presubmit build, we want to know the difference from the target branch. - TARGET_BRANCH="${TARGET_BRANCH:-main}" - if [ -n "${TARGET_BRANCH}" ]; then - git fetch origin "${TARGET_BRANCH}" --depth=1 || true - fi - GIT_DIFF_ARG="origin/${TARGET_BRANCH}" - - elif [[ ${BUILD_TYPE} == "continuous" ]]; then - # For continuous build, we want to know the difference in the last - # commit. This assumes we use squash commit when merging PRs. - GIT_DIFF_ARG="HEAD~1.." - - else - # Run everything. - GIT_DIFF_ARG="" - fi - subdirs=(${PACKAGE_DIRS:-packages preview-packages}) for subdir in ${subdirs[@]}; do @@ -125,7 +112,6 @@ else changed=$? set -e if [[ "${changed}" -ne 0 ]]; then - echo "change detected in ${d}" should_test=true fi else @@ -144,8 +130,11 @@ if [ ${#dirs_to_test[@]} -eq 0 ]; then exit 0 fi -# Run tests across target packages in parallel using available CPU cores -NPROC=$(nproc 2>/dev/null || echo 4) -echo "Running tests across ${#dirs_to_test[@]} package(s) using ${NPROC} parallel workers..." +PARALLEL_WORKERS="${PARALLEL_WORKERS:-1}" +AVAIL_CORES=$(nproc 2>/dev/null || echo 4) +if [ "${PARALLEL_WORKERS}" -gt "${AVAIL_CORES}" ]; then + PARALLEL_WORKERS="${AVAIL_CORES}" +fi -printf "%s\0" "${dirs_to_test[@]}" | xargs -0 -P "${NPROC}" -I {} bash -c 'run_test_in_dir "$@"' _ {} +echo "Running tests across ${#dirs_to_test[@]} package(s) using ${PARALLEL_WORKERS} parallel worker(s)..." +printf "%s\0" "${dirs_to_test[@]}" | xargs -0 -P "${PARALLEL_WORKERS}" -I {} bash -c 'run_test_in_dir "$@"' _ {} From f1d158fac83f3cb907092c6e79e3b4934992cebd Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 31 Jul 2026 14:56:25 +0000 Subject: [PATCH 22/22] Restore --- .github/workflows/unittest.yml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index c9e8f5239bf1..bf08d7832869 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -83,7 +83,7 @@ jobs: strategy: fail-fast: true matrix: - python: ['3.10', "3.11", "3.12", "3.13", "3.14"] + python: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] package_shard: ${{ fromJson(needs.initialize.outputs.matrix) }} name: ${{ matrix.package_shard.is_sharded && format('unit ({0}, {1})', matrix.python, matrix.package_shard.name) || format('unit ({0})', matrix.python) }} steps: @@ -100,10 +100,38 @@ jobs: with: python-version: ${{ matrix.python }} cache: 'pip' + allow-prereleases: true + - name: Setup uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 + with: + enable-cache: true + cache-dependency-glob: 'packages/**/testing/constraints*.txt' - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel - python -m pip install nox uv + python -m pip install nox + - name: Run unit tests + 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@0057852bfaa89a56745cba8c7296529d2fc39830 # 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- + # Pre-cache heavy dependencies sequentially before running parallel tests + - name: Pre-cache heavy dependencies (grpcio) + if: matrix.python == '3.15' + run: | + # Create the directory so uv doesn't crash if there is a `cache miss` + mkdir -p .uv-wheel-cache + # Download and compile grpcio sequentially so that parallel test workers + # immediately hit the warm cache instead of deadlocking on compilation. + uv pip install --system grpcio --find-links .uv-wheel-cache - name: Run unit tests for ${{ matrix.package_shard.description }} env: BUILD_TYPE: presubmit