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
@@ -1,789 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe LaunchDarkly::Evaluation do
4
- subject { LaunchDarkly::Evaluation }
5
-
6
- include LaunchDarkly::Evaluation
7
-
8
- let(:features) { LaunchDarkly::InMemoryFeatureStore.new }
9
-
10
- let(:factory) { LaunchDarkly::Impl::EventFactory.new(false) }
11
-
12
- let(:user) {
13
- {
14
- key: "userkey",
15
- email: "test@example.com",
16
- name: "Bob"
17
- }
18
- }
19
-
20
- let(:logger) { $null_log }
21
-
22
- def boolean_flag_with_rules(rules)
23
- { key: 'feature', on: true, rules: rules, fallthrough: { variation: 0 }, variations: [ false, true ] }
24
- end
25
-
26
- def boolean_flag_with_clauses(clauses)
27
- boolean_flag_with_rules([{ id: 'ruleid', clauses: clauses, variation: 1 }])
28
- end
29
-
30
- describe "evaluate" do
31
- it "returns off variation if flag is off" do
32
- flag = {
33
- key: 'feature',
34
- on: false,
35
- offVariation: 1,
36
- fallthrough: { variation: 0 },
37
- variations: ['a', 'b', 'c']
38
- }
39
- user = { key: 'x' }
40
- detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'OFF' })
41
- result = evaluate(flag, user, features, logger, factory)
42
- expect(result.detail).to eq(detail)
43
- expect(result.events).to eq([])
44
- end
45
-
46
- it "returns nil if flag is off and off variation is unspecified" do
47
- flag = {
48
- key: 'feature',
49
- on: false,
50
- fallthrough: { variation: 0 },
51
- variations: ['a', 'b', 'c']
52
- }
53
- user = { key: 'x' }
54
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'OFF' })
55
- result = evaluate(flag, user, features, logger, factory)
56
- expect(result.detail).to eq(detail)
57
- expect(result.events).to eq([])
58
- end
59
-
60
- it "returns an error if off variation is too high" do
61
- flag = {
62
- key: 'feature',
63
- on: false,
64
- offVariation: 999,
65
- fallthrough: { variation: 0 },
66
- variations: ['a', 'b', 'c']
67
- }
68
- user = { key: 'x' }
69
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
70
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
71
- result = evaluate(flag, user, features, logger, factory)
72
- expect(result.detail).to eq(detail)
73
- expect(result.events).to eq([])
74
- end
75
-
76
- it "returns an error if off variation is negative" do
77
- flag = {
78
- key: 'feature',
79
- on: false,
80
- offVariation: -1,
81
- fallthrough: { variation: 0 },
82
- variations: ['a', 'b', 'c']
83
- }
84
- user = { key: 'x' }
85
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
86
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
87
- result = evaluate(flag, user, features, logger, factory)
88
- expect(result.detail).to eq(detail)
89
- expect(result.events).to eq([])
90
- end
91
-
92
- it "returns off variation if prerequisite is not found" do
93
- flag = {
94
- key: 'feature0',
95
- on: true,
96
- prerequisites: [{key: 'badfeature', variation: 1}],
97
- fallthrough: { variation: 0 },
98
- offVariation: 1,
99
- variations: ['a', 'b', 'c']
100
- }
101
- user = { key: 'x' }
102
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
103
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'badfeature' })
104
- result = evaluate(flag, user, features, logger, factory)
105
- expect(result.detail).to eq(detail)
106
- expect(result.events).to eq([])
107
- end
108
-
109
- it "returns off variation and event if prerequisite of a prerequisite is not found" do
110
- flag = {
111
- key: 'feature0',
112
- on: true,
113
- prerequisites: [{key: 'feature1', variation: 1}],
114
- fallthrough: { variation: 0 },
115
- offVariation: 1,
116
- variations: ['a', 'b', 'c'],
117
- version: 1
118
- }
119
- flag1 = {
120
- key: 'feature1',
121
- on: true,
122
- prerequisites: [{key: 'feature2', variation: 1}], # feature2 doesn't exist
123
- fallthrough: { variation: 0 },
124
- variations: ['d', 'e'],
125
- version: 2
126
- }
127
- features.upsert(LaunchDarkly::FEATURES, flag1)
128
- user = { key: 'x' }
129
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
130
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
131
- events_should_be = [{
132
- kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0'
133
- }]
134
- result = evaluate(flag, user, features, logger, factory)
135
- expect(result.detail).to eq(detail)
136
- expect(result.events).to eq(events_should_be)
137
- end
138
-
139
- it "returns off variation and event if prerequisite is off" do
140
- flag = {
141
- key: 'feature0',
142
- on: true,
143
- prerequisites: [{key: 'feature1', variation: 1}],
144
- fallthrough: { variation: 0 },
145
- offVariation: 1,
146
- variations: ['a', 'b', 'c'],
147
- version: 1
148
- }
149
- flag1 = {
150
- key: 'feature1',
151
- on: false,
152
- # note that even though it returns the desired variation, it is still off and therefore not a match
153
- offVariation: 1,
154
- fallthrough: { variation: 0 },
155
- variations: ['d', 'e'],
156
- version: 2
157
- }
158
- features.upsert(LaunchDarkly::FEATURES, flag1)
159
- user = { key: 'x' }
160
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
161
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
162
- events_should_be = [{
163
- kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
164
- }]
165
- result = evaluate(flag, user, features, logger, factory)
166
- expect(result.detail).to eq(detail)
167
- expect(result.events).to eq(events_should_be)
168
- end
169
-
170
- it "returns off variation and event if prerequisite is not met" do
171
- flag = {
172
- key: 'feature0',
173
- on: true,
174
- prerequisites: [{key: 'feature1', variation: 1}],
175
- fallthrough: { variation: 0 },
176
- offVariation: 1,
177
- variations: ['a', 'b', 'c'],
178
- version: 1
179
- }
180
- flag1 = {
181
- key: 'feature1',
182
- on: true,
183
- fallthrough: { variation: 0 },
184
- variations: ['d', 'e'],
185
- version: 2
186
- }
187
- features.upsert(LaunchDarkly::FEATURES, flag1)
188
- user = { key: 'x' }
189
- detail = LaunchDarkly::EvaluationDetail.new('b', 1,
190
- { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' })
191
- events_should_be = [{
192
- kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0'
193
- }]
194
- result = evaluate(flag, user, features, logger, factory)
195
- expect(result.detail).to eq(detail)
196
- expect(result.events).to eq(events_should_be)
197
- end
198
-
199
- it "returns fallthrough variation and event if prerequisite is met and there are no rules" do
200
- flag = {
201
- key: 'feature0',
202
- on: true,
203
- prerequisites: [{key: 'feature1', variation: 1}],
204
- fallthrough: { variation: 0 },
205
- offVariation: 1,
206
- variations: ['a', 'b', 'c'],
207
- version: 1
208
- }
209
- flag1 = {
210
- key: 'feature1',
211
- on: true,
212
- fallthrough: { variation: 1 },
213
- variations: ['d', 'e'],
214
- version: 2
215
- }
216
- features.upsert(LaunchDarkly::FEATURES, flag1)
217
- user = { key: 'x' }
218
- detail = LaunchDarkly::EvaluationDetail.new('a', 0, { kind: 'FALLTHROUGH' })
219
- events_should_be = [{
220
- kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
221
- }]
222
- result = evaluate(flag, user, features, logger, factory)
223
- expect(result.detail).to eq(detail)
224
- expect(result.events).to eq(events_should_be)
225
- end
226
-
227
- it "returns an error if fallthrough variation is too high" do
228
- flag = {
229
- key: 'feature',
230
- on: true,
231
- fallthrough: { variation: 999 },
232
- offVariation: 1,
233
- variations: ['a', 'b', 'c']
234
- }
235
- user = { key: 'userkey' }
236
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
237
- result = evaluate(flag, user, features, logger, factory)
238
- expect(result.detail).to eq(detail)
239
- expect(result.events).to eq([])
240
- end
241
-
242
- it "returns an error if fallthrough variation is negative" do
243
- flag = {
244
- key: 'feature',
245
- on: true,
246
- fallthrough: { variation: -1 },
247
- offVariation: 1,
248
- variations: ['a', 'b', 'c']
249
- }
250
- user = { key: 'userkey' }
251
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
252
- result = evaluate(flag, user, features, logger, factory)
253
- expect(result.detail).to eq(detail)
254
- expect(result.events).to eq([])
255
- end
256
-
257
- it "returns an error if fallthrough has no variation or rollout" do
258
- flag = {
259
- key: 'feature',
260
- on: true,
261
- fallthrough: { },
262
- offVariation: 1,
263
- variations: ['a', 'b', 'c']
264
- }
265
- user = { key: 'userkey' }
266
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
267
- result = evaluate(flag, user, features, logger, factory)
268
- expect(result.detail).to eq(detail)
269
- expect(result.events).to eq([])
270
- end
271
-
272
- it "returns an error if fallthrough has a rollout with no variations" do
273
- flag = {
274
- key: 'feature',
275
- on: true,
276
- fallthrough: { rollout: { variations: [] } },
277
- offVariation: 1,
278
- variations: ['a', 'b', 'c']
279
- }
280
- user = { key: 'userkey' }
281
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
282
- result = evaluate(flag, user, features, logger, factory)
283
- expect(result.detail).to eq(detail)
284
- expect(result.events).to eq([])
285
- end
286
-
287
- it "matches user from targets" do
288
- flag = {
289
- key: 'feature',
290
- on: true,
291
- targets: [
292
- { values: [ 'whoever', 'userkey' ], variation: 2 }
293
- ],
294
- fallthrough: { variation: 0 },
295
- offVariation: 1,
296
- variations: ['a', 'b', 'c']
297
- }
298
- user = { key: 'userkey' }
299
- detail = LaunchDarkly::EvaluationDetail.new('c', 2, { kind: 'TARGET_MATCH' })
300
- result = evaluate(flag, user, features, logger, factory)
301
- expect(result.detail).to eq(detail)
302
- expect(result.events).to eq([])
303
- end
304
-
305
- it "matches user from rules" do
306
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 1 }
307
- flag = boolean_flag_with_rules([rule])
308
- user = { key: 'userkey' }
309
- detail = LaunchDarkly::EvaluationDetail.new(true, 1,
310
- { kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid' })
311
- result = evaluate(flag, user, features, logger, factory)
312
- expect(result.detail).to eq(detail)
313
- expect(result.events).to eq([])
314
- end
315
-
316
- it "returns an error if rule variation is too high" do
317
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: 999 }
318
- flag = boolean_flag_with_rules([rule])
319
- user = { key: 'userkey' }
320
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
321
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
322
- result = evaluate(flag, user, features, logger, factory)
323
- expect(result.detail).to eq(detail)
324
- expect(result.events).to eq([])
325
- end
326
-
327
- it "returns an error if rule variation is negative" do
328
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], variation: -1 }
329
- flag = boolean_flag_with_rules([rule])
330
- user = { key: 'userkey' }
331
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
332
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
333
- result = evaluate(flag, user, features, logger, factory)
334
- expect(result.detail).to eq(detail)
335
- expect(result.events).to eq([])
336
- end
337
-
338
- it "returns an error if rule has neither variation nor rollout" do
339
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }] }
340
- flag = boolean_flag_with_rules([rule])
341
- user = { key: 'userkey' }
342
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
343
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
344
- result = evaluate(flag, user, features, logger, factory)
345
- expect(result.detail).to eq(detail)
346
- expect(result.events).to eq([])
347
- end
348
-
349
- it "returns an error if rule has a rollout with no variations" do
350
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
351
- rollout: { variations: [] } }
352
- flag = boolean_flag_with_rules([rule])
353
- user = { key: 'userkey' }
354
- detail = LaunchDarkly::EvaluationDetail.new(nil, nil,
355
- { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' })
356
- result = evaluate(flag, user, features, logger, factory)
357
- expect(result.detail).to eq(detail)
358
- expect(result.events).to eq([])
359
- end
360
-
361
- it "coerces user key to a string for evaluation" do
362
- clause = { attribute: 'key', op: 'in', values: ['999'] }
363
- flag = boolean_flag_with_clauses([clause])
364
- user = { key: 999 }
365
- result = evaluate(flag, user, features, logger, factory)
366
- expect(result.detail.value).to eq(true)
367
- end
368
-
369
- it "coerces secondary key to a string for evaluation" do
370
- # We can't really verify that the rollout calculation works correctly, but we can at least
371
- # make sure it doesn't error out if there's a non-string secondary value (ch35189)
372
- rule = { id: 'ruleid', clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }],
373
- rollout: { salt: '', variations: [ { weight: 100000, variation: 1 } ] } }
374
- flag = boolean_flag_with_rules([rule])
375
- user = { key: "userkey", secondary: 999 }
376
- result = evaluate(flag, user, features, logger, factory)
377
- expect(result.detail.reason).to eq({ kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid'})
378
- end
379
- end
380
-
381
- describe "clause" do
382
- it "can match built-in attribute" do
383
- user = { key: 'x', name: 'Bob' }
384
- clause = { attribute: 'name', op: 'in', values: ['Bob'] }
385
- flag = boolean_flag_with_clauses([clause])
386
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
387
- end
388
-
389
- it "can match custom attribute" do
390
- user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
391
- clause = { attribute: 'legs', op: 'in', values: [4] }
392
- flag = boolean_flag_with_clauses([clause])
393
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
394
- end
395
-
396
- it "returns false for missing attribute" do
397
- user = { key: 'x', name: 'Bob' }
398
- clause = { attribute: 'legs', op: 'in', values: [4] }
399
- flag = boolean_flag_with_clauses([clause])
400
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
401
- end
402
-
403
- it "returns false for unknown operator" do
404
- user = { key: 'x', name: 'Bob' }
405
- clause = { attribute: 'name', op: 'unknown', values: [4] }
406
- flag = boolean_flag_with_clauses([clause])
407
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
408
- end
409
-
410
- it "does not stop evaluating rules after clause with unknown operator" do
411
- user = { key: 'x', name: 'Bob' }
412
- clause0 = { attribute: 'name', op: 'unknown', values: [4] }
413
- rule0 = { clauses: [ clause0 ], variation: 1 }
414
- clause1 = { attribute: 'name', op: 'in', values: ['Bob'] }
415
- rule1 = { clauses: [ clause1 ], variation: 1 }
416
- flag = boolean_flag_with_rules([rule0, rule1])
417
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
418
- end
419
-
420
- it "can be negated" do
421
- user = { key: 'x', name: 'Bob' }
422
- clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true }
423
- flag = boolean_flag_with_clauses([clause])
424
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
425
- end
426
-
427
- it "retrieves segment from segment store for segmentMatch operator" do
428
- segment = {
429
- key: 'segkey',
430
- included: [ 'userkey' ],
431
- version: 1,
432
- deleted: false
433
- }
434
- features.upsert(LaunchDarkly::SEGMENTS, segment)
435
-
436
- user = { key: 'userkey' }
437
- clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
438
- flag = boolean_flag_with_clauses([clause])
439
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be true
440
- end
441
-
442
- it "falls through with no errors if referenced segment is not found" do
443
- user = { key: 'userkey' }
444
- clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
445
- flag = boolean_flag_with_clauses([clause])
446
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be false
447
- end
448
-
449
- it "can be negated" do
450
- user = { key: 'x', name: 'Bob' }
451
- clause = { attribute: 'name', op: 'in', values: ['Bob'] }
452
- flag = boolean_flag_with_clauses([clause])
453
- expect {
454
- clause[:negate] = true
455
- }.to change {evaluate(flag, user, features, logger, factory).detail.value}.from(true).to(false)
456
- end
457
- end
458
-
459
- describe "operators" do
460
- dateStr1 = "2017-12-06T00:00:00.000-07:00"
461
- dateStr2 = "2017-12-06T00:01:01.000-07:00"
462
- dateMs1 = 10000000
463
- dateMs2 = 10000001
464
- invalidDate = "hey what's this?"
465
-
466
- operatorTests = [
467
- # numeric comparisons
468
- [ :in, 99, 99, true ],
469
- [ :in, 99.0001, 99.0001, true ],
470
- [ :in, 99, 99.0001, false ],
471
- [ :in, 99.0001, 99, false ],
472
- [ :lessThan, 99, 99.0001, true ],
473
- [ :lessThan, 99.0001, 99, false ],
474
- [ :lessThan, 99, 99, false ],
475
- [ :lessThanOrEqual, 99, 99.0001, true ],
476
- [ :lessThanOrEqual, 99.0001, 99, false ],
477
- [ :lessThanOrEqual, 99, 99, true ],
478
- [ :greaterThan, 99.0001, 99, true ],
479
- [ :greaterThan, 99, 99.0001, false ],
480
- [ :greaterThan, 99, 99, false ],
481
- [ :greaterThanOrEqual, 99.0001, 99, true ],
482
- [ :greaterThanOrEqual, 99, 99.0001, false ],
483
- [ :greaterThanOrEqual, 99, 99, true ],
484
-
485
- # string comparisons
486
- [ :in, "x", "x", true ],
487
- [ :in, "x", "xyz", false ],
488
- [ :startsWith, "xyz", "x", true ],
489
- [ :startsWith, "x", "xyz", false ],
490
- [ :endsWith, "xyz", "z", true ],
491
- [ :endsWith, "z", "xyz", false ],
492
- [ :contains, "xyz", "y", true ],
493
- [ :contains, "y", "xyz", false ],
494
-
495
- # mixed strings and numbers
496
- [ :in, "99", 99, false ],
497
- [ :in, 99, "99", false ],
498
- [ :contains, "99", 99, false ],
499
- [ :startsWith, "99", 99, false ],
500
- [ :endsWith, "99", 99, false ],
501
- [ :lessThanOrEqual, "99", 99, false ],
502
- [ :lessThanOrEqual, 99, "99", false ],
503
- [ :greaterThanOrEqual, "99", 99, false ],
504
- [ :greaterThanOrEqual, 99, "99", false ],
505
-
506
- # regex
507
- [ :matches, "hello world", "hello.*rld", true ],
508
- [ :matches, "hello world", "hello.*orl", true ],
509
- [ :matches, "hello world", "l+", true ],
510
- [ :matches, "hello world", "(world|planet)", true ],
511
- [ :matches, "hello world", "aloha", false ],
512
- [ :matches, "hello world", "***not a regex", false ],
513
-
514
- # dates
515
- [ :before, dateStr1, dateStr2, true ],
516
- [ :before, dateMs1, dateMs2, true ],
517
- [ :before, dateStr2, dateStr1, false ],
518
- [ :before, dateMs2, dateMs1, false ],
519
- [ :before, dateStr1, dateStr1, false ],
520
- [ :before, dateMs1, dateMs1, false ],
521
- [ :before, dateStr1, invalidDate, false ],
522
- [ :after, dateStr1, dateStr2, false ],
523
- [ :after, dateMs1, dateMs2, false ],
524
- [ :after, dateStr2, dateStr1, true ],
525
- [ :after, dateMs2, dateMs1, true ],
526
- [ :after, dateStr1, dateStr1, false ],
527
- [ :after, dateMs1, dateMs1, false ],
528
- [ :after, dateStr1, invalidDate, false ],
529
-
530
- # semver
531
- [ :semVerEqual, "2.0.1", "2.0.1", true ],
532
- [ :semVerEqual, "2.0", "2.0.0", true ],
533
- [ :semVerEqual, "2-rc1", "2.0.0-rc1", true ],
534
- [ :semVerEqual, "2+build2", "2.0.0+build2", true ],
535
- [ :semVerLessThan, "2.0.0", "2.0.1", true ],
536
- [ :semVerLessThan, "2.0", "2.0.1", true ],
537
- [ :semVerLessThan, "2.0.1", "2.0.0", false ],
538
- [ :semVerLessThan, "2.0.1", "2.0", false ],
539
- [ :semVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true ],
540
- [ :semVerGreaterThan, "2.0.1", "2.0.0", true ],
541
- [ :semVerGreaterThan, "2.0.1", "2.0", true ],
542
- [ :semVerGreaterThan, "2.0.0", "2.0.1", false ],
543
- [ :semVerGreaterThan, "2.0", "2.0.1", false ],
544
- [ :semVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true ],
545
- [ :semVerLessThan, "2.0.1", "xbad%ver", false ],
546
- [ :semVerGreaterThan, "2.0.1", "xbad%ver", false ]
547
- ]
548
-
549
- operatorTests.each do |params|
550
- op = params[0]
551
- value1 = params[1]
552
- value2 = params[2]
553
- shouldBe = params[3]
554
- it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
555
- user = { key: 'x', custom: { foo: value1 } }
556
- clause = { attribute: 'foo', op: op, values: [value2] }
557
- flag = boolean_flag_with_clauses([clause])
558
- expect(evaluate(flag, user, features, logger, factory).detail.value).to be shouldBe
559
- end
560
- end
561
- end
562
-
563
- describe "variation_index_for_user" do
564
- it "matches bucket" do
565
- user = { key: "userkey" }
566
- flag_key = "flagkey"
567
- salt = "salt"
568
-
569
- # First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
570
- # so we can construct a rollout whose second bucket just barely contains that value
571
- bucket_value = (bucket_user(user, flag_key, "key", salt) * 100000).truncate()
572
- expect(bucket_value).to be > 0
573
- expect(bucket_value).to be < 100000
574
-
575
- bad_variation_a = 0
576
- matched_variation = 1
577
- bad_variation_b = 2
578
- rule = {
579
- rollout: {
580
- variations: [
581
- { variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
582
- { variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
583
- { variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
584
- ]
585
- }
586
- }
587
- flag = { key: flag_key, salt: salt }
588
-
589
- result_variation = variation_index_for_user(flag, rule, user)
590
- expect(result_variation).to be matched_variation
591
- end
592
-
593
- it "uses last bucket if bucket value is equal to total weight" do
594
- user = { key: "userkey" }
595
- flag_key = "flagkey"
596
- salt = "salt"
597
-
598
- bucket_value = (bucket_user(user, flag_key, "key", salt) * 100000).truncate()
599
-
600
- # We'll construct a list of variations that stops right at the target bucket value
601
- rule = {
602
- rollout: {
603
- variations: [
604
- { variation: 0, weight: bucket_value }
605
- ]
606
- }
607
- }
608
- flag = { key: flag_key, salt: salt }
609
-
610
- result_variation = variation_index_for_user(flag, rule, user)
611
- expect(result_variation).to be 0
612
- end
613
- end
614
-
615
- describe "bucket_user" do
616
- it "gets expected bucket values for specific keys" do
617
- user = { key: "userKeyA" }
618
- bucket = bucket_user(user, "hashKey", "key", "saltyA")
619
- expect(bucket).to be_within(0.0000001).of(0.42157587);
620
-
621
- user = { key: "userKeyB" }
622
- bucket = bucket_user(user, "hashKey", "key", "saltyA")
623
- expect(bucket).to be_within(0.0000001).of(0.6708485);
624
-
625
- user = { key: "userKeyC" }
626
- bucket = bucket_user(user, "hashKey", "key", "saltyA")
627
- expect(bucket).to be_within(0.0000001).of(0.10343106);
628
- end
629
-
630
- it "can bucket by int value (equivalent to string)" do
631
- user = {
632
- key: "userkey",
633
- custom: {
634
- stringAttr: "33333",
635
- intAttr: 33333
636
- }
637
- }
638
- stringResult = bucket_user(user, "hashKey", "stringAttr", "saltyA")
639
- intResult = bucket_user(user, "hashKey", "intAttr", "saltyA")
640
-
641
- expect(intResult).to be_within(0.0000001).of(0.54771423)
642
- expect(intResult).to eq(stringResult)
643
- end
644
-
645
- it "cannot bucket by float value" do
646
- user = {
647
- key: "userkey",
648
- custom: {
649
- floatAttr: 33.5
650
- }
651
- }
652
- result = bucket_user(user, "hashKey", "floatAttr", "saltyA")
653
- expect(result).to eq(0.0)
654
- end
655
-
656
-
657
- it "cannot bucket by bool value" do
658
- user = {
659
- key: "userkey",
660
- custom: {
661
- boolAttr: true
662
- }
663
- }
664
- result = bucket_user(user, "hashKey", "boolAttr", "saltyA")
665
- expect(result).to eq(0.0)
666
- end
667
- end
668
-
669
- def make_segment(key)
670
- {
671
- key: key,
672
- included: [],
673
- excluded: [],
674
- salt: 'abcdef',
675
- version: 1
676
- }
677
- end
678
-
679
- def make_segment_match_clause(segment)
680
- {
681
- op: :segmentMatch,
682
- values: [ segment[:key] ],
683
- negate: false
684
- }
685
- end
686
-
687
- def make_user_matching_clause(user, attr)
688
- {
689
- attribute: attr.to_s,
690
- op: :in,
691
- values: [ user[attr.to_sym] ],
692
- negate: false
693
- }
694
- end
695
-
696
- describe 'segment matching' do
697
- def test_segment_match(segment)
698
- features.upsert(LaunchDarkly::SEGMENTS, segment)
699
- clause = make_segment_match_clause(segment)
700
- flag = boolean_flag_with_clauses([clause])
701
- evaluate(flag, user, features, logger, factory).detail.value
702
- end
703
-
704
- it 'explicitly includes user' do
705
- segment = make_segment('segkey')
706
- segment[:included] = [ user[:key] ]
707
- expect(test_segment_match(segment)).to be true
708
- end
709
-
710
- it 'explicitly excludes user' do
711
- segment = make_segment('segkey')
712
- segment[:excluded] = [ user[:key] ]
713
- expect(test_segment_match(segment)).to be false
714
- end
715
-
716
- it 'both includes and excludes user; include takes priority' do
717
- segment = make_segment('segkey')
718
- segment[:included] = [ user[:key] ]
719
- segment[:excluded] = [ user[:key] ]
720
- expect(test_segment_match(segment)).to be true
721
- end
722
-
723
- it 'matches user by rule when weight is absent' do
724
- segClause = make_user_matching_clause(user, :email)
725
- segRule = {
726
- clauses: [ segClause ]
727
- }
728
- segment = make_segment('segkey')
729
- segment[:rules] = [ segRule ]
730
- expect(test_segment_match(segment)).to be true
731
- end
732
-
733
- it 'matches user by rule when weight is nil' do
734
- segClause = make_user_matching_clause(user, :email)
735
- segRule = {
736
- clauses: [ segClause ],
737
- weight: nil
738
- }
739
- segment = make_segment('segkey')
740
- segment[:rules] = [ segRule ]
741
- expect(test_segment_match(segment)).to be true
742
- end
743
-
744
- it 'matches user with full rollout' do
745
- segClause = make_user_matching_clause(user, :email)
746
- segRule = {
747
- clauses: [ segClause ],
748
- weight: 100000
749
- }
750
- segment = make_segment('segkey')
751
- segment[:rules] = [ segRule ]
752
- expect(test_segment_match(segment)).to be true
753
- end
754
-
755
- it "doesn't match user with zero rollout" do
756
- segClause = make_user_matching_clause(user, :email)
757
- segRule = {
758
- clauses: [ segClause ],
759
- weight: 0
760
- }
761
- segment = make_segment('segkey')
762
- segment[:rules] = [ segRule ]
763
- expect(test_segment_match(segment)).to be false
764
- end
765
-
766
- it "matches user with multiple clauses" do
767
- segClause1 = make_user_matching_clause(user, :email)
768
- segClause2 = make_user_matching_clause(user, :name)
769
- segRule = {
770
- clauses: [ segClause1, segClause2 ]
771
- }
772
- segment = make_segment('segkey')
773
- segment[:rules] = [ segRule ]
774
- expect(test_segment_match(segment)).to be true
775
- end
776
-
777
- it "doesn't match user with multiple clauses if a clause doesn't match" do
778
- segClause1 = make_user_matching_clause(user, :email)
779
- segClause2 = make_user_matching_clause(user, :name)
780
- segClause2[:values] = [ 'wrong' ]
781
- segRule = {
782
- clauses: [ segClause1, segClause2 ]
783
- }
784
- segment = make_segment('segkey')
785
- segment[:rules] = [ segRule ]
786
- expect(test_segment_match(segment)).to be false
787
- end
788
- end
789
- end