launchdarkly-server-sdk 6.1.1 → 6.4.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 +4 -4
- data/README.md +4 -5
- data/lib/ldclient-rb/config.rb +118 -4
- data/lib/ldclient-rb/evaluation_detail.rb +104 -14
- data/lib/ldclient-rb/events.rb +201 -107
- data/lib/ldclient-rb/file_data_source.rb +9 -300
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +116 -62
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +22 -9
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +53 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
- data/lib/ldclient-rb/impl/event_summarizer.rb +63 -0
- data/lib/ldclient-rb/impl/event_types.rb +90 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +177 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +7 -37
- data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
- data/lib/ldclient-rb/impl/util.rb +62 -1
- data/lib/ldclient-rb/integrations/consul.rb +8 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +48 -3
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +42 -2
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
- data/lib/ldclient-rb/integrations/test_data.rb +209 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
- data/lib/ldclient-rb/integrations.rb +2 -51
- data/lib/ldclient-rb/interfaces.rb +152 -2
- data/lib/ldclient-rb/ldclient.rb +131 -33
- data/lib/ldclient-rb/polling.rb +22 -41
- data/lib/ldclient-rb/requestor.rb +3 -3
- data/lib/ldclient-rb/stream.rb +4 -3
- data/lib/ldclient-rb/util.rb +10 -1
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +0 -1
- metadata +35 -132
- data/.circleci/config.yml +0 -40
- 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 -29
- data/.ldrelease/circleci/template/publish.sh +0 -23
- data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
- 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 -351
- 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 -45
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/impl/event_factory.rb +0 -120
- data/spec/config_spec.rb +0 -63
- data/spec/diagnostic_events_spec.rb +0 -163
- 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 -111
- 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 -96
- data/spec/impl/evaluator_segment_spec.rb +0 -125
- data/spec/impl/evaluator_spec.rb +0 -305
- data/spec/impl/evaluator_spec_base.rb +0 -75
- 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 -643
- 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 -196
- 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/flags_state_spec.rb
DELETED
@@ -1,81 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
require "json"
|
3
|
-
|
4
|
-
describe LaunchDarkly::FeatureFlagsState do
|
5
|
-
subject { LaunchDarkly::FeatureFlagsState }
|
6
|
-
|
7
|
-
it "can get flag value" do
|
8
|
-
state = subject.new(true)
|
9
|
-
flag = { key: 'key' }
|
10
|
-
state.add_flag(flag, 'value', 1)
|
11
|
-
|
12
|
-
expect(state.flag_value('key')).to eq 'value'
|
13
|
-
end
|
14
|
-
|
15
|
-
it "returns nil for unknown flag" do
|
16
|
-
state = subject.new(true)
|
17
|
-
|
18
|
-
expect(state.flag_value('key')).to be nil
|
19
|
-
end
|
20
|
-
|
21
|
-
it "can be converted to values map" do
|
22
|
-
state = subject.new(true)
|
23
|
-
flag1 = { key: 'key1' }
|
24
|
-
flag2 = { key: 'key2' }
|
25
|
-
state.add_flag(flag1, 'value1', 0)
|
26
|
-
state.add_flag(flag2, 'value2', 1)
|
27
|
-
|
28
|
-
expect(state.values_map).to eq({ 'key1' => 'value1', 'key2' => 'value2' })
|
29
|
-
end
|
30
|
-
|
31
|
-
it "can be converted to JSON structure" do
|
32
|
-
state = subject.new(true)
|
33
|
-
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
|
34
|
-
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
|
35
|
-
state.add_flag(flag1, 'value1', 0)
|
36
|
-
state.add_flag(flag2, 'value2', 1)
|
37
|
-
|
38
|
-
result = state.as_json
|
39
|
-
expect(result).to eq({
|
40
|
-
'key1' => 'value1',
|
41
|
-
'key2' => 'value2',
|
42
|
-
'$flagsState' => {
|
43
|
-
'key1' => {
|
44
|
-
:variation => 0,
|
45
|
-
:version => 100
|
46
|
-
},
|
47
|
-
'key2' => {
|
48
|
-
:variation => 1,
|
49
|
-
:version => 200,
|
50
|
-
:trackEvents => true,
|
51
|
-
:debugEventsUntilDate => 1000
|
52
|
-
}
|
53
|
-
},
|
54
|
-
'$valid' => true
|
55
|
-
})
|
56
|
-
end
|
57
|
-
|
58
|
-
it "can be converted to JSON string" do
|
59
|
-
state = subject.new(true)
|
60
|
-
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
|
61
|
-
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
|
62
|
-
state.add_flag(flag1, 'value1', 0)
|
63
|
-
state.add_flag(flag2, 'value2', 1)
|
64
|
-
|
65
|
-
object = state.as_json
|
66
|
-
str = state.to_json
|
67
|
-
expect(object.to_json).to eq(str)
|
68
|
-
end
|
69
|
-
|
70
|
-
it "uses our custom serializer with JSON.generate" do
|
71
|
-
state = subject.new(true)
|
72
|
-
flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
|
73
|
-
flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
|
74
|
-
state.add_flag(flag1, 'value1', 0)
|
75
|
-
state.add_flag(flag2, 'value2', 1)
|
76
|
-
|
77
|
-
stringFromToJson = state.to_json
|
78
|
-
stringFromGenerate = JSON.generate(state)
|
79
|
-
expect(stringFromGenerate).to eq(stringFromToJson)
|
80
|
-
end
|
81
|
-
end
|
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,111 +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
|
-
it "gets expected bucket values for specific keys" do
|
8
|
-
user = { key: "userKeyA" }
|
9
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
10
|
-
expect(bucket).to be_within(0.0000001).of(0.42157587);
|
11
|
-
|
12
|
-
user = { key: "userKeyB" }
|
13
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
14
|
-
expect(bucket).to be_within(0.0000001).of(0.6708485);
|
15
|
-
|
16
|
-
user = { key: "userKeyC" }
|
17
|
-
bucket = subject.bucket_user(user, "hashKey", "key", "saltyA")
|
18
|
-
expect(bucket).to be_within(0.0000001).of(0.10343106);
|
19
|
-
end
|
20
|
-
|
21
|
-
it "can bucket by int value (equivalent to string)" do
|
22
|
-
user = {
|
23
|
-
key: "userkey",
|
24
|
-
custom: {
|
25
|
-
stringAttr: "33333",
|
26
|
-
intAttr: 33333
|
27
|
-
}
|
28
|
-
}
|
29
|
-
stringResult = subject.bucket_user(user, "hashKey", "stringAttr", "saltyA")
|
30
|
-
intResult = subject.bucket_user(user, "hashKey", "intAttr", "saltyA")
|
31
|
-
|
32
|
-
expect(intResult).to be_within(0.0000001).of(0.54771423)
|
33
|
-
expect(intResult).to eq(stringResult)
|
34
|
-
end
|
35
|
-
|
36
|
-
it "cannot bucket by float value" do
|
37
|
-
user = {
|
38
|
-
key: "userkey",
|
39
|
-
custom: {
|
40
|
-
floatAttr: 33.5
|
41
|
-
}
|
42
|
-
}
|
43
|
-
result = subject.bucket_user(user, "hashKey", "floatAttr", "saltyA")
|
44
|
-
expect(result).to eq(0.0)
|
45
|
-
end
|
46
|
-
|
47
|
-
|
48
|
-
it "cannot bucket by bool value" do
|
49
|
-
user = {
|
50
|
-
key: "userkey",
|
51
|
-
custom: {
|
52
|
-
boolAttr: true
|
53
|
-
}
|
54
|
-
}
|
55
|
-
result = subject.bucket_user(user, "hashKey", "boolAttr", "saltyA")
|
56
|
-
expect(result).to eq(0.0)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
describe "variation_index_for_user" do
|
61
|
-
it "matches bucket" do
|
62
|
-
user = { key: "userkey" }
|
63
|
-
flag_key = "flagkey"
|
64
|
-
salt = "salt"
|
65
|
-
|
66
|
-
# First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
|
67
|
-
# so we can construct a rollout whose second bucket just barely contains that value
|
68
|
-
bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
|
69
|
-
expect(bucket_value).to be > 0
|
70
|
-
expect(bucket_value).to be < 100000
|
71
|
-
|
72
|
-
bad_variation_a = 0
|
73
|
-
matched_variation = 1
|
74
|
-
bad_variation_b = 2
|
75
|
-
rule = {
|
76
|
-
rollout: {
|
77
|
-
variations: [
|
78
|
-
{ variation: bad_variation_a, weight: bucket_value }, # end of bucket range is not inclusive, so it will *not* match the target value
|
79
|
-
{ variation: matched_variation, weight: 1 }, # size of this bucket is 1, so it only matches that specific value
|
80
|
-
{ variation: bad_variation_b, weight: 100000 - (bucket_value + 1) }
|
81
|
-
]
|
82
|
-
}
|
83
|
-
}
|
84
|
-
flag = { key: flag_key, salt: salt }
|
85
|
-
|
86
|
-
result_variation = subject.variation_index_for_user(flag, rule, user)
|
87
|
-
expect(result_variation).to be matched_variation
|
88
|
-
end
|
89
|
-
|
90
|
-
it "uses last bucket if bucket value is equal to total weight" do
|
91
|
-
user = { key: "userkey" }
|
92
|
-
flag_key = "flagkey"
|
93
|
-
salt = "salt"
|
94
|
-
|
95
|
-
bucket_value = (subject.bucket_user(user, flag_key, "key", salt) * 100000).truncate()
|
96
|
-
|
97
|
-
# We'll construct a list of variations that stops right at the target bucket value
|
98
|
-
rule = {
|
99
|
-
rollout: {
|
100
|
-
variations: [
|
101
|
-
{ variation: 0, weight: bucket_value }
|
102
|
-
]
|
103
|
-
}
|
104
|
-
}
|
105
|
-
flag = { key: flag_key, salt: salt }
|
106
|
-
|
107
|
-
result_variation = subject.variation_index_for_user(flag, rule, user)
|
108
|
-
expect(result_variation).to be 0
|
109
|
-
end
|
110
|
-
end
|
111
|
-
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,96 +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
|
-
end
|
95
|
-
end
|
96
|
-
end
|