Compile packages for selected targets
This commit is contained in:
204
internal/validation/compile_test.go
Normal file
204
internal/validation/compile_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunCompilesTargetsAfterTestAndVetInSelectionOrder(t *testing.T) {
|
||||
logPath := installGoShim(t, "", false)
|
||||
targets := []Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, {OS: "darwin", Arch: "arm64"}}
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{FullVet: true, CompileTargets: targets},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
var commands []string
|
||||
for _, command := range readGoShimLog(t, logPath) {
|
||||
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") || strings.HasPrefix(command, "build ") {
|
||||
commands = append(commands, command)
|
||||
}
|
||||
}
|
||||
if len(commands) != len(targets)+2 || !strings.HasPrefix(commands[0], "test ") || !strings.HasPrefix(commands[1], "vet ") {
|
||||
t.Fatalf("validation commands = %q, want test, vet, then one build per target", commands)
|
||||
}
|
||||
for index, target := range targets {
|
||||
command := commands[index+2]
|
||||
if !strings.HasPrefix(command, "build ") {
|
||||
t.Fatalf("command %d = %q, want package build", index+2, command)
|
||||
}
|
||||
for _, argument := range []string{"build", "-mod=readonly", "-o " + os.DevNull, "./..."} {
|
||||
if !strings.Contains(" "+command+" ", " "+argument+" ") {
|
||||
t.Errorf("build command %q does not contain %q", command, argument)
|
||||
}
|
||||
}
|
||||
if got, want := strings.Fields(command)[0], "build"; got != want {
|
||||
t.Errorf("command = %q, want %q", got, want)
|
||||
}
|
||||
if target.OS == "" || target.Arch == "" {
|
||||
t.Fatalf("test target is incomplete: %#v", target)
|
||||
}
|
||||
}
|
||||
|
||||
environments, err := os.ReadFile(os.Getenv("RELEASEKIT_GO_ENV_LOG"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := strings.Split(strings.TrimSpace(string(environments)), "\n")
|
||||
if len(got) != len(targets) {
|
||||
t.Fatalf("static build environments = %q, want %d entries", got, len(targets))
|
||||
}
|
||||
for index, target := range targets {
|
||||
if want := fmt.Sprintf("%s/%s|0", target.OS, target.Arch); got[index] != want {
|
||||
t.Errorf("static build environment %d = %q, want %q", index, got[index], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAttributesTargetCompileFailureAndStops(t *testing.T) {
|
||||
logPath := installGoShim(t, "darwin/arm64", false)
|
||||
targets := []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "windows", Arch: "amd64"}}
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{FullVet: true, CompileTargets: targets},
|
||||
})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != PackageCompile {
|
||||
t.Fatalf("Run() error = %v, want a package-compile failure", err)
|
||||
}
|
||||
if len(failure.Findings) != 1 || failure.Findings[0].Target != "darwin/arm64" {
|
||||
t.Fatalf("package compilation findings = %#v, want target darwin/arm64", failure.Findings)
|
||||
}
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
t.Fatalf("Run() error = %v, want the Go command failure preserved", err)
|
||||
}
|
||||
var commands []string
|
||||
for _, command := range readGoShimLog(t, logPath) {
|
||||
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") || strings.HasPrefix(command, "build ") {
|
||||
commands = append(commands, command)
|
||||
}
|
||||
}
|
||||
if len(commands) != 4 || !strings.HasPrefix(commands[0], "test ") || !strings.HasPrefix(commands[1], "vet ") || !strings.HasPrefix(commands[2], "build ") || !strings.HasPrefix(commands[3], "build ") {
|
||||
t.Fatalf("validation commands after target failure = %q, want test, vet, two builds", commands)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRejectsIncompleteCompileTargets(t *testing.T) {
|
||||
installGoShim(t, "", false)
|
||||
for _, target := range []Target{{OS: "linux"}, {Arch: "amd64"}, {OS: " ", Arch: "amd64"}, {OS: "linux/other", Arch: "amd64"}} {
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{CompileTargets: []Target{target}},
|
||||
})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != PackageCompile {
|
||||
t.Errorf("Run(%#v) error = %v, want a package-compile validation failure", target, err)
|
||||
continue
|
||||
}
|
||||
if len(failure.Findings) != 1 || failure.Findings[0].Target != target.OS+"/"+target.Arch {
|
||||
t.Errorf("Run(%#v) findings = %#v, want the incomplete target", target, failure.Findings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts(t *testing.T) {
|
||||
fixtures := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
}{
|
||||
{
|
||||
name: "source library",
|
||||
files: map[string]string{
|
||||
"library.go": "package fixture\n\nconst Value = 1\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single main",
|
||||
files: map[string]string{
|
||||
"library.go": "package fixture\n\nconst Value = 1\n",
|
||||
"cmd/app/main.go": "package main\n\nfunc main() {}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple mains with repeated basenames",
|
||||
files: map[string]string{
|
||||
"library.go": "package fixture\n\nconst Value = 1\n",
|
||||
"cmd/one/tool/main.go": "package main\n\nfunc main() {}\n",
|
||||
"cmd/two/tool/main.go": "package main\n\nfunc main() {}\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, fixture := range fixtures {
|
||||
t.Run(fixture.name, func(t *testing.T) {
|
||||
module := writeCompileFixture(t, fixture.files)
|
||||
before := readCompileFixture(t, module, fixture.files)
|
||||
var targets []Target
|
||||
if fixture.name == "source library" {
|
||||
targets = []Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, {OS: "darwin", Arch: "arm64"}}
|
||||
} else {
|
||||
targets = []Target{{OS: "linux", Arch: "amd64"}}
|
||||
}
|
||||
if err := Run(context.Background(), Request{
|
||||
RepositoryDir: module,
|
||||
Settings: Settings{CompileTargets: targets},
|
||||
}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
after := readCompileFixture(t, module, fixture.files)
|
||||
for path, contents := range before {
|
||||
if string(after[path]) != string(contents) {
|
||||
t.Errorf("candidate file %q changed during compilation", path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeCompileFixture(t *testing.T, files map[string]string) string {
|
||||
t.Helper()
|
||||
module := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte("module example.com/compilefixture\n\ngo 1.25.0\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for path, contents := range files {
|
||||
filename := filepath.Join(module, filepath.FromSlash(path))
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return module
|
||||
}
|
||||
|
||||
func readCompileFixture(t *testing.T, module string, files map[string]string) map[string][]byte {
|
||||
t.Helper()
|
||||
contents := make(map[string][]byte, len(files)+1)
|
||||
for _, path := range append([]string{"go.mod"}, fixturePaths(files)...) {
|
||||
data, err := os.ReadFile(filepath.Join(module, filepath.FromSlash(path)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents[path] = data
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
func fixturePaths(files map[string]string) []string {
|
||||
paths := make([]string, 0, len(files))
|
||||
for path := range files {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
@@ -13,6 +13,8 @@ const (
|
||||
GoTest Check = "go-test"
|
||||
// GoVet identifies the separate full vet pass.
|
||||
GoVet Check = "go-vet"
|
||||
// PackageCompile identifies selected-target package compilation.
|
||||
PackageCompile Check = "package-compile"
|
||||
)
|
||||
|
||||
// Finding describes a validation observation at an optional source location.
|
||||
|
||||
@@ -213,6 +213,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")
|
||||
settings := map[string]string{
|
||||
"GOHOSTOS": runtime.GOOS,
|
||||
"GOHOSTARCH": runtime.GOARCH,
|
||||
@@ -250,8 +251,15 @@ if [ "${1-}" = "vet" ] && [ "%s" = "vet" ]; then
|
||||
echo "Go vet failed" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "${1-}" = "build" ]; then
|
||||
printf '%%s/%%s|%%s\n' "${GOOS-}" "${GOARCH-}" "${CGO_ENABLED-}" >> "$RELEASEKIT_GO_ENV_LOG"
|
||||
if [ "%s" = "build" ] || [ "%s" = "${GOOS-}/${GOARCH-}" ]; then
|
||||
echo "Go build failed" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
%s
|
||||
`, failure, failure, failure, vetDelay)
|
||||
`, failure, failure, failure, failure, failure, vetDelay)
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||
@@ -272,6 +280,7 @@ exit 0
|
||||
}
|
||||
t.Setenv("PATH", path)
|
||||
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
||||
t.Setenv("RELEASEKIT_GO_ENV_LOG", envLogPath)
|
||||
return logPath
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,25 @@ package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
)
|
||||
|
||||
// Settings selects shared validation behavior.
|
||||
type Settings struct {
|
||||
Race bool
|
||||
FullVet bool
|
||||
Race bool
|
||||
FullVet bool
|
||||
CompileTargets []Target
|
||||
}
|
||||
|
||||
// Target selects one package-compilation target.
|
||||
type Target struct {
|
||||
OS string
|
||||
Arch string
|
||||
}
|
||||
|
||||
// Request identifies the captured source and output used by shared validation.
|
||||
@@ -30,6 +40,17 @@ func (request Request) ReadCandidateTree(ctx context.Context) (*CandidateTree, e
|
||||
|
||||
// Run executes shared validation for the captured candidate.
|
||||
func Run(ctx context.Context, request Request) error {
|
||||
for _, target := range request.Settings.CompileTargets {
|
||||
if strings.TrimSpace(target.OS) == "" || strings.TrimSpace(target.Arch) == "" || strings.Contains(target.OS, "/") || strings.Contains(target.Arch, "/") {
|
||||
return &Failure{
|
||||
Check: PackageCompile,
|
||||
Findings: []Finding{{
|
||||
Target: target.OS + "/" + target.Arch,
|
||||
Message: "target must be one complete GOOS/GOARCH pair",
|
||||
}},
|
||||
}
|
||||
}
|
||||
}
|
||||
tool, err := gotool.New(ctx, request.RepositoryDir)
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
@@ -52,5 +73,20 @@ func Run(ctx context.Context, request Request) error {
|
||||
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,
|
||||
"build", "-mod=readonly", "-o", os.DevNull, "./...")
|
||||
if err != nil {
|
||||
return &Failure{
|
||||
Check: PackageCompile,
|
||||
Findings: []Finding{{
|
||||
Target: fmt.Sprintf("%s/%s", target.OS, target.Arch),
|
||||
Message: "package compilation failed",
|
||||
}},
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user