Files
ReleaseKit/build_test.go

651 lines
23 KiB
Go

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 TestBuildIgnoresAmbientAndPersistentArchitectureTuning(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()) } }
`)
for _, source := range []string{"process environment", "Go environment file"} {
t.Run(source, func(t *testing.T) {
unsetBuildEnv(t, "GOCACHE")
unsetBuildEnv(t, "GOAMD64")
unsetBuildEnv(t, "GOEXPERIMENT")
unsetBuildEnv(t, "GOFLAGS")
cache := filepath.Join(t.TempDir(), "configured-cache")
goEnv := filepath.Join(t.TempDir(), "go-env")
if source == "Go environment file" {
contents := fmt.Sprintf("GOAMD64=v2\nGOEXPERIMENT=loopvar\nGOFLAGS=-run=^$\nGOCACHE=%s\n", cache)
if err := os.WriteFile(goEnv, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
} else {
if err := os.WriteFile(goEnv, nil, 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("GOAMD64", "v2")
t.Setenv("GOEXPERIMENT", "loopvar")
t.Setenv("GOFLAGS", "-run=^$")
}
t.Setenv("GOENV", goEnv)
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: "linux", Arch: "amd64"}},
},
OutputDir: filepath.Join(t.TempDir(), "artifacts"),
})
if err != nil {
t.Fatalf("Build() error = %v; partial result = %#v", err, result)
}
metadata := goBuildMetadata(t, result.Artifacts[0].Path)
if !strings.Contains(metadata, "GOAMD64=v1") || strings.Contains(metadata, "GOAMD64=v2") || strings.Contains(metadata, "GOEXPERIMENT=loopvar") {
t.Fatalf("build metadata includes ambient tuning:\n%s", metadata)
}
if source == "Go environment file" {
entries, err := os.ReadDir(cache)
if err != nil || len(entries) == 0 {
t.Fatalf("configured persistent cache was not used: entries=%d error=%v", len(entries), err)
}
}
})
}
}
func goBuildMetadata(t *testing.T, binary string) string {
t.Helper()
t.Setenv("GOENV", "off")
t.Setenv("GOFLAGS", "")
t.Setenv("GOEXPERIMENT", "")
t.Setenv("GOAMD64", "")
t.Setenv("GOOS", "")
t.Setenv("GOARCH", "")
output, err := exec.Command("go", "version", "-m", binary).CombinedOutput()
if err != nil {
t.Fatalf("go version -m: %v\n%s", err, output)
}
return string(output)
}
func unsetBuildEnv(t *testing.T, key string) {
t.Helper()
value, exists := os.LookupEnv(key)
if err := os.Unsetenv(key); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if exists {
_ = os.Setenv(key, value)
} else {
_ = os.Unsetenv(key)
}
})
}
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 TestBuildRegeneratesOwnedOutputsAndPreservesUnrelatedFiles(t *testing.T) {
const source = `package main
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
func main(){fmt.Println("fixture",buildinfo.Version())}
`
repository := newBuildConsumer(t, source)
outputDir := t.TempDir()
unrelated := filepath.Join(outputDir, "keep.txt")
if err := os.WriteFile(unrelated, []byte("keep this file"), 0600); 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: runtime.GOOS, Arch: runtime.GOARCH}}}, OutputDir: outputDir}
unsafe := request
unsafe.Spec.BinaryName = "../unsafe"
if _, err := releasekit.Build(context.Background(), unsafe); err == nil {
t.Fatal("unsafe binary name accepted")
}
first, err := releasekit.Build(context.Background(), request)
if err != nil {
t.Fatal(err)
}
repeated, err := releasekit.Build(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if repeated.Artifacts[0].SHA256 != first.Artifacts[0].SHA256 {
t.Fatal("identical build changed output")
}
writeBuildFixture(t, repository, "cmd/app/main.go", source+"\nfunc init(){if len([]byte(buildinfo.Version()))==0 {panic(\"changed build\")}}\n")
changed, err := releasekit.Build(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if changed.Artifacts[0].SHA256 == first.Artifacts[0].SHA256 {
t.Fatal("changed source did not regenerate binary")
}
content, err := os.ReadFile(unrelated)
if err != nil || string(content) != "keep this file" {
t.Fatalf("unrelated file=%q %v", content, 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 TestCheckBuildsAfterValidationAndKeepsOutputOutsideWorktree(t *testing.T) {
repository := newPublishFixture(t)
prepareCandidateBuild(t, repository.work, `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()) } }
`)
var validations int
candidate, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
RepositoryDir: repository.work,
Product: "Sample App",
Version: "v1.2.3",
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
validations++
return nil
},
Build: &releasekit.BuildSpec{
MainPackage: "./cmd/app",
BinaryName: "fixture",
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
},
})
if err != nil {
t.Fatalf("Check() error = %v", err)
}
if validations != 1 || candidate.Commit != gitText(t, repository.work, "rev-parse", "HEAD") {
t.Fatalf("candidate = %#v, validation calls = %d", candidate, validations)
}
if got := gitText(t, repository.work, "status", "--porcelain"); got != "" {
t.Fatalf("candidate build dirtied worktree: %s", got)
}
if paths, err := filepath.Glob(filepath.Join(filepath.Dir(repository.work), ".releasekit-candidate-build-*")); err != nil || len(paths) != 0 {
t.Fatalf("temporary candidate output remains: %v, error=%v", paths, err)
}
}
func TestPublishBuildFailurePreventsTagCreation(t *testing.T) {
repository := newPublishFixture(t)
prepareCandidateBuild(t, repository.work, "package main\nfunc main() {}\n")
var validations int
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
RepositoryDir: repository.work,
Product: "Sample App",
Version: "v1.2.3",
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
validations++
return nil
},
Build: &releasekit.BuildSpec{
MainPackage: "./cmd/missing",
BinaryName: "fixture",
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
},
})
if err == nil || !strings.Contains(err.Error(), "build candidate") || result.TagObject != "" {
t.Fatalf("Publish() result=%#v error=%v; want build failure before tag creation", result, err)
}
if validations != 1 {
t.Fatalf("validation calls = %d, want 1", validations)
}
if _, err := exec.Command("git", "--git-dir", repository.bare, "show-ref", "--verify", "--quiet", "refs/tags/v1.2.3").CombinedOutput(); err == nil {
t.Fatal("build failure published the release tag")
}
if _, err := exec.Command("git", "-C", repository.work, "show-ref", "--verify", "--quiet", "refs/tags/v1.2.3").CombinedOutput(); err == nil {
t.Fatal("build failure created a local release tag")
}
}
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)
writeBuildFixture(t, repository, "cmd/app/main_test.go", "package main\nimport \"testing\"\nfunc TestCandidateFixture(t *testing.T) {}\n")
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 prepareCandidateBuild(t *testing.T, repository, mainSource string) {
t.Helper()
root, err := filepath.Abs(".")
if err != nil {
t.Fatal(err)
}
module := fmt.Sprintf("module example.com/candidatebuild\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))
writeFile(t, repository, "go.mod", module)
writeFile(t, repository, ".gitignore", "dist/\n")
writeFile(t, repository, "cmd/app/main.go", mainSource)
writeFile(t, repository, "cmd/app/main_test.go", "package main\nimport \"testing\"\nfunc TestCandidateFixture(t *testing.T) {}\n")
gitRun(t, repository, "add", "--all")
gitRun(t, repository, "commit", "-m", "add buildable candidate")
gitRun(t, repository, "config", "remote.origin.mirror", "false")
gitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
}
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))
}
func TestBuildSourceEligibilitySurvivesLaterCheckoutCleanup(t *testing.T) {
const source = `package main
import("fmt";"os";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
func main(){ if len(os.Args)>1 && os.Args[1]=="version" {fmt.Println("fixture",buildinfo.Version())} }
`
for _, mode := range []string{"clean", "dirty before build", "changed during build", "commit during build"} {
t.Run(mode, func(t *testing.T) {
repository := newBuildConsumer(t, source)
writeBuildFixture(t, repository, "docs/releases/v1.2.3.md", "# Fixture v1.2.3\n")
buildGit(t, repository, "add", "--all")
buildGit(t, repository, "commit", "-m", "release note")
buildGit(t, repository, "tag", "v1.2.3")
mutate := func() { writeBuildFixture(t, repository, "cmd/app/main.go", source+"\n// local change\n") }
if mode == "dirty before build" {
mutate()
}
var writer *sourceChangeWriter
request := 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: t.TempDir()}
if mode == "changed during build" || mode == "commit during build" {
writer = &sourceChangeWriter{change: func() {
mutate()
if mode == "commit during build" {
buildGit(t, repository, "add", "--all")
buildGit(t, repository, "commit", "-m", "source advanced during compilation")
}
}}
request.Output = writer
}
result, err := releasekit.Build(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if result.SourceVerified != (mode == "clean") {
t.Fatalf("source eligibility=%t for %s", result.SourceVerified, mode)
}
if mode == "clean" {
return
}
buildGit(t, repository, "restore", "cmd/app/main.go")
buildGit(t, repository, "checkout", "--detach", "v1.2.3")
delivered, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{RepositoryDir: repository, Product: "Fixture", Version: "v1.2.3", Build: &result})
if err == nil || !strings.Contains(err.Error(), "build source") || delivered.State != releasekit.DeliveryNotAttempted {
t.Fatalf("dirty source delivered: %#v %v", delivered, err)
}
})
}
}
type sourceChangeWriter struct{ change func() }
func (w *sourceChangeWriter) Write(p []byte) (int, error) {
if w.change != nil {
w.change()
w.change = nil
}
return len(p), nil
}
func TestCandidateBuildDoesNotRequireWritableRepositoryParent(t *testing.T) {
if runtime.GOOS == "windows" || os.Getuid() == 0 {
t.Skip("requires ordinary Unix permission checks")
}
repository := newBuildConsumer(t, `package main
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
func main(){fmt.Println("fixture",buildinfo.Version())}
`)
writeBuildFixture(t, repository, "docs/releases/v1.2.3.md", "# Fixture v1.2.3\n")
buildGit(t, repository, "add", "--all")
buildGit(t, repository, "commit", "-m", "note")
parent := t.TempDir()
moved := filepath.Join(parent, "repo")
if err := os.Rename(repository, moved); err != nil {
t.Fatal(err)
}
if err := os.Chmod(parent, 0555); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.Chmod(parent, 0700) })
_, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{RepositoryDir: moved, Product: "Fixture", Version: "v1.2.3", AdditionalChecks: func(context.Context, releasekit.Candidate) error { return nil }, Build: &releasekit.BuildSpec{MainPackage: "./cmd/app", BinaryName: "fixture", Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}}})
if err != nil {
t.Fatal(err)
}
if status := buildGit(t, moved, "status", "--porcelain"); status != "" {
t.Fatalf("candidate dirtied: %s", status)
}
}