flipper 0.3.0 → 0.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.
- data/.rspec +1 -0
- data/Changelog.md +12 -0
- data/Gemfile +4 -7
- data/Guardfile +16 -4
- data/README.md +63 -34
- data/examples/basic.rb +1 -1
- data/examples/dsl.rb +10 -12
- data/examples/group.rb +10 -4
- data/examples/individual_actor.rb +9 -6
- data/examples/instrumentation.rb +39 -0
- data/examples/percentage_of_actors.rb +12 -9
- data/examples/percentage_of_random.rb +4 -2
- data/lib/flipper.rb +43 -10
- data/lib/flipper/adapter.rb +106 -21
- data/lib/flipper/adapters/memoized.rb +7 -0
- data/lib/flipper/adapters/memory.rb +10 -3
- data/lib/flipper/adapters/operation_logger.rb +7 -0
- data/lib/flipper/dsl.rb +73 -16
- data/lib/flipper/errors.rb +6 -0
- data/lib/flipper/feature.rb +117 -19
- data/lib/flipper/gate.rb +72 -4
- data/lib/flipper/gates/actor.rb +41 -12
- data/lib/flipper/gates/boolean.rb +21 -11
- data/lib/flipper/gates/group.rb +45 -12
- data/lib/flipper/gates/percentage_of_actors.rb +29 -10
- data/lib/flipper/gates/percentage_of_random.rb +22 -9
- data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
- data/lib/flipper/instrumentation/metriks.rb +6 -0
- data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
- data/lib/flipper/instrumenters/memory.rb +25 -0
- data/lib/flipper/instrumenters/noop.rb +9 -0
- data/lib/flipper/key.rb +23 -4
- data/lib/flipper/registry.rb +22 -6
- data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
- data/lib/flipper/toggle.rb +19 -2
- data/lib/flipper/toggles/boolean.rb +36 -3
- data/lib/flipper/toggles/set.rb +9 -3
- data/lib/flipper/toggles/value.rb +9 -3
- data/lib/flipper/type.rb +1 -0
- data/lib/flipper/types/actor.rb +12 -14
- data/lib/flipper/types/percentage.rb +8 -2
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +163 -27
- data/spec/flipper/adapters/memoized_spec.rb +6 -6
- data/spec/flipper/dsl_spec.rb +51 -54
- data/spec/flipper/feature_spec.rb +179 -17
- data/spec/flipper/gate_spec.rb +47 -0
- data/spec/flipper/gates/actor_spec.rb +52 -0
- data/spec/flipper/gates/boolean_spec.rb +52 -0
- data/spec/flipper/gates/group_spec.rb +79 -0
- data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
- data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
- data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
- data/spec/flipper/instrumenters/memory_spec.rb +26 -0
- data/spec/flipper/instrumenters/noop_spec.rb +22 -0
- data/spec/flipper/key_spec.rb +8 -2
- data/spec/flipper/registry_spec.rb +20 -2
- data/spec/flipper/toggle_spec.rb +22 -0
- data/spec/flipper/toggles/boolean_spec.rb +40 -0
- data/spec/flipper/toggles/set_spec.rb +35 -0
- data/spec/flipper/toggles/value_spec.rb +55 -0
- data/spec/flipper/types/actor_spec.rb +28 -33
- data/spec/flipper_spec.rb +16 -3
- data/spec/helper.rb +37 -3
- data/spec/integration_spec.rb +90 -83
- metadata +40 -4
@@ -3,25 +3,44 @@ require 'zlib'
|
|
3
3
|
module Flipper
|
4
4
|
module Gates
|
5
5
|
class PercentageOfActors < Gate
|
6
|
-
|
6
|
+
# Internal: The name of the gate. Used for instrumentation, etc.
|
7
|
+
def name
|
8
|
+
:percentage_of_actors
|
9
|
+
end
|
7
10
|
|
8
|
-
|
9
|
-
|
11
|
+
# Internal: The piece of the adapter key that is unique to the gate class.
|
12
|
+
def key
|
13
|
+
:perc_actors
|
10
14
|
end
|
11
15
|
|
12
|
-
|
13
|
-
|
16
|
+
# Internal: Checks if the gate is open for a thing.
|
17
|
+
#
|
18
|
+
# Returns true if gate open for thing, false if not.
|
19
|
+
def open?(thing)
|
20
|
+
instrument(:open?, thing) { |payload|
|
21
|
+
percentage = toggle.value.to_i
|
14
22
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
23
|
+
if Types::Actor.wrappable?(thing)
|
24
|
+
actor = Types::Actor.wrap(thing)
|
25
|
+
key = "#{@feature.name}#{actor.value}"
|
26
|
+
Zlib.crc32(key) % 100 < percentage
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
}
|
20
31
|
end
|
21
32
|
|
22
33
|
def protects?(thing)
|
23
34
|
thing.is_a?(Flipper::Types::PercentageOfActors)
|
24
35
|
end
|
36
|
+
|
37
|
+
def description
|
38
|
+
if enabled?
|
39
|
+
"#{toggle.value}% of actors"
|
40
|
+
else
|
41
|
+
'disabled'
|
42
|
+
end
|
43
|
+
end
|
25
44
|
end
|
26
45
|
end
|
27
46
|
end
|
@@ -1,25 +1,38 @@
|
|
1
1
|
module Flipper
|
2
2
|
module Gates
|
3
3
|
class PercentageOfRandom < Gate
|
4
|
-
|
4
|
+
# Internal: The name of the gate. Used for instrumentation, etc.
|
5
|
+
def name
|
6
|
+
:percentage_of_random
|
7
|
+
end
|
5
8
|
|
6
|
-
|
7
|
-
|
9
|
+
# Internal: The piece of the adapter key that is unique to the gate class.
|
10
|
+
def key
|
11
|
+
:perc_time
|
8
12
|
end
|
9
13
|
|
10
|
-
|
11
|
-
|
14
|
+
# Internal: Checks if the gate is open for a thing.
|
15
|
+
#
|
16
|
+
# Returns true if gate open for thing, false if not.
|
17
|
+
def open?(thing)
|
18
|
+
instrument(:open?, thing) { |payload|
|
19
|
+
percentage = toggle.value.to_i
|
12
20
|
|
13
|
-
if percentage.nil?
|
14
|
-
false
|
15
|
-
else
|
16
21
|
rand < (percentage / 100.0)
|
17
|
-
|
22
|
+
}
|
18
23
|
end
|
19
24
|
|
20
25
|
def protects?(thing)
|
21
26
|
thing.is_a?(Flipper::Types::PercentageOfRandom)
|
22
27
|
end
|
28
|
+
|
29
|
+
def description
|
30
|
+
if enabled?
|
31
|
+
"#{toggle.value}% of the time"
|
32
|
+
else
|
33
|
+
'disabled'
|
34
|
+
end
|
35
|
+
end
|
23
36
|
end
|
24
37
|
end
|
25
38
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'active_support/notifications'
|
3
|
+
require 'active_support/log_subscriber'
|
4
|
+
|
5
|
+
module Flipper
|
6
|
+
module Instrumentation
|
7
|
+
class LogSubscriber < ::ActiveSupport::LogSubscriber
|
8
|
+
# Logs a feature operation.
|
9
|
+
#
|
10
|
+
# Example Output
|
11
|
+
#
|
12
|
+
# flipper[:search].enabled?(user)
|
13
|
+
# # Flipper feature(search) enabled? false (1.2ms) [ thing=#<struct flipper_id="1"> ]
|
14
|
+
#
|
15
|
+
# Returns nothing.
|
16
|
+
def feature_operation(event)
|
17
|
+
return unless logger.debug?
|
18
|
+
|
19
|
+
feature_name = event.payload[:feature_name]
|
20
|
+
gate_name = event.payload[:gate_name]
|
21
|
+
operation = event.payload[:operation]
|
22
|
+
result = event.payload[:result]
|
23
|
+
thing = event.payload[:thing]
|
24
|
+
|
25
|
+
description = "Flipper feature(#{feature_name}) #{operation} #{result.inspect}"
|
26
|
+
details = "thing=#{thing.inspect}"
|
27
|
+
|
28
|
+
unless gate_name.nil?
|
29
|
+
details += " gate_name=#{gate_name}"
|
30
|
+
end
|
31
|
+
|
32
|
+
name = '%s (%.1fms)' % [description, event.duration]
|
33
|
+
debug " #{color(name, CYAN, true)} [ #{details} ]"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Logs an adapter operation. If operation is for a feature, then that
|
37
|
+
# feature is included in log output.
|
38
|
+
#
|
39
|
+
# Example Output
|
40
|
+
#
|
41
|
+
# # log output for adapter operation with feature
|
42
|
+
# # Flipper feature(search) adapter(memory) set_add("search/actors") (0.0ms) [ result=#<Set: {"1"}> value="1" ]
|
43
|
+
#
|
44
|
+
# # log output for adapter operation with no feature
|
45
|
+
# # Flipper adapter(memory) set_add("features") (0.0ms) [ result=#<Set: {"search"}> value="search" ]
|
46
|
+
#
|
47
|
+
# Returns nothing.
|
48
|
+
def adapter_operation(event)
|
49
|
+
return unless logger.debug?
|
50
|
+
|
51
|
+
adapter_name = event.payload[:adapter_name]
|
52
|
+
operation = event.payload[:operation]
|
53
|
+
result = event.payload[:result]
|
54
|
+
value = event.payload[:value]
|
55
|
+
key = event.payload[:key]
|
56
|
+
|
57
|
+
feature_description = if key.respond_to?(:feature_name)
|
58
|
+
"Flipper feature(#{key.feature_name})"
|
59
|
+
else
|
60
|
+
"Flipper"
|
61
|
+
end
|
62
|
+
|
63
|
+
adapter_description = "adapter(#{adapter_name})"
|
64
|
+
operation_description = "#{operation}(#{key.to_s.inspect})"
|
65
|
+
description = "#{feature_description} #{adapter_description} #{operation_description}"
|
66
|
+
details = "result=#{result.inspect}"
|
67
|
+
|
68
|
+
if event.payload.key?(:value)
|
69
|
+
details += " value=#{value.inspect}"
|
70
|
+
end
|
71
|
+
|
72
|
+
name = '%s (%.1fms)' % [description, event.duration]
|
73
|
+
debug " #{color(name, CYAN, true)} [ #{details} ]"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Logs a gate operation.
|
77
|
+
#
|
78
|
+
# Example Output
|
79
|
+
#
|
80
|
+
# flipper[:search].enabled?(user)
|
81
|
+
# # Flipper feature(search) gate(boolean) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
|
82
|
+
# # Flipper feature(search) gate(group) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
|
83
|
+
# # Flipper feature(search) gate(actor) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
|
84
|
+
# # Flipper feature(search) gate(percentage_of_actors) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
|
85
|
+
# # Flipper feature(search) gate(percentage_of_random) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
|
86
|
+
#
|
87
|
+
# Returns nothing.
|
88
|
+
def gate_operation(event)
|
89
|
+
return unless logger.debug?
|
90
|
+
|
91
|
+
feature_name = event.payload[:feature_name]
|
92
|
+
gate_name = event.payload[:gate_name]
|
93
|
+
operation = event.payload[:operation]
|
94
|
+
result = event.payload[:result]
|
95
|
+
thing = event.payload[:thing]
|
96
|
+
|
97
|
+
description = "Flipper feature(#{feature_name}) gate(#{gate_name}) #{operation} #{result.inspect}"
|
98
|
+
details = "thing=#{thing.inspect}"
|
99
|
+
|
100
|
+
name = '%s (%.1fms)' % [description, event.duration]
|
101
|
+
debug " #{color(name, CYAN, true)} [ #{details} ]"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
Instrumentation::LogSubscriber.attach_to InstrumentationNamespace
|
107
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# Note: You should never need to require this file directly if you are using
|
2
|
+
# ActiveSupport::Notifications. Instead, you should require the metriks file
|
3
|
+
# that lives in the same directory as this file. The benefit is that it
|
4
|
+
# subscribes to the correct events and does everything for your.
|
5
|
+
require 'metriks'
|
6
|
+
|
7
|
+
module Flipper
|
8
|
+
module Instrumentation
|
9
|
+
class MetriksSubscriber
|
10
|
+
# Public: Use this as the subscribed block.
|
11
|
+
def self.call(name, start, ending, transaction_id, payload)
|
12
|
+
new(name, start, ending, transaction_id, payload).update
|
13
|
+
end
|
14
|
+
|
15
|
+
# Private: Initializes a new event processing instance.
|
16
|
+
def initialize(name, start, ending, transaction_id, payload)
|
17
|
+
@name = name
|
18
|
+
@start = start
|
19
|
+
@ending = ending
|
20
|
+
@payload = payload
|
21
|
+
@duration = ending - start
|
22
|
+
@transaction_id = transaction_id
|
23
|
+
end
|
24
|
+
|
25
|
+
def update
|
26
|
+
operation_type = @name.split('.').first
|
27
|
+
method_name = "update_#{operation_type}_metrics"
|
28
|
+
|
29
|
+
if respond_to?(method_name)
|
30
|
+
send(method_name)
|
31
|
+
else
|
32
|
+
puts "Could not update #{operation_type} metrics as MetriksSubscriber did not respond to `#{method_name}`"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_feature_operation_metrics
|
37
|
+
feature_name = @payload[:feature_name]
|
38
|
+
gate_name = @payload[:gate_name]
|
39
|
+
operation = strip_trailing_question_mark(@payload[:operation])
|
40
|
+
result = @payload[:result]
|
41
|
+
thing = @payload[:thing]
|
42
|
+
|
43
|
+
Metriks.timer("flipper.feature_operation.#{operation}").update(@duration)
|
44
|
+
|
45
|
+
if @payload[:operation] == :enabled?
|
46
|
+
metric_name = if result
|
47
|
+
"flipper.feature.#{feature_name}.enabled"
|
48
|
+
else
|
49
|
+
"flipper.feature.#{feature_name}.disabled"
|
50
|
+
end
|
51
|
+
|
52
|
+
Metriks.meter(metric_name).mark
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_adapter_operation_metrics
|
57
|
+
adapter_name = @payload[:adapter_name]
|
58
|
+
operation = @payload[:operation]
|
59
|
+
result = @payload[:result]
|
60
|
+
value = @payload[:value]
|
61
|
+
key = @payload[:key]
|
62
|
+
|
63
|
+
Metriks.timer("flipper.adapter.#{adapter_name}.#{operation}").update(@duration)
|
64
|
+
end
|
65
|
+
|
66
|
+
def update_gate_operation_metrics
|
67
|
+
feature_name = @payload[:feature_name]
|
68
|
+
gate_name = @payload[:gate_name]
|
69
|
+
operation = strip_trailing_question_mark(@payload[:operation])
|
70
|
+
result = @payload[:result]
|
71
|
+
thing = @payload[:thing]
|
72
|
+
|
73
|
+
Metriks.timer("flipper.gate_operation.#{gate_name}.#{operation}").update(@duration)
|
74
|
+
Metriks.timer("flipper.feature.#{feature_name}.gate_operation.#{gate_name}.#{operation}").update(@duration)
|
75
|
+
|
76
|
+
if @payload[:operation] == :open?
|
77
|
+
metric_name = if result
|
78
|
+
"flipper.feature.#{feature_name}.gate.#{gate_name}.open"
|
79
|
+
else
|
80
|
+
"flipper.feature.#{feature_name}.gate.#{gate_name}.closed"
|
81
|
+
end
|
82
|
+
|
83
|
+
Metriks.meter(metric_name).mark
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def strip_trailing_question_mark(operation)
|
88
|
+
operation.to_s.gsub(/\?$/, '')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Instrumenters
|
3
|
+
# Instrumentor that is useful for tests as it stores each of the events that
|
4
|
+
# are instrumented.
|
5
|
+
class Memory
|
6
|
+
Event = Struct.new(:name, :payload, :result)
|
7
|
+
|
8
|
+
attr_reader :events
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@events = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def instrument(name, payload = {})
|
15
|
+
result = if block_given?
|
16
|
+
yield payload
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
@events << Event.new(name, payload, result)
|
21
|
+
result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/flipper/key.rb
CHANGED
@@ -1,19 +1,38 @@
|
|
1
1
|
module Flipper
|
2
|
+
# Private: Used internally in flipper to create key to be used for feature in
|
3
|
+
# the adapter. You should never need to use this.
|
2
4
|
class Key
|
5
|
+
# Private
|
3
6
|
Separator = '/'
|
4
7
|
|
5
|
-
|
8
|
+
# Private
|
9
|
+
attr_reader :feature_name
|
6
10
|
|
7
|
-
|
8
|
-
|
11
|
+
# Private
|
12
|
+
attr_reader :gate_key
|
13
|
+
|
14
|
+
# Internal
|
15
|
+
def initialize(feature_name, gate_key)
|
16
|
+
@feature_name, @gate_key = feature_name, gate_key
|
9
17
|
end
|
10
18
|
|
19
|
+
# Private
|
11
20
|
def separator
|
12
21
|
Separator.dup
|
13
22
|
end
|
14
23
|
|
24
|
+
# Private
|
15
25
|
def to_s
|
16
|
-
"#{
|
26
|
+
"#{feature_name}#{separator}#{gate_key}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Internal: Pretty string version for debugging.
|
30
|
+
def inspect
|
31
|
+
attributes = [
|
32
|
+
"feature_name=#{feature_name.inspect}",
|
33
|
+
"gate_key=#{gate_key.inspect}",
|
34
|
+
]
|
35
|
+
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
17
36
|
end
|
18
37
|
end
|
19
38
|
end
|
data/lib/flipper/registry.rb
CHANGED
@@ -1,12 +1,22 @@
|
|
1
1
|
require 'thread'
|
2
2
|
|
3
3
|
module Flipper
|
4
|
+
# Internal: Used to store registry of groups by name.
|
4
5
|
class Registry
|
5
6
|
include Enumerable
|
6
7
|
|
7
8
|
class Error < StandardError; end
|
8
9
|
class DuplicateKey < Error; end
|
9
|
-
|
10
|
+
|
11
|
+
class KeyNotFound < Error
|
12
|
+
# Public: The key that was not found
|
13
|
+
attr_reader :key
|
14
|
+
|
15
|
+
def initialize(key)
|
16
|
+
@key = key
|
17
|
+
super("Key #{key.inspect} not found")
|
18
|
+
end
|
19
|
+
end
|
10
20
|
|
11
21
|
def initialize(source = {})
|
12
22
|
@mutex = Mutex.new
|
@@ -22,19 +32,25 @@ module Flipper
|
|
22
32
|
end
|
23
33
|
|
24
34
|
def add(key, value)
|
25
|
-
|
35
|
+
key = key.to_sym
|
36
|
+
|
37
|
+
@mutex.synchronize {
|
26
38
|
if @source[key]
|
27
39
|
raise DuplicateKey, "#{key} is already registered"
|
28
40
|
else
|
29
41
|
@source[key] = value
|
30
42
|
end
|
31
|
-
|
43
|
+
}
|
32
44
|
end
|
33
45
|
|
34
46
|
def get(key)
|
35
|
-
|
36
|
-
|
37
|
-
|
47
|
+
key = key.to_sym
|
48
|
+
|
49
|
+
@mutex.synchronize {
|
50
|
+
@source.fetch(key) {
|
51
|
+
raise KeyNotFound.new(key)
|
52
|
+
}
|
53
|
+
}
|
38
54
|
end
|
39
55
|
|
40
56
|
def each(&block)
|
@@ -1,5 +1,12 @@
|
|
1
1
|
require 'set'
|
2
2
|
|
3
|
+
shared_examples_for 'a working percentage' do
|
4
|
+
it "does not raise when used" do
|
5
|
+
feature.enable percentage
|
6
|
+
expect { feature.enabled?(actor) }.to_not raise_error
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
3
10
|
# Requires the following methods
|
4
11
|
# subject
|
5
12
|
# read_key(key)
|
@@ -35,24 +42,24 @@ shared_examples_for 'a flipper adapter' do
|
|
35
42
|
|
36
43
|
describe "#set_add" do
|
37
44
|
it "adds value to store" do
|
38
|
-
subject.set_add(key, 1)
|
39
|
-
read_key(key).should eq(Set[1])
|
45
|
+
subject.set_add(key, '1')
|
46
|
+
read_key(key).should eq(Set['1'])
|
40
47
|
end
|
41
48
|
|
42
49
|
it "does not add same value more than once" do
|
43
|
-
subject.set_add(key, 1)
|
44
|
-
subject.set_add(key, 1)
|
45
|
-
subject.set_add(key, 1)
|
46
|
-
subject.set_add(key, 2)
|
47
|
-
read_key(key).should eq(Set[1, 2])
|
50
|
+
subject.set_add(key, '1')
|
51
|
+
subject.set_add(key, '1')
|
52
|
+
subject.set_add(key, '1')
|
53
|
+
subject.set_add(key, '2')
|
54
|
+
read_key(key).should eq(Set['1', '2'])
|
48
55
|
end
|
49
56
|
end
|
50
57
|
|
51
58
|
describe "#set_delete" do
|
52
59
|
it "removes value from set if key in store" do
|
53
|
-
write_key key, Set[1, 2]
|
54
|
-
subject.set_delete(key, 1)
|
55
|
-
read_key(key).should eq(Set[2])
|
60
|
+
write_key key, Set['1', '2']
|
61
|
+
subject.set_delete(key, '1')
|
62
|
+
read_key(key).should eq(Set['2'])
|
56
63
|
end
|
57
64
|
|
58
65
|
it "works fine if key not in store" do
|
@@ -66,12 +73,52 @@ shared_examples_for 'a flipper adapter' do
|
|
66
73
|
end
|
67
74
|
|
68
75
|
it "returns set if in store" do
|
69
|
-
write_key key, Set[1, 2]
|
70
|
-
subject.set_members(key).should eq(Set[1, 2])
|
76
|
+
write_key key, Set['1', '2']
|
77
|
+
subject.set_members(key).should eq(Set['1', '2'])
|
71
78
|
end
|
72
79
|
end
|
73
80
|
|
74
81
|
it "should work with Flipper.new" do
|
75
82
|
Flipper.new(subject).should_not be_nil
|
76
83
|
end
|
84
|
+
|
85
|
+
context "working with values" do
|
86
|
+
it "always uses strings" do
|
87
|
+
subject.read(key).should be_nil
|
88
|
+
subject.write key, true
|
89
|
+
subject.read(key).should eq('true')
|
90
|
+
|
91
|
+
subject.write key, 22
|
92
|
+
subject.read(key).should eq('22')
|
93
|
+
|
94
|
+
subject.delete(key)
|
95
|
+
subject.read(key).should be_nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "working with sets" do
|
100
|
+
it "always uses strings" do
|
101
|
+
subject.set_add key, 1
|
102
|
+
subject.set_add key, 2
|
103
|
+
subject.set_members(key).should eq(Set['1', '2'])
|
104
|
+
subject.set_delete key, 2
|
105
|
+
subject.set_members(key).should eq(Set['1'])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "integration spot-checks" do
|
110
|
+
let(:instance) { Flipper.new(subject) }
|
111
|
+
let(:feature) { instance[:feature] }
|
112
|
+
let(:actor) { Struct.new(:id).new(1) }
|
113
|
+
|
114
|
+
context "percentage of actors" do
|
115
|
+
let(:percentage) { instance.actors(10) }
|
116
|
+
it_should_behave_like 'a working percentage'
|
117
|
+
end
|
118
|
+
|
119
|
+
context "random percentage" do
|
120
|
+
let(:percentage) { instance.random(10) }
|
121
|
+
it_should_behave_like 'a working percentage'
|
122
|
+
end
|
123
|
+
end
|
77
124
|
end
|