From 21144af79b0ddb2ea70fc442e8cf86c143f9006b Mon Sep 17 00:00:00 2001 From: chichuan Date: Thu, 30 Jul 2026 20:06:24 +0800 Subject: [PATCH] fix: align scoped coverage and test timeout --- .github/workflows/ci.yml | 8 +- scripts/policy/check-coverage-gate.sh | 6 +- test/scripts/changelog_pr_gate_test.go | 35 +++++ .../coverage_workflow_contract_test.go | 120 ++++++++++++++++++ 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 test/scripts/coverage_workflow_contract_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa4433736..255148826 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -438,7 +438,7 @@ jobs: needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite != 'true' }} runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v4 @@ -1160,14 +1160,20 @@ jobs: - name: Enforce coverage gate if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' env: + FULL_SUITE: ${{ needs.lint.outputs.full_suite }} COVERAGE_TARGET: "100" COVERAGE_ENFORCE_OVERALL: "false" COVERAGE_OVERALL_TOLERANCE: "0" run: | + policy_profile=coverage-policy.txt + if [ "$FULL_SUITE" != true ]; then + policy_profile= + fi additional_profile= if [ -f coverage-shortcut.txt ]; then additional_profile=coverage-shortcut.txt fi + COVERAGE_DIFF_PROFILE="$policy_profile" \ COVERAGE_ADDITIONAL_DIFF_PROFILE="$additional_profile" \ make coverage-gate BASE_REF="$COVERAGE_BASE_REF" diff --git a/scripts/policy/check-coverage-gate.sh b/scripts/policy/check-coverage-gate.sh index 2db179f8e..7d70265a5 100755 --- a/scripts/policy/check-coverage-gate.sh +++ b/scripts/policy/check-coverage-gate.sh @@ -6,7 +6,7 @@ BASE_REF="" OVERALL_PROFILE="coverage.txt" ADDITIONAL_DIFF_PROFILE="${COVERAGE_ADDITIONAL_DIFF_PROFILE:-${COVERAGE_ADDITIONAL_PROFILE:-}}" BASELINE_PROFILE="coverage-base.txt" -DIFF_PROFILE="coverage-policy.txt" +DIFF_PROFILE="${COVERAGE_DIFF_PROFILE-coverage-policy.txt}" TARGET="${COVERAGE_TARGET:-100}" OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0}" ENFORCE_OVERALL="${COVERAGE_ENFORCE_OVERALL:-false}" @@ -77,13 +77,15 @@ go build -o "$CHECKER" ./scripts/policy/coverage-gate module="$(go list -m -f '{{.Path}}')" set -- "$CHECKER" \ - --diff-profile "$DIFF_PROFILE" \ --base-ref "$BASE_REF" \ --module "$module" \ --overall-tolerance "$OVERALL_TOLERANCE" \ --target "$TARGET" \ --enforce-overall-target="$ENFORCE_OVERALL" +if [ -n "$DIFF_PROFILE" ]; then + set -- "$@" --diff-profile "$DIFF_PROFILE" +fi if [ "$CHANGED_ONLY" = "true" ]; then set -- "$@" --changed-only else diff --git a/test/scripts/changelog_pr_gate_test.go b/test/scripts/changelog_pr_gate_test.go index 537c05d7e..5b9bfb11f 100644 --- a/test/scripts/changelog_pr_gate_test.go +++ b/test/scripts/changelog_pr_gate_test.go @@ -514,6 +514,35 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { t.Error("Code Admission must not suppress required contexts with paths-ignore") } + focusedStart := strings.Index(admission, "\n test-focused:\n") + focusedEnd := strings.Index(admission, "\n test-race:\n") + if focusedStart < 0 || focusedEnd <= focusedStart { + t.Fatal("Code Admission workflow missing focused test job boundaries") + } + focusedJob := admission[focusedStart:focusedEnd] + if !strings.Contains(focusedJob, "timeout-minutes: 20") { + t.Error("focused test job must allow the scoped race suite up to 20 minutes") + } + + coverageStart := strings.Index(admission, "\n coverage:\n") + coverageEnd := strings.Index(admission, "\n policy:\n") + if coverageStart < 0 || coverageEnd <= coverageStart { + t.Fatal("Code Admission workflow missing coverage job boundaries") + } + coverageJob := admission[coverageStart:coverageEnd] + for _, want := range []string{ + `FULL_SUITE: ${{ needs.lint.outputs.full_suite }}`, + "policy_profile=coverage-policy.txt\n" + + ` if [ "$FULL_SUITE" != true ]; then` + "\n" + + " policy_profile=\n" + + " fi", + `COVERAGE_DIFF_PROFILE="$policy_profile"`, + } { + if !strings.Contains(coverageJob, want) { + t.Errorf("Code Admission workflow missing scoped coverage contract %q", want) + } + } + notification := readWorkflow(".github/workflows/notify-wukong.yml") if !strings.Contains(notification, "- CI") { t.Error("Wukong notification must follow the renamed CI workflow") @@ -529,6 +558,12 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { if !strings.Contains(coverageGate, `OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0}"`) { t.Error("coverage gate must reject any reported overall regression") } + if !strings.Contains(coverageGate, `DIFF_PROFILE="${COVERAGE_DIFF_PROFILE-coverage-policy.txt}"`) { + t.Error("coverage gate must allow scoped CI to explicitly omit the supporting policy profile") + } + if !strings.Contains(coverageGate, `if [ -n "$DIFF_PROFILE" ]; then`) { + t.Error("coverage gate must add the supporting policy profile only when configured") + } if !strings.Contains(coverageGate, `--baseline-profile "$BASELINE_PROFILE"`) { t.Error("coverage gate must evaluate the merge-base profile with the candidate checker") } diff --git a/test/scripts/coverage_workflow_contract_test.go b/test/scripts/coverage_workflow_contract_test.go new file mode 100644 index 000000000..a591b93c9 --- /dev/null +++ b/test/scripts/coverage_workflow_contract_test.go @@ -0,0 +1,120 @@ +package scripts_test + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestCoverageGatePolicyProfileCanBeExplicitlyOmitted(t *testing.T) { + root, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + + binDir := t.TempDir() + fakeGoPath := filepath.Join(binDir, "go") + const fakeGo = `#!/bin/sh +set -eu +case "$1" in + build) + shift + output= + while [ "$#" -gt 0 ]; do + case "$1" in + -o) + output="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + cat > "$output" <<'EOF' +#!/bin/sh +printf '%s\n' "$@" > "$COVERAGE_ARGS_LOG" +EOF + chmod +x "$output" + ;; + list) + printf '%s\n' "example.com/coverage-fixture" + ;; + *) + printf 'unexpected fake go command: %s\n' "$1" >&2 + exit 2 + ;; +esac +` + if err := os.WriteFile(fakeGoPath, []byte(fakeGo), 0o755); err != nil { + t.Fatalf("WriteFile(fake go) error = %v", err) + } + + baseEnv := make([]string, 0, len(os.Environ())+2) + for _, value := range os.Environ() { + if strings.HasPrefix(value, "PATH=") || + strings.HasPrefix(value, "COVERAGE_DIFF_PROFILE=") || + strings.HasPrefix(value, "COVERAGE_ARGS_LOG=") { + continue + } + baseEnv = append(baseEnv, value) + } + baseEnv = append(baseEnv, "PATH="+binDir+":"+os.Getenv("PATH")) + + runGate := func(t *testing.T, diffProfile *string) []string { + t.Helper() + + argsLog := filepath.Join(t.TempDir(), "args.log") + cmd := exec.Command( + "sh", + "./scripts/policy/check-coverage-gate.sh", + "--base-ref", + "HEAD", + ) + cmd.Dir = root + cmd.Env = append(append([]string{}, baseEnv...), "COVERAGE_ARGS_LOG="+argsLog) + if diffProfile != nil { + cmd.Env = append(cmd.Env, "COVERAGE_DIFF_PROFILE="+*diffProfile) + } + output, runErr := cmd.CombinedOutput() + if runErr != nil { + t.Fatalf("coverage gate error = %v\noutput:\n%s", runErr, output) + } + data, readErr := os.ReadFile(argsLog) + if readErr != nil { + t.Fatalf("ReadFile(args log) error = %v", readErr) + } + return strings.Fields(string(data)) + } + + assertDiffProfiles := func(t *testing.T, args []string, want ...string) { + t.Helper() + + var got []string + for i := 0; i+1 < len(args); i++ { + if args[i] == "--diff-profile" { + got = append(got, args[i+1]) + i++ + } + } + if strings.Join(got, "\n") != strings.Join(want, "\n") { + t.Fatalf("diff profiles = %q, want %q; args = %q", got, want, args) + } + } + + t.Run("unset keeps strict policy profile", func(t *testing.T) { + assertDiffProfiles( + t, + runGate(t, nil), + "coverage-policy.txt", + "coverage.txt", + ) + }) + + t.Run("explicit empty omits only policy profile", func(t *testing.T) { + empty := "" + assertDiffProfiles(t, runGate(t, &empty), "coverage.txt") + }) +}