Skip to content

Commit 86db057

Browse files
committed
testing: add version code
This commit adds the code to parse versions in cockroachdb/cockroach/pkg/util/version. The code is copied to avoid pulling in a huge dependency. This allows for better version comparisons.
1 parent 4d69a8e commit 86db057

File tree

2 files changed

+264
-11
lines changed

2 files changed

+264
-11
lines changed

testing/main_test.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import (
77
"net/url"
88
"os"
99
"os/exec"
10-
"strings"
1110
"syscall"
1211
"testing"
1312
"time"
1413

14+
"github.com/cockroachdb/examples-orms/version"
15+
1516
"github.com/cockroachdb/cockroach-go/testserver"
1617
// Import postgres driver.
1718
_ "github.com/lib/pq"
@@ -71,16 +72,17 @@ func initTestDatabase(t *testing.T, app application) (*sql.DB, *url.URL, string,
7172
t.Fatal(err)
7273
}
7374

74-
var version string
75-
if err := db.QueryRow(`SELECT value FROM crdb_internal.node_build_info where field = 'Version'`,
76-
).Scan(&version); err != nil {
75+
var crdbVersion string
76+
if err := db.QueryRow(
77+
`SELECT value FROM crdb_internal.node_build_info where field = 'Version'`,
78+
).Scan(&crdbVersion); err != nil {
7779
t.Fatal(err)
7880
}
7981

8082
if scheme, ok := customURLSchemes[app]; ok {
8183
url.Scheme = scheme
8284
}
83-
return db, url, version, func() {
85+
return db, url, crdbVersion, func() {
8486
_ = db.Close()
8587
ts.Stop()
8688
}
@@ -146,6 +148,20 @@ func initORMApp(app application, dbURL *url.URL) (func() error, error) {
146148
}
147149
}
148150

151+
var minRequiredVersionsByORMName = map[string]struct {
152+
v *version.Version
153+
skipMsg string
154+
}{
155+
"django": {
156+
v: version.MustParse("v19.1.0-alpha"),
157+
skipMsg: "TestDjango fails on CRDB <=v2.1 due to missing foreign key support.",
158+
},
159+
"activerecord": {
160+
v: version.MustParse("v19.2.0-alpha"),
161+
skipMsg: "TestActiveRecord fails on CRDB <=v19.1 due to missing pg_catalog support.",
162+
},
163+
}
164+
149165
func testORM(
150166
t *testing.T, language, orm string, tableNames testTableNames, columnNames testColumnNames,
151167
) {
@@ -154,15 +170,17 @@ func testORM(
154170
orm: orm,
155171
}
156172

157-
db, dbURL, version, stopDB := initTestDatabase(t, app)
173+
db, dbURL, crdbVersion, stopDB := initTestDatabase(t, app)
158174
defer stopDB()
159175

160-
if orm == "django" && (strings.HasPrefix(version, "v2.0") || strings.HasPrefix(version, "v2.1")) {
161-
t.Skip("TestDjango fails on CRDB <=v2.1 due to missing foreign key support.")
176+
v, err := version.Parse(crdbVersion)
177+
if err != nil {
178+
t.Fatal(err)
162179
}
163-
164-
if orm == "activerecord" && (strings.HasPrefix(version, "v2.") || strings.HasPrefix(version, "v19.1")) {
165-
t.Skip("TestActiveRecord fails on CRDB <=v19.1 due to missing pg_catalog support.")
180+
if info, ok := minRequiredVersionsByORMName[orm]; ok {
181+
if !v.AtLeast(info.v) {
182+
t.Skip(info.skipMsg)
183+
}
166184
}
167185

168186
td := testDriver{

version/version.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright 2018 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package version
12+
13+
import (
14+
"bytes"
15+
"fmt"
16+
"regexp"
17+
"strconv"
18+
"strings"
19+
20+
"github.com/pkg/errors"
21+
)
22+
23+
// Version represents a semantic version; see
24+
// https://semver.org/spec/v2.0.0.html.
25+
type Version struct {
26+
major int32
27+
minor int32
28+
patch int32
29+
preRelease string
30+
metadata string
31+
}
32+
33+
// Major returns the major (first) version number.
34+
func (v *Version) Major() int {
35+
return int(v.major)
36+
}
37+
38+
// Minor returns the minor (second) version number.
39+
func (v *Version) Minor() int {
40+
return int(v.minor)
41+
}
42+
43+
// Patch returns the patch (third) version number.
44+
func (v *Version) Patch() int {
45+
return int(v.patch)
46+
}
47+
48+
// PreRelease returns the pre-release version (if present).
49+
func (v *Version) PreRelease() string {
50+
return v.preRelease
51+
}
52+
53+
// Metadata returns the metadata (if present).
54+
func (v *Version) Metadata() string {
55+
return v.metadata
56+
}
57+
58+
// String returns the string representation, in the format:
59+
// "v1.2.3-beta+md"
60+
func (v Version) String() string {
61+
var b bytes.Buffer
62+
fmt.Fprintf(&b, "v%d.%d.%d", v.major, v.minor, v.patch)
63+
if v.preRelease != "" {
64+
fmt.Fprintf(&b, "-%s", v.preRelease)
65+
}
66+
if v.metadata != "" {
67+
fmt.Fprintf(&b, "+%s", v.metadata)
68+
}
69+
return b.String()
70+
}
71+
72+
// versionRE is the regexp that is used to verify that a version string is
73+
// of the form "vMAJOR.MINOR.PATCH[-PRERELEASE][+METADATA]". This
74+
// conforms to https://semver.org/spec/v2.0.0.html
75+
var versionRE = regexp.MustCompile(
76+
`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-.]+)?(\+[0-9A-Za-z-.]+|)?$`,
77+
// ^major ^minor ^patch ^preRelease ^metadata
78+
)
79+
80+
// numericRE is the regexp used to check if an identifier is numeric.
81+
var numericRE = regexp.MustCompile(`^(0|[1-9][0-9]*)$`)
82+
83+
// Parse creates a version from a string. The string must be a valid semantic
84+
// version (as per https://semver.org/spec/v2.0.0.html) in the format:
85+
// "vMINOR.MAJOR.PATCH[-PRERELEASE][+METADATA]".
86+
// MINOR, MAJOR, and PATCH are numeric values (without any leading 0s).
87+
// PRERELEASE and METADATA can contain ASCII characters and digits, hyphens and
88+
// dots.
89+
func Parse(str string) (*Version, error) {
90+
if !versionRE.MatchString(str) {
91+
return nil, errors.Errorf("invalid version string '%s'", str)
92+
}
93+
94+
var v Version
95+
r := strings.NewReader(str)
96+
// Read the major.minor.patch part.
97+
_, err := fmt.Fscanf(r, "v%d.%d.%d", &v.major, &v.minor, &v.patch)
98+
if err != nil {
99+
panic(fmt.Sprintf("invalid version '%s' passed the regex: %s", str, err))
100+
}
101+
remaining := str[len(str)-r.Len():]
102+
// Read the pre-release, if present.
103+
if len(remaining) > 0 && remaining[0] == '-' {
104+
p := strings.IndexRune(remaining, '+')
105+
if p == -1 {
106+
p = len(remaining)
107+
}
108+
v.preRelease = remaining[1:p]
109+
remaining = remaining[p:]
110+
}
111+
// Read the metadata, if present.
112+
if len(remaining) > 0 {
113+
if remaining[0] != '+' {
114+
panic(fmt.Sprintf("invalid version '%s' passed the regex", str))
115+
}
116+
v.metadata = remaining[1:]
117+
}
118+
return &v, nil
119+
}
120+
121+
// MustParse is like Parse but panics on any error. Recommended as an
122+
// initializer for global values.
123+
func MustParse(str string) *Version {
124+
v, err := Parse(str)
125+
if err != nil {
126+
panic(err)
127+
}
128+
return v
129+
}
130+
131+
func cmpVal(a, b int32) int {
132+
if a > b {
133+
return +1
134+
}
135+
if a < b {
136+
return -1
137+
}
138+
return 0
139+
}
140+
141+
// Compare returns -1, 0, or +1 indicating the relative ordering of versions.
142+
func (v *Version) Compare(w *Version) int {
143+
if v := cmpVal(v.major, w.major); v != 0 {
144+
return v
145+
}
146+
if v := cmpVal(v.minor, w.minor); v != 0 {
147+
return v
148+
}
149+
if v := cmpVal(v.patch, w.patch); v != 0 {
150+
return v
151+
}
152+
if v.preRelease != w.preRelease {
153+
if v.preRelease == "" && w.preRelease != "" {
154+
// 1.0.0 is greater than 1.0.0-alpha.
155+
return 1
156+
}
157+
if v.preRelease != "" && w.preRelease == "" {
158+
// 1.0.0-alpha is less than 1.0.0.
159+
return -1
160+
}
161+
162+
// Quoting from https://semver.org/spec/v2.0.0.html:
163+
// Precedence for two pre-release versions with the same major, minor, and
164+
// patch version MUST be determined by comparing each dot separated
165+
// identifier from left to right until a difference is found as follows:
166+
// (1) Identifiers consisting of only digits are compared numerically.
167+
// (2) identifiers with letters or hyphens are compared lexically in ASCII
168+
// sort order.
169+
// (3) Numeric identifiers always have lower precedence than non-numeric
170+
// identifiers.
171+
// (4) A larger set of pre-release fields has a higher precedence than a
172+
// smaller set, if all of the preceding identifiers are equal.
173+
//
174+
vs := strings.Split(v.preRelease, ".")
175+
ws := strings.Split(w.preRelease, ".")
176+
for ; len(vs) > 0 && len(ws) > 0; vs, ws = vs[1:], ws[1:] {
177+
vStr, wStr := vs[0], ws[0]
178+
if vStr == wStr {
179+
continue
180+
}
181+
vNumeric := numericRE.MatchString(vStr)
182+
wNumeric := numericRE.MatchString(wStr)
183+
switch {
184+
case vNumeric && wNumeric:
185+
// Case 1.
186+
vVal, err := strconv.Atoi(vStr)
187+
if err != nil {
188+
panic(err)
189+
}
190+
wVal, err := strconv.Atoi(wStr)
191+
if err != nil {
192+
panic(err)
193+
}
194+
if vVal == wVal {
195+
panic("different strings yield the same numbers")
196+
}
197+
if vVal < wVal {
198+
return -1
199+
}
200+
return 1
201+
202+
case vNumeric:
203+
// Case 3.
204+
return -1
205+
206+
case wNumeric:
207+
// Case 3.
208+
return 1
209+
210+
default:
211+
// Case 2.
212+
if vStr < wStr {
213+
return -1
214+
}
215+
return 1
216+
}
217+
}
218+
219+
if len(vs) > 0 {
220+
// Case 4.
221+
return +1
222+
}
223+
if len(ws) > 0 {
224+
// Case 4.
225+
return -1
226+
}
227+
}
228+
229+
return 0
230+
}
231+
232+
// AtLeast returns true if v >= w.
233+
func (v *Version) AtLeast(w *Version) bool {
234+
return v.Compare(w) >= 0
235+
}

0 commit comments

Comments
 (0)