Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e070a7e24 |
@@ -1,4 +1,4 @@
|
||||
name: Code Admission — PR 合入门禁
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -714,9 +714,10 @@ jobs:
|
||||
- name: Enforce coverage gate
|
||||
if: needs.lint.outputs.changelog_only != 'true'
|
||||
env:
|
||||
COVERAGE_TARGET: "80"
|
||||
COVERAGE_TARGET: "100"
|
||||
COVERAGE_ENFORCE_OVERALL: "false"
|
||||
run: COVERAGE_ADDITIONAL_PROFILE=coverage-shortcut.txt make coverage-gate BASE_REF="$COVERAGE_BASE_REF"
|
||||
COVERAGE_OVERALL_TOLERANCE: "0"
|
||||
run: COVERAGE_ADDITIONAL_DIFF_PROFILE=coverage-shortcut.txt make coverage-gate BASE_REF="$COVERAGE_BASE_REF"
|
||||
|
||||
- name: Generate coverage report
|
||||
if: needs.lint.outputs.changelog_only != 'true'
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Main Integration — Wukong Overlay
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Code Admission — PR 合入门禁
|
||||
- CI
|
||||
types:
|
||||
- completed
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ help:
|
||||
@printf " make policy - Check the built dws plus open-source and Schema policies\n"
|
||||
@printf " make interface-integrity - Check historical commands and help contracts still work\n"
|
||||
@printf " make authoritative-interface-integrity BASE_REF=<ref> - Check the Git-owned PR merge-base\n"
|
||||
@printf " make coverage-gate BASE_REF=<ref> - Enforce overall non-regression and changed-code coverage\n"
|
||||
@printf " make coverage-gate-platform BASE_REF=<ref> PROFILE=<file> - Enforce native-platform changed-code coverage\n"
|
||||
@printf " make coverage-gate BASE_REF=<ref> - Enforce overall non-regression and 100%% changed-code coverage\n"
|
||||
@printf " make coverage-gate-platform BASE_REF=<ref> PROFILE=<file> - Enforce 100%% native changed-code coverage\n"
|
||||
@printf " make update-interface-baseline - Add new CLI contracts without removing history\n"
|
||||
@printf " make reset-interface-baseline - DANGEROUS: replace all CLI compatibility history\n"
|
||||
@printf " make schema-compatibility BASE_REF=<ref> - Check the complete Schema contract against the PR merge-base\n"
|
||||
|
||||
@@ -52,7 +52,7 @@ run.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
PR["Pull request"] --> CA["Code Admission — PR 合入门禁"]
|
||||
PR["Pull request"] --> CA["CI"]
|
||||
subgraph CA_CHECKS["Nine required contexts"]
|
||||
L["Lint"]
|
||||
T["Test"]
|
||||
|
||||
+11
-5
@@ -1,4 +1,4 @@
|
||||
# Code Admission — PR 合入门禁
|
||||
# CI — PR 合入门禁
|
||||
|
||||
The pull-request admission layer has exactly nine required external contexts:
|
||||
|
||||
@@ -6,7 +6,7 @@ The pull-request admission layer has exactly nine required external contexts:
|
||||
|---|---|
|
||||
| `Lint` | Stable PR revision classification, formatting, `go vet`, and Actionlint |
|
||||
| `Test` | Race/unit/release-script tests plus fast cross-platform compilation |
|
||||
| `Coverage` | Overall non-regression and changed-code coverage |
|
||||
| `Coverage` | Overall non-regression and 100% changed-code coverage |
|
||||
| `Policy` | Repository policy and the fail-closed CHANGELOG contract |
|
||||
| `Edition` | Edition contract tests |
|
||||
| `Interface Integrity` | CLI, Schema, Skill, and stable-release compatibility |
|
||||
@@ -14,7 +14,7 @@ The pull-request admission layer has exactly nine required external contexts:
|
||||
| `CLI Smoke` | Offline help for every public top-level command |
|
||||
| `Mock MCP` | HTTP and stdio MCP lifecycle smoke tests |
|
||||
|
||||
The workflow display name is `Code Admission — PR 合入门禁`. Parallel helper
|
||||
The workflow display name is `CI`. Parallel helper
|
||||
jobs may implement `Test` and `Coverage`, but they are not ruleset contexts.
|
||||
Do not require an aggregate alias or a downstream integration check in place of
|
||||
the nine contracts above.
|
||||
@@ -81,7 +81,7 @@ successful PR check.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
PR["Pull request"] --> ADMISSION["Code Admission — PR 合入门禁"]
|
||||
PR["Pull request"] --> ADMISSION["CI"]
|
||||
ADMISSION --> L["Lint"]
|
||||
ADMISSION --> T["Test"]
|
||||
ADMISSION --> C["Coverage"]
|
||||
@@ -122,7 +122,13 @@ base_ref=$(git merge-base HEAD origin/main)
|
||||
|
||||
`make coverage-gate` is an enforcement step, not a profile generator. CI
|
||||
generates the candidate, supporting, merge-base, and (when risk-selected)
|
||||
native profiles before the aggregate `Coverage` context evaluates them.
|
||||
native profiles before the aggregate `Coverage` context evaluates them. The
|
||||
aggregate and native gates require 100% coverage for changed executable Go
|
||||
statements. Overall coverage remains an unrounded, zero-tolerance merge-base
|
||||
non-regression check. Candidate and baseline profiles are evaluated by the
|
||||
same block-deduplicating checker; supporting policy and shortcut profiles
|
||||
contribute to changed-code coverage only. The checked-in badge is presentation
|
||||
only and is never read as a gate input.
|
||||
|
||||
Compatibility checks derive authoritative Interface snapshots from the PR
|
||||
merge-base and the latest reachable stable release. The candidate cannot bless
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
发布只走一条链路:本地脚本负责封板、验证并推送 annotated tag;GitHub Actions 负责构建和发布最终产物。不要直接运行 `goreleaser release`,也不要手工补打或移动 tag。
|
||||
|
||||
发布前必须完成平台治理:目标 GitHub 仓库已启用 immutable releases,`main` 精确要求 `Code Admission — PR 合入门禁` 的九个 context:`Lint`、`Test`、`Coverage`、`Policy`、`Edition`、`Interface Integrity`、`AI Behavior`、`CLI Smoke`、`Mock MCP`,操作机已安装并登录 `gh`。本地脚本会在封 tag 前通过 API 检查 immutable releases、当前 SHA 的全部九个 context 和在途 Release;`v*` tag ruleset 仍需仓库管理员预先配置并由操作人确认。
|
||||
发布前必须完成平台治理:目标 GitHub 仓库已启用 immutable releases,`main` 精确要求 `CI` workflow 的九个 context:`Lint`、`Test`、`Coverage`、`Policy`、`Edition`、`Interface Integrity`、`AI Behavior`、`CLI Smoke`、`Mock MCP`,操作机已安装并登录 `gh`。本地脚本会在封 tag 前通过 API 检查 immutable releases、当前 SHA 的全部九个 context 和在途 Release;`v*` tag ruleset 仍需仓库管理员预先配置并由操作人确认。
|
||||
|
||||
## 日常只用一个入口
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ set -eu
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
BASE_REF=""
|
||||
OVERALL_PROFILE="coverage.txt"
|
||||
ADDITIONAL_PROFILE="${COVERAGE_ADDITIONAL_PROFILE:-}"
|
||||
ADDITIONAL_DIFF_PROFILE="${COVERAGE_ADDITIONAL_DIFF_PROFILE:-${COVERAGE_ADDITIONAL_PROFILE:-}}"
|
||||
BASELINE_PROFILE="coverage-base.txt"
|
||||
DIFF_PROFILE="coverage-policy.txt"
|
||||
TARGET="${COVERAGE_TARGET:-80}"
|
||||
OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0.1}"
|
||||
TARGET="${COVERAGE_TARGET:-100}"
|
||||
OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0}"
|
||||
ENFORCE_OVERALL="${COVERAGE_ENFORCE_OVERALL:-false}"
|
||||
CHANGED_ONLY="false"
|
||||
SCOPE_BUILDABLE="false"
|
||||
|
||||
usage() {
|
||||
printf '%s\n' "usage: $0 --base-ref <ref> [--changed-only] [--scope-buildable] [--overall-profile <file>] [--additional-profile <file>] [--baseline-profile <file>] [--diff-profile <file>]" >&2
|
||||
printf '%s\n' "usage: $0 --base-ref <ref> [--changed-only] [--scope-buildable] [--overall-profile <file>] [--additional-diff-profile <file>] [--baseline-profile <file>] [--diff-profile <file>]" >&2
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
@@ -29,9 +29,9 @@ while [ "$#" -gt 0 ]; do
|
||||
OVERALL_PROFILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--additional-profile)
|
||||
--additional-diff-profile|--additional-profile)
|
||||
[ "$#" -ge 2 ] || { usage; exit 2; }
|
||||
ADDITIONAL_PROFILE="$2"
|
||||
ADDITIONAL_DIFF_PROFILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--baseline-profile)
|
||||
@@ -87,19 +87,12 @@ set -- "$CHECKER" \
|
||||
if [ "$CHANGED_ONLY" = "true" ]; then
|
||||
set -- "$@" --changed-only
|
||||
else
|
||||
baseline="$(go tool cover -func="$BASELINE_PROFILE" | awk '/^total:/ { gsub(/%/, "", $3); print $3 }')"
|
||||
[ -n "$baseline" ] || {
|
||||
printf 'error: cannot parse authoritative coverage from %s\n' "$BASELINE_PROFILE" >&2
|
||||
exit 2
|
||||
}
|
||||
set -- "$@" \
|
||||
--overall-profile "$OVERALL_PROFILE" \
|
||||
--diff-profile "$OVERALL_PROFILE" \
|
||||
--baseline-overall "$baseline"
|
||||
if [ -n "$ADDITIONAL_PROFILE" ]; then
|
||||
set -- "$@" \
|
||||
--overall-profile "$ADDITIONAL_PROFILE" \
|
||||
--diff-profile "$ADDITIONAL_PROFILE"
|
||||
--baseline-profile "$BASELINE_PROFILE"
|
||||
if [ -n "$ADDITIONAL_DIFF_PROFILE" ]; then
|
||||
set -- "$@" --diff-profile "$ADDITIONAL_DIFF_PROFILE"
|
||||
fi
|
||||
fi
|
||||
if [ "$SCOPE_BUILDABLE" = "true" ]; then
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -37,11 +36,13 @@ func (values *stringList) Set(value string) error {
|
||||
}
|
||||
|
||||
type coverageBlock struct {
|
||||
File string
|
||||
StartLine int
|
||||
EndLine int
|
||||
Statements int
|
||||
Count int
|
||||
File string
|
||||
StartLine int
|
||||
StartColumn int
|
||||
EndLine int
|
||||
EndColumn int
|
||||
Statements int
|
||||
Count int
|
||||
}
|
||||
|
||||
type lineRange struct {
|
||||
@@ -78,6 +79,7 @@ func run(
|
||||
buildableLoader func() (map[string]bool, error),
|
||||
) int {
|
||||
var overallPaths stringList
|
||||
var baselinePaths stringList
|
||||
var diffPaths stringList
|
||||
var baseRef string
|
||||
var modulePath string
|
||||
@@ -90,12 +92,13 @@ func run(
|
||||
flags := flag.NewFlagSet("coverage-gate", flag.ContinueOnError)
|
||||
flags.SetOutput(stderr)
|
||||
flags.Var(&overallPaths, "overall-profile", "coverage profile used for overall coverage (repeatable)")
|
||||
flags.Var(&baselinePaths, "baseline-profile", "merge-base coverage profile evaluated with the same model as the candidate (repeatable)")
|
||||
flags.Var(&diffPaths, "diff-profile", "coverage profile used for changed-code coverage (repeatable)")
|
||||
flags.StringVar(&baseRef, "base-ref", "", "Git merge-base or previous main SHA")
|
||||
flags.StringVar(&modulePath, "module", "", "Go module path used to normalize profile filenames")
|
||||
flags.Float64Var(&baselineOverall, "baseline-overall", -1, "authoritative overall coverage percentage")
|
||||
flags.Float64Var(&overallTolerance, "overall-tolerance", 0.1, "allowed overall coverage measurement variance in percentage points")
|
||||
flags.Float64Var(&target, "target", 80, "required changed-code and eventual overall coverage percentage")
|
||||
flags.Float64Var(&overallTolerance, "overall-tolerance", 0, "allowed overall coverage measurement variance in percentage points")
|
||||
flags.Float64Var(&target, "target", 100, "required changed-code coverage percentage and optional overall floor")
|
||||
flags.BoolVar(&enforceOverall, "enforce-overall-target", false, "require overall coverage to reach target")
|
||||
flags.BoolVar(&changedOnly, "changed-only", false, "enforce changed-code coverage without an overall baseline")
|
||||
flags.BoolVar(&scopeBuildable, "scope-buildable", false, "only evaluate changed files buildable on the current platform")
|
||||
@@ -103,8 +106,13 @@ func run(
|
||||
return 2
|
||||
}
|
||||
|
||||
if len(diffPaths) == 0 || baseRef == "" || modulePath == "" || (!changedOnly && (len(overallPaths) == 0 || baselineOverall < 0)) {
|
||||
fmt.Fprintln(stderr, "coverage-gate requires --diff-profile, --base-ref, and --module; overall mode also requires --overall-profile and --baseline-overall")
|
||||
if len(diffPaths) == 0 || baseRef == "" || modulePath == "" ||
|
||||
(!changedOnly && (len(overallPaths) == 0 || (len(baselinePaths) == 0 && baselineOverall < 0))) {
|
||||
fmt.Fprintln(stderr, "coverage-gate requires --diff-profile, --base-ref, and --module; overall mode also requires --overall-profile and either --baseline-profile or --baseline-overall")
|
||||
return 2
|
||||
}
|
||||
if len(baselinePaths) > 0 && baselineOverall >= 0 {
|
||||
fmt.Fprintln(stderr, "coverage-gate accepts either --baseline-profile or --baseline-overall, not both")
|
||||
return 2
|
||||
}
|
||||
var overall []coverageBlock
|
||||
@@ -115,6 +123,14 @@ func run(
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 2
|
||||
}
|
||||
if len(baselinePaths) > 0 {
|
||||
baseline, baselineErr := readProfiles(baselinePaths, modulePath)
|
||||
if baselineErr != nil {
|
||||
fmt.Fprintln(stderr, baselineErr)
|
||||
return 2
|
||||
}
|
||||
baselineOverall = coveragePercent(baseline)
|
||||
}
|
||||
}
|
||||
diff, err := readProfiles(diffPaths, modulePath)
|
||||
if err != nil {
|
||||
@@ -150,12 +166,12 @@ func run(
|
||||
if enforceOverall {
|
||||
mode = "required"
|
||||
}
|
||||
fmt.Fprintf(stdout, "overall coverage: %.1f%% (merge-base %.1f%%; tolerance %.1fpp; target %.1f%%; %s)\n", result.Overall, baselineOverall, overallTolerance, target, mode)
|
||||
fmt.Fprintf(stdout, "overall coverage: %.4f%% (merge-base %.4f%%; tolerance %.4fpp; target %.4f%%; %s)\n", result.Overall, baselineOverall, overallTolerance, target, mode)
|
||||
}
|
||||
if result.ChangedStatements == 0 {
|
||||
fmt.Fprintf(stdout, "changed code coverage: n/a (no changed executable statements; target %.1f%%)\n", target)
|
||||
fmt.Fprintf(stdout, "changed code coverage: n/a (no changed executable statements; target %.4f%%)\n", target)
|
||||
} else {
|
||||
fmt.Fprintf(stdout, "changed code coverage: %.1f%% (%d executable statements; target %.1f%%)\n", result.ChangedCoverage, result.ChangedStatements, target)
|
||||
fmt.Fprintf(stdout, "changed code coverage: %.4f%% (%d executable statements; target %.4f%%)\n", result.ChangedCoverage, result.ChangedStatements, target)
|
||||
}
|
||||
if len(result.Failures) > 0 {
|
||||
fmt.Fprintln(stderr, "coverage gate failed:")
|
||||
@@ -171,13 +187,11 @@ func evaluate(input gateInput) gateResult {
|
||||
result := gateResult{Failures: []string{}}
|
||||
if !input.ChangedOnly {
|
||||
result.Overall = coveragePercent(input.Overall)
|
||||
baselineRounded := roundOne(input.BaselineOverall)
|
||||
overallRounded := roundOne(result.Overall)
|
||||
if overallRounded+input.OverallTolerance+1e-9 < baselineRounded {
|
||||
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage regressed from %.1f%% to %.1f%%", baselineRounded, overallRounded))
|
||||
if result.Overall+input.OverallTolerance < input.BaselineOverall {
|
||||
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage regressed from %.4f%% to %.4f%%", input.BaselineOverall, result.Overall))
|
||||
}
|
||||
if input.EnforceOverall && overallRounded < input.Target {
|
||||
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage %.1f%% is below target %.1f%%", overallRounded, input.Target))
|
||||
if input.EnforceOverall && result.Overall < input.Target {
|
||||
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage %.4f%% is below target %.4f%%", result.Overall, input.Target))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,8 +220,8 @@ func evaluate(input gateInput) gateResult {
|
||||
result.ChangedStatements = total
|
||||
if total > 0 {
|
||||
result.ChangedCoverage = float64(covered) * 100 / float64(total)
|
||||
if result.ChangedCoverage+1e-9 < input.Target {
|
||||
result.Failures = append(result.Failures, fmt.Sprintf("changed code coverage %.1f%% is below target %.1f%%", result.ChangedCoverage, input.Target))
|
||||
if result.ChangedCoverage < input.Target {
|
||||
result.Failures = append(result.Failures, fmt.Sprintf("changed code coverage %.4f%% is below target %.4f%%", result.ChangedCoverage, input.Target))
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -253,11 +267,13 @@ func readProfiles(paths []string, modulePath string) ([]coverageBlock, error) {
|
||||
values = append(values, value)
|
||||
}
|
||||
blocks = append(blocks, coverageBlock{
|
||||
File: normalizeProfilePath(match[1], modulePath),
|
||||
StartLine: values[0],
|
||||
EndLine: values[2],
|
||||
Statements: values[4],
|
||||
Count: values[5],
|
||||
File: normalizeProfilePath(match[1], modulePath),
|
||||
StartLine: values[0],
|
||||
StartColumn: values[1],
|
||||
EndLine: values[2],
|
||||
EndColumn: values[3],
|
||||
Statements: values[4],
|
||||
Count: values[5],
|
||||
})
|
||||
}
|
||||
err = scanner.Err()
|
||||
@@ -402,7 +418,15 @@ func coveragePercent(blocks []coverageBlock) float64 {
|
||||
func mergeCoverageBlocks(blocks []coverageBlock) []coverageBlock {
|
||||
merged := make(map[string]coverageBlock, len(blocks))
|
||||
for _, block := range blocks {
|
||||
key := fmt.Sprintf("%s:%d:%d:%d", block.File, block.StartLine, block.EndLine, block.Statements)
|
||||
key := fmt.Sprintf(
|
||||
"%s:%d:%d:%d:%d:%d",
|
||||
block.File,
|
||||
block.StartLine,
|
||||
block.StartColumn,
|
||||
block.EndLine,
|
||||
block.EndColumn,
|
||||
block.Statements,
|
||||
)
|
||||
current, ok := merged[key]
|
||||
if !ok || block.Count > current.Count {
|
||||
merged[key] = block
|
||||
@@ -419,5 +443,3 @@ func mergeCoverageBlocks(blocks []coverageBlock) []coverageBlock {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func roundOne(value float64) float64 { return math.Round(value*10) / 10 }
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestRun(t *testing.T) {
|
||||
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
|
||||
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "overall coverage: 100.0%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
|
||||
if !strings.Contains(stdout.String(), "overall coverage: 100.0000%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0000%") {
|
||||
t.Fatalf("unexpected output %q", stdout.String())
|
||||
}
|
||||
|
||||
@@ -54,6 +54,115 @@ func TestRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDefaultsToFullChangedCodeCoverage(t *testing.T) {
|
||||
profile := filepath.Join(t.TempDir(), "coverage.out")
|
||||
partialBody := "mode: atomic\n" +
|
||||
"example.com/project/internal/a.go:10.1,12.2 9 1\n" +
|
||||
"example.com/project/internal/a.go:13.1,13.2 1 0\n"
|
||||
if err := os.WriteFile(profile, []byte(partialBody), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := []string{
|
||||
"--changed-only",
|
||||
"--diff-profile", profile,
|
||||
"--base-ref", "base",
|
||||
"--module", "example.com/project",
|
||||
}
|
||||
loader := func(string) (map[string][]lineRange, error) {
|
||||
return map[string][]lineRange{"internal/a.go": {{Start: 10, End: 13}}}, nil
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run(args, &stdout, &stderr, loader, nil); code != 1 {
|
||||
t.Fatalf("run code=%d, want 1; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "changed code coverage 90.0000% is below target 100.0000%") {
|
||||
t.Fatalf("default target was not enforced: %q", stderr.String())
|
||||
}
|
||||
|
||||
fullBody := "mode: atomic\n" +
|
||||
"example.com/project/internal/a.go:10.1,12.2 9 1\n" +
|
||||
"example.com/project/internal/a.go:13.1,13.2 1 1\n"
|
||||
if err := os.WriteFile(profile, []byte(fullBody), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
|
||||
t.Fatalf("100%% changed coverage code=%d; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEvaluatesBaselineProfileWithCandidateCoverageModel(t *testing.T) {
|
||||
profile := filepath.Join(t.TempDir(), "coverage.out")
|
||||
body := "mode: atomic\n" +
|
||||
"example.com/project/internal/a.go:10.1,12.2 5 0\n" +
|
||||
"example.com/project/internal/a.go:10.1,12.2 5 1\n" +
|
||||
"example.com/project/internal/a.go:20.1,22.2 5 0\n"
|
||||
if err := os.WriteFile(profile, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := []string{
|
||||
"--overall-profile", profile,
|
||||
"--baseline-profile", profile,
|
||||
"--diff-profile", profile,
|
||||
"--base-ref", "base",
|
||||
"--module", "example.com/project",
|
||||
}
|
||||
loader := func(string) (map[string][]lineRange, error) {
|
||||
return map[string][]lineRange{}, nil
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
|
||||
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "overall coverage: 50.0000% (merge-base 50.0000%") {
|
||||
t.Fatalf("baseline and candidate did not share one coverage model: %q", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFailsClosedWhenBaselineProfileIsMissing(t *testing.T) {
|
||||
profile := filepath.Join(t.TempDir(), "coverage.out")
|
||||
body := "mode: atomic\nexample.com/project/internal/a.go:10.1,12.2 5 1\n"
|
||||
if err := os.WriteFile(profile, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := []string{
|
||||
"--overall-profile", profile,
|
||||
"--baseline-profile", filepath.Join(t.TempDir(), "missing.out"),
|
||||
"--diff-profile", profile,
|
||||
"--base-ref", "base",
|
||||
"--module", "example.com/project",
|
||||
}
|
||||
loader := func(string) (map[string][]lineRange, error) {
|
||||
return map[string][]lineRange{}, nil
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run(args, &stdout, &stderr, loader, nil); code != 2 {
|
||||
t.Fatalf("missing baseline profile code=%d, want 2; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "open coverage profile") {
|
||||
t.Fatalf("missing baseline profile did not fail closed: %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRejectsConflictingBaselineSources(t *testing.T) {
|
||||
args := []string{
|
||||
"--overall-profile", "candidate.out",
|
||||
"--baseline-profile", "baseline.out",
|
||||
"--baseline-overall", "100",
|
||||
"--diff-profile", "candidate.out",
|
||||
"--base-ref", "base",
|
||||
"--module", "example.com/project",
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run(args, &stdout, &stderr, nil, nil); code != 2 {
|
||||
t.Fatalf("conflicting baseline sources code=%d, want 2; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "either --baseline-profile or --baseline-overall, not both") {
|
||||
t.Fatalf("conflicting baseline sources were not rejected clearly: %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRunUnionsRepeatedOverallProfiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
uncovered := filepath.Join(dir, "uncovered.out")
|
||||
@@ -81,7 +190,7 @@ func TestCrossPlatformCoverageRunUnionsRepeatedOverallProfiles(t *testing.T) {
|
||||
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
|
||||
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "overall coverage: 100.0%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
|
||||
if !strings.Contains(stdout.String(), "overall coverage: 100.0000%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0000%") {
|
||||
t.Fatalf("repeated profiles were not unioned: %q", stdout.String())
|
||||
}
|
||||
}
|
||||
@@ -113,7 +222,7 @@ func TestRunChangedOnlyWithBuildableScope(t *testing.T) {
|
||||
if code := run(args, &stdout, &stderr, loader, buildableLoader); code != 0 {
|
||||
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
if strings.Contains(stdout.String(), "overall coverage") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
|
||||
if strings.Contains(stdout.String(), "overall coverage") || !strings.Contains(stdout.String(), "changed code coverage: 100.0000%") {
|
||||
t.Fatalf("unexpected changed-only output %q", stdout.String())
|
||||
}
|
||||
|
||||
@@ -143,7 +252,11 @@ func TestReadProfiles(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(blocks) != 1 || blocks[0].File != "internal/a.go" || blocks[0].Statements != 3 {
|
||||
if len(blocks) != 1 ||
|
||||
blocks[0].File != "internal/a.go" ||
|
||||
blocks[0].StartColumn != 1 ||
|
||||
blocks[0].EndColumn != 2 ||
|
||||
blocks[0].Statements != 3 {
|
||||
t.Fatalf("blocks=%v", blocks)
|
||||
}
|
||||
if _, err := readProfiles([]string{filepath.Join(t.TempDir(), "missing")}, "example.com/project"); err == nil {
|
||||
@@ -275,6 +388,41 @@ func TestCrossPlatformCoverageUnionsDuplicateCrossPackageCoverageBlocks(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoverageKeepsDistinctBlocksOnTheSameLine(t *testing.T) {
|
||||
result := evaluate(gateInput{
|
||||
Diff: []coverageBlock{
|
||||
{
|
||||
File: "internal/a.go",
|
||||
StartLine: 10,
|
||||
StartColumn: 1,
|
||||
EndLine: 10,
|
||||
EndColumn: 5,
|
||||
Statements: 1,
|
||||
Count: 1,
|
||||
},
|
||||
{
|
||||
File: "internal/a.go",
|
||||
StartLine: 10,
|
||||
StartColumn: 6,
|
||||
EndLine: 10,
|
||||
EndColumn: 10,
|
||||
Statements: 1,
|
||||
Count: 0,
|
||||
},
|
||||
},
|
||||
Changed: map[string][]lineRange{"internal/a.go": {{Start: 10, End: 10}}},
|
||||
Target: 100,
|
||||
ChangedOnly: true,
|
||||
})
|
||||
if result.ChangedCoverage != 50 || result.ChangedStatements != 2 {
|
||||
t.Fatalf("same-line blocks were collapsed: %#v", result)
|
||||
}
|
||||
if len(result.Failures) != 1 ||
|
||||
!strings.Contains(result.Failures[0], "changed code coverage 50.0000% is below target 100.0000%") {
|
||||
t.Fatalf("same-line uncovered block did not fail the gate: %v", result.Failures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAllowsMeasurementTolerance(t *testing.T) {
|
||||
result := evaluate(gateInput{
|
||||
Overall: []coverageBlock{{Statements: 403, Count: 1}, {Statements: 597, Count: 0}},
|
||||
@@ -288,6 +436,52 @@ func TestEvaluateAllowsMeasurementTolerance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDefaultsToZeroOverallTolerance(t *testing.T) {
|
||||
result := evaluate(gateInput{
|
||||
Overall: []coverageBlock{{Statements: 403, Count: 1}, {Statements: 597, Count: 0}},
|
||||
Changed: map[string][]lineRange{},
|
||||
BaselineOverall: 40.4,
|
||||
OverallTolerance: 0,
|
||||
Target: 100,
|
||||
})
|
||||
if len(result.Failures) != 1 ||
|
||||
!strings.Contains(result.Failures[0], "overall coverage regressed from 40.4000% to 40.3000%") {
|
||||
t.Fatalf("zero-tolerance regression failures = %v", result.Failures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateRejectsRegressionHiddenByOneDecimalRounding(t *testing.T) {
|
||||
result := evaluate(gateInput{
|
||||
Overall: []coverageBlock{
|
||||
{Statements: 1999, Count: 1},
|
||||
{Statements: 1, Count: 0},
|
||||
},
|
||||
Changed: map[string][]lineRange{},
|
||||
BaselineOverall: 100,
|
||||
Target: 100,
|
||||
})
|
||||
if len(result.Failures) != 1 ||
|
||||
!strings.Contains(result.Failures[0], "overall coverage regressed from 100.0000% to 99.9500%") {
|
||||
t.Fatalf("sub-tenth regression failures = %v", result.Failures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateRejectsChangedCoverageBelow100WithoutRounding(t *testing.T) {
|
||||
result := evaluate(gateInput{
|
||||
Diff: []coverageBlock{
|
||||
{File: "internal/a.go", StartLine: 1, EndLine: 1999, Statements: 1999, Count: 1},
|
||||
{File: "internal/a.go", StartLine: 2000, EndLine: 2000, Statements: 1, Count: 0},
|
||||
},
|
||||
Changed: map[string][]lineRange{"internal/a.go": {{Start: 1, End: 2000}}},
|
||||
Target: 100,
|
||||
ChangedOnly: true,
|
||||
})
|
||||
if len(result.Failures) != 1 ||
|
||||
!strings.Contains(result.Failures[0], "changed code coverage 99.9500% is below target 100.0000%") {
|
||||
t.Fatalf("sub-100 changed-code failures = %v", result.Failures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateFailsClosed(t *testing.T) {
|
||||
result := evaluate(gateInput{
|
||||
Overall: []coverageBlock{
|
||||
|
||||
@@ -401,7 +401,7 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
||||
|
||||
admission := readWorkflow(".github/workflows/ci.yml")
|
||||
for _, want := range []string{
|
||||
"name: Code Admission — PR 合入门禁",
|
||||
"name: CI",
|
||||
"files.length === 1",
|
||||
"files[0].filename === 'CHANGELOG.md'",
|
||||
"files[0].status === 'modified'",
|
||||
@@ -417,6 +417,10 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
||||
"mode=--fast-path",
|
||||
`"$mode" "$PR_BASE_SHA" HEAD`,
|
||||
"needs.lint.outputs.platform_sensitive == 'true'",
|
||||
`COVERAGE_TARGET: "100"`,
|
||||
`COVERAGE_ENFORCE_OVERALL: "false"`,
|
||||
`COVERAGE_OVERALL_TOLERANCE: "0"`,
|
||||
`COVERAGE_ADDITIONAL_DIFF_PROFILE=coverage-shortcut.txt`,
|
||||
} {
|
||||
if !strings.Contains(admission, want) {
|
||||
t.Errorf("Code Admission workflow missing contract %q", want)
|
||||
@@ -451,6 +455,31 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
||||
t.Error("Code Admission must not suppress required contexts with paths-ignore")
|
||||
}
|
||||
|
||||
notification := readWorkflow(".github/workflows/notify-wukong.yml")
|
||||
if !strings.Contains(notification, "- CI") {
|
||||
t.Error("Wukong notification must follow the renamed CI workflow")
|
||||
}
|
||||
if strings.Contains(notification, "Code Admission — PR 合入门禁") {
|
||||
t.Error("Wukong notification still follows the retired workflow display name")
|
||||
}
|
||||
|
||||
coverageGate := readWorkflow("scripts/policy/check-coverage-gate.sh")
|
||||
if !strings.Contains(coverageGate, `TARGET="${COVERAGE_TARGET:-100}"`) {
|
||||
t.Error("coverage gate must default to 100% changed-code coverage")
|
||||
}
|
||||
if !strings.Contains(coverageGate, `OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0}"`) {
|
||||
t.Error("coverage gate must reject any reported overall regression")
|
||||
}
|
||||
if !strings.Contains(coverageGate, `--baseline-profile "$BASELINE_PROFILE"`) {
|
||||
t.Error("coverage gate must evaluate the merge-base profile with the candidate checker")
|
||||
}
|
||||
if strings.Contains(coverageGate, `--overall-profile "$ADDITIONAL_DIFF_PROFILE"`) {
|
||||
t.Error("supporting changed-code coverage must not inflate candidate overall coverage")
|
||||
}
|
||||
if strings.Contains(coverageGate, `go tool cover -func="$BASELINE_PROFILE"`) {
|
||||
t.Error("coverage baseline must not use a different coverage calculator")
|
||||
}
|
||||
|
||||
aiBehavior := readWorkflow(".github/workflows/ai-behavior-check.yml")
|
||||
for _, want := range []string{
|
||||
"name: Code Admission — AI Behavior",
|
||||
|
||||
Reference in New Issue
Block a user