Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
159 changes: 148 additions & 11 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,48 @@ name: unittest
permissions:
contents: read

# Configurable global environment variables for batching
env:
TEST_ALL_PACKAGES: "false" # Set to "false" to only run tests for packages with a git diff
ALL_PYTHON: "['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15']"

jobs:
# Dynamic package discovery job to calculate required matrix size automatically
python_config:
runs-on: ubuntu-latest
outputs:
all_python: ${{ steps.export.outputs.python_list }}
steps:
- id: export
run: echo "python_list=${{ env.ALL_PYTHON }}" >> "$GITHUB_OUTPUT"

discover-packages:
runs-on: ubuntu-latest
outputs:
batch-indices: ${{ steps.set-matrix.outputs.indices }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.14"
- name: Generate Batch Indices
id: set-matrix
run: |
INDICES=$(python -c 'import sys; sys.path.append("ci"); import get_batches; print(get_batches.get_batch_indices())')
echo "indices=${INDICES}" >> "$GITHUB_OUTPUT"

unit:
name: "unit-run (${{ matrix.python }}, Batch ${{ matrix.batch-index }})"
runs-on: ubuntu-22.04
needs: [python_config, discover-packages]
strategy:
fail-fast: true
matrix:
python: ['3.9', '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
Expand All @@ -32,30 +67,83 @@ 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}"

# Only inject overrides if we are strictly on the 3.15 pre-release
# This is needed to speed up builds
if [ "${{ matrix.python }}" = "3.15" ]; then
# Tell uv it is allowed to use the pre-release 3.15 toolchain
export UV_PRERELEASE="allow"

# Route uv's wheel and environment cache to our persistent workspace directory
export UV_CACHE_DIR="${{ github.workspace }}/.uv-wheel-cache"
fi

ci/run_conditional_tests.sh ${UNIQUE_BATCH_PACKAGES}

- name: Save Status Footprint
run: |
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
Expand Down Expand Up @@ -167,3 +255,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
2 changes: 2 additions & 0 deletions .kokoro/presubmit/system.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ env_vars: {
key: "BIGFRAMES_TEST_PROJECT"
value: "bigframes-testing"
}

timeout_mins: 360
Original file line number Diff line number Diff line change
Expand Up @@ -357,52 +357,6 @@ replacements:

Quick Start
count: 1
- paths: [
packages/google-cloud-bigtable/mypy.ini
]
before: |
\[mypy\]
[\s\S]*?incremental = True
after: |
[mypy]
python_version = 3.13
namespace_packages = True
check_untyped_defs = True
warn_unreachable = True
disallow_any_generics = True
exclude = tests/unit/gapic/

[mypy-grpc.*]
ignore_missing_imports = True

[mypy-google.auth.*]
ignore_missing_imports = True

[mypy-google.iam.*]
ignore_missing_imports = True

[mypy-google.longrunning.*]
ignore_missing_imports = True

[mypy-google.oauth2.*]
ignore_missing_imports = True

[mypy-google.rpc.*]
ignore_missing_imports = True

[mypy-proto.*]
ignore_missing_imports = True

[mypy-pytest]
ignore_missing_imports = True

[mypy-google.cloud.*]
ignore_errors = True

# only verify data client
[mypy-google.cloud.bigtable.data.*]
ignore_errors = False
count: 1
# Note: noxfile.py is heavily customized so we clobber the whole file.
- paths: [
packages/google-cloud-bigtable/noxfile.py
Expand Down Expand Up @@ -431,6 +385,7 @@ replacements:
"3.12",
"3.13",
"3.14",
"3.15",
]

UNIT_TEST_STANDARD_DEPENDENCIES = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,6 @@ replacements:
"google-cloud-core >= 2.0.0, <3.0.0",
"grpcio >= 1.59.0, < 2.0.0",
count: 1
- paths: [
"packages/google-cloud-datastore/mypy.ini",
]
before: |-
# Performance: reuse results from previous runs to speed up 'nox'
incremental = True
after: |-
# Performance: reuse results from previous runs to speed up "nox"
incremental = True

[mypy-google.cloud.datastore._app_engine_key_pb2]
ignore_errors = True

# TODO(https://github.com/googleapis/gapic-generator-python/issues/2410):
# Remove once this generator bug is fixed
[mypy-google.cloud.datastore_v1.services.datastore.async_client]
ignore_errors = True

# TODO(https://github.com/googleapis/gapic-generator-python/issues/2410):
# Remove once this generator bug is fixed
[mypy-google.cloud.datastore_v1.services.datastore.client]
ignore_errors = True
count: 1
- paths: [
"packages/google-cloud-datastore/mypy.ini",
]
before: |
ignore_missing_imports = False

# TODO\(https://github.com/googleapis/gapic-generator-python/issues/2563\):
# Dependencies that historically lacks py.typed markers
\[mypy-google\.iam\.\*\]
ignore_missing_imports = True
after: |
ignore_missing_imports = True
count: 1
- paths: [
"packages/google-cloud-datastore/docs/index.rst",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ replacements:
"3.12",
"3.13",
"3.14",
"3.15",
]
UNIT_TEST_STANDARD_DEPENDENCIES = [
"mock",
Expand Down
81 changes: 81 additions & 0 deletions ci/get_batches.py
Original file line number Diff line number Diff line change
@@ -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])))
Loading
Loading