chore: add test sharding to unit tests #16512
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| - preview | |
| # Trigger workflow on GitHub merge queue events | |
| # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#merge_group | |
| merge_group: | |
| types: [checks_requested] | |
| name: unittest | |
| permissions: | |
| contents: read | |
| 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 |