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
@@ -0,0 +1,55 @@
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
@@ -0,0 +1,141 @@
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
@@ -0,0 +1,96 @@
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
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+ require "impl/evaluator_spec_base"
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ describe "Evaluator (segments)", :evaluator_spec_base => true do
7
+ subject { Evaluator }
8
+
9
+ def test_segment_match(segment)
10
+ clause = make_segment_match_clause(segment)
11
+ flag = boolean_flag_with_clauses([clause])
12
+ e = Evaluator.new(get_nothing, get_things({ segment[:key] => segment }), logger)
13
+ e.evaluate(flag, user, factory).detail.value
14
+ end
15
+
16
+ it "retrieves segment from segment store for segmentMatch operator" do
17
+ segment = {
18
+ key: 'segkey',
19
+ included: [ 'userkey' ],
20
+ version: 1,
21
+ deleted: false
22
+ }
23
+ get_segment = get_things({ 'segkey' => segment })
24
+ e = subject.new(get_nothing, get_segment, logger)
25
+ user = { key: 'userkey' }
26
+ clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
27
+ flag = boolean_flag_with_clauses([clause])
28
+ expect(e.evaluate(flag, user, factory).detail.value).to be true
29
+ end
30
+
31
+ it "falls through with no errors if referenced segment is not found" do
32
+ e = subject.new(get_nothing, get_things({ 'segkey' => nil }), logger)
33
+ user = { key: 'userkey' }
34
+ clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
35
+ flag = boolean_flag_with_clauses([clause])
36
+ expect(e.evaluate(flag, user, factory).detail.value).to be false
37
+ end
38
+
39
+ it 'explicitly includes user' do
40
+ segment = make_segment('segkey')
41
+ segment[:included] = [ user[:key] ]
42
+ expect(test_segment_match(segment)).to be true
43
+ end
44
+
45
+ it 'explicitly excludes user' do
46
+ segment = make_segment('segkey')
47
+ segment[:excluded] = [ user[:key] ]
48
+ expect(test_segment_match(segment)).to be false
49
+ end
50
+
51
+ it 'both includes and excludes user; include takes priority' do
52
+ segment = make_segment('segkey')
53
+ segment[:included] = [ user[:key] ]
54
+ segment[:excluded] = [ user[:key] ]
55
+ expect(test_segment_match(segment)).to be true
56
+ end
57
+
58
+ it 'matches user by rule when weight is absent' do
59
+ segClause = make_user_matching_clause(user, :email)
60
+ segRule = {
61
+ clauses: [ segClause ]
62
+ }
63
+ segment = make_segment('segkey')
64
+ segment[:rules] = [ segRule ]
65
+ expect(test_segment_match(segment)).to be true
66
+ end
67
+
68
+ it 'matches user by rule when weight is nil' do
69
+ segClause = make_user_matching_clause(user, :email)
70
+ segRule = {
71
+ clauses: [ segClause ],
72
+ weight: nil
73
+ }
74
+ segment = make_segment('segkey')
75
+ segment[:rules] = [ segRule ]
76
+ expect(test_segment_match(segment)).to be true
77
+ end
78
+
79
+ it 'matches user with full rollout' do
80
+ segClause = make_user_matching_clause(user, :email)
81
+ segRule = {
82
+ clauses: [ segClause ],
83
+ weight: 100000
84
+ }
85
+ segment = make_segment('segkey')
86
+ segment[:rules] = [ segRule ]
87
+ expect(test_segment_match(segment)).to be true
88
+ end
89
+
90
+ it "doesn't match user with zero rollout" do
91
+ segClause = make_user_matching_clause(user, :email)
92
+ segRule = {
93
+ clauses: [ segClause ],
94
+ weight: 0
95
+ }
96
+ segment = make_segment('segkey')
97
+ segment[:rules] = [ segRule ]
98
+ expect(test_segment_match(segment)).to be false
99
+ end
100
+
101
+ it "matches user with multiple clauses" do
102
+ segClause1 = make_user_matching_clause(user, :email)
103
+ segClause2 = make_user_matching_clause(user, :name)
104
+ segRule = {
105
+ clauses: [ segClause1, segClause2 ]
106
+ }
107
+ segment = make_segment('segkey')
108
+ segment[:rules] = [ segRule ]
109
+ expect(test_segment_match(segment)).to be true
110
+ end
111
+
112
+ it "doesn't match user with multiple clauses if a clause doesn't match" do
113
+ segClause1 = make_user_matching_clause(user, :email)
114
+ segClause2 = make_user_matching_clause(user, :name)
115
+ segClause2[:values] = [ 'wrong' ]
116
+ segRule = {
117
+ clauses: [ segClause1, segClause2 ]
118
+ }
119
+ segment = make_segment('segkey')
120
+ segment[:rules] = [ segRule ]
121
+ expect(test_segment_match(segment)).to be false
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,305 @@
1
+ require "spec_helper"
2
+ require "impl/evaluator_spec_base"
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ describe "Evaluator (general)", :evaluator_spec_base => true do
7
+ subject { Evaluator }
8
+
9
+ describe "evaluate" do
10
+ it "returns off variation if flag is off" do
11
+ flag = {
12
+ key: 'feature',
13
+ on: false,
14
+ offVariation: 1,
15
+ fallthrough: { variation: 0 },
16
+ variations: ['a', 'b', 'c']
17
+ }
18
+ user = { key: 'x' }
19
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::off)
20
+ result = basic_evaluator.evaluate(flag, user, factory)
21
+ expect(result.detail).to eq(detail)
22
+ expect(result.events).to eq(nil)
23
+ end
24
+
25
+ it "returns nil if flag is off and off variation is unspecified" do
26
+ flag = {
27
+ key: 'feature',
28
+ on: false,
29
+ fallthrough: { variation: 0 },
30
+ variations: ['a', 'b', 'c']
31
+ }
32
+ user = { key: 'x' }
33
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::off)
34
+ result = basic_evaluator.evaluate(flag, user, factory)
35
+ expect(result.detail).to eq(detail)
36
+ expect(result.events).to eq(nil)
37
+ end
38
+
39
+ it "returns an error if off variation is too high" do
40
+ flag = {
41
+ key: 'feature',
42
+ on: false,
43
+ offVariation: 999,
44
+ fallthrough: { variation: 0 },
45
+ variations: ['a', 'b', 'c']
46
+ }
47
+ user = { key: 'x' }
48
+ detail = EvaluationDetail.new(nil, nil,
49
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
50
+ result = basic_evaluator.evaluate(flag, user, factory)
51
+ expect(result.detail).to eq(detail)
52
+ expect(result.events).to eq(nil)
53
+ end
54
+
55
+ it "returns an error if off variation is negative" do
56
+ flag = {
57
+ key: 'feature',
58
+ on: false,
59
+ offVariation: -1,
60
+ fallthrough: { variation: 0 },
61
+ variations: ['a', 'b', 'c']
62
+ }
63
+ user = { key: 'x' }
64
+ detail = EvaluationDetail.new(nil, nil,
65
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
66
+ result = basic_evaluator.evaluate(flag, user, factory)
67
+ expect(result.detail).to eq(detail)
68
+ expect(result.events).to eq(nil)
69
+ end
70
+
71
+ it "returns off variation if prerequisite is not found" do
72
+ flag = {
73
+ key: 'feature0',
74
+ on: true,
75
+ prerequisites: [{key: 'badfeature', variation: 1}],
76
+ fallthrough: { variation: 0 },
77
+ offVariation: 1,
78
+ variations: ['a', 'b', 'c']
79
+ }
80
+ user = { key: 'x' }
81
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('badfeature'))
82
+ e = subject.new(get_things( 'badfeature' => nil ), get_nothing, logger)
83
+ result = e.evaluate(flag, user, factory)
84
+ expect(result.detail).to eq(detail)
85
+ expect(result.events).to eq(nil)
86
+ end
87
+
88
+ it "reuses prerequisite-failed reason instances if possible" do
89
+ flag = {
90
+ key: 'feature0',
91
+ on: true,
92
+ prerequisites: [{key: 'badfeature', variation: 1}],
93
+ fallthrough: { variation: 0 },
94
+ offVariation: 1,
95
+ variations: ['a', 'b', 'c']
96
+ }
97
+ Model.postprocess_item_after_deserializing!(FEATURES, flag) # now there's a cached reason
98
+ user = { key: 'x' }
99
+ e = subject.new(get_things( 'badfeature' => nil ), get_nothing, logger)
100
+ result1 = e.evaluate(flag, user, factory)
101
+ expect(result1.detail.reason).to eq EvaluationReason::prerequisite_failed('badfeature')
102
+ result2 = e.evaluate(flag, user, factory)
103
+ expect(result2.detail.reason).to be result1.detail.reason
104
+ end
105
+
106
+ it "returns off variation and event if prerequisite of a prerequisite is not found" do
107
+ flag = {
108
+ key: 'feature0',
109
+ on: true,
110
+ prerequisites: [{key: 'feature1', variation: 1}],
111
+ fallthrough: { variation: 0 },
112
+ offVariation: 1,
113
+ variations: ['a', 'b', 'c'],
114
+ version: 1
115
+ }
116
+ flag1 = {
117
+ key: 'feature1',
118
+ on: true,
119
+ prerequisites: [{key: 'feature2', variation: 1}], # feature2 doesn't exist
120
+ fallthrough: { variation: 0 },
121
+ variations: ['d', 'e'],
122
+ version: 2
123
+ }
124
+ user = { key: 'x' }
125
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
126
+ events_should_be = [{
127
+ kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0'
128
+ }]
129
+ get_flag = get_things('feature1' => flag1, 'feature2' => nil)
130
+ e = subject.new(get_flag, get_nothing, logger)
131
+ result = e.evaluate(flag, user, factory)
132
+ expect(result.detail).to eq(detail)
133
+ expect(result.events).to eq(events_should_be)
134
+ end
135
+
136
+ it "returns off variation and event if prerequisite is off" do
137
+ flag = {
138
+ key: 'feature0',
139
+ on: true,
140
+ prerequisites: [{key: 'feature1', variation: 1}],
141
+ fallthrough: { variation: 0 },
142
+ offVariation: 1,
143
+ variations: ['a', 'b', 'c'],
144
+ version: 1
145
+ }
146
+ flag1 = {
147
+ key: 'feature1',
148
+ on: false,
149
+ # note that even though it returns the desired variation, it is still off and therefore not a match
150
+ offVariation: 1,
151
+ fallthrough: { variation: 0 },
152
+ variations: ['d', 'e'],
153
+ version: 2
154
+ }
155
+ user = { key: 'x' }
156
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
157
+ events_should_be = [{
158
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
159
+ }]
160
+ get_flag = get_things({ 'feature1' => flag1 })
161
+ e = subject.new(get_flag, get_nothing, logger)
162
+ result = e.evaluate(flag, user, factory)
163
+ expect(result.detail).to eq(detail)
164
+ expect(result.events).to eq(events_should_be)
165
+ end
166
+
167
+ it "returns off variation and event if prerequisite is not met" do
168
+ flag = {
169
+ key: 'feature0',
170
+ on: true,
171
+ prerequisites: [{key: 'feature1', variation: 1}],
172
+ fallthrough: { variation: 0 },
173
+ offVariation: 1,
174
+ variations: ['a', 'b', 'c'],
175
+ version: 1
176
+ }
177
+ flag1 = {
178
+ key: 'feature1',
179
+ on: true,
180
+ fallthrough: { variation: 0 },
181
+ variations: ['d', 'e'],
182
+ version: 2
183
+ }
184
+ user = { key: 'x' }
185
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
186
+ events_should_be = [{
187
+ kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0'
188
+ }]
189
+ get_flag = get_things({ 'feature1' => flag1 })
190
+ e = subject.new(get_flag, get_nothing, logger)
191
+ result = e.evaluate(flag, user, factory)
192
+ expect(result.detail).to eq(detail)
193
+ expect(result.events).to eq(events_should_be)
194
+ end
195
+
196
+ it "returns fallthrough variation and event if prerequisite is met and there are no rules" do
197
+ flag = {
198
+ key: 'feature0',
199
+ on: true,
200
+ prerequisites: [{key: 'feature1', variation: 1}],
201
+ fallthrough: { variation: 0 },
202
+ offVariation: 1,
203
+ variations: ['a', 'b', 'c'],
204
+ version: 1
205
+ }
206
+ flag1 = {
207
+ key: 'feature1',
208
+ on: true,
209
+ fallthrough: { variation: 1 },
210
+ variations: ['d', 'e'],
211
+ version: 2
212
+ }
213
+ user = { key: 'x' }
214
+ detail = EvaluationDetail.new('a', 0, EvaluationReason::fallthrough)
215
+ events_should_be = [{
216
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
217
+ }]
218
+ get_flag = get_things({ 'feature1' => flag1 })
219
+ e = subject.new(get_flag, get_nothing, logger)
220
+ result = e.evaluate(flag, user, factory)
221
+ expect(result.detail).to eq(detail)
222
+ expect(result.events).to eq(events_should_be)
223
+ end
224
+
225
+ it "returns an error if fallthrough variation is too high" do
226
+ flag = {
227
+ key: 'feature',
228
+ on: true,
229
+ fallthrough: { variation: 999 },
230
+ offVariation: 1,
231
+ variations: ['a', 'b', 'c']
232
+ }
233
+ user = { key: 'userkey' }
234
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
235
+ result = basic_evaluator.evaluate(flag, user, factory)
236
+ expect(result.detail).to eq(detail)
237
+ expect(result.events).to eq(nil)
238
+ end
239
+
240
+ it "returns an error if fallthrough variation is negative" do
241
+ flag = {
242
+ key: 'feature',
243
+ on: true,
244
+ fallthrough: { variation: -1 },
245
+ offVariation: 1,
246
+ variations: ['a', 'b', 'c']
247
+ }
248
+ user = { key: 'userkey' }
249
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
250
+ result = basic_evaluator.evaluate(flag, user, factory)
251
+ expect(result.detail).to eq(detail)
252
+ expect(result.events).to eq(nil)
253
+ end
254
+
255
+ it "returns an error if fallthrough has no variation or rollout" do
256
+ flag = {
257
+ key: 'feature',
258
+ on: true,
259
+ fallthrough: { },
260
+ offVariation: 1,
261
+ variations: ['a', 'b', 'c']
262
+ }
263
+ user = { key: 'userkey' }
264
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
265
+ result = basic_evaluator.evaluate(flag, user, factory)
266
+ expect(result.detail).to eq(detail)
267
+ expect(result.events).to eq(nil)
268
+ end
269
+
270
+ it "returns an error if fallthrough has a rollout with no variations" do
271
+ flag = {
272
+ key: 'feature',
273
+ on: true,
274
+ fallthrough: { rollout: { variations: [] } },
275
+ offVariation: 1,
276
+ variations: ['a', 'b', 'c']
277
+ }
278
+ user = { key: 'userkey' }
279
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
280
+ result = basic_evaluator.evaluate(flag, user, factory)
281
+ expect(result.detail).to eq(detail)
282
+ expect(result.events).to eq(nil)
283
+ end
284
+
285
+ it "matches user from targets" do
286
+ flag = {
287
+ key: 'feature',
288
+ on: true,
289
+ targets: [
290
+ { values: [ 'whoever', 'userkey' ], variation: 2 }
291
+ ],
292
+ fallthrough: { variation: 0 },
293
+ offVariation: 1,
294
+ variations: ['a', 'b', 'c']
295
+ }
296
+ user = { key: 'userkey' }
297
+ detail = EvaluationDetail.new('c', 2, EvaluationReason::target_match)
298
+ result = basic_evaluator.evaluate(flag, user, factory)
299
+ expect(result.detail).to eq(detail)
300
+ expect(result.events).to eq(nil)
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end