diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index c6b1d5195d..b382be55ca 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -127,10 +127,14 @@ jobs: steps: - run: '[ "${{ needs.test-sqlite-shards.result }}" = "success" ]' - test-unit-nogogit: + test-unit-shards: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3] services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 @@ -175,57 +179,36 @@ jobs: - uses: ./.github/actions/go-cache with: cache-name: unit - build-cache-rotate: "true" - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts' - run: make deps-backend - - run: make backend + - run: make generate-go env: TAGS: bindata - name: unit-tests - run: make test-backend test-check + run: make test-backend-shard test-check env: + TEST_SHARD: ${{ matrix.shard }} + TEST_TOTAL_SHARDS: 3 GOTEST_FLAGS: -race -timeout=20m TAGS: bindata GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} - - test-unit-gogit: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' - needs: files-changed - runs-on: ubuntu-latest - # the gogit-affected package set (modules/git, modules/gitrepo, modules/lfs and - # their direct importers) doesn't touch elasticsearch/meilisearch/redis/minio/ - # azurite — no services needed. - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - check-latest: true - cache: false - - uses: ./.github/actions/go-cache - with: - cache-name: unit-gogit - build-cache-rotate: "true" - - run: make deps-backend - - run: make backend - env: - TAGS: bindata gogit - GOEXPERIMENT: - name: unit-tests-gogit - run: make test-backend-gogit test-check + run: make test-backend-gogit-shard test-check env: + TEST_SHARD: ${{ matrix.shard }} + TEST_TOTAL_SHARDS: 3 GOTEST_FLAGS: -race -timeout=20m TAGS: bindata gogit GOEXPERIMENT: GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} test-unit: - needs: [files-changed, test-unit-nogogit, test-unit-gogit] + needs: [files-changed, test-unit-shards] if: always() && (needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true') runs-on: ubuntu-latest steps: - - run: '[ "${{ needs.test-unit-nogogit.result }}" = "success" ] && [ "${{ needs.test-unit-gogit.result }}" = "success" ]' + - run: '[ "${{ needs.test-unit-shards.result }}" = "success" ]' test-mysql-shards: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' diff --git a/Makefile b/Makefile index 4179d91cdb..3c6b6905f5 100644 --- a/Makefile +++ b/Makefile @@ -388,6 +388,20 @@ test-backend-gogit: ## test packages whose code or tests import the gogit-affect echo "Running go test with $(GOTEST_FLAGS) -tags '$(TAGS)' over $$(echo $$pkgs | wc -w) gogit-affected packages..." && \ $(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $$pkgs +.PHONY: test-backend-shard +test-backend-shard: ## run the TEST_SHARD/TEST_TOTAL_SHARDS slice of test-backend + @pkgs=$$(echo "$(GO_TEST_PACKAGES)" | tr ' ' '\n' | ./tools/partition-by-shard.sh | tr '\n' ' ') && \ + if [ -z "$$pkgs" ]; then echo "shard $$TEST_SHARD/$$TEST_TOTAL_SHARDS has no test-backend packages" >&2; exit 1; fi && \ + echo "Running shard $$TEST_SHARD/$$TEST_TOTAL_SHARDS of test-backend ($$(echo $$pkgs | wc -w) packages)..." && \ + $(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $$pkgs + +.PHONY: test-backend-gogit-shard +test-backend-gogit-shard: ## run the TEST_SHARD/TEST_TOTAL_SHARDS slice of test-backend-gogit + @pkgs=$$(./tools/find-gogit-test-pkgs.sh '$(TAGS)' | ./tools/partition-by-shard.sh) && \ + if [ -z "$$pkgs" ]; then echo "shard $$TEST_SHARD/$$TEST_TOTAL_SHARDS has no gogit-affected packages" >&2; exit 1; fi && \ + echo "Running shard $$TEST_SHARD/$$TEST_TOTAL_SHARDS of test-backend-gogit ($$(echo $$pkgs | wc -w) packages)..." && \ + $(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $$pkgs + .PHONY: test-frontend test-frontend: node_modules ## test frontend files pnpm exec vitest diff --git a/tools/partition-by-shard.sh b/tools/partition-by-shard.sh new file mode 100755 index 0000000000..7e55368f27 --- /dev/null +++ b/tools/partition-by-shard.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +# Print the TEST_SHARD/TEST_TOTAL_SHARDS slice of stdin (newline-separated +# items), partitioned round-robin by line number after a deterministic sort. +# Required env: TEST_SHARD (1..TEST_TOTAL_SHARDS), TEST_TOTAL_SHARDS (>= 1). + +shard=${TEST_SHARD:?missing TEST_SHARD} +total=${TEST_TOTAL_SHARDS:?missing TEST_TOTAL_SHARDS} + +if ! [[ "$total" =~ ^[1-9][0-9]*$ ]]; then + echo "TEST_TOTAL_SHARDS must be a positive integer, got: $total" >&2 + exit 2 +fi +if ! [[ "$shard" =~ ^[1-9][0-9]*$ ]] || [ "$shard" -gt "$total" ]; then + echo "TEST_SHARD must be in [1, $total], got: $shard" >&2 + exit 2 +fi + +LC_ALL=C sort -u | awk -v r=$((shard - 1)) -v t="$total" 'NR % t == r' diff --git a/tools/test-integration-shard.sh b/tools/test-integration-shard.sh index 14828edbf4..f51543bb5f 100755 --- a/tools/test-integration-shard.sh +++ b/tools/test-integration-shard.sh @@ -7,29 +7,17 @@ set -euo pipefail # a configured database. binary=$1 -shard=${TEST_SHARD:?missing TEST_SHARD} -total=${TEST_TOTAL_SHARDS:?missing TEST_TOTAL_SHARDS} - -if ! [[ "$total" =~ ^[1-9][0-9]*$ ]]; then - echo "TEST_TOTAL_SHARDS must be a positive integer, got: $total" >&2 - exit 2 -fi -if ! [[ "$shard" =~ ^[1-9][0-9]*$ ]] || [ "$shard" -gt "$total" ]; then - echo "TEST_SHARD must be in [1, $total], got: $shard" >&2 - exit 2 -fi # match `func Test*(t *testing.T|TB)` only — excludes TestMain (takes *testing.M) names=$(grep -hE '^func Test[A-Z][A-Za-z0-9_]*\([a-zA-Z_][a-zA-Z0-9_]* \*testing\.(T|TB)\)' tests/integration/*.go \ | sed -E 's/^func (Test[A-Z][A-Za-z0-9_]*).*/\1/' \ - | LC_ALL=C sort -u \ - | awk -v r=$((shard - 1)) -v t="$total" 'NR % t == r') + | ./tools/partition-by-shard.sh) if [ -z "$names" ]; then - echo "no tests assigned to shard $shard/$total — likely a misconfiguration" >&2 + echo "no tests assigned to shard $TEST_SHARD/$TEST_TOTAL_SHARDS — likely a misconfiguration" >&2 exit 1 fi pattern=$(echo "$names" | paste -sd '|' -) -echo "Running shard $shard/$total ($(echo "$names" | wc -l | tr -d ' ') tests)" +echo "Running shard $TEST_SHARD/$TEST_TOTAL_SHARDS ($(echo "$names" | wc -l | tr -d ' ') tests)" exec "$binary" -test.run "^($pattern)\$"