flipper 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.rspec +1 -0
  2. data/Changelog.md +12 -0
  3. data/Gemfile +4 -7
  4. data/Guardfile +16 -4
  5. data/README.md +63 -34
  6. data/examples/basic.rb +1 -1
  7. data/examples/dsl.rb +10 -12
  8. data/examples/group.rb +10 -4
  9. data/examples/individual_actor.rb +9 -6
  10. data/examples/instrumentation.rb +39 -0
  11. data/examples/percentage_of_actors.rb +12 -9
  12. data/examples/percentage_of_random.rb +4 -2
  13. data/lib/flipper.rb +43 -10
  14. data/lib/flipper/adapter.rb +106 -21
  15. data/lib/flipper/adapters/memoized.rb +7 -0
  16. data/lib/flipper/adapters/memory.rb +10 -3
  17. data/lib/flipper/adapters/operation_logger.rb +7 -0
  18. data/lib/flipper/dsl.rb +73 -16
  19. data/lib/flipper/errors.rb +6 -0
  20. data/lib/flipper/feature.rb +117 -19
  21. data/lib/flipper/gate.rb +72 -4
  22. data/lib/flipper/gates/actor.rb +41 -12
  23. data/lib/flipper/gates/boolean.rb +21 -11
  24. data/lib/flipper/gates/group.rb +45 -12
  25. data/lib/flipper/gates/percentage_of_actors.rb +29 -10
  26. data/lib/flipper/gates/percentage_of_random.rb +22 -9
  27. data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
  28. data/lib/flipper/instrumentation/metriks.rb +6 -0
  29. data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
  30. data/lib/flipper/instrumenters/memory.rb +25 -0
  31. data/lib/flipper/instrumenters/noop.rb +9 -0
  32. data/lib/flipper/key.rb +23 -4
  33. data/lib/flipper/registry.rb +22 -6
  34. data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
  35. data/lib/flipper/toggle.rb +19 -2
  36. data/lib/flipper/toggles/boolean.rb +36 -3
  37. data/lib/flipper/toggles/set.rb +9 -3
  38. data/lib/flipper/toggles/value.rb +9 -3
  39. data/lib/flipper/type.rb +1 -0
  40. data/lib/flipper/types/actor.rb +12 -14
  41. data/lib/flipper/types/percentage.rb +8 -2
  42. data/lib/flipper/version.rb +1 -1
  43. data/spec/flipper/adapter_spec.rb +163 -27
  44. data/spec/flipper/adapters/memoized_spec.rb +6 -6
  45. data/spec/flipper/dsl_spec.rb +51 -54
  46. data/spec/flipper/feature_spec.rb +179 -17
  47. data/spec/flipper/gate_spec.rb +47 -0
  48. data/spec/flipper/gates/actor_spec.rb +52 -0
  49. data/spec/flipper/gates/boolean_spec.rb +52 -0
  50. data/spec/flipper/gates/group_spec.rb +79 -0
  51. data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
  52. data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
  53. data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
  54. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
  55. data/spec/flipper/instrumenters/memory_spec.rb +26 -0
  56. data/spec/flipper/instrumenters/noop_spec.rb +22 -0
  57. data/spec/flipper/key_spec.rb +8 -2
  58. data/spec/flipper/registry_spec.rb +20 -2
  59. data/spec/flipper/toggle_spec.rb +22 -0
  60. data/spec/flipper/toggles/boolean_spec.rb +40 -0
  61. data/spec/flipper/toggles/set_spec.rb +35 -0
  62. data/spec/flipper/toggles/value_spec.rb +55 -0
  63. data/spec/flipper/types/actor_spec.rb +28 -33
  64. data/spec/flipper_spec.rb +16 -3
  65. data/spec/helper.rb +37 -3
  66. data/spec/integration_spec.rb +90 -83
  67. metadata +40 -4
@@ -0,0 +1,52 @@
1
+ require 'helper'
2
+ require 'flipper/instrumenters/memory'
3
+
4
+ describe Flipper::Gates::Actor do
5
+ let(:adapter) { double('Adapter', :set_members => []) }
6
+ let(:feature) { double('Feature', :name => :search, :adapter => adapter) }
7
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
8
+
9
+ subject {
10
+ described_class.new(feature, :instrumenter => instrumenter)
11
+ }
12
+
13
+ describe "instrumentation" do
14
+ it "is recorded for open" do
15
+ thing = Struct.new(:flipper_id).new('22')
16
+ subject.open?(thing)
17
+
18
+ event = instrumenter.events.last
19
+ event.should_not be_nil
20
+ event.name.should eq('gate_operation.flipper')
21
+ event.payload.should eq({
22
+ :thing => thing,
23
+ :operation => :open?,
24
+ :result => false,
25
+ :gate_name => :actor,
26
+ :feature_name => :search,
27
+ })
28
+ end
29
+ end
30
+
31
+ describe "#description" do
32
+ context "with actors in set" do
33
+ before do
34
+ adapter.stub(:set_members => Set['bacon', 'ham'])
35
+ end
36
+
37
+ it "returns text" do
38
+ subject.description.should eq("actors (bacon, ham)")
39
+ end
40
+ end
41
+
42
+ context "with no actors in set" do
43
+ before do
44
+ adapter.stub(:set_members => Set.new)
45
+ end
46
+
47
+ it "returns disabled" do
48
+ subject.description.should eq('disabled')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ require 'helper'
2
+ require 'flipper/instrumenters/memory'
3
+
4
+ describe Flipper::Gates::Boolean do
5
+ let(:adapter) { double('Adapter', :read => nil) }
6
+ let(:feature) { double('Feature', :name => :search, :adapter => adapter) }
7
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
8
+
9
+ subject {
10
+ described_class.new(feature, :instrumenter => instrumenter)
11
+ }
12
+
13
+ describe "#description" do
14
+ context "for enabled" do
15
+ before do
16
+ subject.stub(:enabled? => true)
17
+ end
18
+
19
+ it "returns Enabled" do
20
+ subject.description.should eq('Enabled')
21
+ end
22
+ end
23
+
24
+ context "for disabled" do
25
+ before do
26
+ subject.stub(:enabled? => false)
27
+ end
28
+
29
+ it "returns Disabled" do
30
+ subject.description.should eq('Disabled')
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "instrumentation" do
36
+ it "is recorded for open" do
37
+ thing = nil
38
+ subject.open?(thing)
39
+
40
+ event = instrumenter.events.last
41
+ event.should_not be_nil
42
+ event.name.should eq('gate_operation.flipper')
43
+ event.payload.should eq({
44
+ :thing => thing,
45
+ :operation => :open?,
46
+ :result => false,
47
+ :gate_name => :boolean,
48
+ :feature_name => :search,
49
+ })
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,79 @@
1
+ require 'helper'
2
+ require 'flipper/instrumenters/memory'
3
+
4
+ describe Flipper::Gates::Group do
5
+ let(:adapter) { double('Adapter', :set_members => []) }
6
+ let(:feature) { double('Feature', :name => :search, :adapter => adapter) }
7
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
8
+
9
+ subject {
10
+ described_class.new(feature, :instrumenter => instrumenter)
11
+ }
12
+
13
+ describe "instrumentation" do
14
+ it "is recorded for open" do
15
+ thing = Struct.new(:flipper_id).new('22')
16
+ subject.open?(thing)
17
+
18
+ event = instrumenter.events.last
19
+ event.should_not be_nil
20
+ event.name.should eq('gate_operation.flipper')
21
+ event.payload.should eq({
22
+ :thing => thing,
23
+ :operation => :open?,
24
+ :result => false,
25
+ :gate_name => :group,
26
+ :feature_name => :search,
27
+ })
28
+ end
29
+ end
30
+
31
+ describe "#description" do
32
+ context "with groups in set" do
33
+ before do
34
+ adapter.stub(:set_members => Set['bacon', 'ham'])
35
+ end
36
+
37
+ it "returns text" do
38
+ subject.description.should eq("groups (bacon, ham)")
39
+ end
40
+ end
41
+
42
+ context "with no groups in set" do
43
+ before do
44
+ adapter.stub(:set_members => Set.new)
45
+ end
46
+
47
+ it "returns disabled" do
48
+ subject.description.should eq('disabled')
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "#open?" do
54
+ context "with a group in adapter, but not registered" do
55
+ before do
56
+ Flipper.register(:staff) { |thing| true }
57
+ adapter.stub(:set_members => Set[:newbs, :staff])
58
+ end
59
+
60
+ it "ignores group" do
61
+ thing = Struct.new(:flipper_id).new('5')
62
+ subject.open?(thing).should be_true
63
+ end
64
+ end
65
+
66
+ context "thing that does not respond to method in group block" do
67
+ before do
68
+ Flipper.register(:stinkers) { |thing| thing.stinker? }
69
+ adapter.stub(:set_members => Set[:stinkers])
70
+ end
71
+
72
+ it "raises error" do
73
+ expect {
74
+ subject.open?(Object.new)
75
+ }.to raise_error(NoMethodError)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,98 @@
1
+ require 'helper'
2
+ require 'flipper/instrumenters/memory'
3
+
4
+ describe Flipper::Gates::PercentageOfActors do
5
+ let(:adapter) { double('Adapter', :read => nil) }
6
+ let(:feature) { double('Feature', :name => :search, :adapter => adapter) }
7
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
8
+
9
+ subject {
10
+ described_class.new(feature, :instrumenter => instrumenter)
11
+ }
12
+
13
+ describe "instrumentation" do
14
+ it "is recorded for open" do
15
+ thing = Struct.new(:flipper_id).new('22')
16
+ subject.open?(thing)
17
+
18
+ event = instrumenter.events.last
19
+ event.should_not be_nil
20
+ event.name.should eq('gate_operation.flipper')
21
+ event.payload.should eq({
22
+ :thing => thing,
23
+ :operation => :open?,
24
+ :result => false,
25
+ :gate_name => :percentage_of_actors,
26
+ :feature_name => :search,
27
+ })
28
+ end
29
+ end
30
+
31
+ describe "#description" do
32
+ context "when enabled" do
33
+ before do
34
+ adapter.stub(:read => 22)
35
+ end
36
+
37
+ it "returns text" do
38
+ subject.description.should eq('22% of actors')
39
+ end
40
+ end
41
+
42
+ context "when disabled" do
43
+ before do
44
+ adapter.stub(:read => nil)
45
+ end
46
+
47
+ it "returns disabled" do
48
+ subject.description.should eq('disabled')
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "#open?" do
54
+ context "when compared against two features" do
55
+ let(:percentage) { 0.05 }
56
+ let(:number_of_actors) { 100 }
57
+
58
+ let(:actors) {
59
+ (1..number_of_actors).map { |n|
60
+ Struct.new(:flipper_id).new(n)
61
+ }
62
+ }
63
+
64
+ let(:feature_one_enabled_actors) do
65
+ feature_one = double('Feature', :name => :name_one, :adapter => adapter)
66
+ gate = described_class.new(feature_one)
67
+ actors.select { |actor| gate.open? actor }
68
+ end
69
+
70
+ let(:feature_two_enabled_actors) do
71
+ feature_two = double('Feature', :name => :name_two, :adapter => adapter)
72
+ gate = described_class.new(feature_two)
73
+ actors.select { |actor| gate.open? actor }
74
+ end
75
+
76
+ before do
77
+ percentage_as_integer = percentage * 100
78
+ adapter.stub(:read => percentage_as_integer)
79
+ end
80
+
81
+ it "does not enable both features for same set of actors" do
82
+ feature_one_enabled_actors.should_not eq(feature_two_enabled_actors)
83
+ end
84
+
85
+ it "enables feature for accurate number of actors for each feature" do
86
+ margin_of_error = 0.02 * number_of_actors # 2 percent margin of error
87
+ expected_enabled_size = number_of_actors * percentage
88
+
89
+ [
90
+ feature_one_enabled_actors.size,
91
+ feature_two_enabled_actors.size,
92
+ ].each do |actual_enabled_size|
93
+ actual_enabled_size.should be_within(margin_of_error).of(expected_enabled_size)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,54 @@
1
+ require 'helper'
2
+ require 'flipper/instrumenters/memory'
3
+
4
+ describe Flipper::Gates::PercentageOfRandom do
5
+ let(:adapter) { double('Adapter', :read => 5) }
6
+ let(:feature) { double('Feature', :name => :search, :adapter => adapter) }
7
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
8
+
9
+ subject {
10
+ described_class.new(feature, :instrumenter => instrumenter)
11
+ }
12
+
13
+ describe "instrumentation" do
14
+ it "is recorded for open" do
15
+ thing = Struct.new(:flipper_id).new('22')
16
+ subject.open?(thing)
17
+
18
+ event = instrumenter.events.last
19
+ event.should_not be_nil
20
+ event.name.should eq('gate_operation.flipper')
21
+
22
+ event.payload[:thing].should eq(thing)
23
+ event.payload[:operation].should eq(:open?)
24
+ event.payload[:gate_name].should eq(:percentage_of_random)
25
+ event.payload[:feature_name].should eq(:search)
26
+
27
+ # random so don't test value
28
+ event.payload.key?(:result).should be_true
29
+ event.payload[:result].should_not be_nil
30
+ end
31
+ end
32
+
33
+ describe "#description" do
34
+ context "when enabled" do
35
+ before do
36
+ adapter.stub(:read => 22)
37
+ end
38
+
39
+ it "returns text" do
40
+ subject.description.should eq('22% of the time')
41
+ end
42
+ end
43
+
44
+ context "when disabled" do
45
+ before do
46
+ adapter.stub(:read => nil)
47
+ end
48
+
49
+ it "returns disabled" do
50
+ subject.description.should eq('disabled')
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,104 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/memory'
3
+ require 'flipper/instrumentation/log_subscriber'
4
+
5
+ describe Flipper::Instrumentation::LogSubscriber do
6
+ let(:adapter) { Flipper::Adapters::Memory.new }
7
+ let(:flipper) {
8
+ Flipper.new(adapter, :instrumenter => ActiveSupport::Notifications)
9
+ }
10
+
11
+ before do
12
+ Flipper.register(:admins) { |thing|
13
+ thing.respond_to?(:admin?) && thing.admin?
14
+ }
15
+
16
+ @io = StringIO.new
17
+ logger = Logger.new(@io)
18
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
19
+ described_class.logger = logger
20
+ end
21
+
22
+ after do
23
+ described_class.logger = nil
24
+ end
25
+
26
+ let(:log) { @io.string }
27
+
28
+ context "feature enabled checks" do
29
+ before do
30
+ clear_logs
31
+ flipper[:search].enabled?
32
+ end
33
+
34
+ it "logs feature calls with result after operation" do
35
+ feature_line = find_line('Flipper feature(search) enabled? false')
36
+ feature_line.should include('[ thing=nil ]')
37
+ end
38
+
39
+ it "logs adapter calls" do
40
+ adapter_line = find_line('Flipper feature(search) adapter(memory) read("search/boolean")')
41
+ adapter_line.should include('[ result=nil ]')
42
+ end
43
+
44
+ it "logs gate calls" do
45
+ gate_line = find_line('Flipper feature(search) gate(boolean) open? false')
46
+ gate_line.should include('[ thing=nil ]')
47
+ end
48
+ end
49
+
50
+ context "feature enabled checks with a thing" do
51
+ let(:user) { Struct.new(:flipper_id).new('1') }
52
+
53
+ before do
54
+ clear_logs
55
+ flipper[:search].enabled?(user)
56
+ end
57
+
58
+ it "logs thing for feature" do
59
+ feature_line = find_line('Flipper feature(search) enabled?')
60
+ feature_line.should include(user.inspect)
61
+ end
62
+
63
+ it "logs thing for gate" do
64
+ gate_line = find_line('Flipper feature(search) gate(boolean) open')
65
+ gate_line.should include(user.inspect)
66
+ end
67
+ end
68
+
69
+ context "changing feature enabled state" do
70
+ let(:user) { Struct.new(:flipper_id).new('1') }
71
+
72
+ before do
73
+ clear_logs
74
+ flipper[:search].enable(user)
75
+ end
76
+
77
+ it "logs feature calls with result in brackets" do
78
+ feature_line = find_line('Flipper feature(search) enable true')
79
+ feature_line.should include("[ thing=#{user.inspect} gate_name=actor ]")
80
+ end
81
+
82
+ it "logs adapter value" do
83
+ adapter_line = find_line('Flipper feature(search) adapter(memory) set_add("search/actors")')
84
+ adapter_line.should include("value=#{user.flipper_id.to_s.inspect}")
85
+ end
86
+
87
+ it "logs adapter calls not related to a specific feature" do
88
+ adapter_line = find_line('Flipper adapter(memory) set_add("features")')
89
+ log.should_not include('Could not log')
90
+ log.should_not include('NoMethodError: undefined method')
91
+ end
92
+ end
93
+
94
+ def find_line(str)
95
+ regex = /#{Regexp.escape(str)}/
96
+ lines = log.split("\n")
97
+ lines.detect { |line| line =~ regex } ||
98
+ raise("Could not find line matching #{str.inspect} in #{lines.inspect}")
99
+ end
100
+
101
+ def clear_logs
102
+ @io.string = ''
103
+ end
104
+ end
@@ -0,0 +1,69 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/memory'
3
+ require 'flipper/instrumentation/metriks'
4
+
5
+ describe Flipper::Instrumentation::MetriksSubscriber do
6
+ let(:adapter) { Flipper::Adapters::Memory.new }
7
+ let(:flipper) {
8
+ Flipper.new(adapter, :instrumenter => ActiveSupport::Notifications)
9
+ }
10
+
11
+ let(:user) { user = Struct.new(:flipper_id).new('1') }
12
+
13
+ before do
14
+ Metriks::Registry.default.clear
15
+ end
16
+
17
+ context "for enabled feature" do
18
+ it "updates feature metrics when calls happen" do
19
+ flipper[:stats].enable(user)
20
+ Metriks.timer("flipper.feature_operation.enable").count.should be(1)
21
+
22
+ flipper[:stats].enabled?(user)
23
+ Metriks.timer("flipper.feature_operation.enabled").count.should be(1)
24
+ Metriks.meter("flipper.feature.stats.enabled").count.should be(1)
25
+ end
26
+ end
27
+
28
+ context "for disabled feature" do
29
+ it "updates feature metrics when calls happen" do
30
+ flipper[:stats].disable(user)
31
+ Metriks.timer("flipper.feature_operation.disable").count.should be(1)
32
+
33
+ flipper[:stats].enabled?(user)
34
+ Metriks.timer("flipper.feature_operation.enabled").count.should be(1)
35
+ Metriks.meter("flipper.feature.stats.disabled").count.should be(1)
36
+ end
37
+ end
38
+
39
+ it "updates adapter metrics when calls happen" do
40
+ flipper[:stats].enable(user)
41
+ # one for features and one for actors
42
+ Metriks.timer("flipper.adapter.memory.set_add").count.should be(2)
43
+
44
+ flipper[:stats].enabled?(user)
45
+ Metriks.timer("flipper.adapter.memory.read").count.should be(1)
46
+ # one for actors and one for groups
47
+ Metriks.timer("flipper.adapter.memory.set_members").count.should be(2)
48
+
49
+ flipper[:stats].disable(user)
50
+ Metriks.timer("flipper.adapter.memory.set_delete").count.should be(1)
51
+ end
52
+
53
+ it "updates gate metrics when calls happen" do
54
+ flipper[:stats].enable(user)
55
+ flipper[:stats].enabled?(user)
56
+
57
+ Metriks.timer("flipper.gate_operation.boolean.open").count.should be(1)
58
+ Metriks.timer("flipper.feature.stats.gate_operation.boolean.open").count.should be(1)
59
+ Metriks.meter("flipper.feature.stats.gate.actor.open").count.should be(1)
60
+ Metriks.meter("flipper.feature.stats.gate.boolean.closed").count.should be(1)
61
+ end
62
+
63
+ # Helper for seeing what is in the metriks registry
64
+ def print_registry_names
65
+ Metriks::Registry.default.each do |name, metric|
66
+ puts name
67
+ end
68
+ end
69
+ end