package version import ( "runtime/debug" "testing" ) // buildInfo returns a *debug.BuildInfo describing a build, so that every case // can be exercised without linker flags — which is the whole point of keeping // resolve separate from Current. func buildInfo(mainVersion string, settings map[string]string) *debug.BuildInfo { info := &debug.BuildInfo{} info.Main.Version = mainVersion for key, value := range settings { info.Settings = append(info.Settings, debug.BuildSetting{Key: key, Value: value}) } return info } func TestAStampedBuildReportsWhatTheLinkerGaveIt(t *testing.T) { got := resolve("v0.2.0", "88a4c38ff11", "2026-08-31T18:04:05Z", buildInfo("(devel)", nil)) want := Info{Number: "0.2.0", Commit: "88a4c38", Built: "2026-08-31T18:04:05Z"} if got != want { t.Errorf("resolve() = %+v, want %+v", got, want) } } func TestTheLinkerOutranksTheBuildSystem(t *testing.T) { // A release binary is built from a checkout, so both sources have an // answer; only the linker's knows the tag. got := resolve("v0.2.0", "", "", buildInfo("(devel)", map[string]string{ "vcs.revision": "88a4c38ff11", })) if got.Number != "0.2.0" { t.Errorf("Number = %q, want the stamped 0.2.0", got.Number) } } func TestAnInstalledModuleReportsItsModuleVersion(t *testing.T) { // `go install @v0.2.0` records the version and no VCS information // at all, because there was no repository to read. got := resolve("", "", "", buildInfo("v0.2.0", nil)) want := Info{Number: "0.2.0"} if got != want { t.Errorf("resolve() = %+v, want %+v", got, want) } } func TestAnUnstampedCheckoutBuildIsCalledDevel(t *testing.T) { // The build system does not read tags, so no tag can be reported here. got := resolve("", "", "", buildInfo("(devel)", map[string]string{ "vcs.revision": "88a4c38ff11", "vcs.modified": "false", })) want := Info{Number: "devel", Commit: "88a4c38"} if got != want { t.Errorf("resolve() = %+v, want %+v", got, want) } } func TestAnUnstampedDirtyCheckoutSaysSo(t *testing.T) { got := resolve("", "", "", buildInfo("(devel)", map[string]string{ "vcs.revision": "88a4c38ff11", "vcs.modified": "true", })) if got.Number != "devel-dirty" { t.Errorf("Number = %q, want devel-dirty", got.Number) } } func TestABuildWithNothingRecordedIsUnknown(t *testing.T) { // Not "0.1.0": a stale number printed as fact is the defect this package // exists to remove. if got := resolve("", "", "", nil); got.Number != unknownNumber { t.Errorf("Number = %q, want %q", got.Number, unknownNumber) } if got := resolve("", "", "", buildInfo("", nil)); got.Number != unknownNumber { t.Errorf("with an empty build info, Number = %q, want %q", got.Number, unknownNumber) } } func TestTheCommitDoesNotComeFromNowhere(t *testing.T) { // An unstamped build with no revision must not invent one. if got := resolve("", "", "", buildInfo("v0.2.0", nil)); got.Commit != "" { t.Errorf("Commit = %q, want nothing", got.Commit) } } func TestTheCommitDateIsNotUsedAsABuildDate(t *testing.T) { // vcs.time is when the commit was made. Reporting it as "Built" would be // wrong on every binary built later than its own commit, which is all of // them. got := resolve("", "", "", buildInfo("(devel)", map[string]string{ "vcs.revision": "88a4c38ff11", "vcs.time": "2026-08-30T09:00:00Z", })) if got.Built != "" { t.Errorf("Built = %q, want nothing", got.Built) } } func TestOnlyAVersionsLeadingVIsDropped(t *testing.T) { cases := map[string]string{ "v0.2.0": "0.2.0", "v0.1.0-14-g88a4c38": "0.1.0-14-g88a4c38", "0.2.0": "0.2.0", "devel": "devel", // not "el" "version": "version", // not "ersion" "v": "v", "": "", } for given, want := range cases { if got := trimV(given); got != want { t.Errorf("trimV(%q) = %q, want %q", given, got, want) } } } func TestARevisionIsAbbreviatedTheWayGitDoes(t *testing.T) { if got := shorten("88a4c38ff1122334455"); got != "88a4c38" { t.Errorf("shorten() = %q, want 88a4c38", got) } if got := shorten("88a4c3"); got != "88a4c3" { t.Errorf("a revision shorter than the limit was changed: %q", got) } if got := shorten(""); got != "" { t.Errorf("shorten(\"\") = %q, want nothing", got) } } func TestStringShowsEveryPartThatIsKnown(t *testing.T) { cases := []struct { name string info Info want string }{ {"everything", Info{"0.2.0", "88a4c38", "2026-08-31T18:04:05Z"}, "0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)"}, {"commit only", Info{Number: "0.2.0", Commit: "88a4c38"}, "0.2.0 (88a4c38)"}, {"date only", Info{Number: "0.2.0", Built: "2026-08-31T18:04:05Z"}, "0.2.0 (built 2026-08-31T18:04:05Z)"}, {"number only", Info{Number: "0.2.0"}, "0.2.0"}, } for _, one := range cases { t.Run(one.name, func(t *testing.T) { if got := one.info.String(); got != one.want { t.Errorf("String() = %q, want %q", got, one.want) } }) } } func TestBuiltAtIsReadable(t *testing.T) { if got := (Info{Built: "2026-08-31T18:04:05Z"}).BuiltAt(); got != "2026-08-31 18:04 UTC" { t.Errorf("BuiltAt() = %q", got) } } func TestBuiltAtNormalisesToUTC(t *testing.T) { // The Makefile stamps UTC, but a hand-run build might not. if got := (Info{Built: "2026-08-31T20:04:05+02:00"}).BuiltAt(); got != "2026-08-31 18:04 UTC" { t.Errorf("BuiltAt() = %q, want the same moment in UTC", got) } } func TestBuiltAtShowsAMalformedStampRatherThanHidingIt(t *testing.T) { if got := (Info{Built: "last Tuesday"}).BuiltAt(); got != "last Tuesday" { t.Errorf("BuiltAt() = %q, want the stamp as given", got) } if got := (Info{}).BuiltAt(); got != "" { t.Errorf("BuiltAt() with no stamp = %q, want nothing", got) } } func TestCurrentAnswersWithoutPanicking(t *testing.T) { // A test binary is one of the builds resolve has to cope with, and its // number is never empty whatever the build system recorded. if got := Current(); got.Number == "" { t.Error("Current() reported an empty version") } } func TestEveryFormOfPseudoVersionIsRecognised(t *testing.T) { // The three shapes the Go tool produces differ in what precedes the // timestamp: a dash when there is no base tag, a dot when the base is a // release or a pre-release. pseudo := []string{ "0.0.0-20260831165958-88a4c3859bf3", "0.1.1-0.20260831165958-88a4c3859bf3", "0.1.1-0.20260831165958-88a4c3859bf3+dirty", "0.2.0-pre.0.20260831165958-88a4c3859bf3", } for _, number := range pseudo { if !isPseudoVersion(number) { t.Errorf("isPseudoVersion(%q) = false, want true", number) } } real := []string{ "0.2.0", "0.2.0-rc.1", "devel", "0.1.0-14-g88a4c38", "", "0.0.0-20260831165958-88a4c3859bZ", // not hex "0.0.0-2026083116595-88a4c3859bf3", // 13 digits } for _, number := range real { if isPseudoVersion(number) { t.Errorf("isPseudoVersion(%q) = true, want false", number) } } } func TestAPseudoVersionIsReportedAsDevel(t *testing.T) { // "0.1.1" in a pseudo-version is a patch release that does not exist. got := resolve("", "", "", buildInfo("v0.1.1-0.20260831165958-88a4c3859bf3+dirty", map[string]string{ "vcs.revision": "88a4c3859bf3e8cb", "vcs.modified": "true", })) want := Info{Number: "devel-dirty", Commit: "88a4c38"} if got != want { t.Errorf("resolve() = %+v, want %+v", got, want) } }