Test release launcher wiring
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Release tooling implementation plan
|
||||
|
||||
Status: Stages 1–2 complete; Stages 3–6 pending.
|
||||
Status: Stages 1–3 complete; Stages 4–6 pending.
|
||||
|
||||
Follow the stages in integer order. This is one end-to-end sprint: intermediate
|
||||
stages may temporarily leave scripts, tests, and documentation inconsistent.
|
||||
@@ -152,6 +152,9 @@ module mapping; publication and delivery remain explicitly separate commands.
|
||||
|
||||
## Stage 3: Replace release-test scaffolding with focused wiring tests
|
||||
|
||||
**Completed.** The focused scripts suite passes with the subprocess fake; the
|
||||
retired publication exercise and its scaffolding have been removed.
|
||||
|
||||
Rewrite `scripts/release_test.go` around the actual scripts and a small fake Go
|
||||
executable at the subprocess boundary. Remove
|
||||
`scripts/release_integration_test.go` and helpers with no remaining users.
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
package release_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReleaseIntegration(t *testing.T) {
|
||||
if os.Getenv("COMMANDKIT_RELEASE_INTEGRATION") != "1" {
|
||||
t.Skip("set COMMANDKIT_RELEASE_INTEGRATION=1 to run the local publication exercise; prepare Go module caches first")
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if _, err := exec.LookPath("sh"); err != nil {
|
||||
t.Skip("POSIX shell is unavailable")
|
||||
}
|
||||
}
|
||||
shell := requirePOSIXShell(t)
|
||||
git, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
t.Skip("Git is unavailable")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
fixture := newPublicationFixture(t, ctx, git)
|
||||
marker := filepath.Join(fixture.root, "suite marker")
|
||||
env := fixture.releaseEnv(marker)
|
||||
|
||||
candidate := gitRun(t, ctx, git, fixture.worktree, env, "rev-parse", "HEAD")
|
||||
stdout, stderr, status := runPublisher(t, ctx, shell, fixture, env, "core", "v0.0.1")
|
||||
if status != 0 {
|
||||
t.Fatalf("publish core v0.0.1 status = %d\nstdout:\n%s\nstderr:\n%s", status, stdout, stderr)
|
||||
}
|
||||
if !strings.Contains(stdout, "v0.0.1") || !strings.Contains(stdout, candidate) {
|
||||
t.Fatalf("publication output does not identify version and candidate %s:\n%s", candidate, stdout)
|
||||
}
|
||||
assertTag(t, ctx, git, fixture, "v0.0.1", candidate)
|
||||
assertTags(t, ctx, git, fixture, "v0.0.1")
|
||||
if got := readLines(t, marker); !equalStrings(got, []string{"candidate suite ran"}) {
|
||||
t.Fatalf("candidate suite marker = %#v, want one run", got)
|
||||
}
|
||||
if got := gitRun(t, ctx, git, fixture.worktree, env, "status", "--porcelain"); got != "" {
|
||||
t.Fatalf("candidate worktree is dirty after publication: %s", got)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fixture.worktree, "dist")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("unexpected binary delivery output at dist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fixture.worktree, ".releasekit", "bin", "releasekit")); err != nil {
|
||||
t.Fatalf("expected ignored bootstrap executable: %v", err)
|
||||
}
|
||||
|
||||
nestedCandidate := gitRun(t, ctx, git, fixture.worktree, env, "rev-parse", "HEAD")
|
||||
stdout, stderr, status = runPublisher(t, ctx, shell, fixture, env, "ckconfig", "v0.0.1")
|
||||
if status != 0 {
|
||||
t.Fatalf("publish ckconfig v0.0.1 status = %d\nstdout:\n%s\nstderr:\n%s", status, stdout, stderr)
|
||||
}
|
||||
nestedTag := "integrations/ckconfig/v0.0.1"
|
||||
if !strings.Contains(stdout, nestedTag) || !strings.Contains(stdout, nestedCandidate) {
|
||||
t.Fatalf("nested publication output does not identify tag %s and candidate %s:\n%s", nestedTag, nestedCandidate, stdout)
|
||||
}
|
||||
assertTag(t, ctx, git, fixture, nestedTag, nestedCandidate)
|
||||
assertTags(t, ctx, git, fixture, "v0.0.1", nestedTag)
|
||||
|
||||
fixture.prepareSecondCandidate(t, ctx, git)
|
||||
stdout, stderr, status = runPublisher(t, ctx, shell, fixture, env, "core", "v0.0.2")
|
||||
if status != 1 {
|
||||
t.Fatalf("publish with mismatched endpoint status = %d, want 1\nstdout:\n%s\nstderr:\n%s", status, stdout, stderr)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(stderr), "unexpected publication push url") {
|
||||
t.Fatalf("endpoint mismatch diagnostic not found in stderr:\n%s", stderr)
|
||||
}
|
||||
assertTags(t, ctx, git, fixture, "v0.0.1", nestedTag)
|
||||
if got := readLines(t, marker); !equalStrings(got, []string{"candidate suite ran"}) {
|
||||
t.Fatalf("candidate suite ran after endpoint preflight failure: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
type publicationFixture struct {
|
||||
root string
|
||||
worktree string
|
||||
bare string
|
||||
outside string
|
||||
profile string
|
||||
nestedProfile string
|
||||
gitConfig string
|
||||
gitEnv []string
|
||||
}
|
||||
|
||||
func newPublicationFixture(t *testing.T, ctx context.Context, git string) publicationFixture {
|
||||
t.Helper()
|
||||
_, sourceFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("could not locate test source")
|
||||
}
|
||||
commandkitRoot := filepath.Dir(filepath.Dir(sourceFile))
|
||||
root := t.TempDir()
|
||||
worktree := filepath.Join(root, "candidate worktree")
|
||||
bare := filepath.Join(root, "authoritative local.git")
|
||||
outside := filepath.Join(root, "outside publisher directory")
|
||||
configPath := filepath.Join(root, "isolated git config")
|
||||
hooks := filepath.Join(root, "empty hooks")
|
||||
for _, dir := range []string{worktree, outside, hooks} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writeFile(t, configPath, fmt.Sprintf(`[user]
|
||||
name = CommandKit Release Test
|
||||
email = release-test@example.invalid
|
||||
[commit]
|
||||
gpgsign = false
|
||||
[tag]
|
||||
gpgSign = false
|
||||
[core]
|
||||
hooksPath = %s
|
||||
`, hooks), 0o600)
|
||||
gitEnv := isolatedProcessEnv(map[string]string{
|
||||
"GIT_CONFIG_GLOBAL": configPath,
|
||||
"GIT_CONFIG_NOSYSTEM": "1",
|
||||
"GIT_TERMINAL_PROMPT": "0",
|
||||
}, gitOverrideKeys...)
|
||||
bareEnv := append([]string(nil), gitEnv...)
|
||||
gitRun(t, ctx, git, root, bareEnv, "init", "--bare", bare)
|
||||
gitRun(t, ctx, git, root, bareEnv, "--git-dir", bare, "symbolic-ref", "HEAD", "refs/heads/main")
|
||||
gitRun(t, ctx, git, worktree, gitEnv, "init")
|
||||
gitRun(t, ctx, git, worktree, gitEnv, "branch", "-M", "main")
|
||||
|
||||
for _, name := range []string{"go.mod", "go.sum", ".gitignore"} {
|
||||
copyFile(t, filepath.Join(commandkitRoot, name), filepath.Join(worktree, name), 0o644)
|
||||
}
|
||||
for _, name := range []string{"COPYRIGHT", "LICENSE", "docs/oss/attribution.md"} {
|
||||
target := filepath.Join(worktree, name)
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copyFile(t, filepath.Join(commandkitRoot, name), target, 0o644)
|
||||
}
|
||||
script := filepath.Join(worktree, "scripts", "release")
|
||||
if err := os.MkdirAll(filepath.Dir(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copyFile(t, filepath.Join(commandkitRoot, "scripts", "release"), script, 0o755)
|
||||
profile := filepath.Join(worktree, "releasekit.json")
|
||||
profileData, err := os.ReadFile(filepath.Join(commandkitRoot, "releasekit.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(profileData, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fields["expected_push_url"], err = json.Marshal(bare)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profileData, err = json.MarshalIndent(fields, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, profile, string(profileData)+"\n", 0o644)
|
||||
nestedModule := filepath.Join(worktree, "integrations", "ckconfig")
|
||||
if err := os.MkdirAll(nestedModule, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, filepath.Join(nestedModule, "go.mod"), "module gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig\n\ngo 1.26.0\n", 0o644)
|
||||
writeFile(t, filepath.Join(nestedModule, "fixture.go"), "package fixture\n\nfunc Ready() bool { return true }\n", 0o644)
|
||||
writeFile(t, filepath.Join(nestedModule, "fixture_test.go"), "package fixture\n\nimport \"testing\"\n\nfunc TestReady(t *testing.T) { if !Ready() { t.Fatal(\"not ready\") } }\n", 0o644)
|
||||
if err := os.MkdirAll(filepath.Join(nestedModule, "docs", "releases"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, filepath.Join(nestedModule, "docs", "releases", "v0.0.1.md"), releaseNoteFor("CommandKit ckconfig", "v0.0.1"), 0o644)
|
||||
nestedProfile := filepath.Join(nestedModule, "releasekit.json")
|
||||
nestedProfileData, err := os.ReadFile(filepath.Join(commandkitRoot, "integrations", "ckconfig", "releasekit.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var nestedFields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(nestedProfileData, &nestedFields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nestedFields["expected_push_url"], err = json.Marshal(bare)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nestedProfileData, err = json.MarshalIndent(nestedFields, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, nestedProfile, string(nestedProfileData)+"\n", 0o644)
|
||||
writeFile(t, filepath.Join(worktree, "fixture_test.go"), fixtureSuiteTest, 0o644)
|
||||
if err := os.MkdirAll(filepath.Join(worktree, "docs", "releases"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, filepath.Join(worktree, "docs", "releases", "v0.0.1.md"), releaseNote("v0.0.1"), 0o644)
|
||||
gitRun(t, ctx, git, worktree, gitEnv, "remote", "add", "origin", bare)
|
||||
gitRun(t, ctx, git, worktree, gitEnv, "add", "--all")
|
||||
gitRun(t, ctx, git, worktree, gitEnv, "commit", "-m", "Prepare local release candidate")
|
||||
gitRun(t, ctx, git, worktree, gitEnv, "push", "origin", "HEAD:refs/heads/main")
|
||||
|
||||
return publicationFixture{
|
||||
root: root,
|
||||
worktree: worktree,
|
||||
bare: bare,
|
||||
outside: outside,
|
||||
profile: profile,
|
||||
nestedProfile: nestedProfile,
|
||||
gitConfig: configPath,
|
||||
gitEnv: gitEnv,
|
||||
}
|
||||
}
|
||||
|
||||
func (fixture publicationFixture) releaseEnv(marker string) []string {
|
||||
values := map[string]string{
|
||||
"COMMANDKIT_FIXTURE_MARKER": marker,
|
||||
"GOWORK": "off",
|
||||
"GOTOOLCHAIN": "local",
|
||||
"GOPROXY": "off",
|
||||
"GONOPROXY": "none",
|
||||
"GOSUMDB": "off",
|
||||
"GIT_CONFIG_GLOBAL": fixture.gitConfig,
|
||||
"GIT_CONFIG_NOSYSTEM": "1",
|
||||
"GIT_TERMINAL_PROMPT": "0",
|
||||
}
|
||||
return isolatedProcessEnv(values, gitOverrideKeys...)
|
||||
}
|
||||
|
||||
func (fixture publicationFixture) prepareSecondCandidate(t *testing.T, ctx context.Context, git string) {
|
||||
t.Helper()
|
||||
writeFile(t, filepath.Join(fixture.worktree, "docs", "releases", "v0.0.2.md"), releaseNote("v0.0.2"), 0o644)
|
||||
profileData, err := os.ReadFile(fixture.profile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(profileData, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrongEndpoint := filepath.Join(fixture.root, "different local destination.git")
|
||||
fields["expected_push_url"], err = json.Marshal(wrongEndpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profileData, err = json.MarshalIndent(fields, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, fixture.profile, string(profileData)+"\n", 0o644)
|
||||
gitRun(t, ctx, git, fixture.worktree, fixture.gitEnv, "add", "--all")
|
||||
gitRun(t, ctx, git, fixture.worktree, fixture.gitEnv, "commit", "-m", "Prepare endpoint mismatch candidate")
|
||||
gitRun(t, ctx, git, fixture.worktree, fixture.gitEnv, "push", "origin", "HEAD:refs/heads/main")
|
||||
}
|
||||
|
||||
func runPublisher(t *testing.T, ctx context.Context, shell string, fixture publicationFixture, env []string, module, version string) (string, string, int) {
|
||||
t.Helper()
|
||||
cmd := exec.CommandContext(ctx, shell, filepath.Join(fixture.worktree, "scripts", "release"), module, version)
|
||||
cmd.Dir = fixture.outside
|
||||
cmd.Env = env
|
||||
var stdout, stderr strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
return stdout.String(), stderr.String(), 0
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return stdout.String(), stderr.String(), exitErr.ExitCode()
|
||||
}
|
||||
t.Fatalf("run release launcher: %v", err)
|
||||
return stdout.String(), stderr.String(), -1
|
||||
}
|
||||
|
||||
func assertTag(t *testing.T, ctx context.Context, git string, fixture publicationFixture, version, candidate string) {
|
||||
t.Helper()
|
||||
tagRef := "refs/tags/" + version
|
||||
tagType := gitRun(t, ctx, git, fixture.worktree, fixture.gitEnv, "cat-file", "-t", tagRef)
|
||||
if tagType != "tag" {
|
||||
t.Fatalf("tag type = %q, want annotated tag", tagType)
|
||||
}
|
||||
tagObject := gitRun(t, ctx, git, fixture.worktree, fixture.gitEnv, "rev-parse", tagRef)
|
||||
tagData := gitRun(t, ctx, git, fixture.worktree, fixture.gitEnv, "cat-file", "-p", tagRef)
|
||||
if !strings.HasPrefix(tagData, "object "+candidate+"\n") || !strings.Contains(tagData, "type commit\n") {
|
||||
t.Fatalf("annotated tag does not identify candidate %s:\n%s", candidate, tagData)
|
||||
}
|
||||
remoteTagObject := gitRun(t, ctx, git, fixture.root, fixture.gitEnv, "--git-dir", fixture.bare, "rev-parse", tagRef)
|
||||
if remoteTagObject != tagObject {
|
||||
t.Fatalf("remote tag object = %s, local tag object = %s", remoteTagObject, tagObject)
|
||||
}
|
||||
remoteMain := gitRun(t, ctx, git, fixture.root, fixture.gitEnv, "--git-dir", fixture.bare, "rev-parse", "refs/heads/main")
|
||||
if remoteMain != candidate {
|
||||
t.Fatalf("remote main = %s, want candidate %s", remoteMain, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTags(t *testing.T, ctx context.Context, git string, fixture publicationFixture, versions ...string) {
|
||||
t.Helper()
|
||||
local := gitRun(t, ctx, git, fixture.worktree, fixture.gitEnv, "for-each-ref", "--format=%(refname)", "refs/tags")
|
||||
remote := gitRun(t, ctx, git, fixture.root, fixture.gitEnv, "--git-dir", fixture.bare, "for-each-ref", "--format=%(refname)", "refs/tags")
|
||||
wantRefs := make([]string, len(versions))
|
||||
for i, version := range versions {
|
||||
wantRefs[i] = "refs/tags/" + version
|
||||
}
|
||||
sort.Strings(wantRefs)
|
||||
want := strings.Join(wantRefs, "\n")
|
||||
if local != want || remote != want {
|
||||
t.Fatalf("tag refs local=%q remote=%q, want only %q", local, remote, want)
|
||||
}
|
||||
}
|
||||
|
||||
func gitRun(t *testing.T, ctx context.Context, git, dir string, env []string, args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.CommandContext(ctx, git, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = env
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %s in %s: %v\n%s", strings.Join(args, " "), dir, err, output)
|
||||
}
|
||||
return strings.TrimSuffix(string(output), "\n")
|
||||
}
|
||||
|
||||
var gitOverrideKeys = []string{
|
||||
"GIT_DIR",
|
||||
"GIT_WORK_TREE",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_CEILING_DIRECTORIES",
|
||||
"GIT_PREFIX",
|
||||
"GIT_CONFIG",
|
||||
"GIT_CONFIG_COUNT",
|
||||
"GIT_CONFIG_PARAMETERS",
|
||||
}
|
||||
|
||||
func isolatedProcessEnv(overrides map[string]string, cleared ...string) []string {
|
||||
blocked := make(map[string]struct{}, len(overrides)+len(cleared))
|
||||
for key := range overrides {
|
||||
blocked[key] = struct{}{}
|
||||
}
|
||||
for _, key := range cleared {
|
||||
blocked[key] = struct{}{}
|
||||
}
|
||||
var env []string
|
||||
for _, entry := range os.Environ() {
|
||||
key, _, _ := strings.Cut(entry, "=")
|
||||
if _, skip := blocked[key]; !skip {
|
||||
env = append(env, entry)
|
||||
}
|
||||
}
|
||||
for key, value := range overrides {
|
||||
env = append(env, key+"="+value)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func releaseNote(version string) string {
|
||||
return releaseNoteFor("CommandKit", version)
|
||||
}
|
||||
|
||||
func releaseNoteFor(product, version string) string {
|
||||
return "# " + product + " " + version + "\n\nLocal fixture release candidate.\n"
|
||||
}
|
||||
|
||||
const fixtureSuiteTest = `package fixture
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCandidateSuiteMarker(t *testing.T) {
|
||||
marker := os.Getenv("COMMANDKIT_FIXTURE_MARKER")
|
||||
if marker == "" {
|
||||
t.Fatal("COMMANDKIT_FIXTURE_MARKER is empty")
|
||||
}
|
||||
file, err := os.OpenFile(marker, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := file.WriteString("candidate suite ran\n"); err != nil {
|
||||
file.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,506 +1,200 @@
|
||||
package release_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
releasePackage = "go.maximumdirect.net/releasekit/cmd/releasekit"
|
||||
releaseVersion = "v1.2.3-rc.1+local"
|
||||
)
|
||||
const tool = "go.maximumdirect.net/releasekit/cmd/releasekit"
|
||||
|
||||
func TestReleaseLauncherArgumentHandling(t *testing.T) {
|
||||
shell := requirePOSIXShell(t)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{name: "no arguments"},
|
||||
{name: "one argument", args: []string{"core"}},
|
||||
{name: "multiple arguments", args: []string{"core", "v1.2.3", "extra"}},
|
||||
{name: "unknown module", args: []string{"unknown", "v1.2.3"}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fixture := newFixture(t)
|
||||
result := runLauncher(t, shell, fixture, test.args, nil)
|
||||
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"},
|
||||
}
|
||||
|
||||
if result.status != 2 {
|
||||
t.Fatalf("status = %d, want 2; stderr: %q", result.status, result.stderr)
|
||||
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()
|
||||
cmd := exec.Command(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"`
|
||||
}
|
||||
if result.stdout != "" {
|
||||
t.Fatalf("stdout = %q, want empty", result.stdout)
|
||||
b, err := os.ReadFile(filepath.Join(f.root, m.profile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.stderr != "usage: ./scripts/release MODULE VERSION\n" {
|
||||
t.Fatalf("stderr = %q, want usage", result.stderr)
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(fixture.goLog); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("Go invocation log exists or could not be checked: %v", 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 TestReleaseLauncherDelegatesPublication(t *testing.T) {
|
||||
shell := requirePOSIXShell(t)
|
||||
for _, module := range []string{"core", "ckconfig", "ckprompt"} {
|
||||
for _, status := range []int{0, 1, 2, 3} {
|
||||
t.Run(fmt.Sprintf("%s exit status %d", module, status), func(t *testing.T) {
|
||||
fixture := newFixture(t)
|
||||
result := runLauncher(t, shell, fixture, []string{module, releaseVersion}, map[string]string{
|
||||
"CDPATH": "/tmp",
|
||||
"GOOS": "windows",
|
||||
"GOARCH": "arm64",
|
||||
"GOWORK": "auto",
|
||||
"GOTOOLCHAIN": "auto",
|
||||
"PUBLISH_EXIT": strconv.Itoa(status),
|
||||
"PUBLISH_STDOUT": "publisher stdout",
|
||||
"PUBLISH_STDERR": "publisher stderr",
|
||||
})
|
||||
|
||||
if result.status != status {
|
||||
t.Fatalf("status = %d, want %d; stderr: %q", result.status, status, result.stderr)
|
||||
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)
|
||||
}
|
||||
if result.stdout != "publisher stdout\n" {
|
||||
t.Fatalf("stdout = %q, want publisher output", result.stdout)
|
||||
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)
|
||||
}
|
||||
if result.stderr != "publisher stderr\n" {
|
||||
t.Fatalf("stderr = %q, want publisher error output", result.stderr)
|
||||
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 err := checkPublication(readLines(t, fixture.publishLog), fixture.repoRoot, module); err != nil {
|
||||
t.Fatal(err)
|
||||
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)
|
||||
}
|
||||
if got := readLines(t, fixture.publishEnvLog); !equalStrings(got, []string{"GOWORK=off", "GOTOOLCHAIN=local"}) {
|
||||
t.Fatalf("publisher environment = %#v, want workspace disabled and local toolchain", got)
|
||||
}
|
||||
assertNativeBootstrap(t, fixture)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLauncherChecksRejectContractViolations(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "repository with spaces")
|
||||
output := filepath.Join(root, ".releasekit", "bin", "releasekit")
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
change func(*goCall)
|
||||
}{
|
||||
{"wrong target", func(c *goCall) { c.goarch = "wrong" }},
|
||||
{"workspace enabled", func(c *goCall) { c.gowork = "auto" }},
|
||||
{"toolchain download", func(c *goCall) { c.toolchain = "auto" }},
|
||||
{"wrong directory", func(c *goCall) { c.cwd = filepath.Dir(root) }},
|
||||
{"missing readonly", func(c *goCall) { c.args = []string{"build", "-o", output, releasePackage} }},
|
||||
{"wrong output", func(c *goCall) { c.args[3] = filepath.Join(root, "wrong") }},
|
||||
{"independent package pin", func(c *goCall) { c.args[4] += "@v0.2.0" }},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
call := goCall{cwd: root, goos: "linux", goarch: "amd64", gowork: "off", toolchain: "local",
|
||||
args: []string{"build", "-mod=readonly", "-o", output, releasePackage}}
|
||||
test.change(&call)
|
||||
if err := checkNativeBootstrap([]goCall{call}, root, "linux", "amd64"); err == nil {
|
||||
t.Fatal("invalid bootstrap accepted")
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, args := range [][]string{
|
||||
{"check", "--repo", root, "--config", filepath.Join(root, "releasekit.json"), "--expected-module", ".", releaseVersion},
|
||||
{"publish", "--repo", "wrong", "--config", filepath.Join(root, "releasekit.json"), "--expected-module", ".", releaseVersion},
|
||||
{"publish", "--repo", root, "--config", "wrong", "--expected-module", ".", releaseVersion},
|
||||
{"publish", "--repo", root, "--config", filepath.Join(root, "releasekit.json"), "--expected-module", ".", "wrong-version"},
|
||||
{"publish", "--repo", root, "--config", filepath.Join(root, "releasekit.json"), releaseVersion, "--expected-module", "."},
|
||||
} {
|
||||
if err := checkPublication(args, root, "core"); err == nil {
|
||||
t.Errorf("invalid publication accepted: %q", args)
|
||||
}
|
||||
|
||||
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"}}} {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseLauncherStopsAfterBootstrapFailure(t *testing.T) {
|
||||
shell := requirePOSIXShell(t)
|
||||
fixture := newFixture(t)
|
||||
result := runLauncher(t, shell, fixture, []string{"core", releaseVersion}, map[string]string{
|
||||
"FAKE_GO_BUILD_EXIT": "23",
|
||||
})
|
||||
|
||||
if result.status != 23 {
|
||||
t.Fatalf("status = %d, want build status 23; stderr: %q", result.status, result.stderr)
|
||||
}
|
||||
if _, err := os.Stat(fixture.publishLog); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("publisher was invoked or its log could not be checked: %v", err)
|
||||
}
|
||||
if !hasGoBuildCall(t, fixture) {
|
||||
t.Fatal("Go build invocation was not recorded")
|
||||
}
|
||||
}
|
||||
|
||||
type launcherResult struct {
|
||||
status int
|
||||
stdout string
|
||||
stderr string
|
||||
}
|
||||
|
||||
type fixture struct {
|
||||
repoRoot string
|
||||
launcher string
|
||||
outside string
|
||||
fakeBin string
|
||||
goLog string
|
||||
publishLog string
|
||||
publishEnvLog string
|
||||
publisher string
|
||||
}
|
||||
|
||||
func newFixture(t *testing.T) fixture {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
if _, err := exec.LookPath("sh"); err != nil {
|
||||
t.Skip("POSIX shell is unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
_, sourceFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("could not locate test source")
|
||||
}
|
||||
repo := filepath.Dir(filepath.Dir(sourceFile))
|
||||
root := t.TempDir()
|
||||
repoRoot := filepath.Join(root, "CommandKit fixture with spaces")
|
||||
outside := filepath.Join(root, "outside working directory")
|
||||
fakeBin := filepath.Join(root, "controlled tools")
|
||||
for _, dir := range []string{filepath.Join(repoRoot, "scripts"), outside, fakeBin} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
copyFile(t, filepath.Join(repo, "scripts", "release"), filepath.Join(repoRoot, "scripts", "release"), 0o755)
|
||||
for _, profile := range []string{
|
||||
"releasekit.json",
|
||||
"integrations/ckconfig/releasekit.json",
|
||||
"integrations/ckprompt/releasekit.json",
|
||||
} {
|
||||
target := filepath.Join(repoRoot, profile)
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copyFile(t, filepath.Join(repo, profile), target, 0o644)
|
||||
}
|
||||
|
||||
goLog := filepath.Join(root, "go calls.log")
|
||||
publishLog := filepath.Join(root, "publisher arguments.log")
|
||||
publishEnvLog := filepath.Join(root, "publisher environment.log")
|
||||
publisher := filepath.Join(root, "fake publisher")
|
||||
writeFile(t, publisher, fakePublisher, 0o644)
|
||||
writeFile(t, filepath.Join(fakeBin, "go"), fakeGo, 0o755)
|
||||
|
||||
return fixture{
|
||||
repoRoot: repoRoot,
|
||||
launcher: filepath.Join(repoRoot, "scripts", "release"),
|
||||
outside: outside,
|
||||
fakeBin: fakeBin,
|
||||
goLog: goLog,
|
||||
publishLog: publishLog,
|
||||
publishEnvLog: publishEnvLog,
|
||||
publisher: publisher,
|
||||
}
|
||||
}
|
||||
|
||||
func runLauncher(t *testing.T, shell string, fixture fixture, args []string, overrides map[string]string) launcherResult {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, shell, append([]string{fixture.launcher}, args...)...)
|
||||
cmd.Dir = fixture.outside
|
||||
cmd.Env = fixtureEnvironment(fixture, overrides)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
|
||||
result := launcherResult{stdout: stdout.String(), stderr: stderr.String()}
|
||||
if err == nil {
|
||||
return result
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
result.status = exitErr.ExitCode()
|
||||
return result
|
||||
}
|
||||
t.Fatalf("run launcher: %v", err)
|
||||
return launcherResult{}
|
||||
}
|
||||
|
||||
func fixtureEnvironment(fixture fixture, overrides map[string]string) []string {
|
||||
values := map[string]string{
|
||||
"PATH": fixture.fakeBin + string(os.PathListSeparator) + os.Getenv("PATH"),
|
||||
"GO_LOG": fixture.goLog,
|
||||
"PUBLISH_ARGS_LOG": fixture.publishLog,
|
||||
"PUBLISH_ENV_LOG": fixture.publishEnvLog,
|
||||
"FAKE_GOHOSTOS": "linux",
|
||||
"FAKE_GOHOSTARCH": "amd64",
|
||||
"FAKE_PUBLISHER_SOURCE": fixture.publisher,
|
||||
"FAKE_GO_BUILD_EXIT": "0",
|
||||
"PUBLISH_EXIT": "0",
|
||||
"PUBLISH_STDOUT": "publisher stdout",
|
||||
"PUBLISH_STDERR": "publisher stderr",
|
||||
}
|
||||
for key, value := range overrides {
|
||||
values[key] = value
|
||||
}
|
||||
var env []string
|
||||
for _, entry := range os.Environ() {
|
||||
key, _, _ := strings.Cut(entry, "=")
|
||||
if _, overridden := values[key]; !overridden {
|
||||
env = append(env, entry)
|
||||
}
|
||||
}
|
||||
for key, value := range values {
|
||||
env = append(env, key+"="+value)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func assertNativeBootstrap(t *testing.T, fixture fixture) {
|
||||
t.Helper()
|
||||
if err := checkNativeBootstrap(readGoCalls(t, fixture.goLog), fixture.repoRoot, "linux", "amd64"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkNativeBootstrap(calls []goCall, root, hostOS, hostArch string) error {
|
||||
foundBuild := false
|
||||
for _, call := range calls {
|
||||
if call.cwd != root {
|
||||
return fmt.Errorf("Go ran in %q, want repository root %q", call.cwd, root)
|
||||
}
|
||||
if call.gowork != "off" || call.toolchain != "local" {
|
||||
return fmt.Errorf("Go settings = GOWORK=%q GOTOOLCHAIN=%q", call.gowork, call.toolchain)
|
||||
}
|
||||
if len(call.args) > 0 && call.args[0] == "build" {
|
||||
foundBuild = true
|
||||
if call.args[len(call.args)-1] != releasePackage {
|
||||
return fmt.Errorf("build does not select module-owned package %q: %q", releasePackage, call.args)
|
||||
}
|
||||
if argumentValue(call.args[1:], "-mod") != "readonly" {
|
||||
return fmt.Errorf("build does not use readonly module metadata: %q", call.args)
|
||||
}
|
||||
output := argumentValue(call.args[1:], "-o")
|
||||
if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(call.cwd, output)
|
||||
}
|
||||
if filepath.Clean(output) != filepath.Join(root, ".releasekit", "bin", "releasekit") {
|
||||
return fmt.Errorf("incorrect build output %q", output)
|
||||
}
|
||||
if call.goos != hostOS || call.goarch != hostArch {
|
||||
return fmt.Errorf("build target = %s/%s, want %s/%s", call.goos, call.goarch, hostOS, hostArch)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundBuild {
|
||||
return errors.New("no native build observed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inspect only the selected build options, leaving other Go flags to Go itself.
|
||||
func argumentValue(args []string, name string) string {
|
||||
value := ""
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] == name && i+1 < len(args) {
|
||||
i++
|
||||
value = args[i]
|
||||
} else if supplied, ok := strings.CutPrefix(args[i], name+"="); ok {
|
||||
value = supplied
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func checkPublication(args []string, root, selector string) error {
|
||||
modules := map[string]string{
|
||||
"core": ".",
|
||||
"ckconfig": "integrations/ckconfig",
|
||||
"ckprompt": "integrations/ckprompt",
|
||||
}
|
||||
module, ok := modules[selector]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown module selector %q", selector)
|
||||
}
|
||||
profile := map[string]string{
|
||||
"core": "releasekit.json",
|
||||
"ckconfig": "integrations/ckconfig/releasekit.json",
|
||||
"ckprompt": "integrations/ckprompt/releasekit.json",
|
||||
}[selector]
|
||||
want := []string{
|
||||
"publish",
|
||||
"--repo", root,
|
||||
"--config", filepath.Join(root, profile),
|
||||
"--expected-module", module,
|
||||
releaseVersion,
|
||||
}
|
||||
if !equalStrings(args, want) {
|
||||
return fmt.Errorf("publication arguments = %q, want %q", args, want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type goCall struct {
|
||||
cwd string
|
||||
goos string
|
||||
goarch string
|
||||
gowork string
|
||||
toolchain string
|
||||
args []string
|
||||
}
|
||||
|
||||
func readGoCalls(t *testing.T, path string) []goCall {
|
||||
t.Helper()
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var calls []goCall
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Split(scanner.Text(), "\t")
|
||||
if len(fields) < 6 {
|
||||
t.Fatalf("malformed Go call record: %q", scanner.Text())
|
||||
}
|
||||
argc, err := strconv.Atoi(fields[5])
|
||||
if err != nil || len(fields) != 6+argc {
|
||||
t.Fatalf("malformed Go arguments in record: %q", scanner.Text())
|
||||
}
|
||||
calls = append(calls, goCall{
|
||||
cwd: fields[0],
|
||||
goos: fields[1],
|
||||
goarch: fields[2],
|
||||
gowork: fields[3],
|
||||
toolchain: fields[4],
|
||||
args: fields[6:],
|
||||
})
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return calls
|
||||
}
|
||||
|
||||
func hasGoBuildCall(t *testing.T, fixture fixture) bool {
|
||||
t.Helper()
|
||||
for _, call := range readGoCalls(t, fixture.goLog) {
|
||||
if len(call.args) > 0 && call.args[0] == "build" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func readLines(t *testing.T, path string) []string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
|
||||
}
|
||||
|
||||
func copyFile(t *testing.T, source, destination string, mode os.FileMode) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, destination, string(data), mode)
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path, data string, mode os.FileMode) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(data), mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func requirePOSIXShell(t *testing.T) string {
|
||||
t.Helper()
|
||||
shell, err := exec.LookPath("sh")
|
||||
if err != nil {
|
||||
t.Skip("POSIX shell is unavailable")
|
||||
}
|
||||
return shell
|
||||
}
|
||||
|
||||
func equalStrings(got, want []string) bool {
|
||||
if len(got) != len(want) {
|
||||
return false
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != want[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const fakeGo = `#!/bin/sh
|
||||
set -eu
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s' "$PWD" "${GOOS-}" "${GOARCH-}" "${GOWORK-}" "${GOTOOLCHAIN-}" "$#" >> "$GO_LOG"
|
||||
for arg do printf '\t%s' "$arg" >> "$GO_LOG"; done
|
||||
printf '\n' >> "$GO_LOG"
|
||||
|
||||
if [ "$1" = env ]; then
|
||||
shift
|
||||
for name do
|
||||
case "$name" in
|
||||
GOHOSTOS) printf '%s\n' "$FAKE_GOHOSTOS" ;;
|
||||
GOHOSTARCH) printf '%s\n' "$FAKE_GOHOSTARCH" ;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" = build ]; then
|
||||
if [ "$FAKE_GO_BUILD_EXIT" -ne 0 ]; then exit "$FAKE_GO_BUILD_EXIT"; fi
|
||||
output=
|
||||
expect_output=0
|
||||
for arg do
|
||||
if [ "$expect_output" -eq 1 ]; then
|
||||
output=$arg
|
||||
expect_output=0
|
||||
continue
|
||||
fi
|
||||
if [ "$arg" = -o ]; then expect_output=1; fi
|
||||
case "$arg" in -o=*) output=${arg#-o=} ;; esac
|
||||
done
|
||||
[ -n "$output" ] || exit 91
|
||||
cat "$FAKE_PUBLISHER_SOURCE" > "$output"
|
||||
chmod +x "$output"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 92
|
||||
`
|
||||
|
||||
const fakePublisher = `#!/bin/sh
|
||||
set -eu
|
||||
printf '%s\n' "$@" > "$PUBLISH_ARGS_LOG"
|
||||
printf 'GOWORK=%s\nGOTOOLCHAIN=%s\n' "${GOWORK-}" "${GOTOOLCHAIN-}" > "$PUBLISH_ENV_LOG"
|
||||
printf '%s\n' "$PUBLISH_STDOUT"
|
||||
printf '%s\n' "$PUBLISH_STDERR" >&2
|
||||
exit "$PUBLISH_EXIT"
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user