685 lines
23 KiB
Go
685 lines
23 KiB
Go
package candidate
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
|
)
|
|
|
|
const testVersion = "v1.2.3"
|
|
|
|
func TestValidStableVersion(t *testing.T) {
|
|
for _, version := range []string{
|
|
"v0.0.0",
|
|
"v1.2.3",
|
|
"v123456789012345678901234567890.0.9",
|
|
} {
|
|
if !validStableVersion(version) {
|
|
t.Errorf("validStableVersion(%q) = false", version)
|
|
}
|
|
}
|
|
for _, version := range []string{
|
|
"",
|
|
"1.2.3",
|
|
"V1.2.3",
|
|
"v01.2.3",
|
|
"v1.02.3",
|
|
"v1.2.03",
|
|
"v1.2",
|
|
"v1.2.3-alpha",
|
|
"v1..3",
|
|
"v+1.2.3",
|
|
} {
|
|
if validStableVersion(version) {
|
|
t.Errorf("validStableVersion(%q) = true", version)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckCapturesCandidateAndCallsAdditionalChecksOnce(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
writeTestFile(t, repository, "guide.md", "# Guide\n[section](#guide)\n")
|
|
gitRun(t, repository, "add", "guide.md")
|
|
gitRun(t, repository, "commit", "-m", "add Markdown guide")
|
|
calls := 0
|
|
var seen Candidate
|
|
candidate, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
Validation: validation.Settings{Markdown: &validation.MarkdownSettings{}},
|
|
AdditionalChecks: func(_ context.Context, captured Candidate) error {
|
|
calls++
|
|
seen = captured
|
|
return nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commit := gitOutputForTest(t, repository, "rev-parse", "HEAD")
|
|
if calls != 1 {
|
|
t.Fatalf("validation callback called %d times, want 1", calls)
|
|
}
|
|
if seen != candidate {
|
|
t.Fatalf("callback candidate = %#v, returned candidate = %#v", seen, candidate)
|
|
}
|
|
if candidate.RepositoryDir != repository || candidate.ModuleDir != repository || candidate.ModulePath != "example.com/candidate" || candidate.Tag != testVersion || candidate.Commit != commit || candidate.Version != testVersion {
|
|
t.Fatalf("Check() candidate = %#v", candidate)
|
|
}
|
|
if tags := gitOutputForTest(t, repository, "tag", "--list"); tags != "" {
|
|
t.Fatalf("Check created local tags: %q", tags)
|
|
}
|
|
if refs := gitOutputForTest(t, repository, "for-each-ref", "--format=%(refname)", "refs/remotes"); refs != "" {
|
|
t.Fatalf("Check created remote-tracking refs: %q", refs)
|
|
}
|
|
}
|
|
|
|
func TestCaptureUsesNestedMajorModuleNoteAndTag(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRoot release.\n")
|
|
moduleDir := "integrations/ckconfig/v2"
|
|
writeTestFile(t, repository, moduleDir+"/go.mod", "module example.com/ckconfig/v2\n\ngo 1.25.0\n")
|
|
writeTestFile(t, repository, moduleDir+"/docs/releases/v2.3.4.md", "# Example Product v2.3.4\n\nNested release.\n")
|
|
gitRun(t, repository, "add", moduleDir)
|
|
gitRun(t, repository, "commit", "-m", "add nested major module")
|
|
|
|
captured, err := Capture(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
ModuleDir: moduleDir,
|
|
Product: "Example Product",
|
|
Version: "v2.3.4",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if captured.RepositoryDir != repository || captured.ModuleDir != filepath.Join(repository, moduleDir) || captured.ModulePath != "example.com/ckconfig/v2" || captured.Tag != "integrations/ckconfig/v2.3.4" {
|
|
t.Fatalf("nested candidate identity = %#v", captured)
|
|
}
|
|
if !strings.Contains(captured.Note, "Nested release.") || strings.Contains(captured.Note, "Root release.") {
|
|
t.Fatalf("captured note = %q, want nested module note", captured.Note)
|
|
}
|
|
|
|
gitRun(t, repository, "tag", captured.Tag)
|
|
conflicted, err := Capture(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
ModuleDir: moduleDir,
|
|
Product: "Example Product",
|
|
Version: "v2.3.4",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), captured.Tag) {
|
|
t.Fatalf("Capture() with existing nested tag returned %#v, %v", conflicted, err)
|
|
}
|
|
if conflicted.ModulePath != captured.ModulePath || conflicted.Tag != captured.Tag || conflicted.Commit != captured.Commit {
|
|
t.Fatalf("capture failure lost identity evidence: %#v", conflicted)
|
|
}
|
|
}
|
|
|
|
func TestCaptureKeepsRepositoryWideCleanlinessGuardForNestedSelection(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n")
|
|
moduleDir := "integrations/ckconfig"
|
|
writeTestFile(t, repository, moduleDir+"/go.mod", "module example.com/ckconfig\n")
|
|
writeTestFile(t, repository, moduleDir+"/docs/releases/"+testVersion+".md", "# Example Product v1.2.3\n")
|
|
gitRun(t, repository, "add", moduleDir)
|
|
gitRun(t, repository, "commit", "-m", "add nested module")
|
|
writeTestFile(t, repository, "sibling/untracked.txt", "dirty\n")
|
|
captured, err := Capture(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
ModuleDir: moduleDir,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "must be clean") {
|
|
t.Fatalf("Capture() = %#v, %v; want repository-wide dirty-state rejection", captured, err)
|
|
}
|
|
if captured.Commit == "" {
|
|
t.Fatalf("dirty-state error lost captured commit: %#v", captured)
|
|
}
|
|
}
|
|
|
|
func TestCheckStopsBeforeProductAndBuildCallbacksWhenGoFormatFails(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
writeTestFile(t, repository, "unformatted.go", "package candidate\nfunc Value( ) int{return 1}\n")
|
|
gitRun(t, repository, "add", "unformatted.go")
|
|
gitRun(t, repository, "commit", "-m", "add unformatted Go source")
|
|
productCalls := 0
|
|
buildCalls := 0
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
Validation: validation.Settings{GoFormat: true},
|
|
AdditionalChecks: func(context.Context, Candidate) error {
|
|
productCalls++
|
|
return nil
|
|
},
|
|
AfterValidate: func(context.Context, Candidate, *gotool.Owner) error {
|
|
buildCalls++
|
|
return nil
|
|
},
|
|
})
|
|
var failure *validation.Failure
|
|
if !errors.As(err, &failure) || failure.Check != validation.GoFormat {
|
|
t.Fatalf("Check error = %v, want go-format failure", err)
|
|
}
|
|
if productCalls != 0 || buildCalls != 0 {
|
|
t.Fatalf("product callback calls = %d, build callback calls = %d; want neither after formatting failure", productCalls, buildCalls)
|
|
}
|
|
}
|
|
|
|
func TestCheckStopsBeforeProductAndBuildCallbacksWhenMarkdownBoundaryFails(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
writeTestFile(t, repository, "guide.md", "[outside](../../outside.md)\n")
|
|
gitRun(t, repository, "add", "guide.md")
|
|
gitRun(t, repository, "commit", "-m", "add outside Markdown destination")
|
|
productCalls := 0
|
|
buildCalls := 0
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
Validation: validation.Settings{
|
|
Markdown: &validation.MarkdownSettings{EnforceCheckoutBoundary: true},
|
|
},
|
|
AdditionalChecks: func(context.Context, Candidate) error {
|
|
productCalls++
|
|
return nil
|
|
},
|
|
AfterValidate: func(context.Context, Candidate, *gotool.Owner) error {
|
|
buildCalls++
|
|
return nil
|
|
},
|
|
})
|
|
var failure *validation.Failure
|
|
if !errors.As(err, &failure) || failure.Check != validation.MarkdownLinks {
|
|
t.Fatalf("Check() error = %v, want markdown-links failure", err)
|
|
}
|
|
if productCalls != 0 || buildCalls != 0 {
|
|
t.Fatalf("product callback calls = %d, build callback calls = %d; want neither after Markdown failure", productCalls, buildCalls)
|
|
}
|
|
}
|
|
|
|
func TestCheckReportsCommittedWorkspaceFilesWhenGoWorkspaceIsDisabled(t *testing.T) {
|
|
t.Setenv("GOWORK", "off")
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
writeTestFile(t, repository, "go.work", "go 1.25.0\n")
|
|
writeTestFile(t, repository, "nested/go.work.sum", "workspace-sum\n")
|
|
if err := os.MkdirAll(filepath.Join(repository, "linked"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("../go.work", filepath.Join(repository, "linked", "go.work")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gitRun(t, repository, "add", "go.work", "nested/go.work.sum", "linked/go.work")
|
|
gitRun(t, repository, "commit", "-m", "add workspace files")
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
Validation: validation.Settings{ModuleDistribution: validation.ModuleDistributionSettings{
|
|
WorkspaceFiles: true,
|
|
}},
|
|
})
|
|
var failure *validation.Failure
|
|
if !errors.As(err, &failure) || failure.Check != validation.WorkspaceFiles {
|
|
t.Fatalf("Check error = %v, want workspace-files failure", err)
|
|
}
|
|
want := map[string]bool{
|
|
"go.work": false,
|
|
"linked/go.work": false,
|
|
"nested/go.work.sum": false,
|
|
}
|
|
if len(failure.Findings) != len(want) {
|
|
t.Fatalf("workspace findings = %#v, want %d paths", failure.Findings, len(want))
|
|
}
|
|
for _, finding := range failure.Findings {
|
|
if _, ok := want[finding.Path]; !ok {
|
|
t.Errorf("unexpected workspace path %q", finding.Path)
|
|
}
|
|
want[finding.Path] = true
|
|
}
|
|
for path, found := range want {
|
|
if !found {
|
|
t.Errorf("workspace path %q was not reported", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckRunsDefaultOfflineGoValidation(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
var output bytes.Buffer
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
Output: &output,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Check() with default validation: %v\n%s", err, output.String())
|
|
}
|
|
}
|
|
|
|
func TestCheckRefreshesGoSettingsAfterAdditionalChecks(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
t.Setenv("GOENV", "off")
|
|
t.Setenv("GOMODCACHE", filepath.Join(t.TempDir(), "initial-module-cache"))
|
|
selectedCache := filepath.Join(t.TempDir(), "callback-module-cache")
|
|
var observed string
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
AdditionalChecks: func(context.Context, Candidate) error {
|
|
t.Setenv("GOMODCACHE", selectedCache)
|
|
return nil
|
|
},
|
|
AfterValidate: func(ctx context.Context, captured Candidate, owner *gotool.Owner) error {
|
|
tool, err := owner.Acquire(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
output, err := tool.Output(ctx, captured.ModuleDir, gotool.Native, gotool.Target{}, nil, "env", "GOMODCACHE")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
observed = strings.TrimSpace(string(output))
|
|
return nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if observed != selectedCache {
|
|
t.Fatalf("Go settings after additional checks = %q, want %q", observed, selectedCache)
|
|
}
|
|
}
|
|
|
|
func TestCheckSharesGoDiscoveryWithAfterValidateCallback(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("the controlled Go command fixture uses a POSIX shell")
|
|
}
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
logPath := installCandidateGoShim(t)
|
|
t.Setenv("GOENV", "off")
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
AfterValidate: func(ctx context.Context, captured Candidate, owner *gotool.Owner) error {
|
|
tool, err := owner.Acquire(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = tool.Output(ctx, captured.ModuleDir, gotool.Native, gotool.Target{}, nil, "version")
|
|
return err
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
contents, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commands := strings.Split(strings.TrimSpace(string(contents)), "\n")
|
|
var discoveries, suites, callbacks int
|
|
for _, command := range commands {
|
|
switch {
|
|
case strings.HasPrefix(command, "env "):
|
|
discoveries++
|
|
case strings.HasPrefix(command, "test "):
|
|
suites++
|
|
case command == "version":
|
|
callbacks++
|
|
}
|
|
}
|
|
if discoveries != 2 || suites != 1 || callbacks != 1 {
|
|
t.Fatalf("Go invocations = %q; discovery=%d suite=%d callback=%d, want 2, 1, 1", commands, discoveries, suites, callbacks)
|
|
}
|
|
}
|
|
|
|
func installCandidateGoShim(t *testing.T) string {
|
|
t.Helper()
|
|
directory := t.TempDir()
|
|
logPath := filepath.Join(directory, "go-commands")
|
|
settings := map[string]string{
|
|
"GOHOSTOS": runtime.GOOS, "GOHOSTARCH": runtime.GOARCH, "GOENV": "off",
|
|
"CC": "", "CXX": "", "CGO_CFLAGS": "", "CGO_CPPFLAGS": "", "CGO_CXXFLAGS": "",
|
|
"CGO_ENABLED": "1", "CGO_LDFLAGS": "", "GOCACHE": "", "GOCACHEPROG": "",
|
|
"GOMODCACHE": "", "GOPATH": "", "GOROOT": "", "GOTMPDIR": "", "PKG_CONFIG": "",
|
|
}
|
|
encoded, err := json.Marshal(settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
script := fmt.Sprintf(`#!/bin/sh
|
|
set -eu
|
|
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
|
if [ "${1-}" = "env" ]; then
|
|
printf '%%s\n' '%s'
|
|
exit 0
|
|
fi
|
|
if [ "${1-}" = "version" ]; then
|
|
echo 'go version fixture'
|
|
exit 0
|
|
fi
|
|
exit 0
|
|
`, encoded)
|
|
shimPath := filepath.Join(directory, "go")
|
|
if err := os.WriteFile(shimPath, []byte(script), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := directory
|
|
if current := os.Getenv("PATH"); current != "" {
|
|
path += string(os.PathListSeparator) + current
|
|
}
|
|
t.Setenv("PATH", path)
|
|
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
|
return logPath
|
|
}
|
|
|
|
func TestDefaultValidationDoesNotInheritGOFLAGSRunFilter(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
|
writeTestFile(t, repository, "failure_test.go", "package candidate\nimport \"testing\"\nfunc TestExpectedFailure(t *testing.T) { t.Fatal(\"expected fixture failure\") }\n")
|
|
gitRun(t, repository, "add", "failure_test.go")
|
|
gitRun(t, repository, "commit", "-m", "add failing validation fixture")
|
|
|
|
for _, source := range []string{"process environment", "Go environment file"} {
|
|
t.Run(source, func(t *testing.T) {
|
|
unsetCandidateEnv(t, "GOFLAGS", "GOOS", "GOARCH")
|
|
goEnv := filepath.Join(t.TempDir(), "go-env")
|
|
if source == "Go environment file" {
|
|
if err := os.WriteFile(goEnv, []byte("GOFLAGS=-run=^$\nGOOS=windows\nGOARCH=arm64\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
} else {
|
|
if err := os.WriteFile(goEnv, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("GOFLAGS", "-run=^$")
|
|
t.Setenv("GOOS", "windows")
|
|
t.Setenv("GOARCH", "arm64")
|
|
}
|
|
t.Setenv("GOENV", goEnv)
|
|
var output bytes.Buffer
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
Output: &output,
|
|
})
|
|
if err == nil || !strings.Contains(output.String(), "TestExpectedFailure") {
|
|
t.Fatalf("Check() error = %v, output = %q; wanted the failing standard test to run", err, output.String())
|
|
}
|
|
var failure *validation.Failure
|
|
if !errors.As(err, &failure) || failure.Check != validation.GoTest {
|
|
t.Fatalf("Check() error = %v; want a go-test validation failure", err)
|
|
}
|
|
var exitErr *exec.ExitError
|
|
if !errors.As(err, &exitErr) {
|
|
t.Fatalf("Check() error = %v; want the Go process exit error preserved", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func unsetCandidateEnv(t *testing.T, keys ...string) {
|
|
t.Helper()
|
|
for _, key := range keys {
|
|
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 TestCheckRejectsInvalidVersionsBeforeGitAccess(t *testing.T) {
|
|
for _, version := range []string{"v01.2.3", "v1.2.3-alpha", "1.2.3"} {
|
|
t.Run(version, func(t *testing.T) {
|
|
_, err := Check(context.Background(), Request{Version: version})
|
|
if !errors.Is(err, ErrInvalidVersion) {
|
|
t.Fatalf("Check() error = %v, want ErrInvalidVersion", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckRequiresMatchingCommittedNote(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
note string
|
|
}{
|
|
{name: "missing note"},
|
|
{name: "wrong product", note: "# Other Product v1.2.3\n"},
|
|
{name: "wrong version", note: "# Example Product v1.2.4\n"},
|
|
{name: "embedded product text", note: "# Example Products v1.2.3\n"},
|
|
{name: "not a level one heading", note: "## Example Product v1.2.3\n"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repository := newRepository(t, test.note)
|
|
called := false
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
AdditionalChecks: func(context.Context, Candidate) error {
|
|
called = true
|
|
return nil
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Check() succeeded with an invalid committed note")
|
|
}
|
|
if called {
|
|
t.Fatal("validation ran before the committed note passed")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsDirtyTrackedAndUntrackedState(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
path string
|
|
value string
|
|
}{
|
|
{name: "tracked", path: "tracked.txt", value: "changed\n"},
|
|
{name: "untracked", path: "new file.txt", value: "untracked\n"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n")
|
|
if err := os.WriteFile(filepath.Join(repository, test.path), []byte(test.value), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := Check(context.Background(), validRequest(repository))
|
|
if err == nil || !strings.Contains(err.Error(), "must be clean") {
|
|
t.Fatalf("Check() error = %v, want dirty state error", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckRequiresMainBranchAndUnusedTag(t *testing.T) {
|
|
t.Run("detached HEAD", func(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n")
|
|
gitRun(t, repository, "checkout", "--detach", "HEAD")
|
|
_, err := Check(context.Background(), validRequest(repository))
|
|
if err == nil || !strings.Contains(err.Error(), "must be on main") {
|
|
t.Fatalf("Check() error = %v, want branch error", err)
|
|
}
|
|
})
|
|
t.Run("existing local tag", func(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n")
|
|
gitRun(t, repository, "tag", testVersion)
|
|
_, err := Check(context.Background(), validRequest(repository))
|
|
if err == nil || !strings.Contains(err.Error(), "already exists") {
|
|
t.Fatalf("Check() error = %v, want existing-tag error", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckStopsOnAdditionalCheckFailure(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n")
|
|
want := errors.New("selected check failed")
|
|
calls := 0
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
AdditionalChecks: func(context.Context, Candidate) error {
|
|
calls++
|
|
return want
|
|
},
|
|
})
|
|
if !errors.Is(err, want) {
|
|
t.Fatalf("Check() error = %v, want wrapped callback error", err)
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("validation callback called %d times, want 1", calls)
|
|
}
|
|
}
|
|
|
|
func TestCheckRechecksHeadAndWorktreeAfterCallback(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
validate func(*testing.T, string)
|
|
want string
|
|
}{
|
|
{
|
|
name: "HEAD changes",
|
|
validate: func(t *testing.T, repository string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gitRun(t, repository, "add", "later.txt")
|
|
gitRun(t, repository, "-c", "commit.gpgsign=false", "commit", "-m", "advance HEAD")
|
|
},
|
|
want: "HEAD changed from",
|
|
},
|
|
{
|
|
name: "worktree changes",
|
|
validate: func(t *testing.T, repository string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
want: "worktree or index changed",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n")
|
|
_, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
AdditionalChecks: func(context.Context, Candidate) error {
|
|
test.validate(t, repository)
|
|
return nil
|
|
},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("Check() error = %v, want text %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckRechecksLocalTagAfterCallback(t *testing.T) {
|
|
repository := newRepository(t, "# Example Product v1.2.3\n")
|
|
captured, err := Check(context.Background(), Request{
|
|
RepositoryDir: repository,
|
|
Product: "Example Product",
|
|
Version: testVersion,
|
|
AdditionalChecks: func(context.Context, Candidate) error {
|
|
gitRun(t, repository, "tag", testVersion)
|
|
return nil
|
|
},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "appeared during validation") {
|
|
t.Fatalf("Check() error = %v, want tag recheck error", err)
|
|
}
|
|
if captured.RepositoryDir != repository || captured.ModuleDir != repository || captured.ModulePath != "example.com/candidate" || captured.Tag != testVersion || captured.Commit == "" {
|
|
t.Fatalf("tag recheck failure lost candidate evidence: %#v", captured)
|
|
}
|
|
}
|
|
|
|
func validRequest(repository string) Request {
|
|
return Request{RepositoryDir: repository, Product: "Example Product", Version: testVersion, AdditionalChecks: func(context.Context, Candidate) error { return nil }}
|
|
}
|
|
|
|
func newRepository(t *testing.T, note string) string {
|
|
t.Helper()
|
|
repository := filepath.Join(t.TempDir(), "repository with spaces")
|
|
if err := os.Mkdir(repository, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gitRun(t, repository, "init", "--initial-branch=main")
|
|
gitRun(t, repository, "config", "user.name", "ReleaseKit Test")
|
|
gitRun(t, repository, "config", "user.email", "releasekit@example.test")
|
|
gitRun(t, repository, "config", "commit.gpgsign", "false")
|
|
writeTestFile(t, repository, "go.mod", "module example.com/candidate\n\ngo 1.25.0\n")
|
|
writeTestFile(t, repository, "candidate_test.go", "package candidate\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n")
|
|
writeTestFile(t, repository, "tracked.txt", "tracked\n")
|
|
if note != "" {
|
|
writeTestFile(t, repository, "docs/releases/"+testVersion+".md", note)
|
|
}
|
|
gitRun(t, repository, "add", "--all")
|
|
gitRun(t, repository, "commit", "-m", "candidate")
|
|
return repository
|
|
}
|
|
|
|
func writeTestFile(t *testing.T, repository, 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)
|
|
}
|
|
}
|
|
|
|
func gitRun(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 gitOutputForTest(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))
|
|
}
|