Skip to content

Commit 2c42fd6

Browse files
authored
initial implementation of go-semver (#1)
1 parent c1a3e9f commit 2c42fd6

21 files changed

+1153
-0
lines changed

.circleci/config.yml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
version: 2.1
2+
3+
orbs:
4+
win: circleci/[email protected]
5+
6+
workflows:
7+
workflow:
8+
jobs:
9+
- go-test:
10+
name: Go 1.14
11+
docker-image: circleci/golang:1.13
12+
run-lint: true
13+
with-coverage: true
14+
- go-test:
15+
name: Go 1.13
16+
docker-image: circleci/golang:1.13
17+
- go-test-windows:
18+
name: Windows
19+
- benchmarks
20+
21+
jobs:
22+
go-test:
23+
parameters:
24+
docker-image:
25+
type: string
26+
run-lint:
27+
type: boolean
28+
default: false
29+
with-coverage:
30+
type: boolean
31+
default: false
32+
33+
docker:
34+
- image: <<parameters.docker-image>>
35+
environment:
36+
CIRCLE_TEST_REPORTS: /tmp/circle-reports
37+
CIRCLE_ARTIFACTS: /tmp/circle-artifacts
38+
COMMON_GO_PACKAGES: >
39+
github.com/golang/dep/cmd/dep
40+
github.com/jstemmer/go-junit-report
41+
42+
working_directory: /go/src/gopkg.in/launchdarkly/go-server-sdk-evaluation.v1
43+
44+
steps:
45+
- checkout
46+
47+
- run:
48+
name: install go-junit-report
49+
command: go get -u github.com/jstemmer/go-junit-report
50+
51+
- run:
52+
name: build
53+
command: make build
54+
55+
- when:
56+
condition: <<parameters.run-lint>>
57+
steps:
58+
- run:
59+
name: lint
60+
command: make lint
61+
62+
- run:
63+
name: test
64+
command: |
65+
mkdir -p $CIRCLE_TEST_REPORTS
66+
mkdir -p $CIRCLE_ARTIFACTS
67+
trap "go-junit-report < $CIRCLE_ARTIFACTS/report.txt > $CIRCLE_TEST_REPORTS/junit.xml" EXIT
68+
make test | tee $CIRCLE_ARTIFACTS/report.txt
69+
70+
- run:
71+
name: Process test results
72+
command: go-junit-report < $CIRCLE_ARTIFACTS/report.txt > $CIRCLE_TEST_REPORTS/junit.xml
73+
when: always
74+
75+
- when:
76+
condition: <<parameters.with-coverage>>
77+
steps:
78+
- run:
79+
name: Verify test coverage
80+
command: make test-coverage
81+
- run:
82+
name: Store coverage results
83+
command: cp build/coverage* /tmp/circle-artifacts
84+
when: always
85+
86+
- store_test_results:
87+
path: /tmp/circle-reports
88+
89+
- store_artifacts:
90+
path: /tmp/circle-artifacts
91+
92+
go-test-windows:
93+
executor:
94+
name: win/vs2019
95+
shell: powershell.exe
96+
97+
environment:
98+
GOPATH: C:\Users\VssAdministrator\go
99+
PACKAGE_PATH: github.com\launchdarkly\go-semver
100+
101+
steps:
102+
- checkout
103+
- run: go version
104+
- run:
105+
name: move source
106+
command: |
107+
go env GOPATH
108+
mkdir ${env:GOPATH}\src\${env:PACKAGE_PATH}
109+
mv * ${env:GOPATH}\src\${env:PACKAGE_PATH}\
110+
- run:
111+
name: build and test
112+
command: |
113+
cd ${env:GOPATH}\src\${env:PACKAGE_PATH}
114+
go get -t ./...
115+
go test -race ./...
116+
117+
benchmarks:
118+
docker:
119+
- image: circleci/golang:1.14
120+
environment:
121+
CIRCLE_ARTIFACTS: /tmp/circle-artifacts
122+
123+
steps:
124+
- checkout
125+
- run: go build ./...
126+
- run:
127+
name: Run benchmarks
128+
command: |
129+
mkdir -p $CIRCLE_ARTIFACTS
130+
make benchmarks | tee $CIRCLE_ARTIFACTS/benchmarks.txt
131+
132+
- store_artifacts:
133+
path: /tmp/circle-artifacts

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
bin
12
build

.ldrelease/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
template:
2+
name: go
3+
4+
publications:
5+
- url: https://godoc.org/github.com/launchdarkly/go-semver
6+
description: documentation

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Change log
2+
3+
All notable changes to the project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).

CONTRIBUTING.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Contributing to this project
2+
3+
## Submitting bug reports and feature requests
4+
5+
The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/go-semver/issues) in tis repository. Bug reports and feature requests specific to this project should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days.
6+
7+
## Submitting pull requests
8+
9+
We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days.
10+
11+
## Build instructions
12+
13+
### Prerequisites
14+
15+
This project should be built against Go 1.13 or newer.
16+
17+
### Building
18+
19+
To build the project without running any tests:
20+
```
21+
make
22+
```
23+
24+
If you wish to clean your working directory between builds, you can clean it by running:
25+
```
26+
make clean
27+
```
28+
29+
To run the linter:
30+
```
31+
make lint
32+
```
33+
34+
### Testing
35+
36+
To build and run all unit tests:
37+
```
38+
make test
39+
```
40+
41+
## Coding best practices
42+
43+
### Test coverage
44+
45+
It is important to keep unit test coverage as close to 100% as possible in this project. You can view the latest code coverage report in CircleCI, as `coverage.html` and `coverage.txt` in the artifacts. You can also generate this information locally with `make test-coverage`.
46+
47+
The build will fail if there are any uncovered blocks of code, unless you explicitly add an override by placing a comment that starts with `// COVERAGE` somewhere within that block. Sometimes a gap in coverage is unavoidable, usually because the compiler requires us to provide a code path for some condition that in practice can't happen and can't be tested. Exclude these paths with a `// COVERAGE` comment.
48+
49+
### Avoid heap allocations
50+
51+
A major design goal in this implementation is to maximize performance and avoid unwanted heap churn. No operations in this package should allocate any data on the heap; everything should be passed by value, and slices or maps should not be used. The `make benchmarks` target will fail if any benchmark shows heap allocations.

LICENSE.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2020 Catamorphic, Co.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

Makefile

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
GOLANGCI_LINT_VERSION=v1.27.0
3+
4+
LINTER=./bin/golangci-lint
5+
LINTER_VERSION_FILE=./bin/.golangci-lint-version-$(GOLANGCI_LINT_VERSION)
6+
7+
ALL_SOURCES := $(shell find * -type f -name "*.go")
8+
9+
COVERAGE_PROFILE_RAW=./build/coverage_raw.out
10+
COVERAGE_PROFILE_RAW_HTML=./build/coverage_raw.html
11+
COVERAGE_PROFILE_FILTERED=./build/coverage.out
12+
COVERAGE_PROFILE_FILTERED_HTML=./build/coverage.html
13+
14+
.PHONY: build clean test test-coverage lint
15+
16+
build:
17+
go build ./...
18+
19+
clean:
20+
go clean
21+
22+
test: build
23+
go test -race -v ./...
24+
25+
benchmarks: build
26+
mkdir -p ./build
27+
go test -benchmem '-run=^$$' -bench . | tee build/benchmarks.out
28+
@if grep <build/benchmarks.out '[1-9][0-9]* allocs/op'; then echo "Heap allocations detected in benchmarks!"; exit 1; fi
29+
30+
test-coverage: $(COVERAGE_PROFILE_RAW)
31+
if [ -z "$(which go-coverage-enforcer)" ]; then go install github.com/launchdarkly-labs/go-coverage-enforcer; fi
32+
go-coverage-enforcer -skipcode "// COVERAGE" -showcode -outprofile $(COVERAGE_PROFILE_FILTERED) $(COVERAGE_PROFILE_RAW)
33+
go tool cover -html $(COVERAGE_PROFILE_FILTERED) -o $(COVERAGE_PROFILE_FILTERED_HTML)
34+
go tool cover -html $(COVERAGE_PROFILE_RAW) -o $(COVERAGE_PROFILE_RAW_HTML)
35+
36+
$(COVERAGE_PROFILE_RAW): $(ALL_SOURCES)
37+
mkdir -p ./build
38+
go test -coverprofile $(COVERAGE_PROFILE_RAW) ./...
39+
40+
$(LINTER_VERSION_FILE):
41+
rm -f $(LINTER)
42+
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s $(GOLANGCI_LINT_VERSION)
43+
touch $(LINTER_VERSION_FILE)
44+
45+
lint: $(LINTER_VERSION_FILE)
46+
$(LINTER) run ./...

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# LaunchDarkly Semantic Version Package
2+
3+
[![Circle CI](https://circleci.com/gh/launchdarkly/go-semver.svg?style=shield)](https://circleci.com/gh/launchdarkly/go-semver) [![Documentation](https://godoc.org/github.com/launchdarkly/go-semver?status.svg)](https://godoc.org/github.com/launchdarkly/go-semver)
4+
5+
## Overview
6+
7+
This Go package implements parsing and comparison of semantic version (semver) strings, as defined by the [Semantic Versioning 2.0.0 specification](https://semver.org/).
8+
9+
Several semver implementations exist for Go. This implementation was designed for high performance in applications where semver operations may be done frequently, such as in the [LaunchDarkly Go SDK](https://github.com/launchdarkly/go-server-sdk). To that end, it does not use regular expressions and it never allocates data on the heap.
10+
11+
It does not include any additional functionality beyond what is defined in the Semantic Versioning 2.0.0 specification, such as comparison against range/wildcard expressions like ">=1.0.0" or "2.5.x".
12+
13+
This package has no external dependencies other than the regular Go runtime.
14+
15+
## Supported Go versions
16+
17+
This version of the project has been tested with Go 1.13 through 1.14.
18+
19+
## Contributing
20+
21+
We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this project.

compare.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package semver
2+
3+
// ComparePrecedence compares this Version to another Version according to the canonical precedence rules. It
4+
// returns -1 if v has lower precedence than other, 1 if v has higher precedence, or 0 if the same.
5+
func (v Version) ComparePrecedence(other Version) int {
6+
if v.major < other.major {
7+
return -1
8+
}
9+
if v.major > other.major {
10+
return 1
11+
}
12+
if v.minor < other.minor {
13+
return -1
14+
}
15+
if v.minor > other.minor {
16+
return 1
17+
}
18+
if v.patch < other.patch {
19+
return -1
20+
}
21+
if v.patch > other.patch {
22+
return 1
23+
}
24+
if v.prerelease == "" && other.prerelease == "" {
25+
return 0
26+
}
27+
// *no* prerelease component always has higher precedence than *any* prerelease component
28+
if v.prerelease == "" {
29+
return 1
30+
}
31+
if other.prerelease == "" {
32+
return -1
33+
}
34+
// compare prerelease components - the build component, if any, has no effect on precedence
35+
return comparePrereleaseIdentifiers(v.prerelease, other.prerelease)
36+
}
37+
38+
func comparePrereleaseIdentifiers(prerel1, prerel2 string) int {
39+
// The parser has already validated the syntax of both of these strings, so we know that they both
40+
// contain one or more identifiers separated by a period, and that they contain only alphanumerics.
41+
// If an identifier contains only digits, and does not have a leading zero, then we treat it as a
42+
// number.
43+
44+
scanner1 := newSimpleASCIIScanner(prerel1)
45+
scanner2 := newSimpleASCIIScanner(prerel2)
46+
47+
for {
48+
// all components up to this point have been determined to be equal
49+
if scanner1.eof() {
50+
if scanner2.eof() {
51+
return 0
52+
}
53+
return -1 // x.y is always less than x.y.z
54+
} else {
55+
if scanner2.eof() {
56+
return 1
57+
}
58+
}
59+
60+
identifier1, _ := scanner1.readUntil(dotTerminator)
61+
identifier2, _ := scanner2.readUntil(dotTerminator)
62+
63+
// each sub-identifier is compared numerically if both are numeric; if both are non-numeric,
64+
// they're compared as strings; otherwise, the numeric one is the lesser one
65+
var n1, n2, d int
66+
var isNum1, isNum2 bool
67+
n1, isNum1 = parsePositiveNumericString(identifier1)
68+
n2, isNum2 = parsePositiveNumericString(identifier2)
69+
if isNum1 && isNum2 {
70+
if n1 < n2 {
71+
d = -1
72+
} else if n1 > n2 {
73+
d = 1
74+
}
75+
} else {
76+
if isNum1 {
77+
d = -1
78+
} else if isNum2 {
79+
d = 1
80+
} else { // string comparison
81+
if identifier1 < identifier2 {
82+
d = -1
83+
} else if identifier1 > identifier2 {
84+
d = 1
85+
}
86+
}
87+
}
88+
89+
if d != 0 {
90+
return d
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)