228 lines
7.9 KiB
Go
228 lines
7.9 KiB
Go
package release_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const tool = "go.maximumdirect.net/releasekit/cmd/releasekit"
|
|
|
|
var modules = []struct{ selector, dir, profile, product string }{
|
|
{"core", ".", "releasekit.json", "CommandKit"},
|
|
{"ckconfig", "integrations/ckconfig", "integrations/ckconfig/releasekit.json", "CommandKit ckconfig"},
|
|
{"ckprompt", "integrations/ckprompt", "integrations/ckprompt/releasekit.json", "CommandKit ckprompt"},
|
|
}
|
|
|
|
type fixture struct{ root, outside, log string }
|
|
|
|
func newFixture(t *testing.T) fixture {
|
|
t.Helper()
|
|
base := t.TempDir()
|
|
f := fixture{filepath.Join(base, "repository with spaces"), filepath.Join(base, "outside"), filepath.Join(base, "calls")}
|
|
for _, path := range []string{"scripts/release", "scripts/releasekit", "releasekit.json", "integrations/ckconfig/releasekit.json", "integrations/ckprompt/releasekit.json"} {
|
|
data, err := os.ReadFile(filepath.Join("..", path))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dest := filepath.Join(f.root, path)
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(dest, data, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.MkdirAll(f.outside, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fake := `#!/bin/sh
|
|
printf 'called\n' >> "$CALLS"
|
|
case "$1 $2" in
|
|
'env GOHOSTOS') echo linux ;;
|
|
'env GOHOSTARCH') echo amd64 ;;
|
|
'env GOFLAGS') echo '-trimpath -mod=mod' ;;
|
|
*) printf '%s\n' "$@" > "$CALLS.args"
|
|
printf '%s\n' "$PWD" "$GOOS" "$GOARCH" "$GOWORK" "$GOTOOLCHAIN" "$GOFLAGS" > "$CALLS.env"
|
|
printf 'tool stdout\n'; printf 'tool stderr\n' >&2; exit "$TOOL_STATUS" ;;
|
|
esac
|
|
`
|
|
if err := os.WriteFile(filepath.Join(base, "go"), []byte(fake), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (f fixture) run(t *testing.T, script string, status int, args ...string) (string, string, int) {
|
|
t.Helper()
|
|
shell, err := exec.LookPath("sh")
|
|
if err != nil {
|
|
t.Skip("POSIX shell is unavailable")
|
|
}
|
|
cmd := exec.Command(shell, append([]string{filepath.Join(f.root, "scripts", script)}, args...)...)
|
|
cmd.Dir = f.outside
|
|
cmd.Env = append(os.Environ(), "PATH="+filepath.Dir(f.root)+string(os.PathListSeparator)+os.Getenv("PATH"), "CALLS="+f.log, fmt.Sprintf("TOOL_STATUS=%d", status), "CDPATH=/tmp", "GOOS=windows", "GOARCH=arm64", "GOWORK=auto", "GOTOOLCHAIN=auto", "GOFLAGS=-trimpath -mod=mod")
|
|
var stdout, stderr strings.Builder
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
err = cmd.Run()
|
|
if err == nil {
|
|
return stdout.String(), stderr.String(), 0
|
|
}
|
|
if e, ok := err.(*exec.ExitError); ok {
|
|
return stdout.String(), stderr.String(), e.ExitCode()
|
|
}
|
|
t.Fatal(err)
|
|
return "", "", -1
|
|
}
|
|
|
|
func lines(t *testing.T, path string) []string {
|
|
t.Helper()
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return strings.Split(strings.TrimSuffix(string(b), "\n"), "\n")
|
|
}
|
|
|
|
func TestProfiles(t *testing.T) {
|
|
f := newFixture(t)
|
|
for _, m := range modules {
|
|
t.Run(m.selector, func(t *testing.T) {
|
|
var p struct {
|
|
Product string `json:"product"`
|
|
Module string `json:"module_dir"`
|
|
Remote string `json:"remote"`
|
|
URL string `json:"expected_push_url"`
|
|
Validation struct {
|
|
Shared []string `json:"shared_files"`
|
|
Format bool `json:"go_format"`
|
|
Markdown map[string]any `json:"markdown"`
|
|
Distribution map[string]bool `json:"module_distribution"`
|
|
Forbidden []string `json:"forbidden_files"`
|
|
} `json:"validation"`
|
|
Delivery map[string]string `json:"delivery"`
|
|
}
|
|
b, err := os.ReadFile(filepath.Join(f.root, m.profile))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(b, &p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Product != m.product || p.Module != m.dir || p.Remote != "origin" || p.URL != "ssh://git@forge.maximumdirect.net:22222/base-layer/commandkit.git" {
|
|
t.Fatalf("profile identity: %+v", p)
|
|
}
|
|
if !reflect.DeepEqual(p.Validation.Shared, []string{"COPYRIGHT", "LICENSE", "docs/oss/attribution.md"}) || !p.Validation.Format || p.Validation.Markdown == nil || len(p.Validation.Markdown) != 0 || !reflect.DeepEqual(p.Validation.Distribution, map[string]bool{"workspace_files": true, "local_replacements": true}) || !reflect.DeepEqual(p.Validation.Forbidden, []string{".releasekit/gitea-token"}) {
|
|
t.Fatalf("validation: %+v", p.Validation)
|
|
}
|
|
if !reflect.DeepEqual(p.Delivery, map[string]string{"forge": "gitea", "base_url": "https://forge.maximumdirect.net", "owner": "base-layer", "repository": "commandkit", "token_file": ".releasekit/gitea-token"}) {
|
|
t.Fatalf("delivery: %v", p.Delivery)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModuleOperations(t *testing.T) {
|
|
for _, m := range modules {
|
|
for _, op := range []string{"publish", "check", "deliver"} {
|
|
t.Run(m.selector+"/"+op, func(t *testing.T) {
|
|
f := newFixture(t)
|
|
_, _, status := f.run(t, "releasekit", 0, m.selector, op, "v1.2.3", "--option", "value with spaces")
|
|
if status != 0 {
|
|
t.Fatalf("status %d", status)
|
|
}
|
|
want := []string{"tool", tool, op, "--repo", f.root, "--config", filepath.Join(f.root, m.profile)}
|
|
if op == "publish" {
|
|
want = append(want, "--expected-module", m.dir)
|
|
}
|
|
want = append(want, "v1.2.3", "--option", "value with spaces")
|
|
if got := lines(t, f.log+".args"); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("args = %#v, want %#v", got, want)
|
|
}
|
|
if got := lines(t, f.log+".env"); !reflect.DeepEqual(got, []string{f.root, "linux", "amd64", "off", "local", "-trimpath -mod=mod -mod=readonly"}) {
|
|
t.Fatalf("environment: %#v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRawAndLegacy(t *testing.T) {
|
|
for _, args := range [][]string{{"version", "--help"}, {"publication-attempts", "--json"}, {"reconcile", "--record", "/absolute/path with spaces/attempt.json"}} {
|
|
f := newFixture(t)
|
|
_, _, status := f.run(t, "releasekit", 0, args...)
|
|
if status != 0 {
|
|
t.Fatal(status)
|
|
}
|
|
want := append([]string{"tool", tool}, args...)
|
|
if got := lines(t, f.log+".args"); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("args = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
f := newFixture(t)
|
|
_, _, status := f.run(t, "release", 3, "ckprompt", "v1.2.3")
|
|
if status != 3 {
|
|
t.Fatal(status)
|
|
}
|
|
want := []string{"tool", tool, "publish", "--repo", f.root, "--config", filepath.Join(f.root, modules[2].profile), "--expected-module", modules[2].dir, "v1.2.3"}
|
|
if got := lines(t, f.log+".args"); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("args = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestStatusAndStreams(t *testing.T) {
|
|
for _, status := range []int{0, 1, 2, 3} {
|
|
t.Run(fmt.Sprint(status), func(t *testing.T) {
|
|
f := newFixture(t)
|
|
out, stderr, got := f.run(t, "releasekit", status, "core", "check", "v1.2.3")
|
|
if got != status || out != "tool stdout\n" || stderr != "tool stderr\n" {
|
|
t.Fatalf("status=%d stdout=%q stderr=%q", got, out, stderr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLauncherUsesShell(t *testing.T) {
|
|
f := newFixture(t)
|
|
if err := os.Chmod(filepath.Join(f.root, "scripts", "releasekit"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, status := f.run(t, "releasekit", 0, "version")
|
|
if status != 0 {
|
|
t.Fatal(status)
|
|
}
|
|
}
|
|
|
|
func TestInvalidForms(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
script string
|
|
args []string
|
|
}{
|
|
{"releasekit", nil},
|
|
{"releasekit", []string{"unknown"}},
|
|
{"releasekit", []string{"core"}},
|
|
{"releasekit", []string{"core", "version"}},
|
|
{"release", nil},
|
|
{"release", []string{"core"}},
|
|
{"release", []string{"core", "v1", "extra"}},
|
|
{"release", []string{"unknown", "v1"}},
|
|
{"release", []string{"version", "v1.2.3"}},
|
|
{"release", []string{"publication-attempts", "v1.2.3"}},
|
|
{"release", []string{"reconcile", "v1.2.3"}},
|
|
} {
|
|
f := newFixture(t)
|
|
_, stderr, status := f.run(t, tc.script, 0, tc.args...)
|
|
if status != 2 || !strings.Contains(stderr, "usage:") {
|
|
t.Fatalf("%s %v: status=%d stderr=%q", tc.script, tc.args, status, stderr)
|
|
}
|
|
if _, err := os.Stat(f.log); !os.IsNotExist(err) {
|
|
t.Fatalf("Go ran or log inaccessible: %v", err)
|
|
}
|
|
}
|
|
}
|