Build selected targets with version injection
This commit is contained in:
377
build_test.go
Normal file
377
build_test.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package releasekit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
||||
)
|
||||
|
||||
func TestBuildCreatesSelectedTargetsAndInjectsVersion(t *testing.T) {
|
||||
repository := newBuildConsumer(t, `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 2 && os.Args[1] == "version" {
|
||||
fmt.Printf("fixture %s\n", buildinfo.Version())
|
||||
}
|
||||
}
|
||||
`)
|
||||
outputDir := filepath.Join(t.TempDir(), "artifacts")
|
||||
t.Setenv("GOOS", "windows")
|
||||
t.Setenv("GOARCH", "arm64")
|
||||
t.Setenv("GOFLAGS", "-race")
|
||||
var progress bytes.Buffer
|
||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
{OS: "linux", Arch: "arm64"},
|
||||
{OS: "darwin", Arch: "arm64"},
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
},
|
||||
},
|
||||
OutputDir: outputDir,
|
||||
Output: &progress,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v; partial result = %#v", err, result)
|
||||
}
|
||||
if result.CandidateCommit == "" || result.CandidateCommit != buildGit(t, repository, "rev-parse", "HEAD") {
|
||||
t.Fatalf("candidate commit = %q", result.CandidateCommit)
|
||||
}
|
||||
if result.Compiler == "" || len(result.Targets) != 3 || len(result.Artifacts) != 3 {
|
||||
t.Fatalf("build result = %#v", result)
|
||||
}
|
||||
wantNames := []string{
|
||||
"fixture-v1.2.3-linux-amd64",
|
||||
"fixture-v1.2.3-linux-arm64",
|
||||
"fixture-v1.2.3-darwin-arm64",
|
||||
}
|
||||
for index, artifact := range result.Artifacts {
|
||||
if artifact.Filename != wantNames[index] || artifact.Target != result.Targets[index] {
|
||||
t.Fatalf("artifact %d = %#v", index, artifact)
|
||||
}
|
||||
contents, err := os.ReadFile(artifact.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256(contents)
|
||||
if artifact.SHA256 != hex.EncodeToString(digest[:]) {
|
||||
t.Fatalf("artifact digest = %q, want %x", artifact.SHA256, digest)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(progress.String(), "checking fixture version output") {
|
||||
t.Fatalf("build progress = %q", progress.String())
|
||||
}
|
||||
for _, artifact := range result.Artifacts {
|
||||
if artifact.Target != (releasekit.Target{OS: runtime.GOOS, Arch: runtime.GOARCH}) {
|
||||
continue
|
||||
}
|
||||
command := exec.Command(artifact.Path, "version")
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil || string(output) != "fixture v1.2.3\n" {
|
||||
t.Fatalf("native version command output = %q, error = %v", output, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUsesGoHostDespiteAmbientTargetEnvironment(t *testing.T) {
|
||||
repository := newBuildConsumer(t, `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixture %s\n", buildinfo.Version()) }
|
||||
}
|
||||
`)
|
||||
t.Setenv("GOOS", "windows")
|
||||
t.Setenv("GOARCH", "arm64")
|
||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v2.3.4",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{{OS: "windows", Arch: "arm64"}},
|
||||
},
|
||||
OutputDir: filepath.Join(t.TempDir(), "artifacts"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v; partial result = %#v", err, result)
|
||||
}
|
||||
if len(result.Artifacts) != 1 || result.Artifacts[0].Filename != "fixture-v2.3.4-windows-arm64.exe" {
|
||||
t.Fatalf("build artifacts = %#v", result.Artifacts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWithNoTargetsDoesNotInvokeCompiler(t *testing.T) {
|
||||
repository := newBuildConsumer(t, `package main
|
||||
func main() {}
|
||||
`)
|
||||
outputDir := filepath.Join(t.TempDir(), "must-not-be-created")
|
||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
OutputDir: outputDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
if result.CandidateCommit == "" || result.Compiler != "" || len(result.Targets) != 0 || len(result.Artifacts) != 0 {
|
||||
t.Fatalf("empty build result = %#v", result)
|
||||
}
|
||||
if _, err := os.Stat(outputDir); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("empty build created output directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRejectsUnsafeNameAndPreservesExistingOutput(t *testing.T) {
|
||||
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||
outputDir := filepath.Join(t.TempDir(), "artifacts")
|
||||
if err := os.MkdirAll(outputDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ownedPath := filepath.Join(outputDir, "fixture-v1.2.3-linux-amd64")
|
||||
if err := os.WriteFile(ownedPath, []byte("keep this file"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{{OS: "linux", Arch: "amd64"}},
|
||||
},
|
||||
OutputDir: outputDir,
|
||||
}
|
||||
unsafeRequest := request
|
||||
unsafeRequest.Spec.BinaryName = "../unsafe"
|
||||
if result, err := releasekit.Build(context.Background(), unsafeRequest); err == nil || !strings.Contains(err.Error(), "unsafe binary name") {
|
||||
t.Fatalf("Build() result = %#v, error = %v; expected unsafe-name failure", result, err)
|
||||
}
|
||||
request.Spec.BinaryName = "fixture"
|
||||
if _, err := releasekit.Build(context.Background(), request); err == nil || !strings.Contains(err.Error(), "already exists") {
|
||||
t.Fatalf("Build() error = %v, want existing-output failure", err)
|
||||
}
|
||||
contents, err := os.ReadFile(ownedPath)
|
||||
if err != nil || string(contents) != "keep this file" {
|
||||
t.Fatalf("caller output = %q, error = %v", contents, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReturnsPartialArtifactsAfterTargetFailure(t *testing.T) {
|
||||
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{
|
||||
{OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
{OS: "invalid-os", Arch: "invalid-arch"},
|
||||
},
|
||||
},
|
||||
OutputDir: filepath.Join(t.TempDir(), "artifacts"),
|
||||
})
|
||||
if err == nil || len(result.Artifacts) != 1 {
|
||||
t.Fatalf("Build() result = %#v, error = %v; want one retained artifact", result, err)
|
||||
}
|
||||
if _, err := os.Stat(result.Artifacts[0].Path); err != nil {
|
||||
t.Fatalf("partial artifact was not retained: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReportsMissingMainPackage(t *testing.T) {
|
||||
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/missing",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||
},
|
||||
OutputDir: filepath.Join(t.TempDir(), "artifacts"),
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "cmd/missing") || len(result.Artifacts) != 0 {
|
||||
t.Fatalf("Build() result = %#v, error = %v; want missing-package failure", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReportsIncorrectVersionOutputAndRetainsBinary(t *testing.T) {
|
||||
repository := newBuildConsumer(t, `package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("wrong") } }
|
||||
`)
|
||||
outputDir := filepath.Join(t.TempDir(), "artifacts")
|
||||
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||
},
|
||||
OutputDir: outputDir,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "native version output") || len(result.Artifacts) != 1 {
|
||||
t.Fatalf("Build() result = %#v, error = %v; want retained-binary smoke failure", result, err)
|
||||
}
|
||||
if _, err := os.Stat(result.Artifacts[0].Path); err != nil {
|
||||
t.Fatalf("built artifact was not retained after smoke failure: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCancellationReturnsCompletedArtifacts(t *testing.T) {
|
||||
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||
realGo, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapperDir := t.TempDir()
|
||||
wrapper := filepath.Join(wrapperDir, "go")
|
||||
callCount := filepath.Join(wrapperDir, "build-count")
|
||||
blocked := filepath.Join(wrapperDir, "second-build-started")
|
||||
contents := "#!/bin/sh\n" +
|
||||
"for argument do\n" +
|
||||
" if [ \"$argument\" = build ]; then\n" +
|
||||
" count=0\n" +
|
||||
" if [ -f \"$RELEASEKIT_BUILD_COUNT\" ]; then count=$(cat \"$RELEASEKIT_BUILD_COUNT\"); fi\n" +
|
||||
" count=$((count + 1))\n" +
|
||||
" printf '%s\\n' \"$count\" > \"$RELEASEKIT_BUILD_COUNT\"\n" +
|
||||
" if [ \"$count\" -eq 2 ]; then : > \"$RELEASEKIT_BUILD_BLOCKED\"; exec sleep 30; fi\n" +
|
||||
" fi\n" +
|
||||
"done\n" +
|
||||
"exec \"$RELEASEKIT_BUILD_REAL_GO\" \"$@\"\n"
|
||||
if err := os.WriteFile(wrapper, []byte(contents), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", wrapperDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv("RELEASEKIT_BUILD_COUNT", callCount)
|
||||
t.Setenv("RELEASEKIT_BUILD_BLOCKED", blocked)
|
||||
t.Setenv("RELEASEKIT_BUILD_REAL_GO", realGo)
|
||||
secondTarget := releasekit.Target{OS: "darwin", Arch: "arm64"}
|
||||
if secondTarget == (releasekit.Target{OS: runtime.GOOS, Arch: runtime.GOARCH}) {
|
||||
secondTarget = releasekit.Target{OS: "linux", Arch: "amd64"}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct {
|
||||
result releasekit.BuildResult
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
result, err := releasekit.Build(ctx, releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}, secondTarget},
|
||||
},
|
||||
OutputDir: filepath.Join(t.TempDir(), "artifacts"),
|
||||
})
|
||||
done <- struct {
|
||||
result releasekit.BuildResult
|
||||
err error
|
||||
}{result: result, err: err}
|
||||
}()
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
deadline := time.NewTimer(30 * time.Second)
|
||||
defer deadline.Stop()
|
||||
for {
|
||||
if _, err := os.Stat(blocked); err == nil {
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
select {
|
||||
case outcome := <-done:
|
||||
cancel()
|
||||
t.Fatalf("Build() ended before cancellation: result=%#v error=%v", outcome.result, outcome.err)
|
||||
case <-deadline.C:
|
||||
cancel()
|
||||
outcome := <-done
|
||||
t.Fatalf("second build did not start: result=%#v error=%v", outcome.result, outcome.err)
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
outcome := <-done
|
||||
if !errors.Is(outcome.err, context.Canceled) || len(outcome.result.Artifacts) != 1 {
|
||||
t.Fatalf("canceled build result=%#v error=%v; want one retained artifact", outcome.result, outcome.err)
|
||||
}
|
||||
}
|
||||
|
||||
func newBuildConsumer(t *testing.T, mainSource string) string {
|
||||
t.Helper()
|
||||
repository, err := os.MkdirTemp("", "releasekit-build-consumer-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(repository) })
|
||||
root, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
module := fmt.Sprintf("module example.com/buildfixture\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", filepath.ToSlash(root))
|
||||
writeBuildFixture(t, repository, "go.mod", module)
|
||||
writeBuildFixture(t, repository, "cmd/app/main.go", mainSource)
|
||||
buildGit(t, repository, "init", "--initial-branch=main")
|
||||
buildGit(t, repository, "config", "user.name", "ReleaseKit Build Test")
|
||||
buildGit(t, repository, "config", "user.email", "releasekit-build@example.test")
|
||||
buildGit(t, repository, "add", "--all")
|
||||
buildGit(t, repository, "commit", "-m", "build fixture")
|
||||
return repository
|
||||
}
|
||||
|
||||
func writeBuildFixture(t *testing.T, directory, name, contents string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(directory, filepath.FromSlash(name))
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildGit(t *testing.T, directory string, args ...string) string {
|
||||
t.Helper()
|
||||
command := exec.Command("git", append([]string{"-C", directory}, args...)...)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, output)
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
Reference in New Issue
Block a user