launchdarkly-server-sdk 5.8.2 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +28 -122
  3. data/.ldrelease/circleci/linux/execute.sh +18 -0
  4. data/.ldrelease/circleci/mac/execute.sh +18 -0
  5. data/.ldrelease/circleci/template/build.sh +29 -0
  6. data/.ldrelease/circleci/template/publish.sh +23 -0
  7. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  8. data/.ldrelease/circleci/template/test.sh +10 -0
  9. data/.ldrelease/circleci/template/update-version.sh +8 -0
  10. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  11. data/.ldrelease/config.yml +7 -3
  12. data/CHANGELOG.md +9 -0
  13. data/CONTRIBUTING.md +1 -1
  14. data/Gemfile.lock +69 -42
  15. data/README.md +2 -2
  16. data/azure-pipelines.yml +1 -1
  17. data/launchdarkly-server-sdk.gemspec +16 -16
  18. data/lib/ldclient-rb.rb +0 -1
  19. data/lib/ldclient-rb/config.rb +15 -3
  20. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  21. data/lib/ldclient-rb/events.rb +1 -4
  22. data/lib/ldclient-rb/file_data_source.rb +1 -1
  23. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  24. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  25. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  26. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  27. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  28. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  29. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
  30. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  32. data/lib/ldclient-rb/ldclient.rb +14 -9
  33. data/lib/ldclient-rb/polling.rb +1 -4
  34. data/lib/ldclient-rb/requestor.rb +25 -15
  35. data/lib/ldclient-rb/stream.rb +9 -6
  36. data/lib/ldclient-rb/util.rb +12 -8
  37. data/lib/ldclient-rb/version.rb +1 -1
  38. data/spec/evaluation_detail_spec.rb +135 -0
  39. data/spec/event_sender_spec.rb +20 -2
  40. data/spec/http_util.rb +11 -1
  41. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  42. data/spec/impl/evaluator_clause_spec.rb +55 -0
  43. data/spec/impl/evaluator_operators_spec.rb +141 -0
  44. data/spec/impl/evaluator_rule_spec.rb +96 -0
  45. data/spec/impl/evaluator_segment_spec.rb +125 -0
  46. data/spec/impl/evaluator_spec.rb +305 -0
  47. data/spec/impl/evaluator_spec_base.rb +75 -0
  48. data/spec/impl/model/serialization_spec.rb +41 -0
  49. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  50. data/spec/ldclient_end_to_end_spec.rb +34 -0
  51. data/spec/ldclient_spec.rb +10 -8
  52. data/spec/polling_spec.rb +2 -2
  53. data/spec/redis_feature_store_spec.rb +2 -2
  54. data/spec/requestor_spec.rb +11 -11
  55. metadata +89 -46
  56. data/lib/ldclient-rb/evaluation.rb +0 -462
  57. data/spec/evaluation_spec.rb +0 -789
@@ -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
@@ -0,0 +1,305 @@
1
+ require "spec_helper"
2
+ require "impl/evaluator_spec_base"
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ describe "Evaluator (general)", :evaluator_spec_base => true do
7
+ subject { Evaluator }
8
+
9
+ describe "evaluate" do
10
+ it "returns off variation if flag is off" do
11
+ flag = {
12
+ key: 'feature',
13
+ on: false,
14
+ offVariation: 1,
15
+ fallthrough: { variation: 0 },
16
+ variations: ['a', 'b', 'c']
17
+ }
18
+ user = { key: 'x' }
19
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::off)
20
+ result = basic_evaluator.evaluate(flag, user, factory)
21
+ expect(result.detail).to eq(detail)
22
+ expect(result.events).to eq(nil)
23
+ end
24
+
25
+ it "returns nil if flag is off and off variation is unspecified" do
26
+ flag = {
27
+ key: 'feature',
28
+ on: false,
29
+ fallthrough: { variation: 0 },
30
+ variations: ['a', 'b', 'c']
31
+ }
32
+ user = { key: 'x' }
33
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::off)
34
+ result = basic_evaluator.evaluate(flag, user, factory)
35
+ expect(result.detail).to eq(detail)
36
+ expect(result.events).to eq(nil)
37
+ end
38
+
39
+ it "returns an error if off variation is too high" do
40
+ flag = {
41
+ key: 'feature',
42
+ on: false,
43
+ offVariation: 999,
44
+ fallthrough: { variation: 0 },
45
+ variations: ['a', 'b', 'c']
46
+ }
47
+ user = { key: 'x' }
48
+ detail = EvaluationDetail.new(nil, nil,
49
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
50
+ result = basic_evaluator.evaluate(flag, user, factory)
51
+ expect(result.detail).to eq(detail)
52
+ expect(result.events).to eq(nil)
53
+ end
54
+
55
+ it "returns an error if off variation is negative" do
56
+ flag = {
57
+ key: 'feature',
58
+ on: false,
59
+ offVariation: -1,
60
+ fallthrough: { variation: 0 },
61
+ variations: ['a', 'b', 'c']
62
+ }
63
+ user = { key: 'x' }
64
+ detail = EvaluationDetail.new(nil, nil,
65
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
66
+ result = basic_evaluator.evaluate(flag, user, factory)
67
+ expect(result.detail).to eq(detail)
68
+ expect(result.events).to eq(nil)
69
+ end
70
+
71
+ it "returns off variation if prerequisite is not found" do
72
+ flag = {
73
+ key: 'feature0',
74
+ on: true,
75
+ prerequisites: [{key: 'badfeature', variation: 1}],
76
+ fallthrough: { variation: 0 },
77
+ offVariation: 1,
78
+ variations: ['a', 'b', 'c']
79
+ }
80
+ user = { key: 'x' }
81
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('badfeature'))
82
+ e = subject.new(get_things( 'badfeature' => nil ), get_nothing, logger)
83
+ result = e.evaluate(flag, user, factory)
84
+ expect(result.detail).to eq(detail)
85
+ expect(result.events).to eq(nil)
86
+ end
87
+
88
+ it "reuses prerequisite-failed reason instances if possible" do
89
+ flag = {
90
+ key: 'feature0',
91
+ on: true,
92
+ prerequisites: [{key: 'badfeature', variation: 1}],
93
+ fallthrough: { variation: 0 },
94
+ offVariation: 1,
95
+ variations: ['a', 'b', 'c']
96
+ }
97
+ Model.postprocess_item_after_deserializing!(FEATURES, flag) # now there's a cached reason
98
+ user = { key: 'x' }
99
+ e = subject.new(get_things( 'badfeature' => nil ), get_nothing, logger)
100
+ result1 = e.evaluate(flag, user, factory)
101
+ expect(result1.detail.reason).to eq EvaluationReason::prerequisite_failed('badfeature')
102
+ result2 = e.evaluate(flag, user, factory)
103
+ expect(result2.detail.reason).to be result1.detail.reason
104
+ end
105
+
106
+ it "returns off variation and event if prerequisite of a prerequisite is not found" do
107
+ flag = {
108
+ key: 'feature0',
109
+ on: true,
110
+ prerequisites: [{key: 'feature1', variation: 1}],
111
+ fallthrough: { variation: 0 },
112
+ offVariation: 1,
113
+ variations: ['a', 'b', 'c'],
114
+ version: 1
115
+ }
116
+ flag1 = {
117
+ key: 'feature1',
118
+ on: true,
119
+ prerequisites: [{key: 'feature2', variation: 1}], # feature2 doesn't exist
120
+ fallthrough: { variation: 0 },
121
+ variations: ['d', 'e'],
122
+ version: 2
123
+ }
124
+ user = { key: 'x' }
125
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
126
+ events_should_be = [{
127
+ kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0'
128
+ }]
129
+ get_flag = get_things('feature1' => flag1, 'feature2' => nil)
130
+ e = subject.new(get_flag, get_nothing, logger)
131
+ result = e.evaluate(flag, user, factory)
132
+ expect(result.detail).to eq(detail)
133
+ expect(result.events).to eq(events_should_be)
134
+ end
135
+
136
+ it "returns off variation and event if prerequisite is off" do
137
+ flag = {
138
+ key: 'feature0',
139
+ on: true,
140
+ prerequisites: [{key: 'feature1', variation: 1}],
141
+ fallthrough: { variation: 0 },
142
+ offVariation: 1,
143
+ variations: ['a', 'b', 'c'],
144
+ version: 1
145
+ }
146
+ flag1 = {
147
+ key: 'feature1',
148
+ on: false,
149
+ # note that even though it returns the desired variation, it is still off and therefore not a match
150
+ offVariation: 1,
151
+ fallthrough: { variation: 0 },
152
+ variations: ['d', 'e'],
153
+ version: 2
154
+ }
155
+ user = { key: 'x' }
156
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
157
+ events_should_be = [{
158
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
159
+ }]
160
+ get_flag = get_things({ 'feature1' => flag1 })
161
+ e = subject.new(get_flag, get_nothing, logger)
162
+ result = e.evaluate(flag, user, factory)
163
+ expect(result.detail).to eq(detail)
164
+ expect(result.events).to eq(events_should_be)
165
+ end
166
+
167
+ it "returns off variation and event if prerequisite is not met" do
168
+ flag = {
169
+ key: 'feature0',
170
+ on: true,
171
+ prerequisites: [{key: 'feature1', variation: 1}],
172
+ fallthrough: { variation: 0 },
173
+ offVariation: 1,
174
+ variations: ['a', 'b', 'c'],
175
+ version: 1
176
+ }
177
+ flag1 = {
178
+ key: 'feature1',
179
+ on: true,
180
+ fallthrough: { variation: 0 },
181
+ variations: ['d', 'e'],
182
+ version: 2
183
+ }
184
+ user = { key: 'x' }
185
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
186
+ events_should_be = [{
187
+ kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0'
188
+ }]
189
+ get_flag = get_things({ 'feature1' => flag1 })
190
+ e = subject.new(get_flag, get_nothing, logger)
191
+ result = e.evaluate(flag, user, factory)
192
+ expect(result.detail).to eq(detail)
193
+ expect(result.events).to eq(events_should_be)
194
+ end
195
+
196
+ it "returns fallthrough variation and event if prerequisite is met and there are no rules" do
197
+ flag = {
198
+ key: 'feature0',
199
+ on: true,
200
+ prerequisites: [{key: 'feature1', variation: 1}],
201
+ fallthrough: { variation: 0 },
202
+ offVariation: 1,
203
+ variations: ['a', 'b', 'c'],
204
+ version: 1
205
+ }
206
+ flag1 = {
207
+ key: 'feature1',
208
+ on: true,
209
+ fallthrough: { variation: 1 },
210
+ variations: ['d', 'e'],
211
+ version: 2
212
+ }
213
+ user = { key: 'x' }
214
+ detail = EvaluationDetail.new('a', 0, EvaluationReason::fallthrough)
215
+ events_should_be = [{
216
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
217
+ }]
218
+ get_flag = get_things({ 'feature1' => flag1 })
219
+ e = subject.new(get_flag, get_nothing, logger)
220
+ result = e.evaluate(flag, user, factory)
221
+ expect(result.detail).to eq(detail)
222
+ expect(result.events).to eq(events_should_be)
223
+ end
224
+
225
+ it "returns an error if fallthrough variation is too high" do
226
+ flag = {
227
+ key: 'feature',
228
+ on: true,
229
+ fallthrough: { variation: 999 },
230
+ offVariation: 1,
231
+ variations: ['a', 'b', 'c']
232
+ }
233
+ user = { key: 'userkey' }
234
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
235
+ result = basic_evaluator.evaluate(flag, user, factory)
236
+ expect(result.detail).to eq(detail)
237
+ expect(result.events).to eq(nil)
238
+ end
239
+
240
+ it "returns an error if fallthrough variation is negative" do
241
+ flag = {
242
+ key: 'feature',
243
+ on: true,
244
+ fallthrough: { variation: -1 },
245
+ offVariation: 1,
246
+ variations: ['a', 'b', 'c']
247
+ }
248
+ user = { key: 'userkey' }
249
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
250
+ result = basic_evaluator.evaluate(flag, user, factory)
251
+ expect(result.detail).to eq(detail)
252
+ expect(result.events).to eq(nil)
253
+ end
254
+
255
+ it "returns an error if fallthrough has no variation or rollout" do
256
+ flag = {
257
+ key: 'feature',
258
+ on: true,
259
+ fallthrough: { },
260
+ offVariation: 1,
261
+ variations: ['a', 'b', 'c']
262
+ }
263
+ user = { key: 'userkey' }
264
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
265
+ result = basic_evaluator.evaluate(flag, user, factory)
266
+ expect(result.detail).to eq(detail)
267
+ expect(result.events).to eq(nil)
268
+ end
269
+
270
+ it "returns an error if fallthrough has a rollout with no variations" do
271
+ flag = {
272
+ key: 'feature',
273
+ on: true,
274
+ fallthrough: { rollout: { variations: [] } },
275
+ offVariation: 1,
276
+ variations: ['a', 'b', 'c']
277
+ }
278
+ user = { key: 'userkey' }
279
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
280
+ result = basic_evaluator.evaluate(flag, user, factory)
281
+ expect(result.detail).to eq(detail)
282
+ expect(result.events).to eq(nil)
283
+ end
284
+
285
+ it "matches user from targets" do
286
+ flag = {
287
+ key: 'feature',
288
+ on: true,
289
+ targets: [
290
+ { values: [ 'whoever', 'userkey' ], variation: 2 }
291
+ ],
292
+ fallthrough: { variation: 0 },
293
+ offVariation: 1,
294
+ variations: ['a', 'b', 'c']
295
+ }
296
+ user = { key: 'userkey' }
297
+ detail = EvaluationDetail.new('c', 2, EvaluationReason::target_match)
298
+ result = basic_evaluator.evaluate(flag, user, factory)
299
+ expect(result.detail).to eq(detail)
300
+ expect(result.events).to eq(nil)
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,75 @@
1
+ require "spec_helper"
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module EvaluatorSpecBase
6
+ def factory
7
+ EventFactory.new(false)
8
+ end
9
+
10
+ def user
11
+ {
12
+ key: "userkey",
13
+ email: "test@example.com",
14
+ name: "Bob"
15
+ }
16
+ end
17
+
18
+ def logger
19
+ ::Logger.new($stdout, level: ::Logger::FATAL)
20
+ end
21
+
22
+ def get_nothing
23
+ lambda { |key| raise "should not have requested #{key}" }
24
+ end
25
+
26
+ def get_things(map)
27
+ lambda { |key|
28
+ raise "should not have requested #{key}" if !map.has_key?(key)
29
+ map[key]
30
+ }
31
+ end
32
+
33
+ def basic_evaluator
34
+ subject.new(get_nothing, get_nothing, logger)
35
+ end
36
+
37
+ def boolean_flag_with_rules(rules)
38
+ { key: 'feature', on: true, rules: rules, fallthrough: { variation: 0 }, variations: [ false, true ] }
39
+ end
40
+
41
+ def boolean_flag_with_clauses(clauses)
42
+ boolean_flag_with_rules([{ id: 'ruleid', clauses: clauses, variation: 1 }])
43
+ end
44
+
45
+ def make_user_matching_clause(user, attr)
46
+ {
47
+ attribute: attr.to_s,
48
+ op: :in,
49
+ values: [ user[attr.to_sym] ],
50
+ negate: false
51
+ }
52
+ end
53
+
54
+ def make_segment(key)
55
+ {
56
+ key: key,
57
+ included: [],
58
+ excluded: [],
59
+ salt: 'abcdef',
60
+ version: 1
61
+ }
62
+ end
63
+
64
+ def make_segment_match_clause(segment)
65
+ {
66
+ op: :segmentMatch,
67
+ values: [ segment[:key] ],
68
+ negate: false
69
+ }
70
+ end
71
+ end
72
+
73
+ RSpec.configure { |c| c.include EvaluatorSpecBase, :evaluator_spec_base => true }
74
+ end
75
+ end