flipper 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|