Files
ReleaseKit/cmd/releasekit/main_test.go

286 lines
10 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli"
)
func TestVersionCommand(t *testing.T) {
var stdout, stderr bytes.Buffer
if status := run(context.Background(), []string{"version"}, &stdout, &stderr); status != 0 {
t.Fatalf("run() status = %d, want 0; stderr = %q", status, stderr.String())
}
if want := fmt.Sprintf("releasekit %s\n", buildinfo.Version()); stdout.String() != want {
t.Fatalf("version output = %q, want %q", stdout.String(), want)
}
if stderr.Len() != 0 {
t.Fatalf("version command wrote to stderr: %q", stderr.String())
}
}
func TestUnknownCommand(t *testing.T) {
var stdout, stderr bytes.Buffer
if status := run(context.Background(), []string{"unknown"}, &stdout, &stderr); status != 2 {
t.Fatalf("run() status = %d, want 2", status)
}
if stdout.Len() != 0 {
t.Fatalf("unknown command wrote to stdout: %q", stdout.String())
}
if want := "unknown command \"unknown\"\n" + cli.Usage + "\n"; stderr.String() != want {
t.Fatalf("usage output = %q, want %q", stderr.String(), want)
}
}
func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
repository := newCLIRepository(t)
profilePath := filepath.Join(t.TempDir(), "profile with spaces.json")
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
settings := cli.Settings{
Product: "Example Product",
Remote: "origin",
Checks: []cli.CheckCommand{{
Args: []string{executable, "-test.run=TestCandidateCheckHelper", "--"},
Env: map[string]string{
"RELEASEKIT_CHECK_HELPER": "1",
"RELEASEKIT_VERSION": "profile version",
"RELEASEKIT_CANDIDATE": "profile commit",
},
}},
}
contents, err := json.Marshal(settings)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(profilePath, contents, 0o600); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
args := []string{"check", "--repo", repository, "--config", profilePath, "v1.2.3"}
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
}
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
if want := "v1.2.3|" + commit; stderr.String() != want {
t.Fatalf("configured check received %q, want captured candidate %q", stderr.String(), want)
}
if want := fmt.Sprintf("local candidate v1.2.3 at %s is valid\n", commit); stdout.String() != want {
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
}
}
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
repository := newCLIRepository(t)
settings := cli.Settings{
Product: "Example Product",
Remote: "origin",
Checks: []cli.CheckCommand{{
Args: []string{"go", "test", "-mod=readonly", "-count=1", "./..."},
Env: map[string]string{
"GOTOOLCHAIN": "local",
"GOPROXY": "off",
"GOSUMDB": "off",
},
}},
}
contents, err := json.Marshal(settings)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repository, "releasekit.json"), contents, 0o600); err != nil {
t.Fatal(err)
}
cliGitRun(t, repository, "add", "releasekit.json")
cliGitRun(t, repository, "commit", "-m", "configure source release")
remote := filepath.Join(t.TempDir(), "origin.git")
cliGitRun(t, repository, "init", "--bare", remote)
cliGitRun(t, repository, "remote", "add", "origin", remote)
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
var stdout, stderr bytes.Buffer
args := []string{"publish", "--repo", repository, "v1.2.3"}
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
}
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
tagObject := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")
if want := fmt.Sprintf("published v1.2.3 at %s (tag object %s)\n", commit, tagObject); stdout.String() != want {
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
}
if !bytes.Contains(stderr.Bytes(), []byte("validating and publishing v1.2.3")) || !bytes.Contains(stderr.Bytes(), []byte("example.com/cli-candidate")) {
t.Fatalf("stderr does not show publication progress and validation: %q", stderr.String())
}
if got := cliGitOutput(t, remote, "rev-parse", "refs/tags/v1.2.3"); got != tagObject {
t.Fatalf("remote tag object = %s, want %s", got, tagObject)
}
}
func TestBuildCommandWritesValidatedReceiptForConfiguredArtifacts(t *testing.T) {
repository := newCLIBuildRepository(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()) }
}
`)
var stdout, stderr bytes.Buffer
args := []string{"build", "--repo", repository, "v1.2.3"}
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
}
receiptPath := filepath.Join(repository, "dist", cli.BuildReceiptName)
receipt, err := cli.LoadBuildReceipt(receiptPath)
if err != nil {
t.Fatal(err)
}
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
settings := cli.BuildSettings{
MainPackage: "./cmd/app",
BinaryName: "fixture",
Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)},
}
if !receipt.MatchesBuild(repository, commit, "v1.2.3", settings) || len(receipt.Artifacts) != 1 {
t.Fatalf("build receipt = %#v", receipt)
}
if !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].Filename)) || !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].SHA256)) {
t.Fatalf("stdout does not report receipt artifacts: %q", stdout.String())
}
if got := cliGitOutput(t, repository, "status", "--porcelain"); got != "" {
t.Fatalf("build output dirtied candidate: %s", got)
}
}
func TestBuildCommandLeavesNoReceiptAfterSmokeFailure(t *testing.T) {
repository := newCLIBuildRepository(t, `package main
import (
"fmt"
"os"
)
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("wrong") } }
`)
var stdout, stderr bytes.Buffer
args := []string{"build", "--repo", repository, "v1.2.3"}
if status := run(context.Background(), args, &stdout, &stderr); status != 1 {
t.Fatalf("run() status = %d; stdout=%q stderr=%q, want 1", status, stdout.String(), stderr.String())
}
if stdout.Len() != 0 || !bytes.Contains(stderr.Bytes(), []byte("native version output")) {
t.Fatalf("stdout=%q stderr=%q; want smoke failure without final output", stdout.String(), stderr.String())
}
if _, err := os.Stat(filepath.Join(repository, "dist", cli.BuildReceiptName)); !os.IsNotExist(err) {
t.Fatalf("partial build wrote receipt: %v", err)
}
filename := fmt.Sprintf("fixture-v1.2.3-%s-%s", runtime.GOOS, runtime.GOARCH)
if runtime.GOOS == "windows" {
filename += ".exe"
}
if _, err := os.Stat(filepath.Join(repository, "dist", filename)); err != nil {
t.Fatalf("completed artifact not reported or retained: %v; stderr=%q", err, stderr.String())
}
}
func TestCandidateCheckHelper(t *testing.T) {
if os.Getenv("RELEASEKIT_CHECK_HELPER") != "1" {
return
}
fmt.Fprintf(os.Stdout, "%s|%s", os.Getenv("RELEASEKIT_VERSION"), os.Getenv("RELEASEKIT_CANDIDATE"))
os.Exit(0)
}
func newCLIRepository(t *testing.T) string {
t.Helper()
repository := filepath.Join(t.TempDir(), "candidate repository")
if err := os.Mkdir(repository, 0o700); err != nil {
t.Fatal(err)
}
cliGitRun(t, repository, "init", "--initial-branch=main")
cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test")
cliGitRun(t, repository, "config", "user.email", "releasekit@example.test")
cliGitRun(t, repository, "config", "commit.gpgsign", "false")
write := func(name, value string) {
t.Helper()
path := filepath.Join(repository, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(value), 0o600); err != nil {
t.Fatal(err)
}
}
write("go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n")
write("candidate_test.go", "package candidate\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n")
write("docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nRelease note.\n")
cliGitRun(t, repository, "add", "--all")
cliGitRun(t, repository, "commit", "-m", "candidate")
return repository
}
func newCLIBuildRepository(t *testing.T, mainSource string) string {
t.Helper()
repository := newCLIRepository(t)
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
module := fmt.Sprintf("module example.com/cli-candidate\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))
write := func(name, contents string) {
t.Helper()
path := filepath.Join(repository, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
write("go.mod", module)
write("cmd/app/main.go", mainSource)
write("releasekit.json", fmt.Sprintf(`{"product":"Example Product","build":{"main_package":"./cmd/app","binary_name":"fixture","targets":[%q]}}`, fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)))
write(".gitignore", "dist/\n")
cliGitRun(t, repository, "add", "--all")
cliGitRun(t, repository, "commit", "-m", "configure build")
return repository
}
func cliGitRun(t *testing.T, repository string, args ...string) {
t.Helper()
commandArgs := append([]string{"git"}, args...)
command := exec.Command(commandArgs[0], commandArgs[1:]...)
command.Dir = repository
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("%v: %v\n%s", commandArgs, err, output)
}
}
func cliGitOutput(t *testing.T, repository string, args ...string) string {
t.Helper()
commandArgs := append([]string{"git"}, args...)
command := exec.Command(commandArgs[0], commandArgs[1:]...)
command.Dir = repository
output, err := command.Output()
if err != nil {
t.Fatalf("%v: %v", commandArgs, err)
}
return strings.TrimSpace(string(output))
}