Skip to content

Commit 763ad9e

Browse files
authored
add version number generation logic and tests (#63)
1 parent 94dd193 commit 763ad9e

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed

version/version.go

+71
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import (
2424
"encoding/json"
2525
"fmt"
2626
"net/http"
27+
"regexp"
2728
"runtime"
29+
"strconv"
30+
"strings"
2831
)
2932

3033
// These values are private which ensures they can only be set with the build flags.
@@ -48,6 +51,10 @@ type Info struct {
4851
BuildUser string `json:"build_user"`
4952
}
5053

54+
// semverRegexp is used to standardize various version strings by pulling out only
55+
// the major.minor.patch submatch
56+
var semverRegexp = regexp.MustCompile(`^v?([0-9]+\.[0-9]+\.[0-9]+).*$`)
57+
5158
// Version returns a structure with the current version information.
5259
func Version() Info {
5360
return Info{
@@ -87,3 +94,67 @@ func Handler() http.Handler {
8794
enc.Encode(v)
8895
})
8996
}
97+
98+
// versionMultiplier consts are used to create a comparable and reversible integer version from a semver string
99+
// by using 1 million, 1 thousand, and 1 for each part we avoid collisions as long as all parts are less than 1 thousand
100+
const (
101+
majorVersionMultiplier int = 1000000
102+
minorVersionMultiplier int = 1000
103+
patchVersionMultiplier int = 1
104+
)
105+
106+
// VersionNum parses the semver version string to look for only the major.minor.patch portion,
107+
// splits that into 3 parts (disregarding any extra portions), and applies a multiplier to each
108+
// part to generate a total int value representing the semver.
109+
// note that this will generate a sortable, and reversible integer as long as all parts remain less than 1000
110+
// This is currently intended for use in generating comparable versions to set within windows registry entries,
111+
// allowing for an easy "upgrade-only" detection configuration within intune.
112+
// Zero is returned for any case where the version cannot be reliably translated
113+
func VersionNum() int {
114+
semverMatch := semverRegexp.FindStringSubmatch(version)
115+
// expect the leftmost match as semverMatch[0] and the semver substring as semverMatch[1]
116+
if semverMatch == nil || len(semverMatch) != 2 {
117+
return 0
118+
}
119+
120+
parts := strings.Split(semverMatch[1], ".")
121+
if len(parts) < 3 {
122+
return 0
123+
}
124+
125+
versionNum := 0
126+
for i, part := range parts[:3] {
127+
partNum, err := strconv.Atoi(part)
128+
if err != nil {
129+
return 0
130+
}
131+
132+
switch i {
133+
case 0:
134+
versionNum += (partNum * majorVersionMultiplier)
135+
case 1:
136+
versionNum += (partNum * minorVersionMultiplier)
137+
case 2:
138+
versionNum += (partNum * patchVersionMultiplier)
139+
}
140+
}
141+
142+
return versionNum
143+
}
144+
145+
// SemverFromVersionNum provides the inverse functionality of VersionNum, allowing us
146+
// to collect and report the integer version in a readable semver format
147+
func SemverFromVersionNum(versionNum int) string {
148+
if versionNum == 0 {
149+
return "0.0.0"
150+
}
151+
152+
major := versionNum / majorVersionMultiplier
153+
remaining := versionNum - (major * majorVersionMultiplier)
154+
minor := remaining / minorVersionMultiplier
155+
remaining = remaining - (minor * minorVersionMultiplier)
156+
// not strictly needed because patchVersionMultiplier is 1 but here because it feels correct
157+
patch := remaining * patchVersionMultiplier
158+
159+
return fmt.Sprintf("%d.%d.%d", major, minor, patch)
160+
}

version/version_test.go

+166
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package version
22

33
import (
4+
"fmt"
45
"testing"
56
"time"
7+
8+
"github.com/stretchr/testify/require"
69
)
710

811
func TestVersion(t *testing.T) {
@@ -26,3 +29,166 @@ func TestVersion(t *testing.T) {
2629
t.Errorf("have %s, want %s", have, want)
2730
}
2831
}
32+
33+
func Test_VersionNum(t *testing.T) {
34+
t.Parallel()
35+
36+
var tests = map[string]struct {
37+
semver string
38+
expectedVersionNum int
39+
}{
40+
"empty version": {
41+
semver: "",
42+
expectedVersionNum: 0,
43+
},
44+
"basic version": {
45+
semver: "1.2.3",
46+
expectedVersionNum: 1002003,
47+
},
48+
"max version": {
49+
semver: "999.999.999",
50+
expectedVersionNum: 999999999,
51+
},
52+
"semver with leading v": {
53+
semver: "v1.1.2",
54+
expectedVersionNum: 1001002,
55+
},
56+
"semver with leading zeros": {
57+
semver: "01.01.002",
58+
expectedVersionNum: 1001002,
59+
},
60+
"semver with trailing branch info": {
61+
semver: "1.10.3-1-g98Paoe",
62+
expectedVersionNum: 1010003,
63+
},
64+
"semver with leading v and trailing branch info": {
65+
semver: "v1.10.3-1-g98Paoe",
66+
expectedVersionNum: 1010003,
67+
},
68+
"zero version": {
69+
semver: "0.0.0",
70+
expectedVersionNum: 0,
71+
},
72+
}
73+
74+
for name, tt := range tests {
75+
tt := tt
76+
t.Run(name, func(t *testing.T) {
77+
t.Parallel()
78+
version = tt.semver
79+
require.Equal(t, tt.expectedVersionNum, VersionNum())
80+
})
81+
}
82+
}
83+
84+
func Test_SemverFromVersionNum(t *testing.T) {
85+
t.Parallel()
86+
87+
var tests = map[string]struct {
88+
versionNum int
89+
expectedSemver string
90+
}{
91+
"zero version": {
92+
versionNum: 0,
93+
expectedSemver: "0.0.0",
94+
},
95+
"1.10.3": {
96+
versionNum: 1010003,
97+
expectedSemver: "1.10.3",
98+
},
99+
"max version": {
100+
versionNum: 999999999,
101+
expectedSemver: "999.999.999",
102+
},
103+
"1.112.43": {
104+
versionNum: 1112043,
105+
expectedSemver: "1.112.43",
106+
},
107+
}
108+
109+
for name, tt := range tests {
110+
tt := tt
111+
t.Run(name, func(t *testing.T) {
112+
t.Parallel()
113+
require.Equal(t, tt.expectedSemver, SemverFromVersionNum(tt.versionNum))
114+
})
115+
}
116+
}
117+
118+
func Test_VersionNumComparisons(t *testing.T) {
119+
t.Parallel()
120+
121+
var tests = map[string]struct {
122+
lesserVersion string
123+
greaterVersion string
124+
}{
125+
"empty version": {
126+
lesserVersion: "",
127+
greaterVersion: "0.0.1",
128+
},
129+
"basic versions": {
130+
lesserVersion: "1.2.3",
131+
greaterVersion: "1.2.4",
132+
},
133+
"max versions": {
134+
lesserVersion: "999.999.998",
135+
greaterVersion: "999.999.999",
136+
},
137+
"large minor versions, no collisions": {
138+
lesserVersion: "v1.999.999",
139+
greaterVersion: "v2.0.0",
140+
},
141+
}
142+
143+
for name, tt := range tests {
144+
tt := tt
145+
t.Run(name, func(t *testing.T) {
146+
t.Parallel()
147+
version = tt.lesserVersion
148+
lesserParsed := VersionNum()
149+
version = tt.greaterVersion
150+
greaterParsed := VersionNum()
151+
require.True(t, lesserParsed < greaterParsed,
152+
fmt.Sprintf("expected %s to parse as lesser than %s. got lesser %d >= greater %d",
153+
tt.lesserVersion,
154+
tt.greaterVersion,
155+
lesserParsed,
156+
greaterParsed,
157+
),
158+
)
159+
})
160+
}
161+
}
162+
163+
func Test_VersionNumIsReversible(t *testing.T) {
164+
t.Parallel()
165+
166+
var tests = map[string]struct {
167+
testedVersion string
168+
}{
169+
"zero version": {
170+
testedVersion: "0.0.0",
171+
},
172+
"basic version": {
173+
testedVersion: "1.2.3",
174+
},
175+
"max version": {
176+
testedVersion: "999.999.999",
177+
},
178+
"random version": {
179+
testedVersion: "107.61.10",
180+
},
181+
"random version 2": {
182+
testedVersion: "0.118.919",
183+
},
184+
}
185+
186+
for name, tt := range tests {
187+
tt := tt
188+
t.Run(name, func(t *testing.T) {
189+
t.Parallel()
190+
version = tt.testedVersion
191+
require.Equal(t, tt.testedVersion, SemverFromVersionNum(VersionNum()))
192+
})
193+
}
194+
}

0 commit comments

Comments
 (0)