The focused path tested every impacted package in a single job with a plain `go test -race`, so internal/app ran inside one long-lived process alongside all of its reverse dependencies. That is exactly the shape scripts/ci/run-app-race-tests.sh exists to avoid: a single app test process retains every constructed command tree in framework registries, so the run grows to 900s and the job stays alive long enough to be reclaimed by the runner. Recent focused runs failed with SIGTERM after 9-10 minutes without a single test failure, and one earlier run failed at `internal/app 902.651s`, 2.65s past the package timeout. Fan the same package plan across the shard matrix test-race already uses, and run each shard the way test-race runs it: internal/app through the process-isolating helper, cli/smoke with their wider package budget, release-scripts without race and with archive tooling. changed-test-packages.sh gains `list-shard`, which intersects the impacted set with scripts/ci/test-packages.sh shard membership so shard definitions stay single-sourced — and so an unknown shard name aborts there rather than reporting an empty selection, which would let a mistyped shard skip every test while reporting success. release-scripts is in the matrix on purpose: its dedicated job only runs at full-suite or release-sensitive scope, so omitting it here would stop testing test/scripts changes altogether. A test pins that the shard selections partition the impacted set exactly, so shard-plan drift cannot silently shrink focused coverage.
512 lines
16 KiB
Go
512 lines
16 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package scripts_test
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestChangedTestPackagesPlansProductionReverseDependencies(t *testing.T) {
|
|
repository := newChangedTestPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial fixture")
|
|
|
|
writeChangedTestPackagesFixture(t, repository, "base/base.go", `package base
|
|
|
|
func Value() string {
|
|
return "changed"
|
|
}
|
|
`)
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "change base")
|
|
|
|
changed := runChangedTestPackages(t, repository, "changed", baseRef, headRef)
|
|
if want := []string{"example.com/fixture/base"}; !slices.Equal(changed, want) {
|
|
t.Fatalf("changed packages = %q, want %q", changed, want)
|
|
}
|
|
|
|
impacted := runChangedTestPackages(t, repository, "list", baseRef, headRef)
|
|
want := []string{
|
|
"example.com/fixture/base",
|
|
"example.com/fixture/dependent",
|
|
"example.com/fixture/testconsumer",
|
|
}
|
|
if !slices.Equal(impacted, want) {
|
|
t.Fatalf("impacted packages = %q, want %q", impacted, want)
|
|
}
|
|
if slices.Contains(impacted, "example.com/fixture/unrelated") {
|
|
t.Fatalf("impacted packages unexpectedly contain unrelated package: %q", impacted)
|
|
}
|
|
}
|
|
|
|
func TestChangedTestPackagesMapsEmbeddedAssetsToTheirOwner(t *testing.T) {
|
|
repository := newChangedTestPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial fixture")
|
|
|
|
writeChangedTestPackagesFixture(
|
|
t,
|
|
repository,
|
|
"assetowner/assets/message.json",
|
|
"{\"message\":\"updated\"}\n",
|
|
)
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "update embedded asset")
|
|
|
|
want := []string{"example.com/fixture/assetowner"}
|
|
if changed := runChangedTestPackages(t, repository, "changed", baseRef, headRef); !slices.Equal(changed, want) {
|
|
t.Fatalf("changed packages = %q, want embedded owner %q", changed, want)
|
|
}
|
|
if impacted := runChangedTestPackages(t, repository, "list", baseRef, headRef); !slices.Equal(impacted, want) {
|
|
t.Fatalf("impacted packages = %q, want embedded owner %q", impacted, want)
|
|
}
|
|
}
|
|
|
|
func TestChangedTestPackagesIgnoresDocumentationOnlyDiff(t *testing.T) {
|
|
repository := newChangedTestPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial fixture")
|
|
|
|
writeChangedTestPackagesFixture(t, repository, "docs/guide.md", "# Updated guide\n")
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "update documentation")
|
|
|
|
for _, mode := range []string{"changed", "list"} {
|
|
if packages := runChangedTestPackages(t, repository, mode, baseRef, headRef); len(packages) != 0 {
|
|
t.Errorf("%s packages = %q, want no packages", mode, packages)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChangedTestPackagesRejectsMismatchedOrDirtyCheckout(t *testing.T) {
|
|
t.Run("mismatched head", func(t *testing.T) {
|
|
repository := newChangedTestPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial fixture")
|
|
writeChangedTestPackagesFixture(t, repository, "base/base.go", "package base\n\nconst Value = \"changed\"\n")
|
|
commitChangedTestPackagesFixture(t, repository, "change base")
|
|
|
|
output, err := runChangedTestPackagesFailure(repository, "changed", baseRef, baseRef)
|
|
if err == nil || !strings.Contains(output, "head does not match the checked-out revision") {
|
|
t.Fatalf("mismatched HEAD failure = %v, output = %q", err, output)
|
|
}
|
|
})
|
|
|
|
t.Run("dirty tracked file", func(t *testing.T) {
|
|
repository := newChangedTestPackagesRepository(t)
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "initial fixture")
|
|
writeChangedTestPackagesFixture(t, repository, "base/base.go", "package base\n\nconst Value = \"dirty\"\n")
|
|
|
|
output, err := runChangedTestPackagesFailure(repository, "changed", headRef, headRef)
|
|
if err == nil || !strings.Contains(output, "requires a clean tracked worktree") {
|
|
t.Fatalf("dirty worktree failure = %v, output = %q", err, output)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestChangedTestPackagesFailsClosedWhenPackageGraphIsInvalid(t *testing.T) {
|
|
repository := newChangedTestPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial fixture")
|
|
|
|
writeChangedTestPackagesFixture(t, repository, "dependent/dependent.go", `package dependent
|
|
|
|
import "example.com/fixture/missing"
|
|
|
|
func Value() string {
|
|
return missing.Value()
|
|
}
|
|
`)
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "break package graph")
|
|
|
|
script := filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh")
|
|
command := exec.Command(script, "list", baseRef, headRef)
|
|
command.Dir = repository
|
|
output, err := command.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("invalid package graph unexpectedly succeeded:\n%s", output)
|
|
}
|
|
if !strings.Contains(string(output), "failed to resolve the module package graph") {
|
|
t.Fatalf("invalid package graph failure = %q, want fail-closed diagnostic", output)
|
|
}
|
|
}
|
|
|
|
// shardChangedPackagesShards mirrors the shard names scripts/ci/test-packages.sh
|
|
// asserts complete coverage for in its verify mode. Keeping the list here in
|
|
// full is deliberate: the union assertion below fails if a shard is added
|
|
// upstream without being selected, and an unknown-shard error fails if one is
|
|
// removed, so shard-plan drift cannot silently shrink focused-test coverage.
|
|
var shardChangedPackagesShards = []string{
|
|
"app",
|
|
"generators",
|
|
"helpers",
|
|
"cli",
|
|
"smoke",
|
|
"remaining",
|
|
"release-scripts",
|
|
}
|
|
|
|
func TestChangedTestPackagesShardSelectionCoversEveryImpactedPackageExactlyOnce(t *testing.T) {
|
|
repository := newShardChangedPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture")
|
|
|
|
writeChangedTestPackagesFixture(t, repository, "core/core.go", `package core
|
|
|
|
func Value() string {
|
|
return "changed"
|
|
}
|
|
`)
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "change core")
|
|
|
|
impacted := runChangedTestPackages(t, repository, "list", baseRef, headRef)
|
|
if len(impacted) == 0 {
|
|
t.Fatal("shard fixture produced no impacted packages")
|
|
}
|
|
|
|
owner := map[string]string{}
|
|
union := make([]string, 0, len(impacted))
|
|
for _, shard := range shardChangedPackagesShards {
|
|
for _, pkg := range runChangedTestPackagesShard(t, repository, shard, baseRef, headRef) {
|
|
if previous, ok := owner[pkg]; ok {
|
|
t.Fatalf("package %q selected by both shard %q and %q", pkg, previous, shard)
|
|
}
|
|
owner[pkg] = shard
|
|
union = append(union, pkg)
|
|
}
|
|
}
|
|
|
|
slices.Sort(union)
|
|
if !slices.Equal(union, impacted) {
|
|
t.Fatalf("shard selection union = %q, want every impacted package %q", union, impacted)
|
|
}
|
|
}
|
|
|
|
func TestChangedTestPackagesShardSelectionFailsClosedOnUnknownShard(t *testing.T) {
|
|
repository := newShardChangedPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture")
|
|
|
|
writeChangedTestPackagesFixture(t, repository, "core/core.go", `package core
|
|
|
|
func Value() string {
|
|
return "changed"
|
|
}
|
|
`)
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "change core")
|
|
|
|
script := filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh")
|
|
command := exec.Command(script, "list-shard", "nonexistent", baseRef, headRef)
|
|
command.Dir = repository
|
|
output, err := command.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("unknown shard unexpectedly succeeded:\n%s", output)
|
|
}
|
|
// An unknown shard must abort rather than report an empty selection: a
|
|
// silently empty shard would let a mistyped CI shard name skip every test
|
|
// while still reporting success.
|
|
if !strings.Contains(string(output), "unknown test package shard") {
|
|
t.Fatalf("unknown shard failure = %q, want fail-closed diagnostic", output)
|
|
}
|
|
if strings.TrimSpace(string(output)) == "" {
|
|
t.Fatal("unknown shard produced empty output instead of a diagnostic")
|
|
}
|
|
}
|
|
|
|
func TestChangedTestPackagesShardSelectionIsEmptyForUnaffectedShard(t *testing.T) {
|
|
repository := newShardChangedPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture")
|
|
|
|
writeChangedTestPackagesFixture(t, repository, "internal/helpers/helpers.go", `package helpers
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " helpers changed"
|
|
}
|
|
`)
|
|
headRef := commitChangedTestPackagesFixture(t, repository, "change helpers only")
|
|
|
|
if selected := runChangedTestPackagesShard(t, repository, "helpers", baseRef, headRef); len(selected) == 0 {
|
|
t.Fatal("helpers shard selected nothing for a helpers-only change")
|
|
}
|
|
// An unaffected shard is a normal outcome and must exit zero with no
|
|
// output so the CI step can no-op instead of failing the job.
|
|
if selected := runChangedTestPackagesShard(t, repository, "app", baseRef, headRef); len(selected) != 0 {
|
|
t.Fatalf("app shard = %q, want empty for a helpers-only change", selected)
|
|
}
|
|
}
|
|
|
|
func TestChangedTestPackagesShardSelectionRejectsMissingShardArgument(t *testing.T) {
|
|
repository := newShardChangedPackagesRepository(t)
|
|
baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture")
|
|
|
|
script := filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh")
|
|
command := exec.Command(script, "list-shard", baseRef, baseRef)
|
|
command.Dir = repository
|
|
output, err := command.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("list-shard without a shard argument unexpectedly succeeded:\n%s", output)
|
|
}
|
|
if !strings.Contains(string(output), "usage:") {
|
|
t.Fatalf("missing shard argument failure = %q, want usage diagnostic", output)
|
|
}
|
|
}
|
|
|
|
func runChangedTestPackagesShard(t *testing.T, repository, shard, baseRef, headRef string) []string {
|
|
t.Helper()
|
|
|
|
output := runChangedTestPackagesCommand(
|
|
t,
|
|
repository,
|
|
filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh"),
|
|
"list-shard",
|
|
shard,
|
|
baseRef,
|
|
headRef,
|
|
)
|
|
return strings.Fields(output)
|
|
}
|
|
|
|
// newShardChangedPackagesRepository builds a fixture whose directory layout
|
|
// matches every shard scripts/ci/test-packages.sh knows about, so shard
|
|
// selection can be exercised without depending on the real repository's working
|
|
// tree state. Every package imports core, letting a single core edit mark the
|
|
// whole module impacted.
|
|
func newShardChangedPackagesRepository(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
repository := t.TempDir()
|
|
for path, contents := range map[string]string{
|
|
"go.mod": `module example.com/shardfixture
|
|
|
|
go 1.22
|
|
`,
|
|
"core/core.go": `package core
|
|
|
|
func Value() string {
|
|
return "core"
|
|
}
|
|
`,
|
|
"internal/app/app.go": `package app
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " app"
|
|
}
|
|
`,
|
|
"internal/generator/gen/gen.go": `package gen
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " gen"
|
|
}
|
|
`,
|
|
"internal/helpers/helpers.go": `package helpers
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " helpers"
|
|
}
|
|
`,
|
|
"internal/cli/cli.go": `package cli
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " cli"
|
|
}
|
|
`,
|
|
"test/smoke/smoke.go": `package smoke
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " smoke"
|
|
}
|
|
`,
|
|
"test/scripts/scripts.go": `package scripts
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " scripts"
|
|
}
|
|
`,
|
|
"pkg/extra/extra.go": `package extra
|
|
|
|
import "example.com/shardfixture/core"
|
|
|
|
func Value() string {
|
|
return core.Value() + " extra"
|
|
}
|
|
`,
|
|
} {
|
|
writeChangedTestPackagesFixture(t, repository, path, contents)
|
|
}
|
|
|
|
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("resolve project root: %v", err)
|
|
}
|
|
for _, name := range []string{"changed-test-packages.sh", "test-packages.sh"} {
|
|
script, err := os.ReadFile(filepath.Join(projectRoot, "scripts", "ci", name))
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", name, err)
|
|
}
|
|
scriptPath := filepath.Join(repository, "scripts", "ci", name)
|
|
if err := os.MkdirAll(filepath.Dir(scriptPath), 0o755); err != nil {
|
|
t.Fatalf("create script directory: %v", err)
|
|
}
|
|
if err := os.WriteFile(scriptPath, script, 0o755); err != nil {
|
|
t.Fatalf("copy %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
runChangedTestPackagesCommand(t, repository, "git", "init", "-q", "-b", "main")
|
|
runChangedTestPackagesCommand(t, repository, "git", "config", "user.name", "DWS CI")
|
|
runChangedTestPackagesCommand(t, repository, "git", "config", "user.email", "dws-ci@example.invalid")
|
|
return repository
|
|
}
|
|
|
|
func newChangedTestPackagesRepository(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
repository := t.TempDir()
|
|
for path, contents := range map[string]string{
|
|
"go.mod": `module example.com/fixture
|
|
|
|
go 1.22
|
|
`,
|
|
"base/base.go": `package base
|
|
|
|
func Value() string {
|
|
return "base"
|
|
}
|
|
`,
|
|
"dependent/dependent.go": `package dependent
|
|
|
|
import "example.com/fixture/base"
|
|
|
|
func Value() string {
|
|
return base.Value()
|
|
}
|
|
`,
|
|
"testconsumer/consumer.go": `package testconsumer
|
|
|
|
func Value() string {
|
|
return "consumer"
|
|
}
|
|
`,
|
|
"testconsumer/consumer_test.go": `package testconsumer
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"example.com/fixture/base"
|
|
)
|
|
|
|
func TestBaseValue(t *testing.T) {
|
|
if base.Value() == "" {
|
|
t.Fatal("base value is empty")
|
|
}
|
|
}
|
|
`,
|
|
"assetowner/asset.go": `package assetowner
|
|
|
|
import _ "embed"
|
|
|
|
//go:embed assets/message.json
|
|
var message []byte
|
|
|
|
func Message() []byte {
|
|
return message
|
|
}
|
|
`,
|
|
"assetowner/assets/message.json": "{\"message\":\"initial\"}\n",
|
|
"unrelated/unrelated.go": `package unrelated
|
|
|
|
func Value() string {
|
|
return "unrelated"
|
|
}
|
|
`,
|
|
"docs/guide.md": "# Guide\n",
|
|
} {
|
|
writeChangedTestPackagesFixture(t, repository, path, contents)
|
|
}
|
|
|
|
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("resolve project root: %v", err)
|
|
}
|
|
script, err := os.ReadFile(filepath.Join(projectRoot, "scripts", "ci", "changed-test-packages.sh"))
|
|
if err != nil {
|
|
t.Fatalf("read changed package planner: %v", err)
|
|
}
|
|
scriptPath := filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh")
|
|
if err := os.MkdirAll(filepath.Dir(scriptPath), 0o755); err != nil {
|
|
t.Fatalf("create script directory: %v", err)
|
|
}
|
|
if err := os.WriteFile(scriptPath, script, 0o755); err != nil {
|
|
t.Fatalf("copy changed package planner: %v", err)
|
|
}
|
|
|
|
runChangedTestPackagesCommand(t, repository, "git", "init", "-q", "-b", "main")
|
|
runChangedTestPackagesCommand(t, repository, "git", "config", "user.name", "DWS CI")
|
|
runChangedTestPackagesCommand(t, repository, "git", "config", "user.email", "dws-ci@example.invalid")
|
|
return repository
|
|
}
|
|
|
|
func writeChangedTestPackagesFixture(t *testing.T, repository, path, contents string) {
|
|
t.Helper()
|
|
|
|
fullPath := filepath.Join(repository, filepath.FromSlash(path))
|
|
if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
|
|
t.Fatalf("create directory for %s: %v", path, err)
|
|
}
|
|
if err := os.WriteFile(fullPath, []byte(contents), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func commitChangedTestPackagesFixture(t *testing.T, repository, message string) string {
|
|
t.Helper()
|
|
|
|
runChangedTestPackagesCommand(t, repository, "git", "add", ".")
|
|
runChangedTestPackagesCommand(t, repository, "git", "commit", "-q", "-m", message)
|
|
return strings.TrimSpace(runChangedTestPackagesCommand(t, repository, "git", "rev-parse", "HEAD"))
|
|
}
|
|
|
|
func runChangedTestPackages(t *testing.T, repository, mode, baseRef, headRef string) []string {
|
|
t.Helper()
|
|
|
|
output := runChangedTestPackagesCommand(
|
|
t,
|
|
repository,
|
|
filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh"),
|
|
mode,
|
|
baseRef,
|
|
headRef,
|
|
)
|
|
return strings.Fields(output)
|
|
}
|
|
|
|
func runChangedTestPackagesFailure(repository, mode, baseRef, headRef string) (string, error) {
|
|
script := filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh")
|
|
command := exec.Command(script, mode, baseRef, headRef)
|
|
command.Dir = repository
|
|
output, err := command.CombinedOutput()
|
|
return string(output), err
|
|
}
|
|
|
|
func runChangedTestPackagesCommand(t *testing.T, directory, name string, args ...string) string {
|
|
t.Helper()
|
|
|
|
command := exec.Command(name, args...)
|
|
command.Dir = directory
|
|
output, err := command.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("%s %s failed: %v\n%s", name, strings.Join(args, " "), err, output)
|
|
}
|
|
return string(output)
|
|
}
|