Run validation in the selected module
This commit is contained in:
@@ -34,6 +34,7 @@ type Request struct {
|
||||
Spec Spec
|
||||
OutputDir string
|
||||
Output io.Writer
|
||||
ToolOwner *gotool.Owner
|
||||
}
|
||||
|
||||
type Artifact struct {
|
||||
@@ -118,7 +119,11 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
return result, fmt.Errorf("create build output directory %q: %w", result.OutputDir, err)
|
||||
}
|
||||
|
||||
goTool, err := gotool.New(ctx, repositoryDir)
|
||||
owner := request.ToolOwner
|
||||
if owner == nil {
|
||||
owner = gotool.NewOwner(repositoryDir)
|
||||
}
|
||||
goTool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("prepare Go toolchain: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,16 +7,17 @@ import (
|
||||
"os"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||
)
|
||||
|
||||
func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candidate.Candidate) error {
|
||||
func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candidate.Candidate, *gotool.Owner) error {
|
||||
if spec == nil || len(spec.Targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
selected := *spec
|
||||
selected.Targets = append([]Target(nil), spec.Targets...)
|
||||
return func(ctx context.Context, captured candidate.Candidate) error {
|
||||
return func(ctx context.Context, captured candidate.Candidate, owner *gotool.Owner) error {
|
||||
temporaryDir, err := tempdir.Outside(captured.RepositoryDir, "releasekit-candidate-build-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary candidate build output: %w", err)
|
||||
@@ -28,6 +29,7 @@ func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candi
|
||||
Spec: selected,
|
||||
OutputDir: temporaryDir,
|
||||
Output: output,
|
||||
ToolOwner: owner,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("build candidate: %w", err)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
@@ -37,8 +38,9 @@ type Request struct {
|
||||
Version string
|
||||
Validation validation.Settings
|
||||
AdditionalChecks func(context.Context, Candidate) error
|
||||
AfterValidate func(context.Context, Candidate) error
|
||||
AfterValidate func(context.Context, Candidate, *gotool.Owner) error
|
||||
Output io.Writer
|
||||
ToolOwner *gotool.Owner
|
||||
}
|
||||
|
||||
func Check(ctx context.Context, request Request) (Candidate, error) {
|
||||
@@ -46,12 +48,18 @@ func Check(ctx context.Context, request Request) (Candidate, error) {
|
||||
if err != nil {
|
||||
return captured, err
|
||||
}
|
||||
request.ToolOwner = gotool.NewOwner(captured.ModuleDir)
|
||||
if err := Validate(ctx, request, captured); err != nil {
|
||||
return captured, err
|
||||
}
|
||||
if request.AfterValidate != nil {
|
||||
if err := request.AfterValidate(ctx, captured); err != nil {
|
||||
return captured, fmt.Errorf("after candidate validation: %w", err)
|
||||
callbackErr := request.AfterValidate(ctx, captured, request.ToolOwner)
|
||||
refreshErr := request.ToolOwner.Refresh(ctx)
|
||||
if callbackErr != nil {
|
||||
return captured, fmt.Errorf("after candidate validation: %w", callbackErr)
|
||||
}
|
||||
if refreshErr != nil {
|
||||
return captured, fmt.Errorf("refresh Go settings after candidate callback: %w", refreshErr)
|
||||
}
|
||||
}
|
||||
if err := Recheck(ctx, captured, ""); err != nil {
|
||||
@@ -120,18 +128,28 @@ func Capture(ctx context.Context, request Request) (Candidate, error) {
|
||||
// Validate runs the selected offline Go suite, followed by AdditionalChecks
|
||||
// when supplied, for the captured candidate.
|
||||
func Validate(ctx context.Context, request Request, captured Candidate) error {
|
||||
owner := request.ToolOwner
|
||||
if owner == nil {
|
||||
owner = gotool.NewOwner(captured.ModuleDir)
|
||||
}
|
||||
if err := validation.Run(ctx, validation.Request{
|
||||
RepositoryDir: captured.RepositoryDir,
|
||||
ModuleDir: captured.ModuleDir,
|
||||
Commit: captured.Commit,
|
||||
Output: request.Output,
|
||||
Settings: request.Validation,
|
||||
ToolOwner: owner,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, err)
|
||||
}
|
||||
if request.AdditionalChecks != nil {
|
||||
if err := request.AdditionalChecks(ctx, captured); err != nil {
|
||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, err)
|
||||
callbackErr := request.AdditionalChecks(ctx, captured)
|
||||
refreshErr := owner.Refresh(ctx)
|
||||
if callbackErr != nil {
|
||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, callbackErr)
|
||||
}
|
||||
if refreshErr != nil {
|
||||
return fmt.Errorf("refresh Go settings after additional checks: %w", refreshErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -3,13 +3,17 @@ package candidate
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
@@ -159,7 +163,7 @@ func TestCheckStopsBeforeProductAndBuildCallbacksWhenGoFormatFails(t *testing.T)
|
||||
productCalls++
|
||||
return nil
|
||||
},
|
||||
AfterValidate: func(context.Context, Candidate) error {
|
||||
AfterValidate: func(context.Context, Candidate, *gotool.Owner) error {
|
||||
buildCalls++
|
||||
return nil
|
||||
},
|
||||
@@ -191,7 +195,7 @@ func TestCheckStopsBeforeProductAndBuildCallbacksWhenMarkdownBoundaryFails(t *te
|
||||
productCalls++
|
||||
return nil
|
||||
},
|
||||
AfterValidate: func(context.Context, Candidate) error {
|
||||
AfterValidate: func(context.Context, Candidate, *gotool.Owner) error {
|
||||
buildCalls++
|
||||
return nil
|
||||
},
|
||||
@@ -265,6 +269,125 @@ func TestCheckRunsDefaultOfflineGoValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRefreshesGoSettingsAfterAdditionalChecks(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
t.Setenv("GOENV", "off")
|
||||
t.Setenv("GOMODCACHE", filepath.Join(t.TempDir(), "initial-module-cache"))
|
||||
selectedCache := filepath.Join(t.TempDir(), "callback-module-cache")
|
||||
var observed string
|
||||
_, err := Check(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
Product: "Example Product",
|
||||
Version: testVersion,
|
||||
AdditionalChecks: func(context.Context, Candidate) error {
|
||||
t.Setenv("GOMODCACHE", selectedCache)
|
||||
return nil
|
||||
},
|
||||
AfterValidate: func(ctx context.Context, captured Candidate, owner *gotool.Owner) error {
|
||||
tool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := tool.Output(ctx, captured.ModuleDir, gotool.Native, gotool.Target{}, nil, "env", "GOMODCACHE")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
observed = strings.TrimSpace(string(output))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if observed != selectedCache {
|
||||
t.Fatalf("Go settings after additional checks = %q, want %q", observed, selectedCache)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSharesGoDiscoveryWithAfterValidateCallback(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("the controlled Go command fixture uses a POSIX shell")
|
||||
}
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
logPath := installCandidateGoShim(t)
|
||||
t.Setenv("GOENV", "off")
|
||||
_, err := Check(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
Product: "Example Product",
|
||||
Version: testVersion,
|
||||
AfterValidate: func(ctx context.Context, captured Candidate, owner *gotool.Owner) error {
|
||||
tool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tool.Output(ctx, captured.ModuleDir, gotool.Native, gotool.Target{}, nil, "version")
|
||||
return err
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
commands := strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||
var discoveries, suites, callbacks int
|
||||
for _, command := range commands {
|
||||
switch {
|
||||
case strings.HasPrefix(command, "env "):
|
||||
discoveries++
|
||||
case strings.HasPrefix(command, "test "):
|
||||
suites++
|
||||
case command == "version":
|
||||
callbacks++
|
||||
}
|
||||
}
|
||||
if discoveries != 2 || suites != 1 || callbacks != 1 {
|
||||
t.Fatalf("Go invocations = %q; discovery=%d suite=%d callback=%d, want 2, 1, 1", commands, discoveries, suites, callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
func installCandidateGoShim(t *testing.T) string {
|
||||
t.Helper()
|
||||
directory := t.TempDir()
|
||||
logPath := filepath.Join(directory, "go-commands")
|
||||
settings := map[string]string{
|
||||
"GOHOSTOS": runtime.GOOS, "GOHOSTARCH": runtime.GOARCH, "GOENV": "off",
|
||||
"CC": "", "CXX": "", "CGO_CFLAGS": "", "CGO_CPPFLAGS": "", "CGO_CXXFLAGS": "",
|
||||
"CGO_ENABLED": "1", "CGO_LDFLAGS": "", "GOCACHE": "", "GOCACHEPROG": "",
|
||||
"GOMODCACHE": "", "GOPATH": "", "GOROOT": "", "GOTMPDIR": "", "PKG_CONFIG": "",
|
||||
}
|
||||
encoded, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||
if [ "${1-}" = "env" ]; then
|
||||
printf '%%s\n' '%s'
|
||||
exit 0
|
||||
fi
|
||||
if [ "${1-}" = "version" ]; then
|
||||
echo 'go version fixture'
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
`, encoded)
|
||||
shimPath := filepath.Join(directory, "go")
|
||||
if err := os.WriteFile(shimPath, []byte(script), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := directory
|
||||
if current := os.Getenv("PATH"); current != "" {
|
||||
path += string(os.PathListSeparator) + current
|
||||
}
|
||||
t.Setenv("PATH", path)
|
||||
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
||||
return logPath
|
||||
}
|
||||
|
||||
func TestDefaultValidationDoesNotInheritGOFLAGSRunFilter(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
writeTestFile(t, repository, "failure_test.go", "package candidate\nimport \"testing\"\nfunc TestExpectedFailure(t *testing.T) { t.Fatal(\"expected fixture failure\") }\n")
|
||||
|
||||
@@ -34,8 +34,9 @@ type Target struct {
|
||||
// Tool holds the host target and effective operational settings resolved from
|
||||
// the process environment and Go's persistent environment file.
|
||||
type Tool struct {
|
||||
host Target
|
||||
settings map[string]string
|
||||
host Target
|
||||
settings map[string]string
|
||||
settingsFile string
|
||||
}
|
||||
|
||||
var operationalSettings = []string{
|
||||
@@ -53,10 +54,11 @@ var architectureFeatures = []string{
|
||||
// configuration from being read.
|
||||
func New(ctx context.Context, directory string) (*Tool, error) {
|
||||
var host struct {
|
||||
OS string `json:"GOHOSTOS"`
|
||||
Arch string `json:"GOHOSTARCH"`
|
||||
OS string `json:"GOHOSTOS"`
|
||||
Arch string `json:"GOHOSTARCH"`
|
||||
SettingsFile string `json:"GOENV"`
|
||||
}
|
||||
output, err := discover(ctx, directory, discoveryEnvironment(), "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH")
|
||||
output, err := discover(ctx, directory, discoveryEnvironment(), "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH", "GOENV")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identify Go host: %w", err)
|
||||
}
|
||||
@@ -66,7 +68,7 @@ func New(ctx context.Context, directory string) (*Tool, error) {
|
||||
if host.OS == "" || host.Arch == "" {
|
||||
return nil, fmt.Errorf("Go returned an incomplete host target: %s", strings.TrimSpace(string(output)))
|
||||
}
|
||||
tool := &Tool{host: Target{OS: host.OS, Arch: host.Arch}}
|
||||
tool := &Tool{host: Target{OS: host.OS, Arch: host.Arch}, settingsFile: host.SettingsFile}
|
||||
|
||||
environment := discoveryEnvironment()
|
||||
environment["GOOS"] = tool.host.OS
|
||||
|
||||
@@ -301,6 +301,72 @@ func TestPersistentSettingsPreserveCachesAndNativeCTools(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerReusesSettingsUntilInheritedEnvironmentChanges(t *testing.T) {
|
||||
clearGoSettings(t)
|
||||
t.Setenv("GOENV", "off")
|
||||
owner := NewOwner(t.TempDir())
|
||||
|
||||
first, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first != second {
|
||||
t.Fatal("unchanged Go inputs caused settings discovery to be repeated")
|
||||
}
|
||||
|
||||
cache := filepath.Join(t.TempDir(), "changed-cache")
|
||||
t.Setenv("GOCACHE", cache)
|
||||
if err := owner.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
third, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if third == first || third.settings["GOCACHE"] != cache {
|
||||
t.Fatalf("changed inherited environment kept stale settings: tool=%p GOCACHE=%q", third, third.settings["GOCACHE"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerRefreshesWhenPersistentSettingsChange(t *testing.T) {
|
||||
clearGoSettings(t)
|
||||
firstCache := filepath.Join(t.TempDir(), "first-cache")
|
||||
secondCache := filepath.Join(t.TempDir(), "second-cache")
|
||||
settingsFile := filepath.Join(t.TempDir(), "go-env")
|
||||
writeSettings := func(cache string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(settingsFile, []byte("GOCACHE="+cache+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writeSettings(firstCache)
|
||||
t.Setenv("GOENV", settingsFile)
|
||||
owner := NewOwner(t.TempDir())
|
||||
|
||||
first, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.settings["GOCACHE"] != firstCache {
|
||||
t.Fatalf("initial GOCACHE = %q, want %q", first.settings["GOCACHE"], firstCache)
|
||||
}
|
||||
writeSettings(secondCache)
|
||||
if err := owner.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second == first || second.settings["GOCACHE"] != secondCache {
|
||||
t.Fatalf("changed persistent settings were not rediscovered: tool=%p GOCACHE=%q", second, second.settings["GOCACHE"])
|
||||
}
|
||||
}
|
||||
|
||||
func readSettings(t *testing.T, tool *Tool, mode Mode, target Target, names ...string) map[string]string {
|
||||
t.Helper()
|
||||
args := append([]string{"env", "-json"}, names...)
|
||||
|
||||
136
internal/gotool/owner.go
Normal file
136
internal/gotool/owner.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package gotool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Owner lazily discovers and reuses one Go tool while its inherited
|
||||
// environment and persistent settings file remain unchanged.
|
||||
type Owner struct {
|
||||
directory string
|
||||
tool *Tool
|
||||
env map[string]string
|
||||
file string
|
||||
contents []byte
|
||||
fileFound bool
|
||||
}
|
||||
|
||||
// NewOwner creates an operation-local Go tool owner without running Go.
|
||||
func NewOwner(directory string) *Owner {
|
||||
return &Owner{directory: directory}
|
||||
}
|
||||
|
||||
// Acquire returns the operation's Go tool, rediscovering settings when their
|
||||
// process or persistent inputs have changed.
|
||||
func (owner *Owner) Acquire(ctx context.Context) (*Tool, error) {
|
||||
if owner.tool != nil {
|
||||
changed, err := owner.inputsChanged()
|
||||
if err == nil && !changed {
|
||||
return owner.tool, nil
|
||||
}
|
||||
}
|
||||
if err := owner.discover(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return owner.tool, nil
|
||||
}
|
||||
|
||||
// Refresh checks the owner's inputs after a consumer callback and rediscovers
|
||||
// settings when the callback changed the environment or persistent file.
|
||||
func (owner *Owner) Refresh(ctx context.Context) error {
|
||||
if owner.tool == nil {
|
||||
return nil
|
||||
}
|
||||
changed, err := owner.inputsChanged()
|
||||
if err == nil && !changed {
|
||||
return nil
|
||||
}
|
||||
return owner.discover(ctx)
|
||||
}
|
||||
|
||||
func (owner *Owner) discover(ctx context.Context) error {
|
||||
environment := inheritedEnvironment()
|
||||
tool, err := New(ctx, owner.directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settingsFile, err := resolveSettingsFile(owner.directory, tool.settingsFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contents, found, err := readSettingsFile(settingsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read Go settings file %q: %w", settingsFile, err)
|
||||
}
|
||||
owner.tool = tool
|
||||
owner.env = environment
|
||||
owner.file = settingsFile
|
||||
owner.contents = contents
|
||||
owner.fileFound = found
|
||||
return nil
|
||||
}
|
||||
|
||||
func (owner *Owner) inputsChanged() (bool, error) {
|
||||
if !sameEnvironment(owner.env, inheritedEnvironment()) {
|
||||
return true, nil
|
||||
}
|
||||
contents, found, err := readSettingsFile(owner.file)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return found != owner.fileFound || !bytes.Equal(contents, owner.contents), nil
|
||||
}
|
||||
|
||||
func inheritedEnvironment() map[string]string {
|
||||
values := make(map[string]string)
|
||||
for _, entry := range os.Environ() {
|
||||
key, value, ok := strings.Cut(entry, "=")
|
||||
if ok {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func sameEnvironment(left, right map[string]string) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for key, value := range left {
|
||||
other, ok := right[key]
|
||||
if !ok || other != value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func resolveSettingsFile(directory, filename string) (string, error) {
|
||||
if filename == "" || filename == "off" {
|
||||
return "", nil
|
||||
}
|
||||
if !filepath.IsAbs(filename) {
|
||||
filename = filepath.Join(directory, filename)
|
||||
}
|
||||
return filepath.Abs(filename)
|
||||
}
|
||||
|
||||
func readSettingsFile(filename string) ([]byte, bool, error) {
|
||||
if filename == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
contents, err := os.ReadFile(filename)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return contents, true, nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
@@ -49,7 +50,7 @@ type Request struct {
|
||||
ExpectedPushURL string
|
||||
Validation validation.Settings
|
||||
AdditionalChecks func(context.Context, candidate.Candidate) error
|
||||
AfterValidate func(context.Context, candidate.Candidate) error
|
||||
AfterValidate func(context.Context, candidate.Candidate, *gotool.Owner) error
|
||||
Output io.Writer
|
||||
}
|
||||
|
||||
@@ -120,12 +121,18 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
||||
}
|
||||
|
||||
result.Phase = Validation
|
||||
checks.ToolOwner = gotool.NewOwner(captured.ModuleDir)
|
||||
if err := candidate.Validate(ctx, checks, captured); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if request.AfterValidate != nil {
|
||||
if err := request.AfterValidate(ctx, captured); err != nil {
|
||||
return result, fmt.Errorf("after candidate validation: %w", err)
|
||||
callbackErr := request.AfterValidate(ctx, captured, checks.ToolOwner)
|
||||
refreshErr := checks.ToolOwner.Refresh(ctx)
|
||||
if callbackErr != nil {
|
||||
return result, fmt.Errorf("after candidate validation: %w", callbackErr)
|
||||
}
|
||||
if refreshErr != nil {
|
||||
return result, fmt.Errorf("refresh Go settings after candidate callback: %w", refreshErr)
|
||||
}
|
||||
}
|
||||
if err := candidate.Recheck(ctx, captured, ""); err != nil {
|
||||
|
||||
@@ -19,25 +19,38 @@ func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) {
|
||||
name string
|
||||
race bool
|
||||
fullVet bool
|
||||
compile []Target
|
||||
}{
|
||||
{name: "default"},
|
||||
{name: "race", race: true},
|
||||
{name: "full vet", fullVet: true},
|
||||
{name: "race and full vet", race: true, fullVet: true},
|
||||
{name: "target compilation", compile: []Target{{OS: "linux", Arch: "amd64"}}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
logPath := installGoShim(t, "", false)
|
||||
repositoryDir := t.TempDir()
|
||||
moduleDir := filepath.Join(repositoryDir, "nested module")
|
||||
if err := os.MkdirAll(moduleDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{Race: test.race, FullVet: test.fullVet},
|
||||
RepositoryDir: repositoryDir,
|
||||
ModuleDir: moduleDir,
|
||||
Settings: Settings{Race: test.race, FullVet: test.fullVet, CompileTargets: test.compile},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
commands := readGoShimLog(t, logPath)
|
||||
var tests, vets []string
|
||||
var testPosition, vetPosition int
|
||||
for _, directory := range readGoShimDirectories(t, os.Getenv("RELEASEKIT_GO_DIR_LOG")) {
|
||||
if directory != moduleDir {
|
||||
t.Errorf("Go command directory = %q, want selected module %q", directory, moduleDir)
|
||||
}
|
||||
}
|
||||
var tests, vets, builds []string
|
||||
var testPosition, vetPosition, buildPosition int
|
||||
for i, command := range commands {
|
||||
if strings.HasPrefix(command, "test ") {
|
||||
tests = append(tests, command)
|
||||
@@ -47,6 +60,10 @@ func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) {
|
||||
vets = append(vets, command)
|
||||
vetPosition = i
|
||||
}
|
||||
if strings.HasPrefix(command, "build ") {
|
||||
builds = append(builds, command)
|
||||
buildPosition = i
|
||||
}
|
||||
}
|
||||
if len(tests) != 1 {
|
||||
t.Fatalf("Go test invocations = %q, want exactly one", tests)
|
||||
@@ -81,6 +98,27 @@ func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) {
|
||||
t.Errorf("Go vet command %q did not follow the test suite %q", vets[0], tests[0])
|
||||
}
|
||||
}
|
||||
wantBuilds := 0
|
||||
if len(test.compile) != 0 {
|
||||
wantBuilds = len(test.compile)
|
||||
}
|
||||
if len(builds) != wantBuilds {
|
||||
t.Fatalf("Go build invocations = %q, want %d", builds, wantBuilds)
|
||||
}
|
||||
if wantBuilds != 0 {
|
||||
buildCommand := " " + builds[0] + " "
|
||||
for _, argument := range []string{"build", "-mod=readonly", "-o", os.DevNull, "./..."} {
|
||||
if !strings.Contains(buildCommand, " "+argument+" ") {
|
||||
t.Errorf("Go compile command %q does not contain %q", builds[0], argument)
|
||||
}
|
||||
}
|
||||
if buildPosition <= testPosition {
|
||||
t.Errorf("Go compile command %q did not follow the test suite %q", builds[0], tests[0])
|
||||
}
|
||||
if environments := readGoShimLog(t, os.Getenv("RELEASEKIT_GO_ENV_LOG")); len(environments) != 1 || environments[0] != "linux/amd64|0" {
|
||||
t.Errorf("Go compile baseline = %q, want linux/amd64|0", environments)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -111,6 +149,49 @@ func TestRunReportsRacePrerequisiteFailureWithoutFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunValidatesOnlyTheSelectedModule(t *testing.T) {
|
||||
t.Setenv("GOENV", "off")
|
||||
repositoryDir := t.TempDir()
|
||||
nestedDir := filepath.Join(repositoryDir, "nested")
|
||||
siblingDir := filepath.Join(repositoryDir, "sibling")
|
||||
for _, directory := range []string{nestedDir, siblingDir} {
|
||||
if err := os.MkdirAll(directory, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writeModule := func(directory, name, testSource string) {
|
||||
t.Helper()
|
||||
files := map[string]string{
|
||||
"go.mod": "module " + name + "\n\ngo 1.25.0\n",
|
||||
"module_test.go": testSource,
|
||||
}
|
||||
for filename, contents := range files {
|
||||
if err := os.WriteFile(filepath.Join(directory, filename), []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
passingTest := "package fixture\nimport \"testing\"\nfunc TestSelected(t *testing.T) {}\n"
|
||||
failingTest := "package fixture\nimport \"testing\"\nfunc TestSibling(t *testing.T) { t.Fatal(\"sibling suite ran\") }\n"
|
||||
writeModule(repositoryDir, "example.com/root", passingTest)
|
||||
writeModule(nestedDir, "example.com/nested", passingTest)
|
||||
writeModule(siblingDir, "example.com/sibling", failingTest)
|
||||
|
||||
if err := Run(context.Background(), Request{RepositoryDir: repositoryDir}); err != nil {
|
||||
t.Fatalf("root module validation: %v", err)
|
||||
}
|
||||
if err := Run(context.Background(), Request{RepositoryDir: repositoryDir, ModuleDir: nestedDir}); err != nil {
|
||||
t.Fatalf("nested module validation ran a sibling suite: %v", err)
|
||||
}
|
||||
|
||||
writeModule(nestedDir, "example.com/nested", failingTest)
|
||||
err := Run(context.Background(), Request{RepositoryDir: repositoryDir, ModuleDir: nestedDir})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != GoTest {
|
||||
t.Fatalf("failing nested module result = %v, want Go test failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStopsAfterGoTestFailure(t *testing.T) {
|
||||
logPath := installGoShim(t, "test", false)
|
||||
err := Run(context.Background(), Request{
|
||||
@@ -214,6 +295,7 @@ func installGoShim(t *testing.T, failure string, delayVet bool) string {
|
||||
directory := t.TempDir()
|
||||
logPath := filepath.Join(directory, "go-commands")
|
||||
envLogPath := filepath.Join(directory, "go-environment")
|
||||
directoryLogPath := filepath.Join(directory, "go-directories")
|
||||
settings := map[string]string{
|
||||
"GOHOSTOS": runtime.GOOS,
|
||||
"GOHOSTARCH": runtime.GOARCH,
|
||||
@@ -263,6 +345,7 @@ fi
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||
pwd -P >> "$RELEASEKIT_GO_DIR_LOG"
|
||||
if [ "${1-}" = "env" ]; then
|
||||
printf '%%s\n' '%s'
|
||||
exit 0
|
||||
@@ -281,6 +364,7 @@ exit 0
|
||||
t.Setenv("PATH", path)
|
||||
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
||||
t.Setenv("RELEASEKIT_GO_ENV_LOG", envLogPath)
|
||||
t.Setenv("RELEASEKIT_GO_DIR_LOG", directoryLogPath)
|
||||
return logPath
|
||||
}
|
||||
|
||||
@@ -293,6 +377,15 @@ func readGoShimLog(t *testing.T, path string) []string {
|
||||
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||
}
|
||||
|
||||
func readGoShimDirectories(t *testing.T, path string) []string {
|
||||
t.Helper()
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||
}
|
||||
|
||||
func raceSupported(goos, goarch string) bool {
|
||||
switch goos {
|
||||
case "darwin":
|
||||
|
||||
@@ -57,6 +57,7 @@ type Request struct {
|
||||
Commit string
|
||||
Output io.Writer
|
||||
Settings Settings
|
||||
ToolOwner *gotool.Owner
|
||||
}
|
||||
|
||||
// ReadCandidateTree loads the committed-file inventory when a selected check
|
||||
@@ -87,7 +88,15 @@ func Run(ctx context.Context, request Request) (result error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
tool, err := gotool.New(ctx, request.RepositoryDir)
|
||||
goDirectory := request.ModuleDir
|
||||
if goDirectory == "" {
|
||||
goDirectory = request.RepositoryDir
|
||||
}
|
||||
owner := request.ToolOwner
|
||||
if owner == nil {
|
||||
owner = gotool.NewOwner(goDirectory)
|
||||
}
|
||||
tool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
@@ -99,19 +108,19 @@ func Run(ctx context.Context, request Request) (result error) {
|
||||
args = append(args, "-vet=off")
|
||||
}
|
||||
args = append(args, "./...")
|
||||
err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, args...)
|
||||
err = tool.Run(ctx, goDirectory, gotool.Native, gotool.Target{}, request.Output, args...)
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
if request.Settings.FullVet {
|
||||
err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, "vet", "-mod=readonly", "./...")
|
||||
err = tool.Run(ctx, goDirectory, gotool.Native, gotool.Target{}, request.Output, "vet", "-mod=readonly", "./...")
|
||||
if err != nil {
|
||||
return &Failure{Check: GoVet, Cause: err}
|
||||
}
|
||||
}
|
||||
for _, target := range request.Settings.CompileTargets {
|
||||
selected := gotool.Target{OS: target.OS, Arch: target.Arch}
|
||||
err = tool.Run(ctx, request.RepositoryDir, gotool.Static, selected, request.Output,
|
||||
err = tool.Run(ctx, goDirectory, gotool.Static, selected, request.Output,
|
||||
"build", "-mod=readonly", "-o", os.DevNull, "./...")
|
||||
if err != nil {
|
||||
return &Failure{
|
||||
|
||||
Reference in New Issue
Block a user