diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml index 733077cc80..5aa0a98bfc 100644 --- a/.github/workflows/cache-seeder.yml +++ b/.github/workflows/cache-seeder.yml @@ -64,6 +64,8 @@ jobs: - uses: ./.github/actions/go-cache with: cache-name: ${{ matrix.job }} + # the gobuild job already saves the shared gobuild cache key; let only it win. + build-cache: "false" lint-cache: "true" - run: make deps-backend deps-tools - run: make ${{ matrix.target }} diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 51e1c805e6..801c77d56d 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -78,7 +78,6 @@ jobs: - uses: ./.github/actions/go-cache with: cache-name: checks-backend - build-cache: "false" - run: make deps-backend deps-tools - run: make --always-make checks-backend # ensure the "go-licenses" make target runs diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index a3f4efdc16..bca502ffe8 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -14,10 +14,14 @@ jobs: files-changed: uses: ./.github/workflows/files-changed.yml - test-pgsql: + test-pgsql-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: pgsql: image: postgres:14 @@ -57,6 +61,7 @@ jobs: env: TAGS: bindata - name: run migration tests + if: matrix.shard == 1 run: GITEA_TEST_DATABASE=pgsql make test-migration - name: run tests run: GITEA_TEST_DATABASE=pgsql make test-integration @@ -66,11 +71,24 @@ jobs: GOTEST_FLAGS: -race -timeout=40m TAGS: bindata gogit TEST_LDAP: 1 + TEST_SHARD: ${{ matrix.shard }} + TEST_TOTAL_SHARDS: 3 - test-sqlite: + test-pgsql: + needs: [files-changed, test-pgsql-shards] + if: always() && (needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true') + runs-on: ubuntu-latest + steps: + - run: '[ "${{ needs.test-pgsql-shards.result }}" = "success" ]' + + test-sqlite-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] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 @@ -87,6 +105,7 @@ jobs: TAGS: bindata gogit GOEXPERIMENT: - name: run migration tests + if: matrix.shard == 1 run: GITEA_TEST_DATABASE=sqlite make test-migration env: TAGS: bindata gogit @@ -98,11 +117,24 @@ jobs: GOTEST_FLAGS: -timeout=40m TAGS: bindata gogit GOEXPERIMENT: + TEST_SHARD: ${{ matrix.shard }} + TEST_TOTAL_SHARDS: 3 - test-unit: + test-sqlite: + needs: [files-changed, test-sqlite-shards] + if: always() && (needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true') + runs-on: ubuntu-latest + steps: + - run: '[ "${{ needs.test-sqlite-shards.result }}" = "success" ]' + + 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, 4] services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 @@ -147,31 +179,45 @@ 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: 4 GOTEST_FLAGS: -race -timeout=20m TAGS: bindata GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} - name: unit-tests-gogit - run: make test-backend test-check + run: make test-backend-gogit-shard test-check env: + TEST_SHARD: ${{ matrix.shard }} + TEST_TOTAL_SHARDS: 4 GOTEST_FLAGS: -race -timeout=20m TAGS: bindata gogit GOEXPERIMENT: GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} - test-mysql: + test-unit: + 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-shards.result }}" = "success" ]' + + test-mysql-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: mysql: # the bitnami mysql image has more options than the official one, it's easier to customize @@ -214,17 +260,31 @@ jobs: env: TAGS: bindata - name: run migration tests + if: matrix.shard == 1 run: GITEA_TEST_DATABASE=mysql make test-migration - name: run tests run: GITEA_TEST_DATABASE=mysql make test-integration env: TAGS: bindata TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" + TEST_SHARD: ${{ matrix.shard }} + TEST_TOTAL_SHARDS: 3 - test-mssql: + test-mysql: + needs: [files-changed, test-mysql-shards] + if: always() && (needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true') + runs-on: ubuntu-latest + steps: + - run: '[ "${{ needs.test-mysql-shards.result }}" = "success" ]' + + test-mssql-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: mssql: image: mcr.microsoft.com/mssql/server:2019-latest @@ -254,9 +314,19 @@ jobs: - run: make backend env: TAGS: bindata - - run: GITEA_TEST_DATABASE=mssql make test-migration + - if: matrix.shard == 1 + run: GITEA_TEST_DATABASE=mssql make test-migration - name: run tests run: GITEA_TEST_DATABASE=mssql make test-integration timeout-minutes: 50 env: TAGS: bindata + TEST_SHARD: ${{ matrix.shard }} + TEST_TOTAL_SHARDS: 3 + + test-mssql: + needs: [files-changed, test-mssql-shards] + if: always() && (needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true') + runs-on: ubuntu-latest + steps: + - run: '[ "${{ needs.test-mssql-shards.result }}" = "success" ]' diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index f81026a5ae..0c2a2634f1 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -28,7 +28,6 @@ jobs: - uses: ./.github/actions/go-cache with: cache-name: e2e - build-cache: "false" - uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: diff --git a/Makefile b/Makefile index 27b2c30295..bf5b29ca53 100644 --- a/Makefile +++ b/Makefile @@ -381,6 +381,21 @@ test-backend: ## test backend files @echo "Running go test with $(GOTEST_FLAGS) -tags '$(TAGS)'..." @$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $(GO_TEST_PACKAGES) +.PHONY: test-backend-gogit +test-backend-gogit: ## test packages whose code or tests import the gogit-affected modules + @pkgs=$$(./tools/find-gogit-test-pkgs.sh '$(TAGS)') && \ + $(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) && \ + $(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) && \ + $(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $$pkgs + .PHONY: test-frontend test-frontend: node_modules ## test frontend files pnpm exec vitest @@ -444,7 +459,14 @@ test-integration: @# would flood output per passing test. testcache can't help these tests anyway — @# they mutate the work directory, so cache inputs change between runs. $(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c code.gitea.io/gitea/tests/integration -o ./test-integration-$(GITEA_TEST_DATABASE).test +ifdef TEST_SHARD +ifndef TEST_TOTAL_SHARDS + $(error TEST_TOTAL_SHARDS must be set when TEST_SHARD is set) +endif + ./tools/test-integration-shard.sh ./test-integration-$(GITEA_TEST_DATABASE).test +else ./test-integration-$(GITEA_TEST_DATABASE).test +endif .PHONY: test-integration\#% test-integration\#%: diff --git a/tests/integration/api_repo_lfs_locks_test.go b/tests/integration/api_repo_lfs_locks_test.go index 460b32a7dd..0b48bd00cc 100644 --- a/tests/integration/api_repo_lfs_locks_test.go +++ b/tests/integration/api_repo_lfs_locks_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -22,7 +23,7 @@ import ( func TestAPILFSLocksNotStarted(t *testing.T) { defer tests.PrepareTestEnv(t)() - setting.LFS.StartServer = false + defer test.MockVariableValue(&setting.LFS.StartServer, false)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -38,7 +39,7 @@ func TestAPILFSLocksNotStarted(t *testing.T) { func TestAPILFSLocksNotLogin(t *testing.T) { defer tests.PrepareTestEnv(t)() - setting.LFS.StartServer = true + defer test.MockVariableValue(&setting.LFS.StartServer, true)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -51,7 +52,7 @@ func TestAPILFSLocksNotLogin(t *testing.T) { func TestAPILFSLocksLogged(t *testing.T) { defer tests.PrepareTestEnv(t)() - setting.LFS.StartServer = true + defer test.MockVariableValue(&setting.LFS.StartServer, true)() user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // in org 3 user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // in org 3 diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go index 47bf244ee6..0f13bb23a8 100644 --- a/tests/integration/api_repo_lfs_test.go +++ b/tests/integration/api_repo_lfs_test.go @@ -28,8 +28,7 @@ import ( func TestAPILFSNotStarted(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.LFS.StartServer = false + defer test.MockVariableValue(&setting.LFS.StartServer, false)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -48,8 +47,7 @@ func TestAPILFSNotStarted(t *testing.T) { func TestAPILFSMediaType(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.LFS.StartServer = true + defer test.MockVariableValue(&setting.LFS.StartServer, true)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -72,8 +70,7 @@ func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Reposito func TestAPILFSBatch(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.LFS.StartServer = true + defer test.MockVariableValue(&setting.LFS.StartServer, true)() repo := createLFSTestRepository(t, "lfs-batch-repo") @@ -326,8 +323,7 @@ func TestAPILFSBatch(t *testing.T) { func TestAPILFSUpload(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.LFS.StartServer = true + defer test.MockVariableValue(&setting.LFS.StartServer, true)() repo := createLFSTestRepository(t, "lfs-upload-repo") oid := storeObjectInRepo(t, repo.ID, "dummy3") @@ -428,8 +424,7 @@ func TestAPILFSUpload(t *testing.T) { func TestAPILFSVerify(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.LFS.StartServer = true + defer test.MockVariableValue(&setting.LFS.StartServer, true)() repo := createLFSTestRepository(t, "lfs-verify-repo") oid := storeObjectInRepo(t, repo.ID, "dummy3") diff --git a/tools/find-gogit-test-pkgs.sh b/tools/find-gogit-test-pkgs.sh new file mode 100755 index 0000000000..1d60a55a06 --- /dev/null +++ b/tools/find-gogit-test-pkgs.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -euo pipefail + +# Print packages with tests whose own code or test code imports any of the +# gogit-affected modules (modules/git, modules/gitrepo, modules/lfs). These +# are the packages whose tests can observe behavioral differences between +# the bindata and bindata+gogit tag sets. +# +# Packages without tests are intentionally skipped — they're compiled +# transitively by their consumers, so any tag-related compile error would +# already surface in those consumers' test builds. + +TAGS=${1:?usage: $0 TAGS} + +# Exclusions mirror the Makefile's GO_TEST_PACKAGES filter — these packages +# need a real database / dedicated harness and are tested separately. +OUT=$(go list -tags "$TAGS" -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}|{{range .Imports}}{{.}};{{end}}{{range .TestImports}}{{.}};{{end}}{{range .XTestImports}}{{.}};{{end}}{{end}}' ./... \ + | awk -F'|' ' + $2 ~ /code\.gitea\.io\/gitea\/modules\/(git|gitrepo|lfs)[\/;]/ && + $1 !~ /^code\.gitea\.io\/gitea\/(models\/migrations(\/|$)|tests(\/integration(\/migration-test)?)?$)/ { + print $1 + }' \ + | sort -u) +if [ -z "$OUT" ]; then + echo "no gogit-affected packages found" >&2 + exit 1 +fi +echo "$OUT" diff --git a/tools/partition-by-shard.sh b/tools/partition-by-shard.sh new file mode 100755 index 0000000000..acb5139251 --- /dev/null +++ b/tools/partition-by-shard.sh @@ -0,0 +1,25 @@ +#!/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 + +OUT=$(LC_ALL=C sort -u | awk -v r=$((SHARD - 1)) -v t="$TOTAL" '(NR - 1) % t == r') +if [ -z "$OUT" ]; then + echo "shard $SHARD/$TOTAL has no items assigned" >&2 + exit 1 +fi +echo "$OUT" diff --git a/tools/test-integration-shard.sh b/tools/test-integration-shard.sh new file mode 100755 index 0000000000..6f007cebd8 --- /dev/null +++ b/tools/test-integration-shard.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +# Run a deterministic shard of the integration test binary. Test names are +# enumerated from source — running the binary with -test.list isn't viable +# because TestMain boots the full Gitea environment and would panic without +# a configured database. + +BINARY=$1 + +# match `func Test...(t *testing.T)` only — `*testing.M` excludes TestMain +NAMES=$(grep -hE '^func Test[A-Za-z0-9_]*\([a-zA-Z_][a-zA-Z0-9_]* \*testing\.T\)' tests/integration/*.go \ + | sed -E 's/^func (Test[A-Za-z0-9_]*).*/\1/' \ + | ./tools/partition-by-shard.sh) + +PATTERN=$(echo "$NAMES" | paste -sd '|' -) +echo "Running shard $TEST_SHARD/$TEST_TOTAL_SHARDS ($(echo "$NAMES" | wc -l | tr -d ' ') tests)" +exec "$BINARY" -test.run "^($PATTERN)\$"