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.
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