launchdarkly-server-sdk 5.7.4 → 6.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +28 -122
- data/.gitignore +1 -1
- data/.ldrelease/build-docs.sh +18 -0
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +29 -0
- data/.ldrelease/circleci/template/publish.sh +23 -0
- data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +14 -2
- data/CHANGELOG.md +28 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +92 -76
- data/README.md +4 -3
- data/azure-pipelines.yml +1 -1
- data/docs/Makefile +26 -0
- data/docs/index.md +9 -0
- data/launchdarkly-server-sdk.gemspec +20 -13
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +293 -0
- data/lib/ldclient-rb/events.rb +3 -4
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +225 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_factory.rb +22 -0
- data/lib/ldclient-rb/impl/event_sender.rb +56 -40
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +8 -7
- data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/integrations/redis.rb +3 -0
- data/lib/ldclient-rb/ldclient.rb +33 -11
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/redis_store.rb +1 -0
- data/lib/ldclient-rb/requestor.rb +25 -23
- data/lib/ldclient-rb/stream.rb +10 -30
- data/lib/ldclient-rb/util.rb +12 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_detail_spec.rb +135 -0
- data/spec/event_sender_spec.rb +20 -2
- data/spec/events_spec.rb +10 -0
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +111 -0
- data/spec/impl/evaluator_clause_spec.rb +55 -0
- data/spec/impl/evaluator_operators_spec.rb +141 -0
- data/spec/impl/evaluator_rule_spec.rb +96 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +305 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/model/serialization_spec.rb +41 -0
- data/spec/launchdarkly-server-sdk_spec.rb +1 -1
- data/spec/ldclient_end_to_end_spec.rb +34 -0
- data/spec/ldclient_spec.rb +60 -8
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +32 -3
- data/spec/requestor_spec.rb +11 -45
- data/spec/spec_helper.rb +0 -3
- data/spec/stream_spec.rb +1 -16
- metadata +110 -60
- data/.yardopts +0 -9
- data/lib/ldclient-rb/evaluation.rb +0 -462
- data/scripts/gendocs.sh +0 -11
- data/scripts/release.sh +0 -27
- data/spec/evaluation_spec.rb +0 -789
@@ -0,0 +1,111 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe LaunchDarkly::Impl::EvaluatorBucketing do
|
4
|
+
subject { LaunchDarkly::Impl::EvaluatorBucketing }
|
5
|
+
|
6
|
+
describe "bucket_user" do
|
7
|
+
it "gets expected bucket values for specific keys" do
|
8
|
+
user = { key: "userKeyA" }
|
9
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
10
|
+
expect(bucket).to be_within(0.0000001).of(0.42157587);
|
11
|
+
|
12
|
+
user = { key: "userKeyB" }
|
13
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
14
|
+
expect(bucket).to be_within(0.0000001).of(0.6708485);
|
15
|
+
|
16
|
+
user = { key: "userKeyC" }
|
17
|
+
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
18
|
+
expect(bucket).to be_within(0.0000001).of(0.10343106);
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can bucket by int value (equivalent to string)" do
|
22
|
+
user = {
|
23
|
+
key: "userkey",
|
24
|
+
custom: {
|
25
|
+
stringAttr: "33333",
|
26
|
+
intAttr: 33333
|
27
|
+
}
|
28
|
+
}
|
29
|
+
stringResult = subject.bucket_user(user, "hashKey", "stringAttr", "saltyA")
|
30
|
+
intResult = subject.bucket_user(user, "hashKey", "intAttr", "saltyA")
|
31
|
+
|
32
|
+
expect(intResult).to be_within(0.0000001).of(0.54771423)
|
33
|
+
expect(intResult).to eq(stringResult)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "cannot bucket by float value" do
|
37
|
+
user = {
|
38
|
+
key: "userkey",
|
39
|
+
custom: {
|
40
|
+
floatAttr: 33.5
|
41
|
+
}
|
42
|
+
}
|
43
|
+
result = subject.bucket_user(user, "hashKey", "floatAttr", "saltyA")
|
44
|
+
expect(result).to eq(0.0)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
it "cannot bucket by bool value" do
|
49
|
+
user = {
|
50
|
+
key: "userkey",
|
51
|
+
custom: {
|
52
|
+
boolAttr: true
|
53
|
+
}
|
54
|
+
}
|
55
|
+
result = subject.bucket_user(user, "hashKey", "boolAttr", "saltyA")
|
56
|
+
expect(result).to eq(0.0)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "variation_index_for_user" do
|
61
|
+
it "matches bucket" do
|
62
|
+
user = { key: "userkey" }
|
63
|
+
flag_key = "flagkey"
|
64
|
+
salt = "salt"
|
65
|
+
|
66
|
+
# First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
|
67
|
+
# so we can construct a rollout whose second bucket just barely contains that value
|
68
|
+
bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
|
69
|
+
expect(bucket_value).to be > 0
|
70
|
+
expect(bucket_value).to be < 100000
|
71
|
+
|
72
|
+
bad_variation_a = 0
|
73
|
+
matched_variation = 1
|
74
|
+
bad_variation_b = 2
|
75
|
+
rule = {
|
76
|
+
rollout: {
|
77
|
+
variations: [
|
78
|
+
{ variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
|
79
|
+
{ variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
|
80
|
+
{ variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
|
81
|
+
]
|
82
|
+
}
|
83
|
+
}
|
84
|
+
flag = { key: flag_key, salt: salt }
|
85
|
+
|
86
|
+
result_variation = subject.variation_index_for_user(flag, rule, user)
|
87
|
+
expect(result_variation).to be matched_variation
|
88
|
+
end
|
89
|
+
|
90
|
+
it "uses last bucket if bucket value is equal to total weight" do
|
91
|
+
user = { key: "userkey" }
|
92
|
+
flag_key = "flagkey"
|
93
|
+
salt = "salt"
|
94
|
+
|
95
|
+
bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
|
96
|
+
|
97
|
+
# We'll construct a list of variations that stops right at the target bucket value
|
98
|
+
rule = {
|
99
|
+
rollout: {
|
100
|
+
variations: [
|
101
|
+
{ variation: 0, weight: bucket_value }
|
102
|
+
]
|
103
|
+
}
|
104
|
+
}
|
105
|
+
flag = { key: flag_key, salt: salt }
|
106
|
+
|
107
|
+
result_variation = subject.variation_index_for_user(flag, rule, user)
|
108
|
+
expect(result_variation).to be 0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "impl/evaluator_spec_base"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
module Impl
|
6
|
+
describe "Evaluator (clauses)", :evaluator_spec_base => true do
|
7
|
+
subject { Evaluator }
|
8
|
+
|
9
|
+
it "can match built-in attribute" do
|
10
|
+
user = { key: 'x', name: 'Bob' }
|
11
|
+
clause = { attribute: 'name', op: 'in', values: ['Bob'] }
|
12
|
+
flag = boolean_flag_with_clauses([clause])
|
13
|
+
expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can match custom attribute" do
|
17
|
+
user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
|
18
|
+
clause = { attribute: 'legs', op: 'in', values: [4] }
|
19
|
+
flag = boolean_flag_with_clauses([clause])
|
20
|
+
expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns false for missing attribute" do
|
24
|
+
user = { key: 'x', name: 'Bob' }
|
25
|
+
clause = { attribute: 'legs', op: 'in', values: [4] }
|
26
|
+
flag = boolean_flag_with_clauses([clause])
|
27
|
+
expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns false for unknown operator" do
|
31
|
+
user = { key: 'x', name: 'Bob' }
|
32
|
+
clause = { attribute: 'name', op: 'unknown', values: [4] }
|
33
|
+
flag = boolean_flag_with_clauses([clause])
|
34
|
+
expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false
|
35
|
+
end
|
36
|
+
|
37
|
+
it "does not stop evaluating rules after clause with unknown operator" do
|
38
|
+
user = { key: 'x', name: 'Bob' }
|
39
|
+
clause0 = { attribute: 'name', op: 'unknown', values: [4] }
|
40
|
+
rule0 = { clauses: [ clause0 ], variation: 1 }
|
41
|
+
clause1 = { attribute: 'name', op: 'in', values: ['Bob'] }
|
42
|
+
rule1 = { clauses: [ clause1 ], variation: 1 }
|
43
|
+
flag = boolean_flag_with_rules([rule0, rule1])
|
44
|
+
expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can be negated" do
|
48
|
+
user = { key: 'x', name: 'Bob' }
|
49
|
+
clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true }
|
50
|
+
flag = boolean_flag_with_clauses([clause])
|
51
|
+
expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe LaunchDarkly::Impl::EvaluatorOperators do
|
4
|
+
subject { LaunchDarkly::Impl::EvaluatorOperators }
|
5
|
+
|
6
|
+
describe "operators" do
|
7
|
+
dateStr1 = "2017-12-06T00:00:00.000-07:00"
|
8
|
+
dateStr2 = "2017-12-06T00:01:01.000-07:00"
|
9
|
+
dateMs1 = 10000000
|
10
|
+
dateMs2 = 10000001
|
11
|
+
invalidDate = "hey what's this?"
|
12
|
+
|
13
|
+
operatorTests = [
|
14
|
+
# numeric comparisons
|
15
|
+
[ :in, 99, 99, true ],
|
16
|
+
[ :in, 99.0001, 99.0001, true ],
|
17
|
+
[ :in, 99, 99.0001, false ],
|
18
|
+
[ :in, 99.0001, 99, false ],
|
19
|
+
[ :lessThan, 99, 99.0001, true ],
|
20
|
+
[ :lessThan, 99.0001, 99, false ],
|
21
|
+
[ :lessThan, 99, 99, false ],
|
22
|
+
[ :lessThanOrEqual, 99, 99.0001, true ],
|
23
|
+
[ :lessThanOrEqual, 99.0001, 99, false ],
|
24
|
+
[ :lessThanOrEqual, 99, 99, true ],
|
25
|
+
[ :greaterThan, 99.0001, 99, true ],
|
26
|
+
[ :greaterThan, 99, 99.0001, false ],
|
27
|
+
[ :greaterThan, 99, 99, false ],
|
28
|
+
[ :greaterThanOrEqual, 99.0001, 99, true ],
|
29
|
+
[ :greaterThanOrEqual, 99, 99.0001, false ],
|
30
|
+
[ :greaterThanOrEqual, 99, 99, true ],
|
31
|
+
|
32
|
+
# string comparisons
|
33
|
+
[ :in, "x", "x", true ],
|
34
|
+
[ :in, "x", "xyz", false ],
|
35
|
+
[ :startsWith, "xyz", "x", true ],
|
36
|
+
[ :startsWith, "x", "xyz", false ],
|
37
|
+
[ :endsWith, "xyz", "z", true ],
|
38
|
+
[ :endsWith, "z", "xyz", false ],
|
39
|
+
[ :contains, "xyz", "y", true ],
|
40
|
+
[ :contains, "y", "xyz", false ],
|
41
|
+
|
42
|
+
# mixed strings and numbers
|
43
|
+
[ :in, "99", 99, false ],
|
44
|
+
[ :in, 99, "99", false ],
|
45
|
+
[ :contains, "99", 99, false ],
|
46
|
+
[ :startsWith, "99", 99, false ],
|
47
|
+
[ :endsWith, "99", 99, false ],
|
48
|
+
[ :lessThanOrEqual, "99", 99, false ],
|
49
|
+
[ :lessThanOrEqual, 99, "99", false ],
|
50
|
+
[ :greaterThanOrEqual, "99", 99, false ],
|
51
|
+
[ :greaterThanOrEqual, 99, "99", false ],
|
52
|
+
|
53
|
+
# regex
|
54
|
+
[ :matches, "hello world", "hello.*rld", true ],
|
55
|
+
[ :matches, "hello world", "hello.*orl", true ],
|
56
|
+
[ :matches, "hello world", "l+", true ],
|
57
|
+
[ :matches, "hello world", "(world|planet)", true ],
|
58
|
+
[ :matches, "hello world", "aloha", false ],
|
59
|
+
[ :matches, "hello world", "***not a regex", false ],
|
60
|
+
|
61
|
+
# dates
|
62
|
+
[ :before, dateStr1, dateStr2, true ],
|
63
|
+
[ :before, dateMs1, dateMs2, true ],
|
64
|
+
[ :before, dateStr2, dateStr1, false ],
|
65
|
+
[ :before, dateMs2, dateMs1, false ],
|
66
|
+
[ :before, dateStr1, dateStr1, false ],
|
67
|
+
[ :before, dateMs1, dateMs1, false ],
|
68
|
+
[ :before, dateStr1, invalidDate, false ],
|
69
|
+
[ :after, dateStr1, dateStr2, false ],
|
70
|
+
[ :after, dateMs1, dateMs2, false ],
|
71
|
+
[ :after, dateStr2, dateStr1, true ],
|
72
|
+
[ :after, dateMs2, dateMs1, true ],
|
73
|
+
[ :after, dateStr1, dateStr1, false ],
|
74
|
+
[ :after, dateMs1, dateMs1, false ],
|
75
|
+
[ :after, dateStr1, invalidDate, false ],
|
76
|
+
|
77
|
+
# semver
|
78
|
+
[ :semVerEqual, "2.0.1", "2.0.1", true ],
|
79
|
+
[ :semVerEqual, "2.0", "2.0.0", true ],
|
80
|
+
[ :semVerEqual, "2-rc1", "2.0.0-rc1", true ],
|
81
|
+
[ :semVerEqual, "2+build2", "2.0.0+build2", true ],
|
82
|
+
[ :semVerLessThan, "2.0.0", "2.0.1", true ],
|
83
|
+
[ :semVerLessThan, "2.0", "2.0.1", true ],
|
84
|
+
[ :semVerLessThan, "2.0.1", "2.0.0", false ],
|
85
|
+
[ :semVerLessThan, "2.0.1", "2.0", false ],
|
86
|
+
[ :semVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true ],
|
87
|
+
[ :semVerGreaterThan, "2.0.1", "2.0.0", true ],
|
88
|
+
[ :semVerGreaterThan, "2.0.1", "2.0", true ],
|
89
|
+
[ :semVerGreaterThan, "2.0.0", "2.0.1", false ],
|
90
|
+
[ :semVerGreaterThan, "2.0", "2.0.1", false ],
|
91
|
+
[ :semVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true ],
|
92
|
+
[ :semVerLessThan, "2.0.1", "xbad%ver", false ],
|
93
|
+
[ :semVerGreaterThan, "2.0.1", "xbad%ver", false ]
|
94
|
+
]
|
95
|
+
|
96
|
+
operatorTests.each do |params|
|
97
|
+
op = params[0]
|
98
|
+
value1 = params[1]
|
99
|
+
value2 = params[2]
|
100
|
+
shouldBe = params[3]
|
101
|
+
it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
|
102
|
+
expect(subject::apply(op, value1, value2)).to be shouldBe
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "user_value" do
|
108
|
+
[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous, :some_custom_attr].each do |attr|
|
109
|
+
it "returns nil if property #{attr} is not defined" do
|
110
|
+
expect(subject::user_value({}, attr)).to be nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name].each do |attr|
|
115
|
+
it "gets string value of string property #{attr}" do
|
116
|
+
expect(subject::user_value({ attr => 'x' }, attr)).to eq 'x'
|
117
|
+
end
|
118
|
+
|
119
|
+
it "coerces non-string value of property #{attr} to string" do
|
120
|
+
expect(subject::user_value({ attr => 3 }, attr)).to eq '3'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it "gets boolean value of property anonymous" do
|
125
|
+
expect(subject::user_value({ anonymous: true }, :anonymous)).to be true
|
126
|
+
expect(subject::user_value({ anonymous: false }, :anonymous)).to be false
|
127
|
+
end
|
128
|
+
|
129
|
+
it "does not coerces non-boolean value of property anonymous" do
|
130
|
+
expect(subject::user_value({ anonymous: 3 }, :anonymous)).to eq 3
|
131
|
+
end
|
132
|
+
|
133
|
+
it "gets string value of custom property" do
|
134
|
+
expect(subject::user_value({ custom: { some_custom_attr: 'x' } }, :some_custom_attr)).to eq 'x'
|
135
|
+
end
|
136
|
+
|
137
|
+
it "gets non-string value of custom property" do
|
138
|
+
expect(subject::user_value({ custom: { some_custom_attr: 3 } }, :some_custom_attr)).to eq 3
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "impl/evaluator_spec_base"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
module Impl
|
6
|
+
describe "Evaluator (rules)", :evaluator_spec_base => true do
|
7
|
+
subject { Evaluator }
|
8
|
+
|
9
|
+
it "matches user from rules" do
|
10
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 1 }
|
11
|
+
flag = boolean_flag_with_rules([rule])
|
12
|
+
user = { key: 'userkey' }
|
13
|
+
detail = EvaluationDetail.new(true, 1, EvaluationReason::rule_match(0, 'ruleid'))
|
14
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
15
|
+
expect(result.detail).to eq(detail)
|
16
|
+
expect(result.events).to eq(nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "reuses rule match reason instances if possible" do
|
20
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 1 }
|
21
|
+
flag = boolean_flag_with_rules([rule])
|
22
|
+
Model.postprocess_item_after_deserializing!(FEATURES, flag) # now there's a cached rule match reason
|
23
|
+
user = { key: 'userkey' }
|
24
|
+
detail = EvaluationDetail.new(true, 1, EvaluationReason::rule_match(0, 'ruleid'))
|
25
|
+
result1 = basic_evaluator.evaluate(flag, user, factory)
|
26
|
+
result2 = basic_evaluator.evaluate(flag, user, factory)
|
27
|
+
expect(result1.detail.reason.rule_id).to eq 'ruleid'
|
28
|
+
expect(result1.detail.reason).to be result2.detail.reason
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns an error if rule variation is too high" do
|
32
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 999 }
|
33
|
+
flag = boolean_flag_with_rules([rule])
|
34
|
+
user = { key: 'userkey' }
|
35
|
+
detail = EvaluationDetail.new(nil, nil,
|
36
|
+
EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
37
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
38
|
+
expect(result.detail).to eq(detail)
|
39
|
+
expect(result.events).to eq(nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns an error if rule variation is negative" do
|
43
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: -1 }
|
44
|
+
flag = boolean_flag_with_rules([rule])
|
45
|
+
user = { key: 'userkey' }
|
46
|
+
detail = EvaluationDetail.new(nil, nil,
|
47
|
+
EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
48
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
49
|
+
expect(result.detail).to eq(detail)
|
50
|
+
expect(result.events).to eq(nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns an error if rule has neither variation nor rollout" do
|
54
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }] }
|
55
|
+
flag = boolean_flag_with_rules([rule])
|
56
|
+
user = { key: 'userkey' }
|
57
|
+
detail = EvaluationDetail.new(nil, nil,
|
58
|
+
EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
59
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
60
|
+
expect(result.detail).to eq(detail)
|
61
|
+
expect(result.events).to eq(nil)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns an error if rule has a rollout with no variations" do
|
65
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
66
|
+
rollout: { variations: [] } }
|
67
|
+
flag = boolean_flag_with_rules([rule])
|
68
|
+
user = { key: 'userkey' }
|
69
|
+
detail = EvaluationDetail.new(nil, nil,
|
70
|
+
EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
71
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
72
|
+
expect(result.detail).to eq(detail)
|
73
|
+
expect(result.events).to eq(nil)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "coerces user key to a string for evaluation" do
|
77
|
+
clause = { attribute: 'key', op: 'in', values: ['999'] }
|
78
|
+
flag = boolean_flag_with_clauses([clause])
|
79
|
+
user = { key: 999 }
|
80
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
81
|
+
expect(result.detail.value).to eq(true)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "coerces secondary key to a string for evaluation" do
|
85
|
+
# We can't really verify that the rollout calculation works correctly, but we can at least
|
86
|
+
# make sure it doesn't error out if there's a non-string secondary value (ch35189)
|
87
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
88
|
+
rollout: { salt: '', variations: [ { weight: 100000, variation: 1 } ] } }
|
89
|
+
flag = boolean_flag_with_rules([rule])
|
90
|
+
user = { key: "userkey", secondary: 999 }
|
91
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
92
|
+
expect(result.detail.reason).to eq(EvaluationReason::rule_match(0, 'ruleid'))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "impl/evaluator_spec_base"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
module Impl
|
6
|
+
describe "Evaluator (segments)", :evaluator_spec_base => true do
|
7
|
+
subject { Evaluator }
|
8
|
+
|
9
|
+
def test_segment_match(segment)
|
10
|
+
clause = make_segment_match_clause(segment)
|
11
|
+
flag = boolean_flag_with_clauses([clause])
|
12
|
+
e = Evaluator.new(get_nothing, get_things({ segment[:key] => segment }), logger)
|
13
|
+
e.evaluate(flag, user, factory).detail.value
|
14
|
+
end
|
15
|
+
|
16
|
+
it "retrieves segment from segment store for segmentMatch operator" do
|
17
|
+
segment = {
|
18
|
+
key: 'segkey',
|
19
|
+
included: [ 'userkey' ],
|
20
|
+
version: 1,
|
21
|
+
deleted: false
|
22
|
+
}
|
23
|
+
get_segment = get_things({ 'segkey' => segment })
|
24
|
+
e = subject.new(get_nothing, get_segment, logger)
|
25
|
+
user = { key: 'userkey' }
|
26
|
+
clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
|
27
|
+
flag = boolean_flag_with_clauses([clause])
|
28
|
+
expect(e.evaluate(flag, user, factory).detail.value).to be true
|
29
|
+
end
|
30
|
+
|
31
|
+
it "falls through with no errors if referenced segment is not found" do
|
32
|
+
e = subject.new(get_nothing, get_things({ 'segkey' => nil }), logger)
|
33
|
+
user = { key: 'userkey' }
|
34
|
+
clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
|
35
|
+
flag = boolean_flag_with_clauses([clause])
|
36
|
+
expect(e.evaluate(flag, user, factory).detail.value).to be false
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'explicitly includes user' do
|
40
|
+
segment = make_segment('segkey')
|
41
|
+
segment[:included] = [ user[:key] ]
|
42
|
+
expect(test_segment_match(segment)).to be true
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'explicitly excludes user' do
|
46
|
+
segment = make_segment('segkey')
|
47
|
+
segment[:excluded] = [ user[:key] ]
|
48
|
+
expect(test_segment_match(segment)).to be false
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'both includes and excludes user; include takes priority' do
|
52
|
+
segment = make_segment('segkey')
|
53
|
+
segment[:included] = [ user[:key] ]
|
54
|
+
segment[:excluded] = [ user[:key] ]
|
55
|
+
expect(test_segment_match(segment)).to be true
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'matches user by rule when weight is absent' do
|
59
|
+
segClause = make_user_matching_clause(user, :email)
|
60
|
+
segRule = {
|
61
|
+
clauses: [ segClause ]
|
62
|
+
}
|
63
|
+
segment = make_segment('segkey')
|
64
|
+
segment[:rules] = [ segRule ]
|
65
|
+
expect(test_segment_match(segment)).to be true
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'matches user by rule when weight is nil' do
|
69
|
+
segClause = make_user_matching_clause(user, :email)
|
70
|
+
segRule = {
|
71
|
+
clauses: [ segClause ],
|
72
|
+
weight: nil
|
73
|
+
}
|
74
|
+
segment = make_segment('segkey')
|
75
|
+
segment[:rules] = [ segRule ]
|
76
|
+
expect(test_segment_match(segment)).to be true
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'matches user with full rollout' do
|
80
|
+
segClause = make_user_matching_clause(user, :email)
|
81
|
+
segRule = {
|
82
|
+
clauses: [ segClause ],
|
83
|
+
weight: 100000
|
84
|
+
}
|
85
|
+
segment = make_segment('segkey')
|
86
|
+
segment[:rules] = [ segRule ]
|
87
|
+
expect(test_segment_match(segment)).to be true
|
88
|
+
end
|
89
|
+
|
90
|
+
it "doesn't match user with zero rollout" do
|
91
|
+
segClause = make_user_matching_clause(user, :email)
|
92
|
+
segRule = {
|
93
|
+
clauses: [ segClause ],
|
94
|
+
weight: 0
|
95
|
+
}
|
96
|
+
segment = make_segment('segkey')
|
97
|
+
segment[:rules] = [ segRule ]
|
98
|
+
expect(test_segment_match(segment)).to be false
|
99
|
+
end
|
100
|
+
|
101
|
+
it "matches user with multiple clauses" do
|
102
|
+
segClause1 = make_user_matching_clause(user, :email)
|
103
|
+
segClause2 = make_user_matching_clause(user, :name)
|
104
|
+
segRule = {
|
105
|
+
clauses: [ segClause1, segClause2 ]
|
106
|
+
}
|
107
|
+
segment = make_segment('segkey')
|
108
|
+
segment[:rules] = [ segRule ]
|
109
|
+
expect(test_segment_match(segment)).to be true
|
110
|
+
end
|
111
|
+
|
112
|
+
it "doesn't match user with multiple clauses if a clause doesn't match" do
|
113
|
+
segClause1 = make_user_matching_clause(user, :email)
|
114
|
+
segClause2 = make_user_matching_clause(user, :name)
|
115
|
+
segClause2[:values] = [ 'wrong' ]
|
116
|
+
segRule = {
|
117
|
+
clauses: [ segClause1, segClause2 ]
|
118
|
+
}
|
119
|
+
segment = make_segment('segkey')
|
120
|
+
segment[:rules] = [ segRule ]
|
121
|
+
expect(test_segment_match(segment)).to be false
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|