Skip to content

chore: add test sharding to unit tests #16512

chore: add test sharding to unit tests

chore: add test sharding to unit tests #16512

Workflow file for this run

on:
pull_request:
branches:
- main
- preview
# Trigger workflow on GitHub merge queue events
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#merge_group
merge_group:
types: [checks_requested]
name: unittest
permissions:
contents: read
jobs:
initialize:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
is_full_run: ${{ steps.check-label.outputs.is_full_run }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- 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 }}
run: |
if [ -n "$TARGET_BRANCH" ]; then
git fetch origin "$TARGET_BRANCH" --deepen=200 || true
fi
echo "matrix=$(python3 ci/get_package_shards.py)" >> $GITHUB_OUTPUT
unit:
needs: initialize
if: needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != ''
runs-on: ubuntu-22.04
strategy:
fail-fast: true
matrix:
python: ['3.9', '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
# Use a fetch-depth of 2 to avoid error `fatal: origin/main...HEAD: no merge base`
# See https://github.com/googleapis/google-cloud-python/issues/12013
# and https://github.com/actions/checkout#checkout-head.
with:
fetch-depth: 2
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ matrix.python }}
- name: Install nox
run: |
python -m pip install --upgrade setuptools pip wheel
python -m pip install nox
- name: Run unit tests for ${{ matrix.package_shard.description }}
env:
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 }}-${{ 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
# Use a fetch-depth of 2 to avoid error `fatal: origin/main...HEAD: no merge base`
# See https://github.com/googleapis/google-cloud-python/issues/12013
# and https://github.com/actions/checkout#checkout-head.
with:
fetch-depth: 2
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.10"
- name: Set number of files changes in packages directory
id: packages
env:
TEST_ALL_PACKAGES: ${{ needs.initialize.outputs.is_full_run }}
run: |
if [[ "${TEST_ALL_PACKAGES}" == "true" ]]; then
echo "num_files_changed=1" >> "$GITHUB_OUTPUT"
else
git diff HEAD~1 -- packages > /dev/null
num_files_changed=$(git diff HEAD~1 -- packages | wc -l | tr -d ' ')
echo "num_files_changed=${num_files_changed}" >> "$GITHUB_OUTPUT"
fi
- name: Install coverage
if: ${{ steps.packages.outputs.num_files_changed > 0 }}
run: |
python -m pip install --upgrade setuptools pip wheel
python -m pip install coverage
- name: Download coverage results
if: ${{ steps.packages.outputs.num_files_changed > 0 }}
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
path: .coverage-results/
- name: Report coverage results
if: ${{ steps.packages.outputs.num_files_changed > 0 }}
env:
# TODO: default to 100% coverage after next gapic-generator release
# https://github.com/googleapis/google-cloud-python/issues/17459
DEFAULT_FAIL_UNDER: 99
TEST_ALL_PACKAGES: ${{ needs.initialize.outputs.is_full_run }}
run: |
if [ -d .coverage-results ]; then
# Unzip any zipped coverage results
find .coverage-results -type f -name '*.zip' -exec unzip -o {} \;
# Find all modified packages
if [[ "${TEST_ALL_PACKAGES}" == "true" ]]; then
modified_packages=$(ls -d packages/*/ | cut -d/ -f1,2 | sort -u)
else
modified_packages=$(git diff --name-only HEAD~1 -- packages | cut -d/ -f1,2 | sort -u)
fi
mkdir -p /tmp/coverage-logs
MAX_JOBS=8
for pkg in ${modified_packages}; do
if [ -d "${pkg}" ]; then
# Limit the number of concurrent background jobs
while [ $(jobs -r | wc -l) -ge ${MAX_JOBS} ]; do
sleep 0.1
done
(
pkg_name_clean=$(echo "${pkg}" | sed 's|/$||' | sed 's|/|_|g')
pkg_log="/tmp/coverage-logs/${pkg_name_clean}.log"
pkg_status="/tmp/coverage-logs/${pkg_name_clean}.status"
set +e
# Find coverage databases belonging specifically to this package across all Python versions
pkg_files=$(find .coverage-results/ -type f -name ".coverage.*.${pkg_name_clean}" -o -name ".coverage.*.${pkg_name_clean}.*")
if [ -n "${pkg_files}" ]; then
# Create a dedicated temp directory for combining this package's coverage databases
temp_combine_dir="/tmp/combine-${pkg_name_clean}"
mkdir -p "${temp_combine_dir}"
cp ${pkg_files} "${temp_combine_dir}/"
pushd "${pkg}" > /dev/null
# Combine only this package's databases into a local .coverage database inside the package directory
COVERAGE_FILE="${PWD}/.coverage" coverage combine "${temp_combine_dir}"/.coverage.* >> "${pkg_log}" 2>&1
# Generate the coverage report using the package-specific local database
if [ -f ".coveragerc" ]; then
echo "Using package-specific configuration: ${pkg}/.coveragerc" > "${pkg_log}"
if grep -q "fail_under" ".coveragerc"; then
COVERAGE_FILE="${PWD}/.coverage" 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="${PWD}/.coverage" 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="${PWD}/.coverage" coverage report --include="$PWD/**" --fail-under="${DEFAULT_FAIL_UNDER}" >> "${pkg_log}" 2>&1
fi
echo $? > "${pkg_status}"
popd > /dev/null
else
echo "Warning: No coverage results found for ${pkg}" > "${pkg_log}"
echo 1 > "${pkg_status}"
fi
set -e
) &
fi
done
# Wait for all background checks to finish
wait
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="/tmp/coverage-logs/${pkg_name_clean}.log"
pkg_status="/tmp/coverage-logs/${pkg_name_clean}.status"
echo "============================================================"
echo "Evaluating coverage for package: ${pkg}"
echo "============================================================"
if [ -f "${pkg_log}" ]; then
cat "${pkg_log}"
fi
status=$(cat "${pkg_status}")
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