launchdarkly-server-sdk 5.7.3 → 6.0.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/.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 +36 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +92 -76
- data/README.md +5 -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 +1 -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_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 +16 -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/user_filter.rb +3 -2
- 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 +11 -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 +10 -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
data/scripts/gendocs.sh
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
# Use this script to generate documentation locally in ./doc so it can be proofed before release.
|
4
|
-
# After release, documentation will be visible at https://www.rubydoc.info/gems/launchdarkly-server-sdk
|
5
|
-
|
6
|
-
gem install --conservative yard
|
7
|
-
gem install --conservative redcarpet # provides Markdown formatting
|
8
|
-
|
9
|
-
rm -rf doc/*
|
10
|
-
|
11
|
-
yard doc
|
data/scripts/release.sh
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env bash
|
2
|
-
# This script updates the version for the launchdarkly-server-sdk library and releases it to RubyGems
|
3
|
-
# It will only work if you have the proper credentials set up in ~/.gem/credentials
|
4
|
-
|
5
|
-
# It takes exactly one argument: the new version.
|
6
|
-
# It should be run from the root of this git repo like this:
|
7
|
-
# ./scripts/release.sh 4.0.9
|
8
|
-
|
9
|
-
# When done you should commit and push the changes made.
|
10
|
-
|
11
|
-
set -uxe
|
12
|
-
echo "Starting ruby-server-sdk release."
|
13
|
-
|
14
|
-
VERSION=$1
|
15
|
-
|
16
|
-
#Update version in lib/ldclient-rb/version.rb
|
17
|
-
VERSION_RB_TEMP=./version.rb.tmp
|
18
|
-
sed "s/VERSION =.*/VERSION = \"${VERSION}\"/g" lib/ldclient-rb/version.rb > ${VERSION_RB_TEMP}
|
19
|
-
mv ${VERSION_RB_TEMP} lib/ldclient-rb/version.rb
|
20
|
-
|
21
|
-
# Build Ruby Gem
|
22
|
-
gem build launchdarkly-server-sdk.gemspec
|
23
|
-
|
24
|
-
# Publish Ruby Gem
|
25
|
-
gem push launchdarkly-server-sdk-${VERSION}.gem
|
26
|
-
|
27
|
-
echo "Done with ruby-server-sdk release"
|
data/spec/evaluation_spec.rb
DELETED
@@ -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
|