Skip to content

Commit 8374bb5

Browse files
authored
Merge pull request #508 from splitio/sermver-class
Sermver class
2 parents 35011e4 + 650094e commit 8374bb5

File tree

8 files changed

+303
-2
lines changed

8 files changed

+303
-2
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
TESTS_REQUIRES = [
88
'flake8',
99
'pytest==7.0.1',
10-
'pytest-mock>=3.5.1',
10+
'pytest-mock==3.13.0',
1111
'coverage==6.2',
1212
'pytest-cov',
1313
'importlib-metadata==4.2',
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Semver matcher classes."""
2+
import logging
3+
import pytest
4+
5+
_LOGGER = logging.getLogger(__name__)
6+
7+
class Semver(object):
8+
"""Semver class."""
9+
10+
_METADATA_DELIMITER = "+"
11+
_PRE_RELEASE_DELIMITER = "-"
12+
_VALUE_DELIMITER = "."
13+
14+
def __init__(self, version):
15+
"""
16+
Class Initializer
17+
18+
:param version: raw version as read from splitChanges response.
19+
:type version: str
20+
"""
21+
self._major = 0
22+
self._minor = 0
23+
self._patch = 0
24+
self._pre_release = []
25+
self._is_stable = False
26+
self.version = ""
27+
self._metadata = ""
28+
self._parse(version)
29+
30+
@classmethod
31+
def build(cls, version):
32+
try:
33+
self = cls(version)
34+
except RuntimeError as e:
35+
_LOGGER.error("Failed to parse Semver data, incorrect data type: %s", e)
36+
return None
37+
38+
return self
39+
40+
def _parse(self, version):
41+
"""
42+
Parse the string in self.version to update the other internal variables
43+
"""
44+
without_metadata = self.remove_metadata_if_exists(version)
45+
46+
index = without_metadata.find(self._PRE_RELEASE_DELIMITER)
47+
if index == -1:
48+
self._is_stable = True
49+
else:
50+
pre_release_data = without_metadata[index+1:]
51+
if pre_release_data == "":
52+
raise RuntimeError("Pre-release is empty despite delimeter exists: " + version)
53+
54+
without_metadata = without_metadata[:index]
55+
self._pre_release = pre_release_data.split(self._VALUE_DELIMITER)
56+
57+
self.set_major_minor_and_patch(without_metadata)
58+
59+
def remove_metadata_if_exists(self, version):
60+
"""
61+
Check if there is any metadata characters in self.version.
62+
63+
:returns: The semver string without the metadata
64+
:rtype: str
65+
"""
66+
index = version.find(self._METADATA_DELIMITER)
67+
if index == -1:
68+
return version
69+
70+
self._metadata = version[index+1:]
71+
if self._metadata == "":
72+
raise RuntimeError("Metadata is empty despite delimeter exists: " + version)
73+
74+
return version[:index]
75+
76+
def set_major_minor_and_patch(self, version):
77+
"""
78+
Set the major, minor and patch internal variables based on string passed.
79+
80+
:param version: raw version containing major.minor.patch numbers.
81+
:type version: str
82+
"""
83+
84+
parts = version.split(self._VALUE_DELIMITER)
85+
if len(parts) != 3 or not (parts[0].isnumeric() and parts[1].isnumeric() and parts[2].isnumeric()):
86+
raise RuntimeError("Unable to convert to Semver, incorrect format: " + version)
87+
88+
self._major = int(parts[0])
89+
self._minor = int(parts[1])
90+
self._patch = int(parts[2])
91+
92+
self.version = "{major}{DELIMITER}{minor}{DELIMITER}{patch}".format(major = self._major, DELIMITER = self._VALUE_DELIMITER,
93+
minor = self._minor, patch = self._patch)
94+
self.version += "{DELIMITER}{pre_release}".format(DELIMITER=self._PRE_RELEASE_DELIMITER,
95+
pre_release = '.'.join(self._pre_release)) if len(self._pre_release) > 0 else ""
96+
self.version += "{DELIMITER}{metadata}".format(DELIMITER=self._METADATA_DELIMITER, metadata = self._metadata) if self._metadata != "" else ""
97+
98+
def compare(self, to_compare):
99+
"""
100+
Compare the current Semver object to a given Semver object, return:
101+
0: if self == passed
102+
1: if self > passed
103+
-1: if self < passed
104+
105+
:param to_compare: a Semver object
106+
:type to_compare: splitio.models.grammar.matchers.semver.Semver
107+
108+
:returns: integer based on comparison
109+
:rtype: int
110+
"""
111+
if self.version == to_compare.version:
112+
return 0
113+
114+
# Compare major, minor, and patch versions numerically
115+
result = self._compare_vars(self._major, to_compare._major)
116+
if result != 0:
117+
return result
118+
119+
result = self._compare_vars(self._minor, to_compare._minor)
120+
if result != 0:
121+
return result
122+
123+
result = self._compare_vars(self._patch, to_compare._patch)
124+
if result != 0:
125+
return result
126+
127+
if not self._is_stable and to_compare._is_stable:
128+
return -1
129+
elif self._is_stable and not to_compare._is_stable:
130+
return 1
131+
132+
# Compare pre-release versions lexically
133+
min_length = min(len(self._pre_release), len(to_compare._pre_release))
134+
for i in range(min_length):
135+
if self._pre_release[i] == to_compare._pre_release[i]:
136+
continue
137+
138+
if self._pre_release[i].isnumeric() and to_compare._pre_release[i].isnumeric():
139+
return self._compare_vars(int(self._pre_release[i]), int(to_compare._pre_release[i]))
140+
141+
return self._compare_vars(self._pre_release[i], to_compare._pre_release[i])
142+
143+
# Compare lengths of pre-release versions
144+
return self._compare_vars(len(self._pre_release), len(to_compare._pre_release))
145+
146+
def _compare_vars(self, var1, var2):
147+
"""
148+
Compare 2 variables and return int as follows:
149+
0: if var1 == var2
150+
1: if var1 > var2
151+
-1: if var1 < var2
152+
153+
:param var1: any object accept ==, < or > operators
154+
:type var1: str/int
155+
:param var2: any object accept ==, < or > operators
156+
:type var2: str/int
157+
158+
:returns: integer based on comparison
159+
:rtype: int
160+
"""
161+
if var1 == var2:
162+
return 0
163+
if var1 > var2:
164+
return 1
165+
return -1

splitio/models/splits.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
"combiner": "AND",
2020
"matchers": [
2121
{
22-
"keySelector": None,
22+
"keySelector": {
23+
"trafficType": "user",
24+
"attribute": None
25+
},
2326
"matcherType": "ALL_KEYS",
2427
"negate": False,
2528
"userDefinedSegmentMatcherData": None,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version1,version2,version3,expected
2+
1.1.1,2.2.2,3.3.3,true
3+
1.1.1-rc.1,1.1.1-rc.2,1.1.1-rc.3,true
4+
1.0.0-alpha,1.0.0-alpha.1,1.0.0-alpha.beta,true
5+
1.0.0-alpha.1,1.0.0-alpha.beta,1.0.0-beta,true
6+
1.0.0-alpha.beta,1.0.0-beta,1.0.0-beta.2,true
7+
1.0.0-beta,1.0.0-beta.2,1.0.0-beta.11,true
8+
1.0.0-beta.2,1.0.0-beta.11,1.0.0-rc.1,true
9+
1.0.0-beta.11,1.0.0-rc.1,1.0.0,true
10+
1.1.2,1.1.3,1.1.4,true
11+
1.2.1,1.3.1,1.4.1,true
12+
2.0.0,3.0.0,4.0.0,true
13+
2.2.2,2.2.3-rc1,2.2.3,true
14+
2.2.2,2.3.2-rc100,2.3.3,true
15+
1.0.0-rc.1+build.1,1.2.3-beta,1.2.3-rc.1+build.123,true
16+
3.3.3,3.3.3-alpha,3.3.4,false
17+
2.2.2-rc.1,2.2.2+metadata,2.2.2-rc.10,false
18+
1.1.1-rc.1,1.1.1-rc.3,1.1.1-rc.2,false
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version1,version2,equals
2+
1.1.1,1.1.1,true
3+
1.1.1,1.1.1+metadata,false
4+
1.1.1,1.1.1-rc.1,false
5+
88.88.88,88.88.88,true
6+
1.2.3----RC-SNAPSHOT.12.9.1--.12,1.2.3----RC-SNAPSHOT.12.9.1--.12,true
7+
10.2.3-DEV-SNAPSHOT,10.2.3-SNAPSHOT-123,false
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
invalid
2+
1
3+
1.2
4+
1.alpha.2
5+
+invalid
6+
-invalid
7+
-invalid+invalid
8+
-invalid.01
9+
alpha
10+
alpha.beta
11+
alpha.beta.1
12+
alpha.1
13+
alpha+beta
14+
alpha_beta
15+
alpha.
16+
alpha..
17+
beta
18+
-alpha.
19+
1.2
20+
1.2.3.DEV
21+
1.2-SNAPSHOT
22+
1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788
23+
1.2-RC-SNAPSHOT
24+
-1.0.3-gamma+b7718
25+
+justmeta
26+
1.1.1+
27+
1.1.1-
28+
#99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
higher,lower
2+
1.1.2,1.1.1
3+
1.0.0,1.0.0-rc.1
4+
1.1.0-rc.1,1.0.0-beta.11
5+
1.0.0-beta.11,1.0.0-beta.2
6+
1.0.0-beta.2,1.0.0-beta
7+
1.0.0-beta,1.0.0-alpha.beta
8+
1.0.0-alpha.beta,1.0.0-alpha.1
9+
1.0.0-alpha.1,1.0.0-alpha
10+
2.2.2-rc.2+metadata-lalala,2.2.2-rc.1.2
11+
1.2.3,0.0.4
12+
1.1.2+meta,1.1.2-prerelease+meta
13+
1.0.0-beta,1.0.0-alpha
14+
1.0.0-alpha0.valid,1.0.0-alpha.0valid
15+
1.0.0-rc.1+build.1,1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay
16+
10.2.3-DEV-SNAPSHOT,1.2.3-SNAPSHOT-123
17+
1.1.1-rc2,1.0.0-0A.is.legal
18+
1.2.3----RC-SNAPSHOT.12.9.1--.12+788,1.2.3----R-S.12.9.1--.12+meta
19+
1.2.3----RC-SNAPSHOT.12.9.1--.12.88,1.2.3----RC-SNAPSHOT.12.9.1--.12
20+
9223372036854775807.9223372036854775807.9223372036854775807,9223372036854775807.9223372036854775807.9223372036854775806
21+
1.1.1-alpha.beta.rc.build.java.pr.support.10,1.1.1-alpha.beta.rc.build.java.pr.support
22+
1.1.2,1.1.1
23+
1.2.1,1.1.1
24+
2.1.1,1.1.1
25+
1.1.1-rc.1,1.1.1-rc.0
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Condition model tests module."""
2+
import pytest
3+
import csv
4+
import os
5+
6+
from splitio.models.grammar.matchers.semver import Semver
7+
8+
valid_versions = os.path.join(os.path.dirname(__file__), 'files', 'valid-semantic-versions.csv')
9+
invalid_versions = os.path.join(os.path.dirname(__file__), 'files', 'invalid-semantic-versions.csv')
10+
equalto_versions = os.path.join(os.path.dirname(__file__), 'files', 'equal-to-semver.csv')
11+
between_versions = os.path.join(os.path.dirname(__file__), 'files', 'between-semver.csv')
12+
13+
class SemverTests(object):
14+
"""Test the semver object model."""
15+
16+
def test_valid_versions(self):
17+
with open(valid_versions) as csvfile:
18+
reader = csv.DictReader(csvfile)
19+
for row in reader:
20+
assert Semver.build(row['higher']) is not None
21+
assert Semver.build(row['lower']) is not None
22+
23+
def test_invalid_versions(self):
24+
with open(invalid_versions) as csvfile:
25+
reader = csv.DictReader(csvfile)
26+
for row in reader:
27+
assert Semver.build(row['invalid']) is None
28+
29+
def test_compare(self):
30+
with open(valid_versions) as csvfile:
31+
reader = csv.DictReader(csvfile)
32+
for row in reader:
33+
assert Semver.build(row['higher']).compare(Semver.build(row['lower'])) == 1
34+
assert Semver.build(row['lower']).compare(Semver.build(row['higher'])) == -1
35+
36+
with open(equalto_versions) as csvfile:
37+
reader = csv.DictReader(csvfile)
38+
for row in reader:
39+
version1 = Semver.build(row['version1'])
40+
version2 = Semver.build(row['version2'])
41+
if row['equals'] == "true":
42+
assert version1.version == version2.version
43+
else:
44+
assert version1.version != version2.version
45+
46+
with open(between_versions) as csvfile:
47+
reader = csv.DictReader(csvfile)
48+
for row in reader:
49+
version1 = Semver.build(row['version1'])
50+
version2 = Semver.build(row['version2'])
51+
version3 = Semver.build(row['version3'])
52+
if row['expected'] == "true":
53+
assert version2.compare(version1) >= 0 and version3.compare(version2) >= 0
54+
else:
55+
assert version2.compare(version1) < 0 or version3.compare(version2) < 0

0 commit comments

Comments
 (0)