launchdarkly-server-sdk 6.2.4 → 6.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/lib/ldclient-rb/config.rb +3 -3
- data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
- data/lib/ldclient-rb/integrations/redis.rb +1 -1
- data/lib/ldclient-rb/interfaces.rb +1 -1
- data/lib/ldclient-rb/ldclient.rb +5 -5
- data/lib/ldclient-rb/version.rb +1 -1
- metadata +6 -121
- data/.circleci/config.yml +0 -43
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
- data/.github/ISSUE_TEMPLATE/config.yml +0 -5
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- data/.github/pull_request_template.md +0 -21
- data/.gitignore +0 -16
- data/.hound.yml +0 -2
- data/.ldrelease/build-docs.sh +0 -18
- data/.ldrelease/circleci/linux/execute.sh +0 -18
- data/.ldrelease/circleci/mac/execute.sh +0 -18
- data/.ldrelease/circleci/template/build.sh +0 -19
- data/.ldrelease/circleci/template/gems-setup.sh +0 -16
- data/.ldrelease/circleci/template/prepare.sh +0 -17
- data/.ldrelease/circleci/template/publish.sh +0 -19
- data/.ldrelease/circleci/template/test.sh +0 -10
- data/.ldrelease/circleci/template/update-version.sh +0 -8
- data/.ldrelease/circleci/windows/execute.ps1 +0 -19
- data/.ldrelease/config.yml +0 -29
- data/.rspec +0 -2
- data/.rubocop.yml +0 -600
- data/.simplecov +0 -4
- data/CHANGELOG.md +0 -371
- data/CODEOWNERS +0 -1
- data/CONTRIBUTING.md +0 -37
- data/Gemfile +0 -3
- data/azure-pipelines.yml +0 -51
- data/docs/Makefile +0 -26
- data/docs/index.md +0 -9
- data/launchdarkly-server-sdk.gemspec +0 -43
- data/spec/config_spec.rb +0 -63
- data/spec/diagnostic_events_spec.rb +0 -165
- data/spec/evaluation_detail_spec.rb +0 -135
- data/spec/event_sender_spec.rb +0 -197
- data/spec/event_summarizer_spec.rb +0 -63
- data/spec/events_spec.rb +0 -607
- data/spec/expiring_cache_spec.rb +0 -76
- data/spec/feature_store_spec_base.rb +0 -213
- data/spec/file_data_source_spec.rb +0 -283
- data/spec/fixtures/feature.json +0 -37
- data/spec/fixtures/feature1.json +0 -36
- data/spec/fixtures/user.json +0 -9
- data/spec/flags_state_spec.rb +0 -81
- data/spec/http_util.rb +0 -132
- data/spec/impl/evaluator_bucketing_spec.rb +0 -216
- data/spec/impl/evaluator_clause_spec.rb +0 -55
- data/spec/impl/evaluator_operators_spec.rb +0 -141
- data/spec/impl/evaluator_rule_spec.rb +0 -128
- data/spec/impl/evaluator_segment_spec.rb +0 -125
- data/spec/impl/evaluator_spec.rb +0 -349
- data/spec/impl/evaluator_spec_base.rb +0 -75
- data/spec/impl/event_factory_spec.rb +0 -108
- data/spec/impl/model/serialization_spec.rb +0 -41
- data/spec/in_memory_feature_store_spec.rb +0 -12
- data/spec/integrations/consul_feature_store_spec.rb +0 -40
- data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
- data/spec/integrations/store_wrapper_spec.rb +0 -276
- data/spec/launchdarkly-server-sdk_spec.rb +0 -13
- data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
- data/spec/ldclient_end_to_end_spec.rb +0 -157
- data/spec/ldclient_spec.rb +0 -635
- data/spec/newrelic_spec.rb +0 -5
- data/spec/polling_spec.rb +0 -120
- data/spec/redis_feature_store_spec.rb +0 -121
- data/spec/requestor_spec.rb +0 -209
- data/spec/segment_store_spec_base.rb +0 -95
- data/spec/simple_lru_cache_spec.rb +0 -24
- data/spec/spec_helper.rb +0 -9
- data/spec/store_spec.rb +0 -10
- data/spec/stream_spec.rb +0 -45
- data/spec/user_filter_spec.rb +0 -91
- data/spec/util_spec.rb +0 -17
- data/spec/version_spec.rb +0 -7
data/spec/http_util.rb
DELETED
@@ -1,132 +0,0 @@
|
|
1
|
-
require "webrick"
|
2
|
-
require "webrick/httpproxy"
|
3
|
-
require "webrick/https"
|
4
|
-
|
5
|
-
class StubHTTPServer
|
6
|
-
attr_reader :requests, :port
|
7
|
-
|
8
|
-
@@next_port = 50000
|
9
|
-
|
10
|
-
def initialize
|
11
|
-
@port = StubHTTPServer.next_port
|
12
|
-
begin
|
13
|
-
base_opts = {
|
14
|
-
BindAddress: '127.0.0.1',
|
15
|
-
Port: @port,
|
16
|
-
AccessLog: [],
|
17
|
-
Logger: NullLogger.new,
|
18
|
-
RequestCallback: method(:record_request)
|
19
|
-
}
|
20
|
-
@server = create_server(@port, base_opts)
|
21
|
-
rescue Errno::EADDRINUSE
|
22
|
-
@port = StubHTTPServer.next_port
|
23
|
-
retry
|
24
|
-
end
|
25
|
-
@requests = []
|
26
|
-
@requests_queue = Queue.new
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.next_port
|
30
|
-
p = @@next_port
|
31
|
-
@@next_port = (p + 1 < 60000) ? p + 1 : 50000
|
32
|
-
p
|
33
|
-
end
|
34
|
-
|
35
|
-
def create_server(port, base_opts)
|
36
|
-
WEBrick::HTTPServer.new(base_opts)
|
37
|
-
end
|
38
|
-
|
39
|
-
def start
|
40
|
-
Thread.new { @server.start }
|
41
|
-
end
|
42
|
-
|
43
|
-
def stop
|
44
|
-
@server.shutdown
|
45
|
-
end
|
46
|
-
|
47
|
-
def base_uri
|
48
|
-
URI("http://127.0.0.1:#{@port}")
|
49
|
-
end
|
50
|
-
|
51
|
-
def setup_response(uri_path, &action)
|
52
|
-
@server.mount_proc(uri_path, action)
|
53
|
-
end
|
54
|
-
|
55
|
-
def setup_status_response(uri_path, status, headers={})
|
56
|
-
setup_response(uri_path) do |req, res|
|
57
|
-
res.status = status
|
58
|
-
headers.each { |n, v| res[n] = v }
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def setup_ok_response(uri_path, body, content_type=nil, headers={})
|
63
|
-
setup_response(uri_path) do |req, res|
|
64
|
-
res.status = 200
|
65
|
-
res.content_type = content_type if !content_type.nil?
|
66
|
-
res.body = body
|
67
|
-
headers.each { |n, v| res[n] = v }
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def record_request(req, res)
|
72
|
-
@requests.push(req)
|
73
|
-
@requests_queue << [req, req.body]
|
74
|
-
end
|
75
|
-
|
76
|
-
def await_request
|
77
|
-
r = @requests_queue.pop
|
78
|
-
r[0]
|
79
|
-
end
|
80
|
-
|
81
|
-
def await_request_with_body
|
82
|
-
r = @requests_queue.pop
|
83
|
-
return r[0], r[1]
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
class StubProxyServer < StubHTTPServer
|
88
|
-
attr_reader :request_count
|
89
|
-
attr_accessor :connect_status
|
90
|
-
|
91
|
-
def initialize
|
92
|
-
super
|
93
|
-
@request_count = 0
|
94
|
-
end
|
95
|
-
|
96
|
-
def create_server(port, base_opts)
|
97
|
-
WEBrick::HTTPProxyServer.new(base_opts.merge({
|
98
|
-
ProxyContentHandler: proc do |req,res|
|
99
|
-
if !@connect_status.nil?
|
100
|
-
res.status = @connect_status
|
101
|
-
end
|
102
|
-
@request_count += 1
|
103
|
-
end
|
104
|
-
}))
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
class NullLogger
|
109
|
-
def method_missing(*)
|
110
|
-
self
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def with_server(server = nil)
|
115
|
-
server = StubHTTPServer.new if server.nil?
|
116
|
-
begin
|
117
|
-
server.start
|
118
|
-
yield server
|
119
|
-
ensure
|
120
|
-
server.stop
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
class SocketFactoryFromHash
|
125
|
-
def initialize(ports = {})
|
126
|
-
@ports = ports
|
127
|
-
end
|
128
|
-
|
129
|
-
def open(uri, timeout)
|
130
|
-
TCPSocket.new 'localhost', @ports[uri]
|
131
|
-
end
|
132
|
-
end
|
@@ -1,216 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe LaunchDarkly::Impl::EvaluatorBucketing do
|
4
|
-
subject { LaunchDarkly::Impl::EvaluatorBucketing }
|
5
|
-
|
6
|
-
describe "bucket_user" do
|
7
|
-
describe "seed exists" do
|
8
|
-
let(:seed) { 61 }
|
9
|
-
it "returns the expected bucket values for seed" do
|
10
|
-
user = { key: "userKeyA" }
|
11
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
12
|
-
expect(bucket).to be_within(0.0000001).of(0.09801207);
|
13
|
-
|
14
|
-
user = { key: "userKeyB" }
|
15
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
16
|
-
expect(bucket).to be_within(0.0000001).of(0.14483777);
|
17
|
-
|
18
|
-
user = { key: "userKeyC" }
|
19
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
20
|
-
expect(bucket).to be_within(0.0000001).of(0.9242641);
|
21
|
-
end
|
22
|
-
|
23
|
-
it "returns the same bucket regardless of hashKey and salt" do
|
24
|
-
user = { key: "userKeyA" }
|
25
|
-
bucket1 = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
26
|
-
bucket2 = subject.bucket_user(user, "hashKey1", "key", "saltyB", seed)
|
27
|
-
bucket3 = subject.bucket_user(user, "hashKey2", "key", "saltyC", seed)
|
28
|
-
expect(bucket1).to eq(bucket2)
|
29
|
-
expect(bucket2).to eq(bucket3)
|
30
|
-
end
|
31
|
-
|
32
|
-
it "returns a different bucket if the seed is not the same" do
|
33
|
-
user = { key: "userKeyA" }
|
34
|
-
bucket1 = subject.bucket_user(user, "hashKey", "key", "saltyA", seed)
|
35
|
-
bucket2 = subject.bucket_user(user, "hashKey1", "key", "saltyB", seed+1)
|
36
|
-
expect(bucket1).to_not eq(bucket2)
|
37
|
-
end
|
38
|
-
|
39
|
-
it "returns a different bucket if the user is not the same" do
|
40
|
-
user1 = { key: "userKeyA" }
|
41
|
-
user2 = { key: "userKeyB" }
|
42
|
-
bucket1 = subject.bucket_user(user1, "hashKey", "key", "saltyA", seed)
|
43
|
-
bucket2 = subject.bucket_user(user2, "hashKey1", "key", "saltyB", seed)
|
44
|
-
expect(bucket1).to_not eq(bucket2)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
it "gets expected bucket values for specific keys" do
|
49
|
-
user = { key: "userKeyA" }
|
50
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", nil)
|
51
|
-
expect(bucket).to be_within(0.0000001).of(0.42157587);
|
52
|
-
|
53
|
-
user = { key: "userKeyB" }
|
54
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", nil)
|
55
|
-
expect(bucket).to be_within(0.0000001).of(0.6708485);
|
56
|
-
|
57
|
-
user = { key: "userKeyC" }
|
58
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA", nil)
|
59
|
-
expect(bucket).to be_within(0.0000001).of(0.10343106);
|
60
|
-
end
|
61
|
-
|
62
|
-
it "can bucket by int value (equivalent to string)" do
|
63
|
-
user = {
|
64
|
-
key: "userkey",
|
65
|
-
custom: {
|
66
|
-
stringAttr: "33333",
|
67
|
-
intAttr: 33333
|
68
|
-
}
|
69
|
-
}
|
70
|
-
stringResult = subject.bucket_user(user, "hashKey", "stringAttr", "saltyA", nil)
|
71
|
-
intResult = subject.bucket_user(user, "hashKey", "intAttr", "saltyA", nil)
|
72
|
-
|
73
|
-
expect(intResult).to be_within(0.0000001).of(0.54771423)
|
74
|
-
expect(intResult).to eq(stringResult)
|
75
|
-
end
|
76
|
-
|
77
|
-
it "cannot bucket by float value" do
|
78
|
-
user = {
|
79
|
-
key: "userkey",
|
80
|
-
custom: {
|
81
|
-
floatAttr: 33.5
|
82
|
-
}
|
83
|
-
}
|
84
|
-
result = subject.bucket_user(user, "hashKey", "floatAttr", "saltyA", nil)
|
85
|
-
expect(result).to eq(0.0)
|
86
|
-
end
|
87
|
-
|
88
|
-
|
89
|
-
it "cannot bucket by bool value" do
|
90
|
-
user = {
|
91
|
-
key: "userkey",
|
92
|
-
custom: {
|
93
|
-
boolAttr: true
|
94
|
-
}
|
95
|
-
}
|
96
|
-
result = subject.bucket_user(user, "hashKey", "boolAttr", "saltyA", nil)
|
97
|
-
expect(result).to eq(0.0)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
describe "variation_index_for_user" do
|
102
|
-
context "rollout is not an experiment" do
|
103
|
-
it "matches bucket" do
|
104
|
-
user = { key: "userkey" }
|
105
|
-
flag_key = "flagkey"
|
106
|
-
salt = "salt"
|
107
|
-
|
108
|
-
# First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
|
109
|
-
# so we can construct a rollout whose second bucket just barely contains that value
|
110
|
-
bucket_value = (subject.bucket_user(user, flag_key, "key", salt, nil) * 100000).truncate()
|
111
|
-
expect(bucket_value).to be > 0
|
112
|
-
expect(bucket_value).to be < 100000
|
113
|
-
|
114
|
-
bad_variation_a = 0
|
115
|
-
matched_variation = 1
|
116
|
-
bad_variation_b = 2
|
117
|
-
rule = {
|
118
|
-
rollout: {
|
119
|
-
variations: [
|
120
|
-
{ variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
|
121
|
-
{ variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
|
122
|
-
{ variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
|
123
|
-
]
|
124
|
-
}
|
125
|
-
}
|
126
|
-
flag = { key: flag_key, salt: salt }
|
127
|
-
|
128
|
-
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user)
|
129
|
-
expect(result_variation).to be matched_variation
|
130
|
-
expect(inExperiment).to be(false)
|
131
|
-
end
|
132
|
-
|
133
|
-
it "uses last bucket if bucket value is equal to total weight" do
|
134
|
-
user = { key: "userkey" }
|
135
|
-
flag_key = "flagkey"
|
136
|
-
salt = "salt"
|
137
|
-
|
138
|
-
bucket_value = (subject.bucket_user(user, flag_key, "key", salt, nil) * 100000).truncate()
|
139
|
-
|
140
|
-
# We'll construct a list of variations that stops right at the target bucket value
|
141
|
-
rule = {
|
142
|
-
rollout: {
|
143
|
-
variations: [
|
144
|
-
{ variation: 0, weight: bucket_value }
|
145
|
-
]
|
146
|
-
}
|
147
|
-
}
|
148
|
-
flag = { key: flag_key, salt: salt }
|
149
|
-
|
150
|
-
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user)
|
151
|
-
expect(result_variation).to be 0
|
152
|
-
expect(inExperiment).to be(false)
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
context "rollout is an experiment" do
|
158
|
-
it "returns whether user is in the experiment or not" do
|
159
|
-
user1 = { key: "userKeyA" }
|
160
|
-
user2 = { key: "userKeyB" }
|
161
|
-
user3 = { key: "userKeyC" }
|
162
|
-
flag_key = "flagkey"
|
163
|
-
salt = "salt"
|
164
|
-
seed = 61
|
165
|
-
|
166
|
-
|
167
|
-
rule = {
|
168
|
-
rollout: {
|
169
|
-
seed: seed,
|
170
|
-
kind: 'experiment',
|
171
|
-
variations: [
|
172
|
-
{ variation: 0, weight: 10000, untracked: false },
|
173
|
-
{ variation: 2, weight: 20000, untracked: false },
|
174
|
-
{ variation: 0, weight: 70000 , untracked: true }
|
175
|
-
]
|
176
|
-
}
|
177
|
-
}
|
178
|
-
flag = { key: flag_key, salt: salt }
|
179
|
-
|
180
|
-
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user1)
|
181
|
-
expect(result_variation).to be(0)
|
182
|
-
expect(inExperiment).to be(true)
|
183
|
-
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user2)
|
184
|
-
expect(result_variation).to be(2)
|
185
|
-
expect(inExperiment).to be(true)
|
186
|
-
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user3)
|
187
|
-
expect(result_variation).to be(0)
|
188
|
-
expect(inExperiment).to be(false)
|
189
|
-
end
|
190
|
-
|
191
|
-
it "uses last bucket if bucket value is equal to total weight" do
|
192
|
-
user = { key: "userkey" }
|
193
|
-
flag_key = "flagkey"
|
194
|
-
salt = "salt"
|
195
|
-
seed = 61
|
196
|
-
|
197
|
-
bucket_value = (subject.bucket_user(user, flag_key, "key", salt, seed) * 100000).truncate()
|
198
|
-
|
199
|
-
# We'll construct a list of variations that stops right at the target bucket value
|
200
|
-
rule = {
|
201
|
-
rollout: {
|
202
|
-
seed: seed,
|
203
|
-
kind: 'experiment',
|
204
|
-
variations: [
|
205
|
-
{ variation: 0, weight: bucket_value, untracked: false }
|
206
|
-
]
|
207
|
-
}
|
208
|
-
}
|
209
|
-
flag = { key: flag_key, salt: salt }
|
210
|
-
|
211
|
-
result_variation, inExperiment = subject.variation_index_for_user(flag, rule, user)
|
212
|
-
expect(result_variation).to be 0
|
213
|
-
expect(inExperiment).to be(true)
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
@@ -1,55 +0,0 @@
|
|
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
|
@@ -1,141 +0,0 @@
|
|
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
|
@@ -1,128 +0,0 @@
|
|
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
|