273 lines
8.6 KiB
Go
273 lines
8.6 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|