whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,163 @@
// Command apidiff determines whether two versions of a package are compatible
package main
import (
"bufio"
"flag"
"fmt"
"go/token"
"go/types"
"os"
"strings"
"golang.org/x/exp/apidiff"
"golang.org/x/tools/go/gcexportdata"
"golang.org/x/tools/go/packages"
)
var (
exportDataOutfile = flag.String("w", "", "file for export data")
incompatibleOnly = flag.Bool("incompatible", false, "display only incompatible changes")
allowInternal = flag.Bool("allow-internal", false, "allow apidiff to compare internal packages")
)
func main() {
flag.Usage = func() {
w := flag.CommandLine.Output()
fmt.Fprintf(w, "usage:\n")
fmt.Fprintf(w, "apidiff OLD NEW\n")
fmt.Fprintf(w, " compares OLD and NEW package APIs\n")
fmt.Fprintf(w, " where OLD and NEW are either import paths or files of export data\n")
fmt.Fprintf(w, "apidiff -w FILE IMPORT_PATH\n")
fmt.Fprintf(w, " writes export data of the package at IMPORT_PATH to FILE\n")
fmt.Fprintf(w, " NOTE: In a GOPATH-less environment, this option consults the\n")
fmt.Fprintf(w, " module cache by default, unless used in the directory that\n")
fmt.Fprintf(w, " contains the go.mod module definition that IMPORT_PATH belongs\n")
fmt.Fprintf(w, " to. In most cases users want the latter behavior, so be sure\n")
fmt.Fprintf(w, " to cd to the exact directory which contains the module\n")
fmt.Fprintf(w, " definition of IMPORT_PATH.\n")
flag.PrintDefaults()
}
flag.Parse()
if *exportDataOutfile != "" {
if len(flag.Args()) != 1 {
flag.Usage()
os.Exit(2)
}
pkg := mustLoadPackage(flag.Arg(0))
if err := writeExportData(pkg, *exportDataOutfile); err != nil {
die("writing export data: %v", err)
}
} else {
if len(flag.Args()) != 2 {
flag.Usage()
os.Exit(2)
}
oldpkg := mustLoadOrRead(flag.Arg(0))
newpkg := mustLoadOrRead(flag.Arg(1))
if !*allowInternal {
if isInternalPackage(oldpkg.Path()) && isInternalPackage(newpkg.Path()) {
fmt.Fprintf(os.Stderr, "Ignoring internal package %s\n", oldpkg.Path())
os.Exit(0)
}
}
report := apidiff.Changes(oldpkg, newpkg)
var err error
if *incompatibleOnly {
err = report.TextIncompatible(os.Stdout, false)
} else {
err = report.Text(os.Stdout)
}
if err != nil {
die("writing report: %v", err)
}
}
}
func mustLoadOrRead(importPathOrFile string) *types.Package {
fileInfo, err := os.Stat(importPathOrFile)
if err == nil && fileInfo.Mode().IsRegular() {
pkg, err := readExportData(importPathOrFile)
if err != nil {
die("reading export data from %s: %v", importPathOrFile, err)
}
return pkg
} else {
return mustLoadPackage(importPathOrFile).Types
}
}
func mustLoadPackage(importPath string) *packages.Package {
pkg, err := loadPackage(importPath)
if err != nil {
die("loading %s: %v", importPath, err)
}
return pkg
}
func loadPackage(importPath string) (*packages.Package, error) {
cfg := &packages.Config{Mode: packages.LoadTypes |
packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
}
pkgs, err := packages.Load(cfg, importPath)
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
return nil, fmt.Errorf("found no packages for import %s", importPath)
}
if len(pkgs[0].Errors) > 0 {
return nil, pkgs[0].Errors[0]
}
return pkgs[0], nil
}
func readExportData(filename string) (*types.Package, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
r := bufio.NewReader(f)
m := map[string]*types.Package{}
pkgPath, err := r.ReadString('\n')
if err != nil {
return nil, err
}
pkgPath = pkgPath[:len(pkgPath)-1] // remove delimiter
return gcexportdata.Read(r, token.NewFileSet(), m, pkgPath)
}
func writeExportData(pkg *packages.Package, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
// Include the package path in the file. The exportdata format does
// not record the path of the package being written.
fmt.Fprintln(f, pkg.PkgPath)
err1 := gcexportdata.Write(f, pkg.Fset, pkg.Types)
err2 := f.Close()
if err1 != nil {
return err1
}
return err2
}
func die(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
func isInternalPackage(pkgPath string) bool {
switch {
case strings.HasSuffix(pkgPath, "/internal"):
return true
case strings.Contains(pkgPath, "/internal/"):
return true
case pkgPath == "internal", strings.HasPrefix(pkgPath, "internal/"):
return true
}
return false
}
@@ -0,0 +1,81 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"errors"
"flag"
"fmt"
"os/exec"
"strings"
"golang.org/x/mod/module"
)
type usageError struct {
err error
}
func usageErrorf(format string, args ...interface{}) error {
return &usageError{err: fmt.Errorf(format, args...)}
}
const usageText = `usage: gorelease [-base=version] [-version=version]`
func (e *usageError) Error() string {
msg := ""
if !errors.Is(e.err, flag.ErrHelp) {
msg = e.err.Error()
}
return usageText + "\n" + msg + "\nFor more information, run go doc golang.org/x/exp/cmd/gorelease"
}
type baseVersionError struct {
err error
modPath string
}
func (e *baseVersionError) Error() string {
firstVersion := "v0.1.0"
_, major, _ := module.SplitPathVersion(e.modPath)
if major != "" {
firstVersion = major[1:] + ".0.0"
}
return fmt.Sprintf("could not find base version. Consider setting -version=%s if this is a first release, or explicitly set -base=none: %v", firstVersion, e.err)
}
func (e *baseVersionError) Unwrap() error {
return e.err
}
type downloadError struct {
m module.Version
err error
}
func (e *downloadError) Error() string {
msg := e.err.Error()
sep := " "
if strings.Contains(msg, "\n") {
sep = "\n"
}
return fmt.Sprintf("error downloading module %s@%s:%s%s", e.m.Path, e.m.Version, sep, msg)
}
// cleanCmdError simplifies error messages from os/exec.Cmd.Run.
// For ExitErrors, it trims and returns stderr. This is useful for go commands
// that print well-formatted errors. By default, ExitError prints the exit
// status but not stderr.
//
// cleanCmdError returns other errors unmodified.
func cleanCmdError(err error) error {
if xerr, ok := err.(*exec.ExitError); ok {
if stderr := strings.TrimSpace(string(xerr.Stderr)); stderr != "" {
return errors.New(stderr)
}
}
return err
}
@@ -0,0 +1,503 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"golang.org/x/mod/module"
"golang.org/x/tools/txtar"
)
var (
testwork = flag.Bool("testwork", false, "preserve work directory")
updateGolden = flag.Bool("u", false, "update expected text in test files instead of failing")
)
var hasGitCache struct {
once sync.Once
found bool
}
// hasGit reports whether the git executable exists on the PATH.
func hasGit() bool {
hasGitCache.once.Do(func() {
if _, err := exec.LookPath("git"); err != nil {
return
}
hasGitCache.found = true
})
return hasGitCache.found
}
// prepareProxy creates a proxy dir and returns an associated ctx.
//
// proxyVersions must be a map of module version to true. If proxyVersions is
// empty, all modules in mod/ will be included in the proxy list. If proxy
// versions is non-empty, only those modules in mod/ that match an entry in
// proxyVersions will be included.
//
// ctx must be used in runRelease.
// cleanup must be called when the relevant tests are finished.
func prepareProxy(proxyVersions map[module.Version]bool, tests []*test) (ctx context.Context, cleanup func(), _ error) {
env := append(os.Environ(), "GO111MODULE=on", "GOSUMDB=off")
proxyDir, proxyURL, err := buildProxyDir(proxyVersions, tests)
if err != nil {
return nil, nil, fmt.Errorf("error building proxy dir: %v", err)
}
env = append(env, fmt.Sprintf("GOPROXY=%s", proxyURL))
cacheDir, err := os.MkdirTemp("", "gorelease_test-gocache")
if err != nil {
return nil, nil, err
}
env = append(env, fmt.Sprintf("GOPATH=%s", cacheDir))
return context.WithValue(context.Background(), "env", env), func() {
if *testwork {
fmt.Fprintf(os.Stderr, "test cache dir: %s\n", cacheDir)
fmt.Fprintf(os.Stderr, "test proxy dir: %s\ntest proxy URL: %s\n", proxyDir, proxyURL)
} else {
cmd := exec.Command("go", "clean", "-modcache")
cmd.Env = env
if err := cmd.Run(); err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error running go clean: %v", err))
}
if err := os.RemoveAll(cacheDir); err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error removing cache dir %s: %v", cacheDir, err))
}
if err := os.RemoveAll(proxyDir); err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error removing proxy dir %s: %v", proxyDir, err))
}
}
}, nil
}
// test describes an individual test case, written as a .test file in the
// testdata directory.
//
// Each test is a txtar archive (see golang.org/x/tools/txtar). The comment
// section (before the first file) contains a sequence of key=value pairs
// (one per line) that configure the test.
//
// Most tests include a file named "want". The output of gorelease is compared
// against this file. If the -u flag is set, this file is replaced with the
// actual output of gorelease, and the test is written back to disk. This is
// useful for updating tests after cosmetic changes.
type test struct {
txtar.Archive
// testPath is the name of the .test file describing the test.
testPath string
// modPath (set with mod=...) is the path of the module being tested. Used
// to retrieve files from the test proxy.
modPath string
// version (set with version=...) is the name of a version to check out
// from the test proxy into the working directory. Some tests use this
// instead of specifying files they need in the txtar archive.
version string
// baseVersion (set with base=...) is the value of the -base flag to pass
// to gorelease.
baseVersion string
// releaseVersion (set with release=...) is the value of the -version flag
// to pass to gorelease.
releaseVersion string
// dir (set with dir=...) is the directory where gorelease should be invoked.
// If unset, gorelease is invoked in the directory where the txtar archive
// is unpacked. This is useful for invoking gorelease in a subdirectory.
dir string
// wantError (set with error=...) is true if the test expects a hard error
// (returned by runRelease).
wantError bool
// wantSuccess (set with success=...) is true if the test expects a report
// to be returned without errors or diagnostics. True by default.
wantSuccess bool
// skip (set with skip=...) is non-empty if the test should be skipped.
skip string
// want is set to the contents of the file named "want" in the txtar archive.
want []byte
// proxyVersions is used to set the exact contents of the GOPROXY.
//
// If empty, all of testadata/mod/ will be included in the proxy.
// If it is not empty, each entry must be of the form <modpath>@v<version>
// and exist in testdata/mod/.
proxyVersions map[module.Version]bool
// vcs is used to set the VCS that the root of the test should
// emulate. Allowed values are git, and hg.
vcs string
}
// readTest reads and parses a .test file with the given name.
func readTest(testPath string) (*test, error) {
arc, err := txtar.ParseFile(testPath)
if err != nil {
return nil, err
}
t := &test{
Archive: *arc,
testPath: testPath,
wantSuccess: true,
}
for n, line := range bytes.Split(t.Comment, []byte("\n")) {
lineNum := n + 1
if i := bytes.IndexByte(line, '#'); i >= 0 {
line = line[:i]
}
line = bytes.TrimSpace(line)
if len(line) == 0 {
continue
}
var key, value string
if i := bytes.IndexByte(line, '='); i < 0 {
return nil, fmt.Errorf("%s:%d: no '=' found", testPath, lineNum)
} else {
key = strings.TrimSpace(string(line[:i]))
value = strings.TrimSpace(string(line[i+1:]))
}
switch key {
case "mod":
t.modPath = value
case "version":
t.version = value
case "base":
t.baseVersion = value
case "release":
t.releaseVersion = value
case "dir":
t.dir = value
case "skip":
t.skip = value
case "success":
t.wantSuccess, err = strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
}
case "error":
t.wantError, err = strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
}
case "proxyVersions":
if len(value) == 0 {
break
}
proxyVersions := make(map[module.Version]bool)
parts := strings.Split(value, ",")
for _, modpathWithVersion := range parts {
vParts := strings.Split(modpathWithVersion, "@")
if len(vParts) != 2 {
return nil, fmt.Errorf("proxyVersions entry %s is invalid: it should be of the format <modpath>@v<semver> (ex: github.com/foo/bar@v1.2.3)", modpathWithVersion)
}
modPath, version := vParts[0], vParts[1]
mv := module.Version{
Path: modPath,
Version: version,
}
proxyVersions[mv] = true
}
t.proxyVersions = proxyVersions
case "vcs":
t.vcs = value
default:
return nil, fmt.Errorf("%s:%d: unknown key: %q", testPath, lineNum, key)
}
}
if t.modPath == "" && (t.version != "" || (t.baseVersion != "" && t.baseVersion != "none")) {
return nil, fmt.Errorf("%s: version or base was set but mod was not set", testPath)
}
haveFiles := false
for _, f := range t.Files {
if f.Name == "want" {
t.want = bytes.TrimSpace(f.Data)
continue
}
haveFiles = true
}
if haveFiles && t.version != "" {
return nil, fmt.Errorf("%s: version is set but files are present", testPath)
}
return t, nil
}
// updateTest replaces the contents of the file named "want" within a test's
// txtar archive, then formats and writes the test file.
func updateTest(t *test, want []byte) error {
var wantFile *txtar.File
for i := range t.Files {
if t.Files[i].Name == "want" {
wantFile = &t.Files[i]
break
}
}
if wantFile == nil {
t.Files = append(t.Files, txtar.File{Name: "want"})
wantFile = &t.Files[len(t.Files)-1]
}
wantFile.Data = want
data := txtar.Format(&t.Archive)
return os.WriteFile(t.testPath, data, 0666)
}
func TestRelease(t *testing.T) {
testPaths, err := filepath.Glob(filepath.FromSlash("testdata/*/*.test"))
if err != nil {
t.Fatal(err)
}
if len(testPaths) == 0 {
t.Fatal("no .test files found in testdata directory")
}
var tests []*test
for _, testPath := range testPaths {
test, err := readTest(testPath)
if err != nil {
t.Fatal(err)
}
tests = append(tests, test)
}
defaultContext, cleanup, err := prepareProxy(nil, tests)
if err != nil {
t.Fatalf("preparing test proxy: %v", err)
}
t.Cleanup(cleanup)
for _, test := range tests {
testName := strings.TrimSuffix(strings.TrimPrefix(filepath.ToSlash(test.testPath), "testdata/"), ".test")
t.Run(testName, testRelease(defaultContext, tests, test))
}
}
func TestRelease_gitRepo_uncommittedChanges(t *testing.T) {
ctx := context.Background()
buf := &bytes.Buffer{}
releaseDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
goModInit(t, releaseDir)
gitInit(t, releaseDir)
// Create an uncommitted change.
bContents := `package b
const B = "b"`
if err := os.WriteFile(filepath.Join(releaseDir, "b.go"), []byte(bContents), 0644); err != nil {
t.Fatal(err)
}
success, err := runRelease(ctx, buf, releaseDir, nil)
if got, want := err.Error(), fmt.Sprintf("repo %s has uncommitted changes", releaseDir); got != want {
t.Errorf("runRelease:\ngot error:\n%q\nwant error\n%q", got, want)
}
if success {
t.Errorf("runRelease: expected failure, got success")
}
}
func testRelease(ctx context.Context, tests []*test, test *test) func(t *testing.T) {
return func(t *testing.T) {
if test.skip != "" {
t.Skip(test.skip)
}
t.Parallel()
if len(test.proxyVersions) > 0 {
var cleanup func()
var err error
ctx, cleanup, err = prepareProxy(test.proxyVersions, tests)
if err != nil {
t.Fatalf("preparing test proxy: %v", err)
}
t.Cleanup(cleanup)
}
// Extract the files in the release version. They may be part of the
// test archive or in testdata/mod.
testDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
if *testwork {
fmt.Fprintf(os.Stderr, "test dir: %s\n", testDir)
} else {
t.Cleanup(func() {
os.RemoveAll(testDir)
})
}
var arc *txtar.Archive
if test.version != "" {
arcBase := fmt.Sprintf("%s_%s.txt", strings.ReplaceAll(test.modPath, "/", "_"), test.version)
arcPath := filepath.Join("testdata/mod", arcBase)
var err error
arc, err = txtar.ParseFile(arcPath)
if err != nil {
t.Fatal(err)
}
} else {
arc = &test.Archive
}
if err := extractTxtar(testDir, arc); err != nil {
t.Fatal(err)
}
switch test.vcs {
case "git":
// Convert testDir to a git repository with a single commit, to
// simulate a real user's module-in-a-git-repo.
gitInit(t, testDir)
case "hg":
// Convert testDir to a mercurial repository to simulate a real
// user's module-in-a-hg-repo.
hgInit(t, testDir)
case "":
// No VCS.
default:
t.Fatalf("unknown vcs %q", test.vcs)
}
// Generate the report and compare it against the expected text.
var args []string
if test.baseVersion != "" {
args = append(args, "-base="+test.baseVersion)
}
if test.releaseVersion != "" {
args = append(args, "-version="+test.releaseVersion)
}
buf := &bytes.Buffer{}
releaseDir := filepath.Join(testDir, test.dir)
success, err := runRelease(ctx, buf, releaseDir, args)
if err != nil {
if !test.wantError {
t.Fatalf("unexpected error: %v", err)
}
if errMsg := []byte(err.Error()); !bytes.Equal(errMsg, bytes.TrimSpace(test.want)) {
if *updateGolden {
if err := updateTest(test, errMsg); err != nil {
t.Fatal(err)
}
} else {
t.Fatalf("got error: %s; want error: %s", errMsg, test.want)
}
}
return
}
if test.wantError {
t.Fatalf("got success; want error %s", test.want)
}
got := bytes.TrimSpace(buf.Bytes())
if filepath.Separator != '/' {
got = bytes.ReplaceAll(got, []byte{filepath.Separator}, []byte{'/'})
}
if !bytes.Equal(got, test.want) {
if *updateGolden {
if err := updateTest(test, got); err != nil {
t.Fatal(err)
}
} else {
t.Fatalf("got:\n%s\n\nwant:\n%s", got, test.want)
}
}
if success != test.wantSuccess {
t.Fatalf("got success: %v; want success %v", success, test.wantSuccess)
}
}
}
// hgInit initialises a directory as a mercurial repo.
func hgInit(t *testing.T, dir string) {
t.Helper()
if err := os.Mkdir(filepath.Join(dir, ".hg"), 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, ".hg", "branch"), []byte("default"), 0777); err != nil {
t.Fatal(err)
}
}
// gitInit initialises a directory as a git repo, and adds a simple commit.
func gitInit(t *testing.T, dir string) {
t.Helper()
if !hasGit() {
t.Skip("PATH does not contain git")
}
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
for _, args := range [][]string{
{"git", "init"},
{"git", "config", "user.name", "Gopher"},
{"git", "config", "user.email", "gopher@golang.org"},
{"git", "checkout", "-b", "test"},
{"git", "add", "-A"},
{"git", "commit", "-m", "test"},
} {
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
cmdArgs := strings.Join(args, " ")
t.Fatalf("%s\n%s\nerror running %q on dir %s: %v", stdout.String(), stderr.String(), cmdArgs, dir, err)
}
}
}
// goModInit runs `go mod init` in the given directory.
func goModInit(t *testing.T, dir string) {
t.Helper()
aContents := `package a
const A = "a"`
if err := os.WriteFile(filepath.Join(dir, "a.go"), []byte(aContents), 0644); err != nil {
t.Fatal(err)
}
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd := exec.Command("go", "mod", "init", "example.com/uncommitted")
cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.Dir = dir
if err := cmd.Run(); err != nil {
t.Fatalf("error running `go mod init`: %s, %v", stderr.String(), err)
}
}
@@ -0,0 +1,87 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"path/filepath"
"strings"
)
// hasPathPrefix reports whether the slash-separated path s
// begins with the elements in prefix.
// Copied from cmd/go/internal/str.HasPathPrefix.
func hasPathPrefix(s, prefix string) bool {
if len(s) == len(prefix) {
return s == prefix
}
if prefix == "" {
return true
}
if len(s) > len(prefix) {
if prefix[len(prefix)-1] == '/' || s[len(prefix)] == '/' {
return s[:len(prefix)] == prefix
}
}
return false
}
// hasFilePathPrefix reports whether the filesystem path s
// begins with the elements in prefix.
// Copied from cmd/go/internal/str.HasFilePathPrefix.
func hasFilePathPrefix(s, prefix string) bool {
sv := strings.ToUpper(filepath.VolumeName(s))
pv := strings.ToUpper(filepath.VolumeName(prefix))
s = s[len(sv):]
prefix = prefix[len(pv):]
switch {
default:
return false
case pv != "" && sv != pv:
return false
case len(s) == len(prefix):
return s == prefix
case prefix == "":
return true
case len(s) > len(prefix):
if prefix[len(prefix)-1] == filepath.Separator {
return strings.HasPrefix(s, prefix)
}
return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
}
}
// trimFilePathPrefix returns the given filesystem path s without the leading
// prefix.
func trimFilePathPrefix(s, prefix string) string {
sv := strings.ToUpper(filepath.VolumeName(s))
pv := strings.ToUpper(filepath.VolumeName(prefix))
s = s[len(sv):]
prefix = prefix[len(pv):]
if !hasFilePathPrefix(s, prefix) || len(prefix) == 0 {
return s
}
if len(s) == len(prefix) {
return ""
}
if prefix[len(prefix)-1] == filepath.Separator {
return strings.TrimPrefix(s, prefix)
}
return s[len(prefix)+1:]
}
// trimPathPrefix returns p without the leading prefix. Unlike
// strings.TrimPrefix, the prefix will only match on slash-separted component
// boundaries, so trimPathPrefix("aa/b", "aa") returns "b", but
// trimPathPrefix("aa/b", "a") returns "aa/b".
func trimPathPrefix(p, prefix string) string {
if prefix == "" {
return p
}
if prefix == p {
return ""
}
return strings.TrimPrefix(p, prefix+"/")
}
@@ -0,0 +1,227 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"runtime"
"testing"
)
func TestHasPathPrefix(t *testing.T) {
for _, test := range []struct {
desc, path, prefix string
want bool
}{
{
desc: "empty_prefix",
path: "a/b",
prefix: "",
want: true,
}, {
desc: "partial_prefix",
path: "a/b",
prefix: "a",
want: true,
}, {
desc: "full_prefix",
path: "a/b",
prefix: "a/b",
want: true,
}, {
desc: "partial_component",
path: "aa/b",
prefix: "a",
want: false,
},
} {
t.Run(test.desc, func(t *testing.T) {
if got := hasPathPrefix(test.path, test.prefix); got != test.want {
t.Errorf("hasPathPrefix(%q, %q): got %v, want %v", test.path, test.prefix, got, test.want)
}
})
}
}
func TestHasFilePathPrefix(t *testing.T) {
type test struct {
desc, path, prefix string
want bool
}
var tests []test
if runtime.GOOS == "windows" {
tests = []test{
{
desc: "empty_prefix",
path: `c:\a\b`,
prefix: "",
want: true,
}, {
desc: "drive_prefix",
path: `c:\a\b`,
prefix: `c:\`,
want: true,
}, {
desc: "partial_prefix",
path: `c:\a\b`,
prefix: `c:\a`,
want: true,
}, {
desc: "full_prefix",
path: `c:\a\b`,
prefix: `c:\a\b`,
want: true,
}, {
desc: "partial_component",
path: `c:\aa\b`,
prefix: `c:\a`,
want: false,
},
}
} else {
tests = []test{
{
desc: "empty_prefix",
path: "/a/b",
prefix: "",
want: true,
}, {
desc: "partial_prefix",
path: "/a/b",
prefix: "/a",
want: true,
}, {
desc: "full_prefix",
path: "/a/b",
prefix: "/a/b",
want: true,
}, {
desc: "partial_component",
path: "/aa/b",
prefix: "/a",
want: false,
},
}
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
if got := hasFilePathPrefix(test.path, test.prefix); got != test.want {
t.Errorf("hasFilePathPrefix(%q, %q): got %v, want %v", test.path, test.prefix, got, test.want)
}
})
}
}
func TestTrimFilePathPrefix(t *testing.T) {
type test struct {
desc, path, prefix, want string
}
var tests []test
if runtime.GOOS == "windows" {
tests = []test{
// Note: these two cases in which the result preserves the leading \
// don't come up in reality in gorelease. That's because prefix is
// always far to the right of the path parts (ex github.com/foo/bar
// in C:\Users\foo\AppData\Local\Temp\...\github.com\foo\bar).
{
desc: "empty_prefix",
path: `c:\a\b`,
prefix: "",
want: `\a\b`,
}, {
desc: "partial_component",
path: `c:\aa\b`,
prefix: `c:\a`,
want: `\aa\b`,
},
{
desc: "drive_prefix",
path: `c:\a\b`,
prefix: `c:\`,
want: `a\b`,
}, {
desc: "partial_prefix",
path: `c:\a\b`,
prefix: `c:\a`,
want: `b`,
}, {
desc: "full_prefix",
path: `c:\a\b`,
prefix: `c:\a\b`,
want: "",
},
}
} else {
tests = []test{
{
desc: "empty_prefix",
path: "/a/b",
prefix: "",
want: "/a/b",
}, {
desc: "partial_prefix",
path: "/a/b",
prefix: "/a",
want: "b",
}, {
desc: "full_prefix",
path: "/a/b",
prefix: "/a/b",
want: "",
}, {
desc: "partial_component",
path: "/aa/b",
prefix: "/a",
want: "/aa/b",
},
}
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
if got := trimFilePathPrefix(test.path, test.prefix); got != test.want {
t.Errorf("hasFilePathPrefix(%q, %q): got %v, want %v", test.path, test.prefix, got, test.want)
}
})
}
}
func TestTrimPathPrefix(t *testing.T) {
for _, test := range []struct {
desc, path, prefix, want string
}{
{
desc: "empty_prefix",
path: "a/b",
prefix: "",
want: "a/b",
}, {
desc: "abs_empty_prefix",
path: "/a/b",
prefix: "",
want: "/a/b",
}, {
desc: "partial_prefix",
path: "a/b",
prefix: "a",
want: "b",
}, {
desc: "full_prefix",
path: "a/b",
prefix: "a/b",
want: "",
}, {
desc: "partial_component",
path: "aa/b",
prefix: "a",
want: "aa/b",
},
} {
t.Run(test.desc, func(t *testing.T) {
if got := trimPathPrefix(test.path, test.prefix); got != test.want {
t.Errorf("trimPathPrefix(%q, %q): got %q, want %q", test.path, test.prefix, got, test.want)
}
})
}
}
@@ -0,0 +1,206 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
"golang.org/x/mod/zip"
"golang.org/x/tools/txtar"
)
// buildProxyDir constructs a temporary directory suitable for use as a
// module proxy with a file:// URL. The caller is responsible for deleting
// the directory when it's no longer needed.
//
// proxyVersions must be a map of module version true. If proxyVersions is
// empty, all modules in mod/ will be included in the proxy list. If proxy
// versions is non-empty, only those modules in mod/ that match an entry in
// proxyVersions will be included.
func buildProxyDir(proxyVersions map[module.Version]bool, tests []*test) (proxyDir, proxyURL string, err error) {
proxyDir, err = os.MkdirTemp("", "gorelease-proxy")
if err != nil {
return "", "", err
}
txtarPaths, err := filepath.Glob(filepath.FromSlash("testdata/mod/*.txt"))
if err != nil {
return "", "", err
}
// Map of modPath to versions for that modPath.
versionLists := make(map[string][]string)
for _, t := range tests {
versionLists[t.modPath] = []string{}
modDir := filepath.Join(proxyDir, t.modPath, "@v")
if err := os.MkdirAll(modDir, 0777); err != nil {
return "", "", err
}
}
for _, txtarPath := range txtarPaths {
base := filepath.Base(txtarPath)
stem := base[:len(base)-len(".txt")]
i := strings.LastIndexByte(base, '_')
if i < 0 {
return "", "", fmt.Errorf("invalid module archive: %s", base)
}
modPath := strings.ReplaceAll(stem[:i], "_", "/")
version := stem[i+1:]
mv := module.Version{
Path: modPath,
Version: version,
}
// User has supplied proxyVersions. Honor proxy versions by only
// accepting those versions supplied in proxyVersions.
if len(proxyVersions) > 0 {
if !proxyVersions[mv] {
// modPath@version is not in proxyVersions: skip.
continue
}
}
versionLists[modPath] = append(versionLists[modPath], version)
modDir := filepath.Join(proxyDir, modPath, "@v")
if err := os.MkdirAll(modDir, 0777); err != nil {
return "", "", err
}
arc, err := txtar.ParseFile(txtarPath)
if err != nil {
return "", "", err
}
isCanonical := version == module.CanonicalVersion(version)
var zipContents []zip.File
var haveInfo, haveMod bool
var goMod txtar.File
for _, af := range arc.Files {
if !isCanonical && af.Name != ".info" {
return "", "", fmt.Errorf("%s: version is non-canonical but contains files other than .info", txtarPath)
}
if af.Name == ".info" || af.Name == ".mod" {
if af.Name == ".info" {
haveInfo = true
} else {
haveMod = true
}
outPath := filepath.Join(modDir, version+af.Name)
if err := os.WriteFile(outPath, af.Data, 0666); err != nil {
return "", "", err
}
continue
}
if af.Name == "go.mod" {
goMod = af
}
zipContents = append(zipContents, txtarFile{af})
}
if !isCanonical && !haveInfo {
return "", "", fmt.Errorf("%s: version is non-canonical but does not have .info", txtarPath)
}
if !haveInfo {
outPath := filepath.Join(modDir, version+".info")
outContent := fmt.Sprintf(`{"Version":"%s"}`, version)
if err := os.WriteFile(outPath, []byte(outContent), 0666); err != nil {
return "", "", err
}
}
if !haveMod && goMod.Name != "" {
outPath := filepath.Join(modDir, version+".mod")
if err := os.WriteFile(outPath, goMod.Data, 0666); err != nil {
return "", "", err
}
}
if len(zipContents) > 0 {
zipPath := filepath.Join(modDir, version+".zip")
zipFile, err := os.Create(zipPath)
if err != nil {
return "", "", err
}
defer zipFile.Close()
if err := zip.Create(zipFile, module.Version{Path: modPath, Version: version}, zipContents); err != nil {
return "", "", err
}
if err := zipFile.Close(); err != nil {
return "", "", err
}
}
}
buf := &bytes.Buffer{}
for modPath, versions := range versionLists {
outPath := filepath.Join(proxyDir, modPath, "@v", "list")
sort.Slice(versions, func(i, j int) bool {
return semver.Compare(versions[i], versions[j]) < 0
})
for _, v := range versions {
fmt.Fprintln(buf, v)
}
if err := os.WriteFile(outPath, buf.Bytes(), 0666); err != nil {
return "", "", err
}
buf.Reset()
}
// Make sure the URL path starts with a slash on Windows. Absolute paths
// normally start with a drive letter.
// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
if strings.HasPrefix(proxyDir, "/") {
proxyURL = "file://" + proxyDir
} else {
proxyURL = "file:///" + filepath.FromSlash(proxyDir)
}
return proxyDir, proxyURL, nil
}
type txtarFile struct {
f txtar.File
}
func (f txtarFile) Path() string { return f.f.Name }
func (f txtarFile) Lstat() (os.FileInfo, error) { return txtarFileInfo{f.f}, nil }
func (f txtarFile) Open() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(f.f.Data)), nil
}
type txtarFileInfo struct {
f txtar.File
}
func (f txtarFileInfo) Name() string { return f.f.Name }
func (f txtarFileInfo) Size() int64 { return int64(len(f.f.Data)) }
func (f txtarFileInfo) Mode() os.FileMode { return 0444 }
func (f txtarFileInfo) ModTime() time.Time { return time.Time{} }
func (f txtarFileInfo) IsDir() bool { return false }
func (f txtarFileInfo) Sys() interface{} { return nil }
func extractTxtar(destDir string, arc *txtar.Archive) error {
for _, f := range arc.Files {
outPath := filepath.Join(destDir, f.Name)
if err := os.MkdirAll(filepath.Dir(outPath), 0777); err != nil {
return err
}
if err := os.WriteFile(outPath, f.Data, 0666); err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,463 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"strings"
"golang.org/x/exp/apidiff"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
"golang.org/x/tools/go/packages"
)
// report describes the differences in the public API between two versions
// of a module.
type report struct {
// base contains information about the "old" module version being compared
// against. base.version may be "none", indicating there is no base version
// (for example, if this is the first release). base.version may not be "".
base moduleInfo
// release contains information about the version of the module to release.
// The version may be set explicitly with -version or suggested using
// suggestVersion, in which case release.versionInferred is true.
release moduleInfo
// packages is a list of package reports, describing the differences
// for individual packages, sorted by package path.
packages []packageReport
// versionInvalid explains why the proposed or suggested version is not valid.
versionInvalid *versionMessage
// haveCompatibleChanges is true if there are any backward-compatible
// changes in non-internal packages.
haveCompatibleChanges bool
// haveIncompatibleChanges is true if there are any backward-incompatible
// changes in non-internal packages.
haveIncompatibleChanges bool
// haveBaseErrors is true if there were errors loading packages
// in the base version.
haveBaseErrors bool
// haveReleaseErrors is true if there were errors loading packages
// in the release version.
haveReleaseErrors bool
}
// String returns a human-readable report that lists errors, compatible changes,
// and incompatible changes in each package. If releaseVersion is set, the
// report states whether releaseVersion is valid (and why). If releaseVersion is
// not set, it suggests a new version.
func (r *report) String() string {
buf := &strings.Builder{}
for _, p := range r.packages {
buf.WriteString(p.String())
}
if !r.canVerifyReleaseVersion() {
return buf.String()
}
if len(r.release.diagnostics) > 0 {
buf.WriteString("# diagnostics\n")
for _, d := range r.release.diagnostics {
fmt.Fprintln(buf, d)
}
buf.WriteByte('\n')
}
buf.WriteString("# summary\n")
baseVersion := r.base.version
if r.base.modPath != r.release.modPath {
baseVersion = r.base.modPath + "@" + baseVersion
}
if r.base.versionInferred {
fmt.Fprintf(buf, "Inferred base version: %s\n", baseVersion)
} else if r.base.versionQuery != "" {
fmt.Fprintf(buf, "Base version: %s (%s)\n", baseVersion, r.base.versionQuery)
}
if r.versionInvalid != nil {
fmt.Fprintln(buf, r.versionInvalid)
} else if r.release.versionInferred {
if r.release.tagPrefix == "" {
fmt.Fprintf(buf, "Suggested version: %s\n", r.release.version)
} else {
fmt.Fprintf(buf, "Suggested version: %[1]s (with tag %[2]s%[1]s)\n", r.release.version, r.release.tagPrefix)
}
} else if r.release.version != "" {
if r.release.tagPrefix == "" {
fmt.Fprintf(buf, "%s is a valid semantic version for this release.\n", r.release.version)
if semver.Compare(r.release.version, "v0.0.0-99999999999999-zzzzzzzzzzzz") < 0 {
fmt.Fprintf(buf, `Note: %s sorts lower in MVS than pseudo-versions, which may be
unexpected for users. So, it may be better to choose a different suffix.`, r.release.version)
}
} else {
fmt.Fprintf(buf, "%[1]s (with tag %[2]s%[1]s) is a valid semantic version for this release\n", r.release.version, r.release.tagPrefix)
}
}
if r.versionInvalid == nil && r.haveBaseErrors {
fmt.Fprintln(buf, "Errors were found in the base version. Some API changes may be omitted.")
}
return buf.String()
}
func (r *report) addPackage(p packageReport) {
r.packages = append(r.packages, p)
if len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
// Only count compatible and incompatible changes if there were no errors.
// When there are errors, definitions may be missing, and fixes may appear
// incompatible when they are not. Changes will still be reported, but
// they won't affect version validation or suggestions.
for _, c := range p.Changes {
if !c.Compatible && len(p.releaseErrors) == 0 {
r.haveIncompatibleChanges = true
} else if c.Compatible && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
r.haveCompatibleChanges = true
}
}
}
if len(p.baseErrors) > 0 {
r.haveBaseErrors = true
}
if len(p.releaseErrors) > 0 {
r.haveReleaseErrors = true
}
}
// validateReleaseVersion checks whether r.release.version is valid.
// If r.release.version is not valid, an error is returned explaining why.
// r.release.version must be set.
func (r *report) validateReleaseVersion() {
if r.release.version == "" {
panic("validateVersion called without version")
}
setNotValid := func(format string, args ...interface{}) {
r.versionInvalid = &versionMessage{
message: fmt.Sprintf("%s is not a valid semantic version for this release.", r.release.version),
reason: fmt.Sprintf(format, args...),
}
}
if r.haveReleaseErrors {
if r.haveReleaseErrors {
setNotValid("Errors were found in one or more packages.")
return
}
}
// TODO(jayconrod): link to documentation for all of these errors.
// Check that the major version matches the module path.
_, suffix, ok := module.SplitPathVersion(r.release.modPath)
if !ok {
setNotValid("%s: could not find version suffix in module path", r.release.modPath)
return
}
if suffix != "" {
if suffix[0] != '/' && suffix[0] != '.' {
setNotValid("%s: unknown module path version suffix: %q", r.release.modPath, suffix)
return
}
pathMajor := suffix[1:]
major := semver.Major(r.release.version)
if pathMajor != major {
setNotValid(`The major version %s does not match the major version suffix
in the module path: %s`, major, r.release.modPath)
return
}
} else if major := semver.Major(r.release.version); major != "v0" && major != "v1" {
setNotValid(`The module path does not end with the major version suffix /%s,
which is required for major versions v2 or greater.`, major)
return
}
for _, v := range r.base.existingVersions {
if semver.Compare(v, r.release.version) == 0 {
setNotValid("version %s already exists", v)
}
}
// Check that compatible / incompatible changes are consistent.
if semver.Major(r.base.version) == "v0" || r.base.modPath != r.release.modPath {
return
}
if r.haveIncompatibleChanges {
setNotValid("There are incompatible changes.")
return
}
if r.haveCompatibleChanges && semver.MajorMinor(r.base.version) == semver.MajorMinor(r.release.version) {
setNotValid(`There are compatible changes, but the minor version is not incremented
over the base version (%s).`, r.base.version)
return
}
if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, r.release.version) > 0 {
setNotValid(`Module indirectly depends on a higher version of itself (%s).
`, r.release.highestTransitiveVersion)
}
}
// suggestReleaseVersion suggests a new version consistent with observed
// changes.
func (r *report) suggestReleaseVersion() {
setNotValid := func(format string, args ...interface{}) {
r.versionInvalid = &versionMessage{
message: "Cannot suggest a release version.",
reason: fmt.Sprintf(format, args...),
}
}
setVersion := func(v string) {
r.release.version = v
r.release.versionInferred = true
}
if r.base.modPath != r.release.modPath {
setNotValid("Base module path is different from release.")
return
}
if r.haveReleaseErrors || r.haveBaseErrors {
setNotValid("Errors were found.")
return
}
var major, minor, patch, pre string
if r.base.version != "none" {
minVersion := r.base.version
if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, minVersion) > 0 {
setNotValid("Module indirectly depends on a higher version of itself (%s) than the base version (%s).", r.release.highestTransitiveVersion, r.base.version)
return
}
var err error
major, minor, patch, pre, _, err = parseVersion(minVersion)
if err != nil {
panic(fmt.Sprintf("could not parse base version: %v", err))
}
}
if r.haveIncompatibleChanges && r.base.version != "none" && pre == "" && major != "0" {
setNotValid("Incompatible changes were detected.")
return
// TODO(jayconrod): briefly explain how to prepare major version releases
// and link to documentation.
}
// Check whether we're comparing to the latest version of base.
//
// This could happen further up, but we want the more pressing errors above
// to take precedence.
var latestForBaseMajor string
for _, v := range r.base.existingVersions {
if semver.Major(v) != semver.Major(r.base.version) {
continue
}
if latestForBaseMajor == "" || semver.Compare(latestForBaseMajor, v) < 0 {
latestForBaseMajor = v
}
}
if latestForBaseMajor != "" && latestForBaseMajor != r.base.version {
setNotValid(fmt.Sprintf("Can only suggest a release version when compared against the most recent version of this major: %s.", latestForBaseMajor))
return
}
if r.base.version == "none" {
if _, pathMajor, ok := module.SplitPathVersion(r.release.modPath); !ok {
panic(fmt.Sprintf("could not parse module path %q", r.release.modPath))
} else if pathMajor == "" {
setVersion("v0.1.0")
} else {
setVersion(pathMajor[1:] + ".0.0")
}
return
}
if pre != "" {
// suggest non-prerelease version
} else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") || r.requirementsChanged() {
minor = incDecimal(minor)
patch = "0"
} else {
patch = incDecimal(patch)
}
setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch))
return
}
// canVerifyReleaseVersion returns true if we can safely suggest a new version
// or if we can verify the version passed in with -version is safe to tag.
func (r *report) canVerifyReleaseVersion() bool {
// For now, return true if the base and release module paths are the same,
// ignoring the major version suffix.
// TODO(#37562, #39192, #39666, #40267): there are many more situations when
// we can't verify a new version.
basePath := strings.TrimSuffix(r.base.modPath, r.base.modPathMajor)
releasePath := strings.TrimSuffix(r.release.modPath, r.release.modPathMajor)
return basePath == releasePath
}
// requirementsChanged reports whether requirements have changed from base to
// version.
//
// requirementsChanged reports true for,
// - A requirement was upgraded to a higher minor version.
// - A requirement was added.
// - The version of Go was incremented.
//
// It does not report true when, for example, a requirement was downgraded or
// remove. We care more about the former since that might force dependent
// modules that have the same dependency to upgrade.
func (r *report) requirementsChanged() bool {
if r.base.goModFile == nil {
// There wasn't a modfile before, and now there is.
return true
}
// baseReqs is a map of module path to MajorMinor of the base module
// requirements.
baseReqs := make(map[string]string)
for _, r := range r.base.goModFile.Require {
baseReqs[r.Mod.Path] = r.Mod.Version
}
for _, r := range r.release.goModFile.Require {
if _, ok := baseReqs[r.Mod.Path]; !ok {
// A module@version was added to the "require" block between base
// and release.
return true
}
if semver.Compare(semver.MajorMinor(r.Mod.Version), semver.MajorMinor(baseReqs[r.Mod.Path])) > 0 {
// The version of r.Mod.Path increased from base to release.
return true
}
}
if r.release.goModFile.Go != nil && r.base.goModFile.Go != nil {
if r.release.goModFile.Go.Version > r.base.goModFile.Go.Version {
// The Go version increased from base to release.
return true
}
}
return false
}
// isSuccessful returns true the module appears to be safe to release at the
// proposed or suggested version.
func (r *report) isSuccessful() bool {
return len(r.release.diagnostics) == 0 && r.versionInvalid == nil
}
type versionMessage struct {
message, reason string
}
func (m versionMessage) String() string {
return m.message + "\n" + m.reason + "\n"
}
// incDecimal returns the decimal string incremented by 1.
func incDecimal(decimal string) string {
// Scan right to left turning 9s to 0s until you find a digit to increment.
digits := []byte(decimal)
i := len(digits) - 1
for ; i >= 0 && digits[i] == '9'; i-- {
digits[i] = '0'
}
if i >= 0 {
digits[i]++
} else {
// digits is all zeros
digits[0] = '1'
digits = append(digits, '0')
}
return string(digits)
}
type packageReport struct {
apidiff.Report
path string
baseErrors, releaseErrors []packages.Error
}
func (p *packageReport) String() string {
if len(p.Changes) == 0 && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
return ""
}
buf := &strings.Builder{}
fmt.Fprintf(buf, "# %s\n", p.path)
if len(p.baseErrors) > 0 {
fmt.Fprintf(buf, "## errors in base version:\n")
for _, e := range p.baseErrors {
fmt.Fprintln(buf, e)
}
buf.WriteByte('\n')
}
if len(p.releaseErrors) > 0 {
fmt.Fprintf(buf, "## errors in release version:\n")
for _, e := range p.releaseErrors {
fmt.Fprintln(buf, e)
}
buf.WriteByte('\n')
}
if len(p.Changes) > 0 {
var compatible, incompatible []apidiff.Change
for _, c := range p.Changes {
if c.Compatible {
compatible = append(compatible, c)
} else {
incompatible = append(incompatible, c)
}
}
if len(incompatible) > 0 {
fmt.Fprintf(buf, "## incompatible changes\n")
for _, c := range incompatible {
fmt.Fprintln(buf, c.Message)
}
}
if len(compatible) > 0 {
fmt.Fprintf(buf, "## compatible changes\n")
for _, c := range compatible {
fmt.Fprintln(buf, c.Message)
}
}
buf.WriteByte('\n')
}
return buf.String()
}
// parseVersion returns the major, minor, and patch numbers, prerelease text,
// and metadata for a given version.
//
// TODO(jayconrod): extend semver to do this and delete this function.
func parseVersion(vers string) (major, minor, patch, pre, meta string, err error) {
if !strings.HasPrefix(vers, "v") {
return "", "", "", "", "", fmt.Errorf("version %q does not start with 'v'", vers)
}
base := vers[1:]
if i := strings.IndexByte(base, '+'); i >= 0 {
meta = base[i+1:]
base = base[:i]
}
if i := strings.IndexByte(base, '-'); i >= 0 {
pre = base[i+1:]
base = base[:i]
}
parts := strings.Split(base, ".")
if len(parts) != 3 {
return "", "", "", "", "", fmt.Errorf("version %q should have three numbers", vers)
}
major, minor, patch = parts[0], parts[1], parts[2]
return major, minor, patch, pre, meta, nil
}
@@ -0,0 +1,95 @@
This directory contains most tests for gorelease. Each test runs gorelease (the
`runRelease` function) with a given set of flags in a temporary directory
populated with files specified in the test itself or files from the module test
proxy. The output is compared against a golden `want` file specified in the
test.
## Test flags
A specific test may be run with a command like:
go test -run=TestRelease/basic/v0_patch_suggest
where `basic/v0_patch_suggest` matches the file
`testdata/basic/v0_patch_suggest.test`.
The `-u` flag adds or updates the `want` file in each test to match the output.
This is useful for fixing tests after an intended change in behavior.
go test -run=TestRelease/basic/v0_patch_suggest -u
The `-testwork` flag instructs the test framework to leave the test's temporary
directory and module proxy in place after running the test. This is useful
for debugging.
## Test format
Tests are written in `.test` files in `testdata` subdirectories. Each `.test`
file is a valid txtar file (see `golang.org/x/tools/txtar`). The comment section
contains the test parameters, which are a series of `key=value` pairs. Blank
lines and comments starting with `#` are allowed in this section. Valid keys
are:
* `mod`: sets the module path. Must be specified together with `version`. Copies
the content of a module out of the test proxy into a temporary directory
where `gorelease` is run.
* `version`: specified together with `mod`, it sets the version to retrieve from
the test proxy. See more information below.
* `base`: the value of the `-base` flag passed to `gorelease`.
* `release`: the value of the `-version` flag passed to `gorelease`.
* `dir`: the directory where `gorelease` should be invoked. Useful when the test
describes a whole repository, and `gorelease` should be invoked in a
subdirectory.
* `error`: true if the test expects a hard error. False by default.
* `success`: true if the test expects a report to be printed with no errors
or diagnostics. True by default.
* `skip`: non-empty if the test should be skipped. The value is a string passed
to `t.Skip`.
* `proxyVersions`: empty if the test should include all `mod/` entries in the
proxy, or else a comma-separated list of the modpath@version's it should
include.
Test archives have a file named `want`, containing the expected output of the
test. A test will fail if the actual output differs from `want`.
If the `mod` and `version` parameters are not set, other files will be extracted
to the temporary directory where `gorelease` runs.
## Populating module contents
When building a test in `testdata/`, there are two ways to populate the module
directory being tested:
### Option 1: inline
You can inline files in the `.test` folder as described in
https://pkg.go.dev/golang.org/x/tools/txtar#hdr-Txtar_format. For example,
```
-- some.file --
the contents
of the file
```
### Option 2: specify an existing file
Often, multiple tests want to share the same setup - the same files. So, users
can write these common files in `testdata/mod/`, and use one of these files as
the module directory contents.
To specify a file in `testdata/mod/` to use as the module contents.
## Module format
Tests run with `GOPROXY` set to a local URL that points to a test proxy. The
test proxy serves modules described by `.txt` files in the `testdata/mod/`
subdirectory.
Each module is a txtar archive named `$modpath_$version.txt` where `$modpath`
is the module path (with slashes replaced with underscores) and `$version` is
the version. If the archive contains a file named `.mod`, that will be used to
respond to `.mod` requests; otherwise, `go.mod` will be used (`.mod` is only
necessary for modules that lack `go.mod` files). If the archive contains a
file named `.info`, that will be used to respond to `.info` requests; otherwise,
`.info` is synthesized from the version. All other files in the archive are
packed into a `.zip` file to satisfy `.zip` requests.
@@ -0,0 +1,24 @@
mod=example.com/basic
base=v1.0.1
success=false
# A() was removed, which is a breaking change: it shouldn't try to suggest a
# higher version.
-- want --
# example.com/basic/a
## incompatible changes
A: removed
## compatible changes
B: added
# summary
Cannot suggest a release version.
Incompatible changes were detected.
-- go.mod --
module example.com/basic
go 1.12
-- a/a.go --
package a
func B() int { return 0 }
@@ -0,0 +1,23 @@
mod=example.com/basic
base=v0.0.1
success=false
# B() was added, so now it should suggest a new minor version. But, there's a
# later version that already exists: so it should not try to suggest anything at
# all.
-- want --
# example.com/basic/a
## compatible changes
B: added
# summary
Cannot suggest a release version.
Can only suggest a release version when compared against the most recent version of this major: v0.1.2.
-- go.mod --
module example.com/basic
go 1.12
-- a/a.go --
package a
func A() int { return 0 }
func B() int { return 0 }
@@ -0,0 +1,23 @@
mod=example.com/basic
base=v0.1.0
success=false
# A() was changed in a small way, so now it should suggest a new patch version.
# But, there's a later version that already exists: so it should not try to
# suggest anything at all.
-- want --
# summary
Cannot suggest a release version.
Can only suggest a release version when compared against the most recent version of this major: v0.1.2.
-- go.mod --
module example.com/basic
go 1.12
-- a/a.go --
package a
func A() int { return 1 }
func A2() int { return 2 }
-- b/b.go --
package b
func B() int { return 3 }
@@ -0,0 +1,18 @@
mod=example.com/basic
base=v0.0.1
release=v0.1.0
success=false
# The contents below are a copy of the v0.0.1 contents - nothing has changed.
# But v0.1.0 already exists, so it should present a diagnostic.
-- want --
# summary
v0.1.0 is not a valid semantic version for this release.
version v0.1.0 already exists
-- go.mod --
module example.com/basic
go 1.12
-- a/a.go --
package a
func A() int { return 0 }
@@ -0,0 +1,11 @@
Module example.com/basic tests basic functionality of gorelease.
It verifies that versions are correctly suggested or verified after
various changes.
All revisions are stored in the mod directory. The same series of
changes is made across three major versions, v0, v1, and v2:
vX.0.1 - simple package
vX.1.0 - compatible change: add a function and a package
vX.1.1 - internal change: function returns different value
vX.1.2 - incompatible change: delete a function and a package
@@ -0,0 +1,15 @@
mod=example.com/basic
version=v0.1.0
base=v0.0.1
proxyVersions=example.com/basic@v0.0.1
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/b
## compatible changes
package added
# summary
Suggested version: v0.1.0
@@ -0,0 +1,16 @@
mod=example.com/basic
version=v0.1.0
base=v0.0.1
proxyVersions=example.com/basic@v0.0.1
vcs=git
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/b
## compatible changes
package added
# summary
Suggested version: v0.1.0
@@ -0,0 +1,16 @@
mod=example.com/basic
version=v0.1.0
base=v0.0.1
proxyVersions=example.com/basic@v0.0.1
vcs=hg
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/b
## compatible changes
package added
# summary
Suggested version: v0.1.0
@@ -0,0 +1,16 @@
mod=example.com/basic
version=v0.1.0
base=v0.0.1
release=v0.1.0
proxyVersions=example.com/basic@v0.0.1
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/b
## compatible changes
package added
# summary
v0.1.0 is a valid semantic version for this release.
@@ -0,0 +1,15 @@
mod=example.com/basic
version=v0.1.2
base=v0.1.1
proxyVersions=example.com/basic@v0.1.1
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
Suggested version: v0.2.0
@@ -0,0 +1,16 @@
mod=example.com/basic
version=v0.1.2
base=v0.1.1
release=v0.1.2
proxyVersions=example.com/basic@v0.1.1
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
v0.1.2 is a valid semantic version for this release.
@@ -0,0 +1,6 @@
mod=example.com/basic
version=v0.1.1
base=none
-- want --
# summary
Suggested version: v0.1.0
@@ -0,0 +1,7 @@
mod=example.com/basic
version=v0.1.1
base=v0.1.0
proxyVersions=example.com/basic@v0.1.0
-- want --
# summary
Suggested version: v0.1.1
@@ -0,0 +1,8 @@
mod=example.com/basic
version=v0.1.1
base=v0.1.0
release=v0.1.1
proxyVersions=example.com/basic@v0.1.0
-- want --
# summary
v0.1.1 is a valid semantic version for this release.
@@ -0,0 +1,15 @@
mod=example.com/basic
version=v0.1.2
base=v0.1.1-pre
proxyVersions=example.com/basic@v0.1.1-pre
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
Suggested version: v0.1.1
@@ -0,0 +1,15 @@
mod=example.com/basic
version=v0.1.2
base=v0.1.1
release=v1.0.0
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
v1.0.0 is a valid semantic version for this release.
@@ -0,0 +1,6 @@
mod=example.com/basic
version=v0.1.2
-- want --
# summary
Inferred base version: v1.1.2
Suggested version: v1.1.3
@@ -0,0 +1,7 @@
mod=example.com/basic
version=v1.0.1
release=v1.0.2
-- want --
# summary
Inferred base version: v1.0.1
v1.0.2 is a valid semantic version for this release.
@@ -0,0 +1,15 @@
mod=example.com/basic
version=v1.1.0
base=v1.0.1
proxyVersions=example.com/basic@v1.0.1
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/b
## compatible changes
package added
# summary
Suggested version: v1.1.0
@@ -0,0 +1,16 @@
mod=example.com/basic
version=v1.1.0
base=v1.0.1
release=v1.1.0
proxyVersions=example.com/basic@v1.0.1
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/b
## compatible changes
package added
# summary
v1.1.0 is a valid semantic version for this release.
@@ -0,0 +1,11 @@
mod=example.com/basicfork
base=example.com/basic@v1.1.1
version=v1.1.2
release=v1.1.2
proxyVersions=example.com/basic@v1.1.1
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
## compatible changes
A3: added
@@ -0,0 +1,15 @@
# Compare a fork (with module path example.com/basic, downloaded from
# example.com/basicfork) with a local module (with module path
# example.com/basic).
mod=example.com/basic
version=v1.1.2
base=example.com/basicfork@v1.1.2
release=v1.1.3
-- want --
# example.com/basicfork/a
## incompatible changes
A3: removed
# example.com/basicfork/b
## incompatible changes
package removed
@@ -0,0 +1,16 @@
mod=example.com/basic
version=v1.1.2
base=v1.1.1
success=false
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
Cannot suggest a release version.
Incompatible changes were detected.
@@ -0,0 +1,18 @@
mod=example.com/basic
version=v1.1.2
base=v1.1.1
success=false
release=v1.1.2
proxyVersions=example.com/basic@v1.1.1
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
v1.1.2 is not a valid semantic version for this release.
There are incompatible changes.
@@ -0,0 +1,7 @@
mod=example.com/basic
version=v1.1.1
base=v1.1.0
proxyVersions=example.com/basic@v1.1.0
-- want --
# summary
Suggested version: v1.1.1
@@ -0,0 +1,8 @@
mod=example.com/basic
version=v1.1.1
base=v1.1.0
release=v1.1.1
proxyVersions=example.com/basic@v1.1.0
-- want --
# summary
v1.1.1 is a valid semantic version for this release.
@@ -0,0 +1,15 @@
mod=example.com/basic
version=v1.1.2
base=v1.1.1-pre
proxyVersions=example.com/basic@v1.1.1-pre
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
Suggested version: v1.1.1
@@ -0,0 +1,7 @@
mod=example.com/basic
version=v1.0.1
release=v1.0.1
base=>v1.0.1
error=true
-- want --
base version v1.1.0 (>v1.0.1) must be lower than release version v1.0.1
@@ -0,0 +1,8 @@
mod=example.com/basic
version=v1.0.1
base=version-1.0.1
proxyVersions=example.com/basic@version-1.0.1,example.com/basic@v1.0.1
-- want --
# summary
Base version: v1.0.1 (version-1.0.1)
Suggested version: v1.0.2
@@ -0,0 +1,8 @@
mod=example.com/basic
version=v1.0.1
base=version-1.0.1
release=v1.0.2
-- want --
# summary
Base version: v1.0.1 (version-1.0.1)
v1.0.2 is a valid semantic version for this release.
@@ -0,0 +1,17 @@
mod=example.com/basic/v2
base=example.com/basic@>=v1.1.0
version=v2.0.1
release=v2.0.1
proxyVersions=example.com/basic@v1.1.0
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
Base version: example.com/basic@v1.1.0 (>=v1.1.0)
v2.0.1 is a valid semantic version for this release.
@@ -0,0 +1,17 @@
mod=example.com/basic/v2
base=example.com/basic
version=v2.1.0
success=false
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/v2/b
## compatible changes
package added
# summary
Inferred base version: example.com/basic@v1.1.2
Cannot suggest a release version.
Base module path is different from release.
@@ -0,0 +1,17 @@
mod=example.com/basic/v2
base=example.com/basic
version=v2.1.0
release=v2.1.0
proxyVersions=example.com/basic@v1.1.2
-- want --
# example.com/basic/a
## compatible changes
A2: added
# example.com/basic/v2/b
## compatible changes
package added
# summary
Inferred base version: example.com/basic@v1.1.2
v2.1.0 is a valid semantic version for this release.
@@ -0,0 +1,16 @@
mod=example.com/basic/v2
base=example.com/basic@v1.1.0
version=v2.0.1
release=v2.0.1
proxyVersions=example.com/basic@v1.1.0
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
v2.0.1 is a valid semantic version for this release.
@@ -0,0 +1,8 @@
mod=example.com/basic/v2
base=v1.1.0
version=v2.1.0
success=false
-- want --
# summary
Cannot suggest a release version.
Base module path is different from release.
@@ -0,0 +1,7 @@
mod=example.com/basic/v2
base=v1.1.0
version=v2.1.0
release=v2.1.0
-- want --
# summary
v2.1.0 is a valid semantic version for this release.
@@ -0,0 +1,18 @@
mod=example.com/basic
version=v1.1.2
base=v1.1.1
release=v2.0.0
success=false
-- want --
# example.com/basic/a
## incompatible changes
A2: removed
# example.com/basic/b
## incompatible changes
package removed
# summary
v2.0.0 is not a valid semantic version for this release.
The module path does not end with the major version suffix /v2,
which is required for major versions v2 or greater.
@@ -0,0 +1,15 @@
mod=example.com/basic/v2
version=v2.1.0
base=v2.0.1
proxyVersions=example.com/basic/v2@v2.0.1
-- want --
# example.com/basic/v2/a
## compatible changes
A2: added
# example.com/basic/v2/b
## compatible changes
package added
# summary
Suggested version: v2.1.0
@@ -0,0 +1,16 @@
mod=example.com/basic/v2
version=v2.1.0
base=v2.0.1
release=v2.1.0
proxyVersions=example.com/basic/v2@v2.0.1
-- want --
# example.com/basic/v2/a
## compatible changes
A2: added
# example.com/basic/v2/b
## compatible changes
package added
# summary
v2.1.0 is a valid semantic version for this release.
@@ -0,0 +1,16 @@
mod=example.com/basic/v2
version=v2.1.2
base=v2.1.1
success=false
-- want --
# example.com/basic/v2/a
## incompatible changes
A2: removed
# example.com/basic/v2/b
## incompatible changes
package removed
# summary
Cannot suggest a release version.
Incompatible changes were detected.
@@ -0,0 +1,18 @@
mod=example.com/basic/v2
version=v2.1.2
base=v2.1.1
success=false
release=v2.1.2
proxyVersions=example.com/basic/v2@v2.1.1
-- want --
# example.com/basic/v2/a
## incompatible changes
A2: removed
# example.com/basic/v2/b
## incompatible changes
package removed
# summary
v2.1.2 is not a valid semantic version for this release.
There are incompatible changes.
@@ -0,0 +1,6 @@
mod=example.com/basic/v2
version=v2.1.1
base=none
-- want --
# summary
Suggested version: v2.0.0
@@ -0,0 +1,7 @@
mod=example.com/basic/v2
version=v2.1.1
base=v2.1.0
proxyVersions=example.com/basic/v2@v2.1.0
-- want --
# summary
Suggested version: v2.1.1
@@ -0,0 +1,8 @@
mod=example.com/basic/v2
version=v2.1.1
base=v2.1.0
release=v2.1.1
proxyVersions=example.com/basic/v2@v2.1.0
-- want --
# summary
v2.1.1 is a valid semantic version for this release.
@@ -0,0 +1,15 @@
mod=example.com/basic/v2
version=v2.1.2
base=v2.1.1-pre
proxyVersions=example.com/basic/v2@v2.1.1-pre
-- want --
# example.com/basic/v2/a
## incompatible changes
A2: removed
# example.com/basic/v2/b
## incompatible changes
package removed
# summary
Suggested version: v2.1.1
@@ -0,0 +1,9 @@
mod=example.com/basic/v3
-- go.mod --
module example.com/basic/v3
go 1.13
-- want --
# summary
Inferred base version: none
Suggested version: v3.0.0
@@ -0,0 +1,6 @@
mod=example.com/basic/v3
version=v3.0.0-ignore
release=v3.1.0
error=true
-- want --
could not find base version. Consider setting -version=v3.0.0 if this is a first release, or explicitly set -base=none: no versions found lower than v3.1.0
@@ -0,0 +1,7 @@
mod=example.com/basic/v3
version=v3.0.0-ignore
release=v3.0.0
-- want --
# summary
Inferred base version: none
v3.0.0 is a valid semantic version for this release.
@@ -0,0 +1,7 @@
Module example.com/cgo is used to test that packages with cgo code
can be loaded without errors when cgo is enabled.
TODO(jayconrod): test modules with cgo-only and cgo / pure Go implementations
with CGO_ENABLED=0 and 1. But first, decide how multiple platforms and
build constraints should be handled. Currently, gorelease only considers
the same configuration as 'go list'.
@@ -0,0 +1,16 @@
base=none
release=v1.0.0
-- go.mod --
module example.com/cgo
go 1.13
-- c.go --
package cgo
// const int x = 12;
import "C"
func X() int { return int(C.x) }
-- want --
# summary
v1.0.0 is a valid semantic version for this release.
@@ -0,0 +1 @@
This directory is for tests related to module cycles.
@@ -0,0 +1,6 @@
mod=example.com/cycle/v2
version=v2.0.0
-- want --
# summary
Inferred base version: v2.0.0
Suggested version: v2.0.1
@@ -0,0 +1,8 @@
mod=example.com/cycle
base=v1.0.0
version=v1.0.0
success=false
-- want --
# summary
Cannot suggest a release version.
Module indirectly depends on a higher version of itself (v1.5.0) than the base version (v1.0.0).
@@ -0,0 +1,9 @@
mod=example.com/cycle
base=v1.0.0
version=v1.0.0
release=v1.0.1
success=false
-- want --
# summary
v1.0.1 is not a valid semantic version for this release.
Module indirectly depends on a higher version of itself (v1.5.0).
@@ -0,0 +1,2 @@
Module example.com/empty is used to test that gorelease works
in a module with no packages.
@@ -0,0 +1,8 @@
mod=example.com/empty
base=v0.0.1
version=v0.0.2
release=v0.0.2
proxyVersions=example.com/empty@v0.0.1
-- want --
# summary
v0.0.2 is a valid semantic version for this release.
@@ -0,0 +1,2 @@
Tests in this directory check that user errors invoking gorelease
are correctly reported.
@@ -0,0 +1,17 @@
mod=example.com/ziperrors
dir=x
base=none
error=true
vcs=git
-- want --
testdata/this_file_also_has_a_bad_filename'.txt: malformed file path "testdata/this_file_also_has_a_bad_filename'.txt": invalid char '\''
testdata/this_file_has_a_bad_filename'.txt: malformed file path "testdata/this_file_has_a_bad_filename'.txt": invalid char '\''
-- x/go.mod --
module example.com/x
go 1.12
-- x/testdata/this_file_has_a_bad_filename'.txt --
-- x/testdata/this_file_also_has_a_bad_filename'.txt --
# Verify that errors in submodules are not reported.
-- x/y/go.mod --
-- x/y/submodule_bad_filename'.txt --
@@ -0,0 +1,8 @@
mod=example.com/errors
base=v0.1.0
release=master
error=true
-- want --
usage: gorelease [-base=version] [-version=version]
release version "master" is not a canonical semantic version
For more information, run go doc golang.org/x/exp/cmd/gorelease
@@ -0,0 +1,8 @@
mod=example.com/errors
base=v0.2.0
release=v0.1.0
error=true
-- want --
usage: gorelease [-base=version] [-version=version]
base version ("v0.2.0") must be lower than release version ("v0.1.0")
For more information, run go doc golang.org/x/exp/cmd/gorelease
@@ -0,0 +1,7 @@
mod=example.com/basic/v2
base=example.com/basic@none
error=true
-- want --
usage: gorelease [-base=version] [-version=version]
base version ("example.com/basic@none") cannot have version "none" with explicit module path
For more information, run go doc golang.org/x/exp/cmd/gorelease
@@ -0,0 +1,38 @@
mod=example.com/errors
version=v0.2.0
base=v0.1.0
release=v0.2.0
success=false
proxyVersions=example.com/errors@v0.1.0
-- want --
# example.com/errors/added
## errors in release version:
added/added.go:3:15: undefined: Missing
## compatible changes
package added
# example.com/errors/broken
## errors in release version:
broken/broken.go:3:15: undefined: Missing
## incompatible changes
X: value changed from 12 to unknown
# example.com/errors/deleted
## errors in base version:
deleted/deleted.go:3:15: undefined: Missing
## incompatible changes
package removed
# example.com/errors/fixed
## errors in base version:
fixed/fixed.go:3:15: undefined: Missing
## incompatible changes
X: value changed from unknown to 12
# summary
v0.2.0 is not a valid semantic version for this release.
Errors were found in one or more packages.
@@ -0,0 +1,8 @@
mod=example.com/errors
base=v0.1.0
release=v0.1.0
error=true
-- want --
usage: gorelease [-base=version] [-version=version]
-base and -version must be different
For more information, run go doc golang.org/x/exp/cmd/gorelease
@@ -0,0 +1,6 @@
mod=example.com/errors
version=v0.1.0
base=upgrade
error=true
-- want --
could not resolve version example.com/errors@upgrade: query is based on requirements in main go.mod file
@@ -0,0 +1 @@
Module example.com/first is used to test the first tag for a major version.
@@ -0,0 +1,12 @@
mod=example.com/first
base=none
release=v0.0.0
-- want --
# summary
v0.0.0 is a valid semantic version for this release.
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package p
@@ -0,0 +1,12 @@
mod=example.com/first
base=none
release=v0.0.1
-- want --
# summary
v0.0.1 is a valid semantic version for this release.
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package p
@@ -0,0 +1,12 @@
mod=example.com/first
base=none
release=v0.1.0-alpha.1
-- want --
# summary
v0.1.0-alpha.1 is a valid semantic version for this release.
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package p
@@ -0,0 +1,12 @@
mod=example.com/first
base=none
release=v0.1.0
-- want --
# summary
v0.1.0 is a valid semantic version for this release.
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package p
@@ -0,0 +1,19 @@
mod=example.com/first
base=none
release=v0.0.0
success=false
# TODO(golang.org/issue/36087): go list doesn't report positions in correct
# place for scanner errors.
skip=packages.Load gives error with extra "-: " prefix
-- want --
example.com/first
-----------------
errors in new version:
p.go:1:9: illegal character U+003F '?'
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package ?
@@ -0,0 +1,12 @@
mod=example.com/first
base=none
release=v1.0.0
-- want --
# summary
v1.0.0 is a valid semantic version for this release.
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package p
@@ -0,0 +1,13 @@
mod=example.com/first
base=none
release=v1.2.3
proxyVersions=
-- want --
# summary
v1.2.3 is a valid semantic version for this release.
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package p
@@ -0,0 +1,19 @@
mod=example.com/first/v2
base=none
release=v2.0.0
success=false
# TODO(golang.org/issue/36087): go list doesn't report positions in correct
# place for scanner errors.
skip=packages.Load gives error with extra "-: " prefix
-- want --
example.com/first
-----------------
errors in new version:
p.go:1:9: illegal character U+003F '?'
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package ?
@@ -0,0 +1,15 @@
mod=example.com/first
base=none
release=v2.0.0
success=false
-- want --
# summary
v2.0.0 is not a valid semantic version for this release.
The module path does not end with the major version suffix /v2,
which is required for major versions v2 or greater.
-- go.mod --
module example.com/first
go 1.12
-- p.go --
package p
@@ -0,0 +1,7 @@
Tests in this directory cover scenarios where errors in a package are fixed.
v1.0.0 is used as the base version for all tests.
It has an error: the return type of bad.Broken is undefined.
Each test fixes the error and may make other changes (compatible or not).
Note that fixing a type error in the API appears to be an incompatible change.
@@ -0,0 +1,19 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-compatible-other
success=false
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
# example.com/fix/good
## compatible changes
Better: added
# summary
Cannot suggest a release version.
Errors were found.
@@ -0,0 +1,19 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-compatible-other
release=v1.1.0
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
# example.com/fix/good
## compatible changes
Better: added
# summary
v1.1.0 is a valid semantic version for this release.
Errors were found in the base version. Some API changes may be omitted.
@@ -0,0 +1,21 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-compatible-other
release=v1.0.1
success=false
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
# example.com/fix/good
## compatible changes
Better: added
# summary
v1.0.1 is not a valid semantic version for this release.
There are compatible changes, but the minor version is not incremented
over the base version (v1.0.0).
@@ -0,0 +1,17 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-compatible-same
success=false
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
## compatible changes
Worse: added
# summary
Cannot suggest a release version.
Errors were found.
@@ -0,0 +1,17 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-compatible-same
release=v1.0.1 # not actually valid, but gorelease can't tell
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
## compatible changes
Worse: added
# summary
v1.0.1 is a valid semantic version for this release.
Errors were found in the base version. Some API changes may be omitted.
@@ -0,0 +1,19 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-incompatible-other
success=false
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
# example.com/fix/good
## incompatible changes
Good: changed from func() int to func() string
# summary
Cannot suggest a release version.
Errors were found.
@@ -0,0 +1,20 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-incompatible-other
release=v1.1.0
success=false
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
# example.com/fix/good
## incompatible changes
Good: changed from func() int to func() string
# summary
v1.1.0 is not a valid semantic version for this release.
There are incompatible changes.
@@ -0,0 +1,16 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-incompatible-same
success=false
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Bad: changed from func() int to func() string
Broken: changed from func() invalid type to func() int
# summary
Cannot suggest a release version.
Errors were found.
@@ -0,0 +1,16 @@
mod=example.com/fix
base=v1.0.0
version=v1.1.0-incompatible-same
release=v1.0.1 # not actually valid, but gorelease can't tell
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Bad: changed from func() int to func() string
Broken: changed from func() invalid type to func() int
# summary
v1.0.1 is a valid semantic version for this release.
Errors were found in the base version. Some API changes may be omitted.
@@ -0,0 +1,15 @@
mod=example.com/fix
base=v1.0.0
version=v1.0.1-patch
success=false
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
# summary
Cannot suggest a release version.
Errors were found.
@@ -0,0 +1,15 @@
mod=example.com/fix
base=v1.0.0
version=v1.0.1-patch
release=v1.0.1
-- want --
# example.com/fix/bad
## errors in base version:
bad/bad.go:3:15: undefined: NOTYPE
## incompatible changes
Broken: changed from func() invalid type to func() int
# summary
v1.0.1 is a valid semantic version for this release.
Errors were found in the base version. Some API changes may be omitted.
@@ -0,0 +1,25 @@
mod=example.com/generics
base=v0.0.1
-- want --
# example.com/generics/a
## incompatible changes
Foo: changed from Foo[V any] to Foo[V Number]
## compatible changes
Number: added
# summary
Suggested version: v0.1.0
-- go.mod --
module example.com/generics
go 1.18
-- a/a.go --
package a
type Number interface {
int64 | float64
}
type Foo[V Number] interface {
Value() any
}
@@ -0,0 +1,19 @@
mod=example.com/generics
base=v0.0.1
-- want --
# example.com/generics/a
## incompatible changes
Foo[V any].Value: changed from func() V to func() int
# summary
Suggested version: v0.1.0
-- go.mod --
module example.com/generics
go 1.18
-- a/a.go --
package a
type Foo[V any] interface {
Value() int
}
@@ -0,0 +1,15 @@
mod=example.com/generics
base=v0.0.1
-- want --
# summary
Suggested version: v0.0.2
-- go.mod --
module example.com/generics
go 1.18
-- a/a.go --
package a
type Foo[V any] interface {
Value() V
}
@@ -0,0 +1,20 @@
Modules example.com/internalcompat/{a,b} are copies. One could be a fork
of the other. An external package p exposes a type from a package q
within the same module.
gorelease should not report differences between these packages. The types
are distinct, but they correspond (in apidiff terminology), which is the
important property when considering differences between modules.
There are three use cases to consider:
1. One module substitutes for the other via a `replace` directive.
Only the replacement module is used, and the package paths are effectively
identical, so the types are not distinct.
2. One module subsititutes for the other by rewriting `import` statements
globally. All references to the original type become references to the
new type, so there is no conflict.
3. One module substitutes for the other by rewriting some `import` statements
but not others (for example, those within a specific consumer package).
In this case, the types are distinct, and even if there are no changes,
the types are not compatible.
@@ -0,0 +1,5 @@
mod=example.com/internalcompat/b
version=v1.0.0
release=v1.0.1
base=example.com/internalcompat/a@v1.0.0
-- want --

Some files were not shown because too many files have changed in this diff Show More