Skip to content

Commit 215aa67

Browse files
authored
fix bug that treated all zeroes as if they were leading zeroes; add test coverage (#3)
1 parent 9d994ec commit 215aa67

File tree

5 files changed

+167
-60
lines changed

5 files changed

+167
-60
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
- checkout
9696
- run: go version
9797
- run: go build ./...
98-
- run: go test -race -v ./...
98+
- run: go test ./...
9999

100100
benchmarks:
101101
docker:

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ clean:
2020
go clean
2121

2222
test: build
23-
go test -race -v ./...
23+
go test ./...
2424

2525
benchmarks: build
2626
mkdir -p ./build

parse_test.go

Lines changed: 76 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package semver
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
89
)
910

11+
// In order to catch parsing bugs that depend on the number or order of digits, we use ValuesGenerator
12+
// (in values_generator.go) to create permutations of many integer values. However, as a practical
13+
// measure to avoid very long test times, we don't use an equally large range for every value: when
14+
// generating major, minor, and patch version numbers, we cover a 3-digit range for the major and only
15+
// one digit for the others, on the assumption that the same numeric parsing logic is used for each.
16+
1017
func assertVersionComponents(t *testing.T, v Version, major int, minor int, patch int, pre string, b string) {
1118
assert.Equal(t, major, v.GetMajor())
1219
assert.Equal(t, minor, v.GetMinor())
@@ -36,8 +43,12 @@ func TestParseStrict(t *testing.T) {
3643
strictParsingTests := func(t *testing.T, parseFn func(string) (Version, error)) {
3744
parsingTestsForAnyMode(t, parseFn)
3845

39-
t.Run("patch version cannot be omitted", parsingShouldFail(parseFn, "2.3"))
40-
t.Run("micro and patch versions cannot be omitted", parsingShouldFail(parseFn, "2"))
46+
NewValuesGenerator().AddValue(0, 199).AddValue(0, 9).TestAll2(t, func(t *testing.T, a, b int) {
47+
t.Run("patch version cannot be omitted", parsingShouldFail(parseFn, fmt.Sprintf("%d.%d", a, b)))
48+
})
49+
NewValuesGenerator().AddValue(0, 199).TestAll1(t, func(t *testing.T, a int) {
50+
t.Run("micro and patch versions cannot be omitted", parsingShouldFail(parseFn, fmt.Sprintf("%d", a)))
51+
})
4152
}
4253

4354
t.Run("Parse(s)", func(t *testing.T) {
@@ -54,86 +65,94 @@ func TestParseAllowMissingMinorAndPatch(t *testing.T) {
5465

5566
parsingTestsForAnyMode(t, parseFn)
5667

57-
t.Run("patch version can be omitted", parsingShouldSucceed(parseFn,
58-
"2.3", 2, 3, 0, "", ""))
68+
NewValuesGenerator().AddValue(0, 199).AddValue(0, 9).TestAll2(t, func(t *testing.T, a, b int) {
69+
s := fmt.Sprintf("%d.%d", a, b)
70+
71+
t.Run("patch version can be omitted", parsingShouldSucceed(parseFn,
72+
s, a, b, 0, "", ""))
5973

60-
t.Run("patch version can be omitted with prerelease", parsingShouldSucceed(parseFn,
61-
"2.3-beta1", 2, 3, 0, "beta1", ""))
74+
t.Run("patch version can be omitted with prerelease", parsingShouldSucceed(parseFn,
75+
s+"-beta1", a, b, 0, "beta1", ""))
76+
77+
t.Run("patch version can be omitted with build", parsingShouldSucceed(parseFn,
78+
s+"+build1", a, b, 0, "", "build1"))
79+
})
6280

63-
t.Run("patch version can be omitted with build", parsingShouldSucceed(parseFn,
64-
"2.3+build1", 2, 3, 0, "", "build1"))
81+
NewValuesGenerator().AddValue(0, 199).TestAll1(t, func(t *testing.T, a int) {
82+
s := fmt.Sprintf("%d", a)
6583

66-
t.Run("micro and patch versions can be omitted", parsingShouldSucceed(parseFn,
67-
"2", 2, 0, 0, "", ""))
84+
t.Run("micro and patch versions can be omitted", parsingShouldSucceed(parseFn,
85+
s, a, 0, 0, "", ""))
6886

69-
t.Run("micro and patch versions can be omitted with prerelease", parsingShouldSucceed(parseFn,
70-
"2-beta1", 2, 0, 0, "beta1", ""))
87+
t.Run("micro and patch versions can be omitted with prerelease", parsingShouldSucceed(parseFn,
88+
s+"-beta1", a, 0, 0, "beta1", ""))
7189

72-
t.Run("micro and patch versions can be omitted with build", parsingShouldSucceed(parseFn,
73-
"2+build1", 2, 0, 0, "", "build1"))
90+
t.Run("micro and patch versions can be omitted with build", parsingShouldSucceed(parseFn,
91+
s+"+build1", a, 0, 0, "", "build1"))
92+
})
7493
}
7594

7695
func parsingTestsForAnyMode(t *testing.T, parseFn func(string) (Version, error)) {
7796
t.Run("valid", func(t *testing.T) {
78-
t.Run("simple complete version", parsingShouldSucceed(parseFn,
79-
"2.3.4", 2, 3, 4, "", ""))
80-
81-
t.Run("version with prerelease (single identifier)", parsingShouldSucceed(parseFn,
82-
"2.3.4-beta1", 2, 3, 4, "beta1", ""))
83-
84-
t.Run("version with prerelease (multi-identifier)", parsingShouldSucceed(parseFn,
85-
"2.3.4-beta1.2", 2, 3, 4, "beta1.2", ""))
86-
87-
t.Run("version with prerelease (multi-identifier with hyphens)", parsingShouldSucceed(parseFn,
88-
"2.3.4-beta1-final.2", 2, 3, 4, "beta1-final.2", ""))
97+
NewValuesGenerator().AddValue(0, 199).AddValue(0, 9).AddValue(0, 9).
98+
TestAll3(t, func(t *testing.T, a, b, c int) {
99+
s := fmt.Sprintf("%d.%d.%d", a, b, c)
100+
t.Run("simple complete version", parsingShouldSucceed(parseFn,
101+
s, a, b, c, "", ""))
89102

90-
t.Run("version with build (single identifier)", parsingShouldSucceed(parseFn,
91-
"2.3.4+build2", 2, 3, 4, "", "build2"))
103+
t.Run("version with prerelease (single identifier)", parsingShouldSucceed(parseFn,
104+
s+"-beta1", a, b, c, "beta1", ""))
92105

93-
t.Run("version with build (multi-identifier)", parsingShouldSucceed(parseFn,
94-
"2.3.4+build2.4", 2, 3, 4, "", "build2.4"))
106+
t.Run("version with build (single identifier)", parsingShouldSucceed(parseFn,
107+
s+"+build2", a, b, c, "", "build2"))
95108

96-
t.Run("version with build (multi-identifier with hyphens)", parsingShouldSucceed(parseFn,
97-
"2.3.4+build2-other.4", 2, 3, 4, "", "build2-other.4"))
109+
t.Run("prerelease identifier of only letters", parsingShouldSucceed(parseFn,
110+
s+"-abc", a, b, c, "abc", ""))
98111

99-
t.Run("version with prerelease and build", parsingShouldSucceed(parseFn,
100-
"2.3.4-beta1.rc2+build2.4", 2, 3, 4, "beta1.rc2", "build2.4"))
112+
t.Run("prerelease identifier of only digits", parsingShouldSucceed(parseFn,
113+
s+"-123", a, b, c, "123", ""))
101114

102-
t.Run("major version zero", parsingShouldSucceed(parseFn,
103-
"0.3.4", 0, 3, 4, "", ""))
115+
t.Run("prerelease identifier of only hyphens", parsingShouldSucceed(parseFn,
116+
s+"----", a, b, c, "---", ""))
104117

105-
t.Run("minor version zero", parsingShouldSucceed(parseFn,
106-
"2.0.4", 2, 0, 4, "", ""))
118+
t.Run("alphanumeric prerelease identifier with leading zero", parsingShouldSucceed(parseFn,
119+
s+"-beta1.0yes", a, b, c, "beta1.0yes", ""))
107120

108-
t.Run("patch version zero", parsingShouldSucceed(parseFn,
109-
"2.3.0", 2, 3, 0, "", ""))
121+
t.Run("build identifier of only letters", parsingShouldSucceed(parseFn,
122+
s+"+abc", a, b, c, "", "abc"))
110123

111-
t.Run("prerelease identifier of only letters", parsingShouldSucceed(parseFn,
112-
"2.3.4-abc", 2, 3, 4, "abc", ""))
124+
t.Run("build identifier of only digits", parsingShouldSucceed(parseFn,
125+
s+"+123", a, b, c, "", "123"))
113126

114-
t.Run("prerelease identifier of only digits", parsingShouldSucceed(parseFn,
115-
"2.3.4-123", 2, 3, 4, "123", ""))
127+
t.Run("build identifier of only hyphens", parsingShouldSucceed(parseFn,
128+
s+"+---", a, b, c, "", "---"))
116129

117-
t.Run("prerelease identifier of only hyphens", parsingShouldSucceed(parseFn,
118-
"2.3.4----", 2, 3, 4, "---", ""))
130+
t.Run("alphanumeric build identifier with leading zero", parsingShouldSucceed(parseFn,
131+
s+"+build1.0yes", a, b, c, "", "build1.0yes"))
119132

120-
t.Run("alphanumeric prerelease identifier with leading zero", parsingShouldSucceed(parseFn,
121-
"2.3.4-beta1.0yes", 2, 3, 4, "beta1.0yes", ""))
133+
t.Run("leading zero in numeric build identifier", parsingShouldSucceed(parseFn,
134+
s+"+build1.02", a, b, c, "", "build1.02"))
135+
})
122136

123-
t.Run("build identifier of only letters", parsingShouldSucceed(parseFn,
124-
"2.3.4+abc", 2, 3, 4, "", "abc"))
137+
NewValuesGenerator().AddValue(0, 199).AddValue(0, 9).AddValue(0, 9).AddValue(0, 2).
138+
TestAll4(t, func(t *testing.T, a, b, c, d int) {
139+
s := fmt.Sprintf("%d.%d.%d", a, b, c)
140+
dd := fmt.Sprintf("%d", d)
141+
t.Run("version with prerelease (multi-identifier)", parsingShouldSucceed(parseFn,
142+
s+"-beta1."+dd, a, b, c, "beta1."+dd, ""))
125143

126-
t.Run("build identifier of only digits", parsingShouldSucceed(parseFn,
127-
"2.3.4+123", 2, 3, 4, "", "123"))
144+
t.Run("version with prerelease (multi-identifier with hyphens)", parsingShouldSucceed(parseFn,
145+
s+"-beta1-final."+dd, a, b, c, "beta1-final."+dd, ""))
128146

129-
t.Run("build identifier of only hyphens", parsingShouldSucceed(parseFn,
130-
"2.3.4+---", 2, 3, 4, "", "---"))
147+
t.Run("version with build (multi-identifier)", parsingShouldSucceed(parseFn,
148+
s+"+build2."+dd, a, b, c, "", "build2."+dd))
131149

132-
t.Run("alphanumeric build identifier with leading zero", parsingShouldSucceed(parseFn,
133-
"2.3.4+build1.0yes", 2, 3, 4, "", "build1.0yes"))
150+
t.Run("version with build (multi-identifier with hyphens)", parsingShouldSucceed(parseFn,
151+
s+"+build2-other."+dd, a, b, c, "", "build2-other."+dd))
134152

135-
t.Run("leading zero in numeric build identifier", parsingShouldSucceed(parseFn,
136-
"2.3.4+build1.02", 2, 3, 4, "", "build1.02"))
153+
t.Run("version with prerelease and build", parsingShouldSucceed(parseFn,
154+
s+"-beta1.rc2+build2."+dd, a, b, c, "beta1.rc2", "build2."+dd))
155+
})
137156
})
138157

139158
t.Run("invalid", func(t *testing.T) {

syntax.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func parsePositiveNumericString(s string) (int, bool) {
3737
if ch < '0' || ch > '9' {
3838
return 0, false
3939
}
40-
if ch == '0' && max > 1 {
40+
if ch == '0' && i == 0 && max > 1 {
4141
return 0, false // leading zeroes aren't allowed
4242
}
4343
n = n*10 + (int(ch) - int('0'))

values_generator.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package semver
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
type ValuesGenerator struct {
9+
valueDefs []valueConstraint
10+
}
11+
12+
type valueConstraint struct {
13+
min int
14+
max int
15+
}
16+
17+
func NewValuesGenerator() *ValuesGenerator {
18+
return &ValuesGenerator{}
19+
}
20+
21+
func (g *ValuesGenerator) AddValue(min, max int) *ValuesGenerator {
22+
g.valueDefs = append(g.valueDefs, valueConstraint{min, max})
23+
return g
24+
}
25+
26+
func (g ValuesGenerator) MakeAllPermutations() [][]int {
27+
var ret [][]int
28+
numValues := len(g.valueDefs)
29+
values := make([]int, numValues)
30+
for i := 0; i < numValues; i++ {
31+
values[i] = g.valueDefs[i].min
32+
}
33+
for {
34+
copied := make([]int, numValues)
35+
copy(copied, values)
36+
ret = append(ret, copied)
37+
38+
// increment the values starting at the left, rolling over and incrementing the next to the right
39+
for pos := 0; pos < numValues; pos++ {
40+
if values[pos] < g.valueDefs[pos].max {
41+
values[pos]++
42+
break
43+
}
44+
if pos == numValues-1 {
45+
return ret // we've covered all permutations
46+
}
47+
values[pos] = g.valueDefs[pos].min
48+
}
49+
}
50+
}
51+
52+
func (g ValuesGenerator) TestAll(t *testing.T, action func(*testing.T, []int)) {
53+
var permutations = g.MakeAllPermutations()
54+
for _, perm := range permutations {
55+
values := perm
56+
desc := "values"
57+
for _, v := range values {
58+
desc = desc + fmt.Sprintf(" %d", v)
59+
}
60+
t.Run(desc, func(t *testing.T) {
61+
action(t, values)
62+
})
63+
}
64+
}
65+
66+
func (g ValuesGenerator) TestAll1(t *testing.T, action func(*testing.T, int)) {
67+
g.TestAll(t, func(t *testing.T, values []int) {
68+
action(t, values[0])
69+
})
70+
}
71+
72+
func (g ValuesGenerator) TestAll2(t *testing.T, action func(*testing.T, int, int)) {
73+
g.TestAll(t, func(t *testing.T, values []int) {
74+
action(t, values[0], values[1])
75+
})
76+
}
77+
78+
func (g ValuesGenerator) TestAll3(t *testing.T, action func(*testing.T, int, int, int)) {
79+
g.TestAll(t, func(t *testing.T, values []int) {
80+
action(t, values[0], values[1], values[2])
81+
})
82+
}
83+
84+
func (g ValuesGenerator) TestAll4(t *testing.T, action func(*testing.T, int, int, int, int)) {
85+
g.TestAll(t, func(t *testing.T, values []int) {
86+
action(t, values[0], values[1], values[2], values[3])
87+
})
88+
}

0 commit comments

Comments
 (0)