Compare commits

...

16 Commits

Author SHA1 Message Date
Michael B. Gale
12bc9f84bd Go: Remove extra "v"s in Autobuilder 2024-05-08 16:06:53 +01:00
Michael B. Gale
f64e617da2 Go: Make RequiredGoVersion resilient to different version formats and add tests 2024-05-08 16:06:53 +01:00
Michael B. Gale
97b95337b6 Go: Make toolchain functions resilient to different version formats 2024-05-08 16:06:53 +01:00
Michael B. Gale
a8d87ea4ea Go: Make getVersionToInstall resilient to different version strings 2024-05-08 16:06:53 +01:00
Michael B. Gale
dfa23197c7 Go: Add UnformatSemVer function 2024-05-08 16:06:53 +01:00
Michael B. Gale
8a9fd8c619 Go: Add FormatSemVer function 2024-05-08 16:06:52 +01:00
Michael B. Gale
42ee9afc69 Go: Improve documentation for GoVersionInfo 2024-05-08 14:29:40 +01:00
Michael B. Gale
c6809c46f5 Go: Improve documentation for GoVersionToSemVer and ToolchainVersionToSemVer 2024-05-08 14:27:05 +01:00
Michael B. Gale
0174a16d5a Go: Add tests for RequiredGoVersion 2024-05-08 13:09:25 +01:00
Michael B. Gale
69bf334a33 Go: Use GoVersionToSemVer 2024-05-08 13:09:25 +01:00
Michael B. Gale
01597ec23b Go: Add GoVersionToSemVer 2024-05-08 13:09:25 +01:00
Michael B. Gale
c9d1aa6354 Go: Use Toolchain directives in go.mod files, if available 2024-05-08 13:09:24 +01:00
Michael B. Gale
4666ed9957 Go: Add constructors for GoVersionInfo 2024-05-08 13:09:24 +01:00
Michael B. Gale
4e701a12db Go: Refactor go.mod version retrieval into its own method 2024-05-08 13:08:14 +01:00
Michael B. Gale
628064118f Go: Use Toolchain directives in go.work files, if available 2024-05-08 13:06:30 +01:00
Michael B. Gale
648b28c1d2 Go: Add ToolchainVersionToSemVer with tests 2024-05-08 13:06:20 +01:00
11 changed files with 338 additions and 58 deletions

View File

@@ -23,4 +23,5 @@ go_test(
name = "autobuilder_test",
srcs = ["build-environment_test.go"],
embed = [":autobuilder"],
deps = ["//go/extractor/util"],
)

View File

@@ -8,6 +8,7 @@ import (
"github.com/github/codeql-go/extractor/diagnostics"
"github.com/github/codeql-go/extractor/project"
"github.com/github/codeql-go/extractor/toolchain"
"github.com/github/codeql-go/extractor/util"
"golang.org/x/mod/semver"
)
@@ -30,13 +31,13 @@ func (v versionInfo) String() string {
// Check if `version` is lower than `minGoVersion`. Note that for this comparison we ignore the
// patch part of the version, so 1.20.1 and 1.20 are considered equal.
func belowSupportedRange(version string) bool {
return semver.Compare(semver.MajorMinor("v"+version), "v"+minGoVersion) < 0
return semver.Compare(semver.MajorMinor(util.FormatSemVer(version)), util.FormatSemVer(minGoVersion)) < 0
}
// Check if `version` is higher than `maxGoVersion`. Note that for this comparison we ignore the
// patch part of the version, so 1.20.1 and 1.20 are considered equal.
func aboveSupportedRange(version string) bool {
return semver.Compare(semver.MajorMinor("v"+version), "v"+maxGoVersion) > 0
return semver.Compare(semver.MajorMinor(util.FormatSemVer(version)), util.FormatSemVer(maxGoVersion)) > 0
}
// Check if `version` is lower than `minGoVersion` or higher than `maxGoVersion`. Note that for
@@ -113,7 +114,7 @@ func getVersionWhenGoModVersionTooHigh(v versionInfo) (msg, version string) {
"). Requesting the maximum supported version of Go (" + maxGoVersion + ")."
version = maxGoVersion
diagnostics.EmitGoModVersionTooHighAndEnvVersionTooLow(msg)
} else if semver.Compare("v"+maxGoVersion, "v"+v.goEnvVersion) > 0 {
} else if semver.Compare(util.FormatSemVer(maxGoVersion), util.FormatSemVer(v.goEnvVersion)) > 0 {
// The version in the `go.mod` file is above the supported range. The version of Go that
// is installed is supported and below the maximum supported version. We install the
// maximum supported version as a best effort.
@@ -195,7 +196,7 @@ func getVersionWhenGoModVersionSupported(v versionInfo) (msg, version string) {
v.goModVersion + ")."
version = v.goModVersion
diagnostics.EmitGoModVersionSupportedAndGoEnvUnsupported(msg)
} else if semver.Compare("v"+v.goModVersion, "v"+v.goEnvVersion) > 0 {
} else if semver.Compare(util.FormatSemVer(v.goModVersion), util.FormatSemVer(v.goEnvVersion)) > 0 {
// The version of Go that is installed is supported. The version in the `go.mod` file is
// supported and is higher than the version that is installed. We install the version from
// the `go.mod` file.
@@ -233,18 +234,18 @@ func getVersionWhenGoModVersionSupported(v versionInfo) (msg, version string) {
// +-----------------------+-----------------------+-----------------------+-----------------------------------------------------+------------------------------------------------+
func getVersionToInstall(v versionInfo) (msg, version string) {
if !v.goModVersionFound {
return getVersionWhenGoModVersionNotFound(v)
msg, version = getVersionWhenGoModVersionNotFound(v)
} else if aboveSupportedRange(v.goModVersion) {
msg, version = getVersionWhenGoModVersionTooHigh(v)
} else if belowSupportedRange(v.goModVersion) {
msg, version = getVersionWhenGoModVersionTooLow(v)
} else {
msg, version = getVersionWhenGoModVersionSupported(v)
}
if aboveSupportedRange(v.goModVersion) {
return getVersionWhenGoModVersionTooHigh(v)
}
if belowSupportedRange(v.goModVersion) {
return getVersionWhenGoModVersionTooLow(v)
}
return getVersionWhenGoModVersionSupported(v)
// Make sure that we return a normal version string, not one starting with "v"
version = util.UnformatSemVer(version)
return
}
// Output some JSON to stdout specifying the version of Go to install, unless `version` is the

View File

@@ -1,6 +1,10 @@
package autobuilder
import "testing"
import (
"testing"
"github.com/github/codeql-go/extractor/util"
)
func TestGetVersionToInstall(t *testing.T) {
tests := map[versionInfo]string{
@@ -44,5 +48,17 @@ func TestGetVersionToInstall(t *testing.T) {
if actual != expected {
t.Errorf("Expected getVersionToInstall(\"%s\") to be \"%s\", but got \"%s\".", input, expected, actual)
}
if input.goEnvVersionFound {
input.goEnvVersion = util.FormatSemVer(input.goEnvVersion)
}
if input.goEnvVersionFound {
input.goModVersion = util.FormatSemVer(input.goModVersion)
}
_, actual = getVersionToInstall(input)
if actual != expected {
t.Errorf("Expected getVersionToInstall(\"%s\") to be \"%s\", but got \"%s\".", input, expected, actual)
}
}
}

View File

@@ -537,12 +537,12 @@ func installDependenciesAndBuild() {
// This diagnostic is not required if the system Go version is 1.21 or greater, since the
// Go tooling should install required Go versions as needed.
if semver.Compare(toolchain.GetEnvGoSemVer(), "v1.21.0") < 0 && greatestGoVersion.Found && semver.Compare("v"+greatestGoVersion.Version, toolchain.GetEnvGoSemVer()) > 0 {
diagnostics.EmitNewerGoVersionNeeded(toolchain.GetEnvGoSemVer(), "v"+greatestGoVersion.Version)
if semver.Compare(toolchain.GetEnvGoSemVer(), "v1.21.0") < 0 && greatestGoVersion.Found && semver.Compare(greatestGoVersion.Version, toolchain.GetEnvGoSemVer()) > 0 {
diagnostics.EmitNewerGoVersionNeeded(toolchain.GetEnvGoSemVer(), greatestGoVersion.Version)
if val, _ := os.LookupEnv("GITHUB_ACTIONS"); val == "true" {
log.Printf(
"A go.mod file requires version %s of Go, but version %s is installed. Consider adding an actions/setup-go step to your workflow.\n",
"v"+greatestGoVersion.Version,
greatestGoVersion.Version,
toolchain.GetEnvGoSemVer())
}
}

View File

@@ -37,6 +37,18 @@ type GoModule struct {
Module *modfile.File // The parsed contents of the `go.mod` file
}
// Tries to find the Go toolchain version required for this module.
func (module *GoModule) RequiredGoVersion() GoVersionInfo {
if module.Module != nil && module.Module.Toolchain != nil {
return VersionFound(toolchain.ToolchainVersionToSemVer(module.Module.Toolchain.Name))
}
if module.Module != nil && module.Module.Go != nil {
return VersionFound(toolchain.GoVersionToSemVer(module.Module.Go.Version))
} else {
return tryReadGoDirective(module.Path)
}
}
// Represents information about a Go project workspace: this may either be a folder containing
// a `go.work` file or a collection of `go.mod` files.
type GoWorkspace struct {
@@ -48,54 +60,69 @@ type GoWorkspace struct {
Extracted bool // A value indicating whether this workspace was extracted successfully
}
// Represents a nullable version string.
// Represents a nullable version string. Use `VersionNotFound` and `VersionFound`
// instead of constructing values of this type directly.
type GoVersionInfo struct {
// The version string, if any
// The semantic version string, such as "v1.20.0-rc1", if any.
// This is a valid semantic version if `Found` is `true` or the empty string if not.
Version string
// A value indicating whether a version string was found
// A value indicating whether a version string was found.
// If this value is `true`, then `Version` is a valid semantic version.
// IF this value is `false`, then `Version` is the empty string.
Found bool
}
// Represents a `GoVersionInfo` indicating that no version was found.
var VersionNotFound GoVersionInfo = GoVersionInfo{"", false}
// Constructs a `GoVersionInfo` for a version we found.
func VersionFound(version string) GoVersionInfo {
return GoVersionInfo{
Version: util.FormatSemVer(version),
Found: true,
}
}
// Determines the version of Go that is required by this workspace. This is, in order of preference:
// 1. The Go version specified in the `go.work` file, if any.
// 2. The greatest Go version specified in any `go.mod` file, if any.
func (workspace *GoWorkspace) RequiredGoVersion() GoVersionInfo {
if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Go != nil {
// If we have parsed a `go.work` file, return the version number from it.
return GoVersionInfo{Version: workspace.WorkspaceFile.Go.Version, Found: true}
// If we have parsed a `go.work` file, we prioritise versions from it over those in individual `go.mod`
// files. We are interested in toolchain versions, so if there is an explicit toolchain declaration in
// a `go.work` file, we use that. Otherwise, we fall back to the language version in the `go.work` file
// and use that as toolchain version. If we didn't parse a `go.work` file, then we try to find the
// greatest version contained in `go.mod` files.
if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Toolchain != nil {
return VersionFound(toolchain.ToolchainVersionToSemVer(workspace.WorkspaceFile.Toolchain.Name))
} else if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Go != nil {
return VersionFound(toolchain.GoVersionToSemVer(workspace.WorkspaceFile.Go.Version))
} else if workspace.Modules != nil && len(workspace.Modules) > 0 {
// Otherwise, if we have `go.work` files, find the greatest Go version in those.
var greatestVersion string = ""
for _, module := range workspace.Modules {
if module.Module != nil && module.Module.Go != nil {
// If we have parsed the file, retrieve the version number we have already obtained.
if greatestVersion == "" || semver.Compare("v"+module.Module.Go.Version, "v"+greatestVersion) > 0 {
greatestVersion = module.Module.Go.Version
}
} else {
modVersion := tryReadGoDirective(module.Path)
if modVersion.Found && (greatestVersion == "" || semver.Compare("v"+modVersion.Version, "v"+greatestVersion) > 0) {
greatestVersion = modVersion.Version
}
moduleVersionInfo := module.RequiredGoVersion()
if greatestVersion == "" || semver.Compare(moduleVersionInfo.Version, greatestVersion) > 0 {
greatestVersion = moduleVersionInfo.Version
}
}
// If we have found some version, return it.
if greatestVersion != "" {
return GoVersionInfo{Version: greatestVersion, Found: true}
return VersionFound(greatestVersion)
}
}
return GoVersionInfo{Version: "", Found: false}
return VersionNotFound
}
// Finds the greatest Go version required by any of the given `workspaces`.
// Returns a `GoVersionInfo` value with `Found: false` if no version information is available.
func RequiredGoVersion(workspaces *[]GoWorkspace) GoVersionInfo {
greatestGoVersion := GoVersionInfo{Version: "", Found: false}
greatestGoVersion := VersionNotFound
for _, workspace := range *workspaces {
goVersionInfo := workspace.RequiredGoVersion()
if goVersionInfo.Found && (!greatestGoVersion.Found || semver.Compare("v"+goVersionInfo.Version, "v"+greatestGoVersion.Version) > 0) {
if goVersionInfo.Found && (!greatestGoVersion.Found || semver.Compare(util.FormatSemVer(goVersionInfo.Version), util.FormatSemVer(greatestGoVersion.Version)) > 0) {
greatestGoVersion = goVersionInfo
}
}
@@ -583,9 +610,9 @@ func tryReadGoDirective(path string) GoVersionInfo {
matches := versionRe.FindSubmatch(goMod)
if matches != nil {
if len(matches) > 1 {
return GoVersionInfo{string(matches[1]), true}
return VersionFound(toolchain.GoVersionToSemVer(string(matches[1])))
}
}
}
return GoVersionInfo{"", false}
return VersionNotFound
}

View File

@@ -28,14 +28,18 @@ func TestStartsWithAnyOf(t *testing.T) {
testStartsWithAnyOf(t, filepath.Join("foo", "bar"), filepath.Join("foo", "baz"), false)
}
func testHasInvalidToolchainVersion(t *testing.T, contents string) bool {
modFile, err := modfile.Parse("test.go", []byte(contents), nil)
func parseModFile(t *testing.T, contents string) *modfile.File {
modFile, err := modfile.Parse("go.mod", []byte(contents), nil)
if err != nil {
t.Errorf("Unable to parse %s: %s.\n", contents, err.Error())
}
return hasInvalidToolchainVersion(modFile)
return modFile
}
func testHasInvalidToolchainVersion(t *testing.T, contents string) bool {
return hasInvalidToolchainVersion(parseModFile(t, contents))
}
func TestHasInvalidToolchainVersion(t *testing.T) {
@@ -62,3 +66,113 @@ func TestHasInvalidToolchainVersion(t *testing.T) {
}
}
}
func parseWorkFile(t *testing.T, contents string) *modfile.WorkFile {
workFile, err := modfile.ParseWork("go.work", []byte(contents), nil)
if err != nil {
t.Errorf("Unable to parse %s: %s.\n", contents, err.Error())
}
return workFile
}
type FileVersionPair struct {
FileContents string
ExpectedVersion string
}
func checkRequiredGoVersionResult(t *testing.T, fun string, file string, testData FileVersionPair, result GoVersionInfo) {
if !result.Found {
t.Errorf(
"Expected %s to return %s for the below `%s` file, but got nothing:\n%s",
fun,
testData.ExpectedVersion,
file,
testData.FileContents,
)
} else if result.Version != testData.ExpectedVersion {
t.Errorf(
"Expected %s to return %s for the below `%s` file, but got %s:\n%s",
fun,
testData.ExpectedVersion,
file,
result.Version,
testData.FileContents,
)
}
}
func TestRequiredGoVersion(t *testing.T) {
testFiles := []FileVersionPair{
{"go 1.20", "v1.20.0"},
{"go 1.21.2", "v1.21.2"},
{"go 1.21rc1", "v1.21.0-rc1"},
{"go 1.21rc1\ntoolchain go1.22.0", "v1.22.0"},
{"go 1.21rc1\ntoolchain go1.22rc1", "v1.22.0-rc1"},
}
var modules []*GoModule = []*GoModule{}
for _, testData := range testFiles {
// `go.mod` and `go.work` files have mostly the same format
modFile := parseModFile(t, testData.FileContents)
workFile := parseWorkFile(t, testData.FileContents)
mod := &GoModule{
Path: "test", // irrelevant
Module: modFile,
}
work := &GoWorkspace{
WorkspaceFile: workFile,
}
result := mod.RequiredGoVersion()
checkRequiredGoVersionResult(t, "mod.RequiredGoVersion()", "go.mod", testData, result)
result = work.RequiredGoVersion()
checkRequiredGoVersionResult(t, "work.RequiredGoVersion()", "go.work", testData, result)
modules = append(modules, mod)
}
// Create a test workspace with all the modules in one workspace.
workspace := GoWorkspace{
Modules: modules,
}
workspaceVer := "v1.22.0"
result := RequiredGoVersion(&[]GoWorkspace{workspace})
if !result.Found {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got nothing.",
workspaceVer,
)
} else if result.Version != workspaceVer {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got %s.",
workspaceVer,
result.Version,
)
}
// Create test workspaces for each module.
workspaces := []GoWorkspace{}
for _, mod := range modules {
workspaces = append(workspaces, GoWorkspace{Modules: []*GoModule{mod}})
}
result = RequiredGoVersion(&workspaces)
if !result.Found {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got nothing.",
workspaceVer,
)
} else if result.Version != workspaceVer {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got %s.",
workspaceVer,
result.Version,
)
}
}

View File

@@ -17,4 +17,5 @@ go_test(
name = "toolchain_test",
srcs = ["toolchain_test.go"],
embed = [":toolchain"],
deps = ["//go/extractor/util"],
)

View File

@@ -25,7 +25,7 @@ var goVersions = map[string]struct{}{}
// Adds an entry to the set of installed Go versions for the normalised `version` number.
func addGoVersion(version string) {
goVersions[semver.Canonical("v"+version)] = struct{}{}
goVersions[semver.Canonical(util.FormatSemVer(version))] = struct{}{}
}
// Returns the current Go version as returned by 'go version', e.g. go1.14.4
@@ -58,7 +58,7 @@ func GetEnvGoVersion() string {
// Determines whether, to our knowledge, `version` is available on the current system.
func HasGoVersion(version string) bool {
_, found := goVersions[semver.Canonical("v"+version)]
_, found := goVersions[semver.Canonical(util.FormatSemVer(version))]
return found
}
@@ -72,7 +72,7 @@ func InstallVersion(workingDir string, version string) bool {
// Construct a command to invoke `go version` with `GOTOOLCHAIN=go1.N.0` to give
// Go a valid toolchain version to download the toolchain we need; subsequent commands
// should then work even with an invalid version that's still in `go.mod`
toolchainArg := "GOTOOLCHAIN=go" + semver.Canonical("v" + version)[1:]
toolchainArg := "GOTOOLCHAIN=go" + semver.Canonical(util.FormatSemVer(version))[1:]
versionCmd := Version()
versionCmd.Dir = workingDir
versionCmd.Env = append(os.Environ(), toolchainArg)
@@ -104,23 +104,35 @@ func InstallVersion(workingDir string, version string) bool {
return true
}
// Returns the current Go version in semver format, e.g. v1.14.4
func GetEnvGoSemVer() string {
goVersion := GetEnvGoVersion()
if !strings.HasPrefix(goVersion, "go") {
log.Fatalf("Expected 'go version' output of the form 'go1.2.3'; got '%s'", goVersion)
}
// Converts a Go version to a semantic version. For example, "1.20" becomes "v1.20" and
// "1.22rc1" becomes "v1.22.0-rc1".
func GoVersionToSemVer(goVersion string) string {
// Go versions don't follow the SemVer format, but the only exception we normally care about
// is release candidates; so this is a horrible hack to convert e.g. `go1.22rc1` into `go1.22-rc1`
// is release candidates; so this is a horrible hack to convert e.g. `1.22rc1` into `1.22-rc1`
// which is compatible with the SemVer specification
rcIndex := strings.Index(goVersion, "rc")
if rcIndex != -1 {
return semver.Canonical("v"+goVersion[2:rcIndex]) + "-" + goVersion[rcIndex:]
return semver.Canonical(util.FormatSemVer(goVersion[:rcIndex])) + "-" + goVersion[rcIndex:]
} else {
return semver.Canonical("v" + goVersion[2:])
return semver.Canonical(util.FormatSemVer(goVersion))
}
}
// Converts a Go toolchain version to a semantic version. For example, "go1.2.3" becomes "v1.2.3"
// and "go1.20rc1" becomes "v1.20.0-rc1".
func ToolchainVersionToSemVer(toolchainVersion string) string {
if !strings.HasPrefix(toolchainVersion, "go") {
log.Fatalf("Expected Go toolchain version of the form 'go1.2.3'; got '%s'", toolchainVersion)
}
return GoVersionToSemVer(toolchainVersion[2:])
}
// Returns the current Go version in semver format, e.g. v1.14.4
func GetEnvGoSemVer() string {
return ToolchainVersionToSemVer(GetEnvGoVersion())
}
// The 'go version' command may output warnings on separate lines before
// the actual version string is printed. This function parses the output
// to retrieve just the version string.

View File

@@ -1,6 +1,10 @@
package toolchain
import "testing"
import (
"testing"
"github.com/github/codeql-go/extractor/util"
)
func TestParseGoVersion(t *testing.T) {
tests := map[string]string{
@@ -16,7 +20,65 @@ func TestParseGoVersion(t *testing.T) {
}
func TestHasGoVersion(t *testing.T) {
if HasGoVersion("1.21") {
t.Error("Expected HasGoVersion(\"1.21\") to be false, but got true")
versions := []string{"1.21", "v1.22", "1.22.3", "v1.21rc4"}
// All versions should be unknown.
for _, version := range versions {
if HasGoVersion(version) {
t.Errorf("Expected HasGoVersion(\"%s\") to be false, but got true", version)
}
if HasGoVersion(util.FormatSemVer(version)) {
t.Errorf("Expected HasGoVersion(\"%s\") to be false, but got true", util.FormatSemVer(version))
}
if HasGoVersion(util.UnformatSemVer(version)) {
t.Errorf("Expected HasGoVersion(\"%s\") to be false, but got true", util.UnformatSemVer(version))
}
// Add the version in preparation for the next part of the test.
addGoVersion(version)
}
// Now we should have all of the versions.
for _, version := range versions {
if !HasGoVersion(version) {
t.Errorf("Expected HasGoVersion(\"%s\") to be true, but got false", version)
}
if !HasGoVersion(util.FormatSemVer(version)) {
t.Errorf("Expected HasGoVersion(\"%s\") to be true, but got false", util.FormatSemVer(version))
}
if !HasGoVersion(util.UnformatSemVer(version)) {
t.Errorf("Expected HasGoVersion(\"%s\") to be true, but got false", util.UnformatSemVer(version))
}
}
}
func testGoVersionToSemVer(t *testing.T, goVersion string, expectedSemVer string) {
result := GoVersionToSemVer(goVersion)
if result != expectedSemVer {
t.Errorf("Expected GoVersionToSemVer(\"%s\") to be %s, but got %s.", goVersion, expectedSemVer, result)
}
}
func TestGoVersionToSemVer(t *testing.T) {
testGoVersionToSemVer(t, "1.20", "v1.20.0")
testGoVersionToSemVer(t, "1.20.1", "v1.20.1")
testGoVersionToSemVer(t, "1.20rc1", "v1.20.0-rc1")
}
func testToolchainVersionToSemVer(t *testing.T, toolchainVersion string, expectedSemVer string) {
result := ToolchainVersionToSemVer(toolchainVersion)
if result != expectedSemVer {
t.Errorf("Expected ToolchainVersionToSemVer(\"%s\") to be %s, but got %s.", toolchainVersion, expectedSemVer, result)
}
}
func TestToolchainVersionToSemVer(t *testing.T) {
testToolchainVersionToSemVer(t, "go1.20", "v1.20.0")
testToolchainVersionToSemVer(t, "go1.20.1", "v1.20.1")
testToolchainVersionToSemVer(t, "go1.20rc1", "v1.20.0-rc1")
}

View File

@@ -409,3 +409,21 @@ func getImportPathFromRepoURL(repourl string) string {
path = regexp.MustCompile(`^/+|\.git$`).ReplaceAllString(path, "")
return host + "/" + path
}
// Prepends "v" to `version`, if not already there.
func FormatSemVer(version string) string {
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
return version
}
// Removes "v" from `semver`, if present.
func UnformatSemVer(semver string) string {
if strings.HasPrefix(semver, "v") {
return semver[1:]
}
return semver
}

View File

@@ -20,3 +20,31 @@ func TestGetImportPathFromRepoURL(t *testing.T) {
}
}
}
func TestFormatSemVer(t *testing.T) {
type TestPair struct {
Input string
Expected string
}
tests := []TestPair{
{"1", "v1"},
{"v1", "v1"},
{"1.2.3", "v1.2.3"},
{"v1.2.3", "v1.2.3"},
}
for _, pair := range tests {
actual := FormatSemVer(pair.Input)
if actual != pair.Expected {
t.Errorf("Expected FormatSemVer(\"%s\") to be \"%s\", but got \"%s\".", pair.Input, pair.Expected, actual)
}
}
for _, pair := range tests {
actual := UnformatSemVer(pair.Input)
if actual != pair.Expected[1:] {
t.Errorf("Expected UnformatSemVer(\"%s\") to be \"%s\", but got \"%s\".", pair.Input, pair.Expected, actual)
}
}
}