diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 12e5e64e80763..a67203394ab70 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -139,12 +139,20 @@ jobs: TAGS: bindata RACE_ENABLED: true GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} + - uses: actions/upload-artifact@v3 + with: + name: unit-test-coverage + path: coverage.out - name: unit-tests-gogit run: make unit-test-coverage test-check env: TAGS: bindata gogit RACE_ENABLED: true GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} + - uses: actions/upload-artifact@v3 + with: + name: unit-test-coverage-gogit + path: coverage.out test-mysql5: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' @@ -190,6 +198,10 @@ jobs: RACE_ENABLED: true USE_REPO_TEST_DIR: 1 TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" + - uses: actions/upload-artifact@v3 + with: + name: unit-test-coverage-integration + path: integration.coverage.out test-mysql8: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' @@ -251,3 +263,17 @@ jobs: env: TAGS: bindata USE_REPO_TEST_DIR: 1 + + check-backend-coverage: + if: needs.files-changed.outputs.backend == 'true' + needs: [files-changed, test-pgsql, test-sqlite, test-unit, test-mysql5, test-mysql8, test-mssql] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: ">=1.20" + check-latest: true + - uses: actions/download-artifact@v3 + - run: go run build/gocoverage.go merge unit-test-coverage/coverage.out unit-test-coverage-gogit/coverage.out unit-test-coverage-integration/coverage.out > coverage.all + - run: make check-backend-coverage diff --git a/Makefile b/Makefile index 08d439f422a72..8af7df1116db3 100644 --- a/Makefile +++ b/Makefile @@ -225,6 +225,7 @@ help: @echo " - test test everything" @echo " - test-frontend test frontend files" @echo " - test-backend test backend files" + @echo " - check-backend-coverage check backend package coverage" @echo " - test-e2e[\#TestSpecificName] test end to end using playwright" @echo " - webpack build webpack files" @echo " - svg build svg files" @@ -472,13 +473,17 @@ test\#%: coverage: grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out - $(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all + $(GO) run build/gocoverage.go merge integration.coverage-bodged.out coverage-bodged.out > coverage.all .PHONY: unit-test-coverage unit-test-coverage: @echo "Running unit-test-coverage $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." @$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 +.PHONY: check-backend-coverage +check-backend-coverage: + @$(GO) run build/gocoverage.go check coverage.all tests/coverage_requirement.txt + .PHONY: tidy tidy: $(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2)) diff --git a/build/gocovmerge.go b/build/gocoverage.go similarity index 51% rename from build/gocovmerge.go rename to build/gocoverage.go index c6f74ed85cd3d..b4e72a63d1475 100644 --- a/build/gocovmerge.go +++ b/build/gocoverage.go @@ -10,12 +10,15 @@ package main import ( - "flag" "fmt" "io" "log" + "math" "os" + "path/filepath" "sort" + "strconv" + "strings" "golang.org/x/tools/cover" ) @@ -99,12 +102,52 @@ func dumpProfiles(profiles []*cover.Profile, out io.Writer) { } } -func main() { - flag.Parse() +func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string) { + mainOptions = map[string]string{} + for i := 1; i < len(os.Args); i++ { + arg := os.Args[i] + if arg == "" { + break + } + if arg[0] == '-' { + arg = strings.TrimPrefix(arg, "-") + arg = strings.TrimPrefix(arg, "-") + fields := strings.SplitN(arg, "=", 2) + if len(fields) == 1 { + mainOptions[fields[0]] = "1" + } else { + mainOptions[fields[0]] = fields[1] + } + } else { + subCmd = arg + subArgs = os.Args[i+1:] + break + } + } + return +} + +func showUsage() { + fmt.Printf(`Usage: %[1]s {command} [arguments] + +Commands: + %[1]s merge ... + %[1]s check coverage_file requirement_file.txt + +Arguments: + {file-list} the file list + +Example: + %[1]s merge {file-list} + %[1]s check coverage.out requirement_file.txt +`, "gocoverage") +} + +func merge(args []string) { var merged []*cover.Profile - for _, file := range flag.Args() { + for _, file := range args { profiles, err := cover.ParseProfiles(file) if err != nil { log.Fatalf("failed to parse profile '%s': %v", file, err) @@ -116,3 +159,88 @@ func main() { dumpProfiles(merged, os.Stdout) } + +func percentToInt64(percent string) int64 { + value := strings.ReplaceAll(percent, "%", "") + i, err := strconv.ParseFloat(value, 10) + if err != nil { + log.Fatalf("invalid percent: %s", percent) + } + return int64(i * 10) +} + +func profileCount(p *cover.Profile) (int64, int64) { + blocks := p.Blocks + var active, total int64 + for i := range blocks { + stmts := int64(blocks[i].NumStmt) + total += stmts + if blocks[i].Count > 0 { + active += stmts + } + } + return total, active +} + +func checkPackages(args []string) { + if len(args) != 2 { + log.Fatalf("invalid arguments: %v", args) + return + } + coverageFile, packagesFile := args[0], args[1] + profiles, err := cover.ParseProfiles(coverageFile) + if err != nil { + log.Fatalf("failed to parse profile '%s': %v", coverageFile, err) + } + packagesRequirements := make(map[string]int64) + packages, err := os.ReadFile(packagesFile) + if err != nil { + log.Fatalf("failed to read packages file '%s': %v", packagesFile, err) + } + lines := strings.Split(string(packages), "\n") + for _, p := range lines { + parts := strings.Split(p, "=") + if len(parts) != 2 { + continue + } + packagesRequirements[parts[0]] = percentToInt64(parts[1]) + } + packagesTotals := make(map[string]int64) + packagesActives := make(map[string]int64) + for _, p := range profiles { + pkg := filepath.Dir(p.FileName) + _, ok := packagesRequirements[pkg] + if !ok { + continue + } + total, active := profileCount(p) + packagesTotals[pkg] += total + packagesActives[pkg] += active + } + var failed bool + for k, v := range packagesRequirements { + actual := 100 * float64(packagesActives[k]) / float64(packagesTotals[k]) + if v > int64(math.Floor(actual*10+0.5)) { + log.Printf("package %s coverage is %.1f%%, required %.1f%%", k, actual, float64(v)/10.0) + failed = true + } + } + if failed { + os.Exit(1) + } +} + +func main() { + _, subCmd, subArgs := parseArgs() + if subCmd == "" { + showUsage() + os.Exit(1) + } + + switch subCmd { + case "merge": + merge(subArgs) + case "check": + checkPackages(subArgs) + } +} diff --git a/tests/coverage_requirement.txt b/tests/coverage_requirement.txt new file mode 100644 index 0000000000000..ecd6e687c0fdb --- /dev/null +++ b/tests/coverage_requirement.txt @@ -0,0 +1,126 @@ +code.gitea.io/gitea/modules/activitypub=60.66% +code.gitea.io/gitea/modules/analyze=73.91% +code.gitea.io/gitea/modules/auth=60.29% +code.gitea.io/gitea/modules/avatar=55.20% +code.gitea.io/gitea/modules/base=92.96% +code.gitea.io/gitea/modules/cache=14.5% +code.gitea.io/gitea/modules/charset=56.67% +code.gitea.io/gitea/modules/container=100.00% +code.gitea.io/gitea/modules/context=58.91% +code.gitea.io/gitea/modules/csv=100.00% +code.gitea.io/gitea/modules/doctor=15.1% +code.gitea.io/gitea/modules/emoji=83.3% +code.gitea.io/gitea/modules/eventsource=53.88% +code.gitea.io/gitea/modules/generate=0.00% +code.gitea.io/gitea/modules/git=61.93% +code.gitea.io/gitea/modules/gitgraph=75.77% +code.gitea.io/gitea/modules/graceful=29.0% +code.gitea.io/gitea/modules/hcaptcha=0.00% +code.gitea.io/gitea/modules/highlight=67.14% +code.gitea.io/gitea/modules/hostmatcher=79.34% +code.gitea.io/gitea/modules/html=100.00% +code.gitea.io/gitea/modules/httpcache=78.95% +code.gitea.io/gitea/modules/httplib=67.86% +code.gitea.io/gitea/modules/indexer=39.21% +code.gitea.io/gitea/modules/issue=75.47% +code.gitea.io/gitea/modules/json=43.75% +code.gitea.io/gitea/modules/lfs=70.98% +code.gitea.io/gitea/modules/log=69.41% +code.gitea.io/gitea/modules/markup=53.32% +code.gitea.io/gitea/modules/mcaptcha=0.00% +code.gitea.io/gitea/modules/metrics=0.00% +code.gitea.io/gitea/modules/migration=70.45% +code.gitea.io/gitea/modules/mirror=16.67% +code.gitea.io/gitea/modules/nosql=29.21% +code.gitea.io/gitea/modules/notification=64.94% +code.gitea.io/gitea/modules/options=48.74% +code.gitea.io/gitea/modules/packages=70.63% +code.gitea.io/gitea/modules/paginator=100.00% +code.gitea.io/gitea/modules/password=66.18% +code.gitea.io/gitea/modules/pprof=0.00% +code.gitea.io/gitea/modules/private=13.40% +code.gitea.io/gitea/modules/process=38.5% +code.gitea.io/gitea/modules/proxy=17.2% +code.gitea.io/gitea/modules/proxyprotocol=0.00% +code.gitea.io/gitea/modules/public=53.26% +code.gitea.io/gitea/modules/queue=39.17% +code.gitea.io/gitea/modules/recaptcha=0.00% +code.gitea.io/gitea/modules/references=80.50% +code.gitea.io/gitea/modules/regexplru=65.22% +code.gitea.io/gitea/modules/repository=49.68% +code.gitea.io/gitea/modules/secret=64.00% +code.gitea.io/gitea/modules/session=18.63% +code.gitea.io/gitea/modules/setting=54.19% +code.gitea.io/gitea/modules/sitemap=82.35% +code.gitea.io/gitea/modules/ssh=45.98% +code.gitea.io/gitea/modules/storage=57.14% +code.gitea.io/gitea/modules/structs=54.22% +code.gitea.io/gitea/modules/svg=94.4% +code.gitea.io/gitea/modules/sync=100.00% +code.gitea.io/gitea/modules/system=70.00% +code.gitea.io/gitea/modules/templates=43.01% +code.gitea.io/gitea/modules/test=68.83% +code.gitea.io/gitea/modules/timeutil=80.80% +code.gitea.io/gitea/modules/translation=60.70% +code.gitea.io/gitea/modules/typesniffer=92.86% +code.gitea.io/gitea/modules/updatechecker=0.00% +code.gitea.io/gitea/modules/upload=73.85% +code.gitea.io/gitea/modules/uri=27.78% +code.gitea.io/gitea/modules/user=23.53% +code.gitea.io/gitea/modules/util=72.93% +code.gitea.io/gitea/modules/validation=79.4% +code.gitea.io/gitea/modules/watcher=0.00% +code.gitea.io/gitea/modules/web=76.76% +code.gitea.io/gitea/modules/webhook=13.3% +code.gitea.io/gitea/models/activities=58.48% +code.gitea.io/gitea/models/admin=33.55% +code.gitea.io/gitea/models/asymkey=41.40% +code.gitea.io/gitea/models/auth=46.36% +code.gitea.io/gitea/models/avatars=31.82% +code.gitea.io/gitea/models/db=41.53% +code.gitea.io/gitea/models/git=44.19% +code.gitea.io/gitea/models/issues=57.31% +code.gitea.io/gitea/models/organization=71.00% +code.gitea.io/gitea/models/packages=71.66% +code.gitea.io/gitea/models/perm=57.05% +code.gitea.io/gitea/models/project=31.85% +code.gitea.io/gitea/models/pull=36.13% +code.gitea.io/gitea/models/repo=58.62% +code.gitea.io/gitea/models/secret=24.56% +code.gitea.io/gitea/models/system=56.16% +code.gitea.io/gitea/models/unit=48.72% +code.gitea.io/gitea/models/unittest=31.91% +code.gitea.io/gitea/models/user=55.82% +code.gitea.io/gitea/models/webhook=66.02% +code.gitea.io/gitea/routers/api=60.66% +code.gitea.io/gitea/routers/common=61.15% +code.gitea.io/gitea/routers/install=5.7% +code.gitea.io/gitea/routers/private=34.74% +code.gitea.io/gitea/routers/utils=100.00% +code.gitea.io/gitea/routers/web=28.95% +code.gitea.io/gitea/services/agit=56.59% +code.gitea.io/gitea/services/asymkey=36.73% +code.gitea.io/gitea/services/attachment=60.87% +code.gitea.io/gitea/services/auth=38.99% +code.gitea.io/gitea/services/automerge=44.83% +code.gitea.io/gitea/services/context=47.22% +code.gitea.io/gitea/services/convert=73.5% +code.gitea.io/gitea/services/cron=56.2% +code.gitea.io/gitea/services/externalaccount=0.00% +code.gitea.io/gitea/services/forms=36.5% +code.gitea.io/gitea/services/gitdiff=70.52% +code.gitea.io/gitea/services/issue=40.50% +code.gitea.io/gitea/services/lfs=57.06% +code.gitea.io/gitea/services/mailer=54.55% +code.gitea.io/gitea/services/markup=100.00% +code.gitea.io/gitea/services/migrations=35.97% +code.gitea.io/gitea/services/mirror=29.83% +code.gitea.io/gitea/services/org=49.02% +code.gitea.io/gitea/services/packages=51.94% +code.gitea.io/gitea/services/pull=43.34% +code.gitea.io/gitea/services/release=45.09% +code.gitea.io/gitea/services/repository=41.19% +code.gitea.io/gitea/services/task=43.75% +code.gitea.io/gitea/services/user=46.24% +code.gitea.io/gitea/services/webhook=66.63% +code.gitea.io/gitea/services/wiki=56.41%