launchdarkly-server-sdk 5.8.0 → 6.1.1

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.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -122
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  4. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. data/.gitignore +2 -1
  6. data/.ldrelease/build-docs.sh +18 -0
  7. data/.ldrelease/circleci/linux/execute.sh +18 -0
  8. data/.ldrelease/circleci/mac/execute.sh +18 -0
  9. data/.ldrelease/circleci/template/build.sh +29 -0
  10. data/.ldrelease/circleci/template/publish.sh +23 -0
  11. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  12. data/.ldrelease/circleci/template/test.sh +10 -0
  13. data/.ldrelease/circleci/template/update-version.sh +8 -0
  14. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  15. data/.ldrelease/config.yml +14 -2
  16. data/CHANGELOG.md +29 -0
  17. data/CONTRIBUTING.md +1 -1
  18. data/README.md +4 -3
  19. data/azure-pipelines.yml +1 -1
  20. data/docs/Makefile +26 -0
  21. data/docs/index.md +9 -0
  22. data/launchdarkly-server-sdk.gemspec +20 -13
  23. data/lib/ldclient-rb.rb +0 -1
  24. data/lib/ldclient-rb/config.rb +15 -3
  25. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  26. data/lib/ldclient-rb/events.rb +6 -7
  27. data/lib/ldclient-rb/file_data_source.rb +1 -1
  28. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  29. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  30. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  31. data/lib/ldclient-rb/impl/event_factory.rb +22 -0
  32. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  33. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  34. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  35. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -7
  36. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  37. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  38. data/lib/ldclient-rb/ldclient.rb +38 -17
  39. data/lib/ldclient-rb/polling.rb +1 -4
  40. data/lib/ldclient-rb/requestor.rb +25 -23
  41. data/lib/ldclient-rb/stream.rb +10 -30
  42. data/lib/ldclient-rb/util.rb +12 -8
  43. data/lib/ldclient-rb/version.rb +1 -1
  44. data/spec/evaluation_detail_spec.rb +135 -0
  45. data/spec/event_sender_spec.rb +20 -2
  46. data/spec/events_spec.rb +10 -0
  47. data/spec/http_util.rb +11 -1
  48. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  49. data/spec/impl/evaluator_clause_spec.rb +55 -0
  50. data/spec/impl/evaluator_operators_spec.rb +141 -0
  51. data/spec/impl/evaluator_rule_spec.rb +96 -0
  52. data/spec/impl/evaluator_segment_spec.rb +125 -0
  53. data/spec/impl/evaluator_spec.rb +305 -0
  54. data/spec/impl/evaluator_spec_base.rb +75 -0
  55. data/spec/impl/model/serialization_spec.rb +41 -0
  56. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  57. data/spec/ldclient_end_to_end_spec.rb +34 -0
  58. data/spec/ldclient_spec.rb +64 -12
  59. data/spec/polling_spec.rb +2 -2
  60. data/spec/redis_feature_store_spec.rb +2 -2
  61. data/spec/requestor_spec.rb +11 -45
  62. data/spec/spec_helper.rb +0 -3
  63. data/spec/stream_spec.rb +1 -16
  64. metadata +111 -61
  65. data/.yardopts +0 -9
  66. data/Gemfile.lock +0 -100
  67. data/lib/ldclient-rb/evaluation.rb +0 -462
  68. data/scripts/gendocs.sh +0 -11
  69. data/scripts/release.sh +0 -27
  70. data/spec/evaluation_spec.rb +0 -789
@@ -37,10 +37,7 @@ module LaunchDarkly
37
37
  def poll
38
38
  all_data = @requestor.request_all_data
39
39
  if all_data
40
- @config.feature_store.init({
41
- FEATURES => all_data[:flags],
42
- SEGMENTS => all_data[:segments]
43
- })
40
+ @config.feature_store.init(all_data)
44
41
  if @initialized.make_true
45
42
  @config.logger.info { "[LDClient] Polling connection initialized" }
46
43
  @ready.set
@@ -1,6 +1,9 @@
1
+ require "ldclient-rb/impl/model/serialization"
2
+
1
3
  require "concurrent/atomics"
2
4
  require "json"
3
5
  require "uri"
6
+ require "http"
4
7
 
5
8
  module LaunchDarkly
6
9
  # @private
@@ -22,45 +25,44 @@ module LaunchDarkly
22
25
  def initialize(sdk_key, config)
23
26
  @sdk_key = sdk_key
24
27
  @config = config
25
- @client = Util.new_http_client(@config.base_uri, @config)
28
+ @http_client = LaunchDarkly::Util.new_http_client(config.base_uri, config)
26
29
  @cache = @config.cache_store
27
30
  end
28
31
 
29
- def request_flag(key)
30
- make_request("/sdk/latest-flags/" + key)
31
- end
32
-
33
- def request_segment(key)
34
- make_request("/sdk/latest-segments/" + key)
35
- end
36
-
37
32
  def request_all_data()
38
- make_request("/sdk/latest-all")
33
+ all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
34
+ Impl::Model.make_all_store_data(all_data)
39
35
  end
40
36
 
41
37
  def stop
42
38
  begin
43
- @client.finish
39
+ @http_client.close
44
40
  rescue
45
41
  end
46
42
  end
47
43
 
48
44
  private
49
45
 
46
+ def request_single_item(kind, path)
47
+ Impl::Model.deserialize(kind, make_request(path))
48
+ end
49
+
50
50
  def make_request(path)
51
- @client.start if !@client.started?
52
51
  uri = URI(@config.base_uri + path)
53
- req = Net::HTTP::Get.new(uri)
54
- Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| req[k] = v }
55
- req["Connection"] = "keep-alive"
52
+ headers = {}
53
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
54
+ headers["Connection"] = "keep-alive"
56
55
  cached = @cache.read(uri)
57
56
  if !cached.nil?
58
- req["If-None-Match"] = cached.etag
57
+ headers["If-None-Match"] = cached.etag
59
58
  end
60
- res = @client.request(req)
61
- status = res.code.to_i
62
- @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{res.to_hash}\n\tbody: #{res.body}" }
63
-
59
+ response = @http_client.request("GET", uri, {
60
+ headers: headers
61
+ })
62
+ status = response.status.code
63
+ @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers}\n\tbody: #{res.to_s}" }
64
+ # must fully read body for persistent connections
65
+ body = response.to_s
64
66
  if status == 304 && !cached.nil?
65
67
  body = cached.body
66
68
  else
@@ -68,11 +70,11 @@ module LaunchDarkly
68
70
  if status < 200 || status >= 300
69
71
  raise UnexpectedResponseError.new(status)
70
72
  end
71
- body = fix_encoding(res.body, res["content-type"])
72
- etag = res["etag"]
73
+ body = fix_encoding(body, response.headers["content-type"])
74
+ etag = response.headers["etag"]
73
75
  @cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
74
76
  end
75
- JSON.parse(body, symbolize_names: true)
77
+ body
76
78
  end
77
79
 
78
80
  def fix_encoding(body, content_type)
@@ -1,3 +1,5 @@
1
+ require "ldclient-rb/impl/model/serialization"
2
+
1
3
  require "concurrent/atomics"
2
4
  require "json"
3
5
  require "ld-eventsource"
@@ -10,10 +12,6 @@ module LaunchDarkly
10
12
  # @private
11
13
  DELETE = :delete
12
14
  # @private
13
- INDIRECT_PUT = :'indirect/put'
14
- # @private
15
- INDIRECT_PATCH = :'indirect/patch'
16
- # @private
17
15
  READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
18
16
 
19
17
  # @private
@@ -24,11 +22,10 @@ module LaunchDarkly
24
22
 
25
23
  # @private
26
24
  class StreamProcessor
27
- def initialize(sdk_key, config, requestor, diagnostic_accumulator = nil)
25
+ def initialize(sdk_key, config, diagnostic_accumulator = nil)
28
26
  @sdk_key = sdk_key
29
27
  @config = config
30
28
  @feature_store = config.feature_store
31
- @requestor = requestor
32
29
  @initialized = Concurrent::AtomicBoolean.new(false)
33
30
  @started = Concurrent::AtomicBoolean.new(false)
34
31
  @stopped = Concurrent::AtomicBoolean.new(false)
@@ -49,7 +46,8 @@ module LaunchDarkly
49
46
  opts = {
50
47
  headers: headers,
51
48
  read_timeout: READ_TIMEOUT_SECONDS,
52
- logger: @config.logger
49
+ logger: @config.logger,
50
+ socket_factory: @config.socket_factory
53
51
  }
54
52
  log_connection_started
55
53
  @es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
@@ -87,10 +85,8 @@ module LaunchDarkly
87
85
  @config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
88
86
  if method == PUT
89
87
  message = JSON.parse(message.data, symbolize_names: true)
90
- @feature_store.init({
91
- FEATURES => message[:data][:flags],
92
- SEGMENTS => message[:data][:segments]
93
- })
88
+ all_data = Impl::Model.make_all_store_data(message[:data])
89
+ @feature_store.init(all_data)
94
90
  @initialized.make_true
95
91
  @config.logger.info { "[LDClient] Stream initialized" }
96
92
  @ready.set
@@ -99,7 +95,9 @@ module LaunchDarkly
99
95
  for kind in [FEATURES, SEGMENTS]
100
96
  key = key_for_path(kind, data[:path])
101
97
  if key
102
- @feature_store.upsert(kind, data[:data])
98
+ data = data[:data]
99
+ Impl::Model.postprocess_item_after_deserializing!(kind, data)
100
+ @feature_store.upsert(kind, data)
103
101
  break
104
102
  end
105
103
  end
@@ -112,24 +110,6 @@ module LaunchDarkly
112
110
  break
113
111
  end
114
112
  end
115
- elsif method == INDIRECT_PUT
116
- all_data = @requestor.request_all_data
117
- @feature_store.init({
118
- FEATURES => all_data[:flags],
119
- SEGMENTS => all_data[:segments]
120
- })
121
- @initialized.make_true
122
- @config.logger.info { "[LDClient] Stream initialized (via indirect message)" }
123
- elsif method == INDIRECT_PATCH
124
- key = key_for_path(FEATURES, message.data)
125
- if key
126
- @feature_store.upsert(FEATURES, @requestor.request_flag(key))
127
- else
128
- key = key_for_path(SEGMENTS, message.data)
129
- if key
130
- @feature_store.upsert(SEGMENTS, @requestor.request_segment(key))
131
- end
132
- end
133
113
  else
134
114
  @config.logger.warn { "[LDClient] Unknown message received: #{method}" }
135
115
  end
@@ -1,5 +1,5 @@
1
- require "net/http"
2
1
  require "uri"
2
+ require "http"
3
3
 
4
4
  module LaunchDarkly
5
5
  # @private
@@ -18,14 +18,18 @@ module LaunchDarkly
18
18
  end
19
19
  ret
20
20
  end
21
-
21
+
22
22
  def self.new_http_client(uri_s, config)
23
- uri = URI(uri_s)
24
- client = Net::HTTP.new(uri.hostname, uri.port)
25
- client.use_ssl = true if uri.scheme == "https"
26
- client.open_timeout = config.connect_timeout
27
- client.read_timeout = config.read_timeout
28
- client
23
+ http_client_options = {}
24
+ if config.socket_factory
25
+ http_client_options["socket_class"] = config.socket_factory
26
+ end
27
+ return HTTP::Client.new(http_client_options)
28
+ .timeout({
29
+ read: config.read_timeout,
30
+ connect: config.connect_timeout
31
+ })
32
+ .persistent(uri_s)
29
33
  end
30
34
 
31
35
  def self.log_exception(logger, message, exc)
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.8.0"
2
+ VERSION = "6.1.1"
3
3
  end
@@ -0,0 +1,135 @@
1
+ require "spec_helper"
2
+
3
+ module LaunchDarkly
4
+ describe "EvaluationDetail" do
5
+ subject { EvaluationDetail }
6
+
7
+ it "sets properties" do
8
+ expect(EvaluationDetail.new("x", 0, EvaluationReason::off).value).to eq "x"
9
+ expect(EvaluationDetail.new("x", 0, EvaluationReason::off).variation_index).to eq 0
10
+ expect(EvaluationDetail.new("x", 0, EvaluationReason::off).reason).to eq EvaluationReason::off
11
+ end
12
+
13
+ it "checks parameter types" do
14
+ expect { EvaluationDetail.new(nil, nil, EvaluationReason::off) }.not_to raise_error
15
+ expect { EvaluationDetail.new(nil, 0, EvaluationReason::off) }.not_to raise_error
16
+ expect { EvaluationDetail.new(nil, "x", EvaluationReason::off) }.to raise_error(ArgumentError)
17
+ expect { EvaluationDetail.new(nil, 0, { kind: "OFF" }) }.to raise_error(ArgumentError)
18
+ expect { EvaluationDetail.new(nil, 0, nil) }.to raise_error(ArgumentError)
19
+ end
20
+
21
+ it "equality test" do
22
+ expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).to eq EvaluationDetail.new("x", 0, EvaluationReason::off)
23
+ expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("y", 0, EvaluationReason::off)
24
+ expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 1, EvaluationReason::off)
25
+ expect(EvaluationDetail.new("x", 0, EvaluationReason::off)).not_to eq EvaluationDetail.new("x", 0, EvaluationReason::fallthrough)
26
+ end
27
+ end
28
+
29
+ describe "EvaluationReason" do
30
+ subject { EvaluationReason }
31
+
32
+ values = [
33
+ [ EvaluationReason::off, EvaluationReason::OFF, { "kind" => "OFF" }, "OFF", nil ],
34
+ [ EvaluationReason::fallthrough, EvaluationReason::FALLTHROUGH,
35
+ { "kind" => "FALLTHROUGH" }, "FALLTHROUGH", nil ],
36
+ [ EvaluationReason::target_match, EvaluationReason::TARGET_MATCH,
37
+ { "kind" => "TARGET_MATCH" }, "TARGET_MATCH", nil ],
38
+ [ EvaluationReason::rule_match(1, "x"), EvaluationReason::RULE_MATCH,
39
+ { "kind" => "RULE_MATCH", "ruleIndex" => 1, "ruleId" => "x" }, "RULE_MATCH(1,x)",
40
+ [ EvaluationReason::rule_match(2, "x"), EvaluationReason::rule_match(1, "y") ] ],
41
+ [ EvaluationReason::prerequisite_failed("x"), EvaluationReason::PREREQUISITE_FAILED,
42
+ { "kind" => "PREREQUISITE_FAILED", "prerequisiteKey" => "x" }, "PREREQUISITE_FAILED(x)" ],
43
+ [ EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND), EvaluationReason::ERROR,
44
+ { "kind" => "ERROR", "errorKind" => "FLAG_NOT_FOUND" }, "ERROR(FLAG_NOT_FOUND)" ]
45
+ ]
46
+ values.each_index do |i|
47
+ params = values[i]
48
+ reason = params[0]
49
+ kind = params[1]
50
+ json_rep = params[2]
51
+ brief_str = params[3]
52
+ unequal_values = params[4]
53
+
54
+ describe "reason #{reason.kind}" do
55
+ it "has correct kind" do
56
+ expect(reason.kind).to eq kind
57
+ end
58
+
59
+ it "equality to self" do
60
+ expect(reason).to eq reason
61
+ end
62
+
63
+ it "inequality to others" do
64
+ values.each_index do |j|
65
+ if i != j
66
+ expect(reason).not_to eq values[j][0]
67
+ end
68
+ end
69
+ if !unequal_values.nil?
70
+ unequal_values.each do |v|
71
+ expect(reason).not_to eq v
72
+ end
73
+ end
74
+ end
75
+
76
+ it "JSON representation" do
77
+ expect(JSON.parse(reason.as_json.to_json)).to eq json_rep
78
+ expect(JSON.parse(reason.to_json)).to eq json_rep
79
+ end
80
+
81
+ it "brief representation" do
82
+ expect(reason.inspect).to eq brief_str
83
+ expect(reason.to_s).to eq brief_str
84
+ end
85
+ end
86
+ end
87
+
88
+ it "reuses singleton reasons" do
89
+ expect(EvaluationReason::off).to be EvaluationReason::off
90
+ expect(EvaluationReason::fallthrough).to be EvaluationReason::fallthrough
91
+ expect(EvaluationReason::target_match).to be EvaluationReason::target_match
92
+ expect(EvaluationReason::rule_match(1, 'x')).not_to be EvaluationReason::rule_match(1, 'x')
93
+ expect(EvaluationReason::prerequisite_failed('x')).not_to be EvaluationReason::prerequisite_failed('x')
94
+ errors = [ EvaluationReason::ERROR_CLIENT_NOT_READY, EvaluationReason::ERROR_FLAG_NOT_FOUND,
95
+ EvaluationReason::ERROR_MALFORMED_FLAG, EvaluationReason::ERROR_USER_NOT_SPECIFIED, EvaluationReason::ERROR_EXCEPTION ]
96
+ errors.each do |e|
97
+ expect(EvaluationReason::error(e)).to be EvaluationReason::error(e)
98
+ end
99
+ end
100
+
101
+ it "supports [] with JSON property names" do
102
+ expect(EvaluationReason::off[:kind]).to eq "OFF"
103
+ expect(EvaluationReason::off[:ruleIndex]).to be nil
104
+ expect(EvaluationReason::off[:ruleId]).to be nil
105
+ expect(EvaluationReason::off[:prerequisiteKey]).to be nil
106
+ expect(EvaluationReason::off[:errorKind]).to be nil
107
+ expect(EvaluationReason::rule_match(1, "x")[:ruleIndex]).to eq 1
108
+ expect(EvaluationReason::rule_match(1, "x")[:ruleId]).to eq "x"
109
+ expect(EvaluationReason::prerequisite_failed("x")[:prerequisiteKey]).to eq "x"
110
+ expect(EvaluationReason::error(EvaluationReason::ERROR_FLAG_NOT_FOUND)[:errorKind]).to eq "FLAG_NOT_FOUND"
111
+ end
112
+
113
+ it "freezes string properties" do
114
+ rm = EvaluationReason::rule_match(1, "x")
115
+ expect { rm.rule_id.upcase! }.to raise_error(RuntimeError)
116
+ pf = EvaluationReason::prerequisite_failed("x")
117
+ expect { pf.prerequisite_key.upcase! }.to raise_error(RuntimeError)
118
+ end
119
+
120
+ it "checks parameter types" do
121
+ expect { EvaluationReason::rule_match(nil, "x") }.to raise_error(ArgumentError)
122
+ expect { EvaluationReason::rule_match(true, "x") }.to raise_error(ArgumentError)
123
+ expect { EvaluationReason::rule_match(1, nil) }.not_to raise_error # we allow nil rule_id for backward compatibility
124
+ expect { EvaluationReason::rule_match(1, 9) }.to raise_error(ArgumentError)
125
+ expect { EvaluationReason::prerequisite_failed(nil) }.to raise_error(ArgumentError)
126
+ expect { EvaluationReason::prerequisite_failed(9) }.to raise_error(ArgumentError)
127
+ expect { EvaluationReason::error(nil) }.to raise_error(ArgumentError)
128
+ expect { EvaluationReason::error(9) }.to raise_error(ArgumentError)
129
+ end
130
+
131
+ it "does not allow direct access to constructor" do
132
+ expect { EvaluationReason.new(:off, nil, nil, nil, nil) }.to raise_error(NoMethodError)
133
+ end
134
+ end
135
+ end
@@ -39,12 +39,29 @@ module LaunchDarkly
39
39
  "authorization" => [ sdk_key ],
40
40
  "content-type" => [ "application/json" ],
41
41
  "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
42
- "x-launchdarkly-event-schema" => [ "3" ]
42
+ "x-launchdarkly-event-schema" => [ "3" ],
43
+ "connection" => [ "Keep-Alive" ]
43
44
  })
44
45
  expect(req.header['x-launchdarkly-payload-id']).not_to eq []
45
46
  end
46
47
  end
47
-
48
+
49
+ it "can use a socket factory" do
50
+ with_server do |server|
51
+ server.setup_ok_response("/bulk", "")
52
+
53
+ config = Config.new(events_uri: "http://events.com/bulk", socket_factory: SocketFactoryFromHash.new({"events.com" => server.port}), logger: $null_log)
54
+ es = subject.new(sdk_key, config, nil, 0.1)
55
+
56
+ result = es.send_event_data(fake_data, "", false)
57
+
58
+ expect(result.success).to be true
59
+ req = server.await_request
60
+ expect(req.body).to eq fake_data
61
+ expect(req.host).to eq "events.com"
62
+ end
63
+ end
64
+
48
65
  it "generates a new payload ID for each payload" do
49
66
  with_sender_and_server do |es, server|
50
67
  server.setup_ok_response("/bulk", "")
@@ -78,6 +95,7 @@ module LaunchDarkly
78
95
  "authorization" => [ sdk_key ],
79
96
  "content-type" => [ "application/json" ],
80
97
  "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
98
+ "connection" => [ "Keep-Alive" ]
81
99
  })
82
100
  expect(req.header['x-launchdarkly-event-schema']).to eq []
83
101
  expect(req.header['x-launchdarkly-payload-id']).to eq []
data/spec/events_spec.rb CHANGED
@@ -408,6 +408,16 @@ describe LaunchDarkly::EventProcessor do
408
408
  end
409
409
  end
410
410
 
411
+ it "queues alias event" do
412
+ with_processor_and_sender(default_config) do |ep, sender|
413
+ e = { kind: "alias", key: "a", contextKind: "user", previousKey: "b", previousContextKind: "user" }
414
+ ep.add_event(e)
415
+
416
+ output = flush_and_get_events(ep, sender)
417
+ expect(output).to contain_exactly(e)
418
+ end
419
+ end
420
+
411
421
  it "treats nil value for custom the same as an empty hash" do
412
422
  with_processor_and_sender(default_config) do |ep, sender|
413
423
  user_with_nil_custom = { key: "userkey", custom: nil }
data/spec/http_util.rb CHANGED
@@ -3,7 +3,7 @@ require "webrick/httpproxy"
3
3
  require "webrick/https"
4
4
 
5
5
  class StubHTTPServer
6
- attr_reader :requests
6
+ attr_reader :requests, :port
7
7
 
8
8
  @@next_port = 50000
9
9
 
@@ -120,3 +120,13 @@ def with_server(server = nil)
120
120
  server.stop
121
121
  end
122
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
@@ -0,0 +1,111 @@
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