launchdarkly-server-sdk 5.8.1 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.circleci/config.yml +28 -122
- data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.gitignore +2 -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 +29 -0
- data/CONTRIBUTING.md +1 -1
- 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 +16 -16
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +324 -0
- data/lib/ldclient-rb/events.rb +6 -7
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +231 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +87 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_factory.rb +28 -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 +5 -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/ldclient.rb +36 -15
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/requestor.rb +25 -15
- data/lib/ldclient-rb/stream.rb +9 -6
- 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 +216 -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 +128 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +349 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/event_factory_spec.rb +108 -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 +64 -12
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +2 -2
- data/spec/requestor_spec.rb +11 -11
- metadata +92 -48
- data/.yardopts +0 -9
- data/Gemfile.lock +0 -89
- 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,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,128 @@
|
|
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
|
+
|
95
|
+
describe "experiment rollout behavior" do
|
96
|
+
it "sets the in_experiment value if rollout kind is experiment " do
|
97
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
98
|
+
rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: false } ] } }
|
99
|
+
flag = boolean_flag_with_rules([rule])
|
100
|
+
user = { key: "userkey", secondary: 999 }
|
101
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
102
|
+
expect(result.detail.reason.to_json).to include('"inExperiment":true')
|
103
|
+
expect(result.detail.reason.in_experiment).to eq(true)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "does not set the in_experiment value if rollout kind is not experiment " do
|
107
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
108
|
+
rollout: { kind: 'rollout', variations: [ { weight: 100000, variation: 1, untracked: false } ] } }
|
109
|
+
flag = boolean_flag_with_rules([rule])
|
110
|
+
user = { key: "userkey", secondary: 999 }
|
111
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
112
|
+
expect(result.detail.reason.to_json).to_not include('"inExperiment":true')
|
113
|
+
expect(result.detail.reason.in_experiment).to eq(nil)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "does not set the in_experiment value if rollout kind is experiment and untracked is true" do
|
117
|
+
rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
|
118
|
+
rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: true } ] } }
|
119
|
+
flag = boolean_flag_with_rules([rule])
|
120
|
+
user = { key: "userkey", secondary: 999 }
|
121
|
+
result = basic_evaluator.evaluate(flag, user, factory)
|
122
|
+
expect(result.detail.reason.to_json).to_not include('"inExperiment":true')
|
123
|
+
expect(result.detail.reason.in_experiment).to eq(nil)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
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
|