blackbeard 0.0.3.1 → 0.0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -20
  3. data/dashboard/public/stylesheets/application.css +4 -0
  4. data/dashboard/routes/features.rb +31 -0
  5. data/dashboard/routes/groups.rb +2 -2
  6. data/dashboard/routes/metrics.rb +6 -6
  7. data/dashboard/routes/tests.rb +2 -2
  8. data/dashboard/views/features/index.erb +21 -0
  9. data/dashboard/views/features/show.erb +163 -0
  10. data/dashboard/views/groups/show.erb +1 -1
  11. data/dashboard/views/layout.erb +2 -1
  12. data/lib/blackbeard.rb +18 -7
  13. data/lib/blackbeard/configuration.rb +3 -2
  14. data/lib/blackbeard/context.rb +11 -7
  15. data/lib/blackbeard/dashboard.rb +2 -0
  16. data/lib/blackbeard/errors.rb +4 -0
  17. data/lib/blackbeard/feature.rb +45 -0
  18. data/lib/blackbeard/feature_rollout.rb +47 -0
  19. data/lib/blackbeard/metric.rb +22 -4
  20. data/lib/blackbeard/pirate.rb +11 -5
  21. data/lib/blackbeard/redis_store.rb +4 -0
  22. data/lib/blackbeard/storable.rb +72 -6
  23. data/lib/blackbeard/storable_attributes.rb +50 -3
  24. data/lib/blackbeard/storable_has_many.rb +1 -0
  25. data/lib/blackbeard/storable_has_set.rb +1 -0
  26. data/lib/blackbeard/version.rb +1 -1
  27. data/spec/blackbeard_spec.rb +13 -0
  28. data/spec/configuration_spec.rb +6 -0
  29. data/spec/context_spec.rb +12 -12
  30. data/spec/dashboard/features_spec.rb +52 -0
  31. data/spec/dashboard/groups_spec.rb +4 -4
  32. data/spec/dashboard/metrics_spec.rb +6 -6
  33. data/spec/dashboard/tests_spec.rb +4 -4
  34. data/spec/feature_rollout_spec.rb +147 -0
  35. data/spec/feature_spec.rb +58 -0
  36. data/spec/group_spec.rb +1 -1
  37. data/spec/metric_data/total_spec.rb +1 -1
  38. data/spec/metric_data/unique_spec.rb +1 -1
  39. data/spec/metric_spec.rb +5 -5
  40. data/spec/pirate_spec.rb +16 -12
  41. data/spec/redis_store_spec.rb +6 -0
  42. data/spec/spec_helper.rb +3 -1
  43. data/spec/storable_attributes_spec.rb +58 -6
  44. data/spec/storable_has_many_spec.rb +2 -2
  45. data/spec/storable_has_set_spec.rb +1 -1
  46. data/spec/storable_spec.rb +119 -23
  47. data/spec/test_spec.rb +1 -1
  48. metadata +13 -2
@@ -49,6 +49,7 @@ module Blackbeard
49
49
  end
50
50
 
51
51
  def #{plural}_set_key
52
+ raise StorableNotSaved if new_record?
52
53
  key+"::#{plural}"
53
54
  end
54
55
  END_OF_RUBY
@@ -47,6 +47,7 @@ module Blackbeard
47
47
  end
48
48
 
49
49
  def #{plural}_key
50
+ raise StorableNotSaved if new_record?
50
51
  key+"::#{plural}"
51
52
  end
52
53
 
@@ -1,3 +1,3 @@
1
1
  module Blackbeard
2
- VERSION = "0.0.3.1"
2
+ VERSION = "0.0.4.0"
3
3
  end
@@ -4,4 +4,17 @@ describe Blackbeard do
4
4
  it "should not sink" do
5
5
  Blackbeard.should_not be_nil
6
6
  end
7
+
8
+ describe "pirate" do
9
+ it "can configure" do
10
+ p = Blackbeard.pirate do |config|
11
+ config.timezone = 'America/Somewhere'
12
+ end
13
+ Blackbeard.config.timezone.should == 'America/Somewhere'
14
+ end
15
+ it "returns a pirate" do
16
+ Blackbeard.pirate.should be_a(Blackbeard::Pirate)
17
+ end
18
+ end
19
+
7
20
  end
@@ -10,6 +10,12 @@ module Blackbeard
10
10
  end
11
11
  config.group_definitions[:hello].call.should == 'world'
12
12
  end
13
+ it "should add segments if any" do
14
+ config.define_group(:hello, ["world"]) do |user,controller|
15
+ "world"
16
+ end
17
+ Group.find(:hello).segments.should include("world")
18
+ end
13
19
  end
14
20
  end
15
21
  end
@@ -6,9 +6,9 @@ module Blackbeard
6
6
  let(:user) { double(:id => 1) }
7
7
  let(:context) { Context.new(pirate, user) }
8
8
  let(:uid) { context.unique_identifier }
9
- let(:total_metric) { Metric.new(:total, :things) }
10
- let(:unique_metric) { Metric.new(:unique, :things) }
11
- let(:test) { Test.new(:example_test) }
9
+ let(:total_metric) { Metric.create(:total, :things) }
10
+ let(:unique_metric) { Metric.create(:unique, :things) }
11
+ let(:test) { Test.create(:example_test) }
12
12
 
13
13
  describe "#add_total" do
14
14
  it "should call add on the total metric" do
@@ -76,23 +76,23 @@ module Blackbeard
76
76
  end
77
77
  end
78
78
 
79
- describe "#active?" do
80
- let(:inactive_test) { Blackbeard::Test.new(:inactive_test) }
81
- let(:active_test) { Blackbeard::Test.new(:active_test) }
79
+ describe "#feature_active?" do
80
+ let(:inactive_feature) { Blackbeard::Feature.create(:inactive_feature) }
81
+ let(:active_feature) { Blackbeard::Feature.create(:active_feature) }
82
82
 
83
83
  before :each do
84
- pirate.stub(:test).with(active_test.id).and_return(active_test)
85
- pirate.stub(:test).with(inactive_test.id).and_return(inactive_test)
84
+ pirate.stub(:feature).with(active_feature.id).and_return(active_feature)
85
+ pirate.stub(:feature).with(inactive_feature.id).and_return(inactive_feature)
86
86
  end
87
87
 
88
88
  it "should return true when active" do
89
- active_test.should_receive(:select_variation).and_return('active')
90
- context.active?(:active_test).should be_true
89
+ active_feature.should_receive(:active_for?).with(context).and_return(true)
90
+ context.feature_active?(:active_feature).should be_true
91
91
  end
92
92
 
93
93
  it "should return true when active" do
94
- inactive_test.should_receive(:select_variation).and_return('inactive')
95
- context.active?(:inactive_test).should be_false
94
+ inactive_feature.should_receive(:active_for?).with(context).and_return(false)
95
+ context.feature_active?(:inactive_feature).should be_false
96
96
  end
97
97
 
98
98
  end
@@ -0,0 +1,52 @@
1
+ require File.expand_path(File.dirname(__FILE__) + './../spec_helper')
2
+
3
+ require 'rack/test'
4
+ require 'blackbeard/dashboard'
5
+
6
+ module Blackbeard
7
+ describe Dashboard do
8
+ include Rack::Test::Methods
9
+
10
+ let(:app) { Dashboard }
11
+
12
+ describe "get /features" do
13
+ it "should list all the features" do
14
+ Feature.create("jostling")
15
+ get "/features"
16
+
17
+ last_response.should be_ok
18
+ last_response.body.should include('jostling')
19
+ end
20
+ end
21
+
22
+ describe "get /features/:id" do
23
+ it "should show a feature" do
24
+ feature = Feature.create("jostling")
25
+ get "/features/#{feature.id}"
26
+
27
+ last_response.should be_ok
28
+ last_response.body.should include("jostling")
29
+ end
30
+ end
31
+
32
+ describe "post /features/:id" do
33
+ it "should update the feature" do
34
+ feature = Feature.create("jostling")
35
+ post "/features/#{feature.id}", :name => 'hello'
36
+
37
+ last_response.should be_ok
38
+ feature.reload.name.should == 'hello'
39
+ end
40
+ end
41
+
42
+ describe "post /features/:id/groups/:group_id" do
43
+ it "should update the features segments" do
44
+ feature = Feature.create("jostling")
45
+ post "/features/#{feature.id}/groups/hello", :segments => ["world","goodbye"]
46
+
47
+ last_response.should be_ok
48
+ feature.reload.segments_for(:hello).should include("world", "goodbye")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -18,7 +18,7 @@ module Blackbeard
18
18
 
19
19
  describe "get /groups" do
20
20
  it "should list all the groups" do
21
- Group.new("jostling")
21
+ Group.create("jostling")
22
22
  get "/groups"
23
23
 
24
24
  last_response.should be_ok
@@ -28,7 +28,7 @@ module Blackbeard
28
28
 
29
29
  describe "get /groups/:id" do
30
30
  it "should show a metric" do
31
- group = Group.new("jostling")
31
+ group = Group.create("jostling")
32
32
  get "/groups/#{group.id}"
33
33
 
34
34
  last_response.should be_ok
@@ -38,11 +38,11 @@ module Blackbeard
38
38
 
39
39
  describe "post /groups/:id" do
40
40
  it "should update the group" do
41
- group = Group.new("jostling")
41
+ group = Group.create("jostling")
42
42
  post "/groups/#{group.id}", :name => 'hello'
43
43
 
44
44
  last_response.should be_ok
45
- Group.new("jostling").name.should == 'hello'
45
+ Group.find("jostling").name.should == 'hello'
46
46
  end
47
47
  end
48
48
 
@@ -11,7 +11,7 @@ module Blackbeard
11
11
 
12
12
  describe "get /metrics" do
13
13
  it "should list all the metrics" do
14
- Metric.new("total", "jostling")
14
+ Metric.create("total", "jostling")
15
15
  get "/metrics"
16
16
 
17
17
  last_response.should be_ok
@@ -21,7 +21,7 @@ module Blackbeard
21
21
 
22
22
  describe "get /metrics/:type/:type_id" do
23
23
  it "should show a metric" do
24
- metric = Metric.new("total", "jostling")
24
+ metric = Metric.create("total", "jostling")
25
25
  get "/metrics/#{metric.type}/#{metric.type_id}"
26
26
 
27
27
  last_response.should be_ok
@@ -31,18 +31,18 @@ module Blackbeard
31
31
 
32
32
  describe "post /metrics/:type/:type_id" do
33
33
  it "should update the metric" do
34
- metric = Metric.new("total", "jostling")
34
+ metric = Metric.create("total", "jostling")
35
35
  post "/metrics/#{metric.type}/#{metric.type_id}", :name => 'hello'
36
36
 
37
37
  last_response.should be_ok
38
- Metric.new(:total, "jostling").name.should == 'hello'
38
+ Metric.find(:total, "jostling").name.should == 'hello'
39
39
  end
40
40
  end
41
41
 
42
42
  describe "post /metrics/:type/:type_id/groups" do
43
43
  it "should add a group to the metric" do
44
- metric = Metric.new("total", "jostling")
45
- group = Group.new("admin")
44
+ metric = Metric.create("total", "jostling")
45
+ group = Group.create("admin")
46
46
  post "/metrics/#{metric.type}/#{metric.type_id}/groups", :group_id => group.id
47
47
 
48
48
  last_response.should be_redirect
@@ -11,7 +11,7 @@ module Blackbeard
11
11
 
12
12
  describe "get /tests" do
13
13
  it "should list all the test" do
14
- Test.new("jostling")
14
+ Test.create("jostling")
15
15
  get "/tests"
16
16
 
17
17
  last_response.should be_ok
@@ -21,7 +21,7 @@ module Blackbeard
21
21
 
22
22
  describe "get /tests/:id" do
23
23
  it "should show a test" do
24
- test = Test.new("jostling")
24
+ test = Test.create("jostling")
25
25
  get "/tests/#{test.id}"
26
26
 
27
27
  last_response.should be_ok
@@ -31,11 +31,11 @@ module Blackbeard
31
31
 
32
32
  describe "post /tests/:id" do
33
33
  it "should update the test" do
34
- test = Test.new("jostling")
34
+ test = Test.create("jostling")
35
35
  post "/tests/#{test.id}", :name => 'hello'
36
36
 
37
37
  last_response.should be_ok
38
- Test.new("jostling").name.should == 'hello'
38
+ test.reload.name.should == 'hello'
39
39
  end
40
40
  end
41
41
 
@@ -0,0 +1,147 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module Blackbeard
4
+ describe FeatureRollout do
5
+ let(:feature){ Blackbeard::Feature.create('example') }
6
+ let(:context) { double }
7
+ describe "rollout?" do
8
+ it "should be true if active_visitor?" do
9
+ feature.should_receive(:active_visitor?).with(context).and_return(true)
10
+ feature.stub(:active_user?).with(context).and_return(false)
11
+ feature.stub(:active_segment?).with(context).and_return(false)
12
+ feature.rollout?(context).should be_true
13
+ end
14
+
15
+ it "should be true if active_user?" do
16
+ feature.should_receive(:active_user?).with(context).and_return(true)
17
+ feature.stub(:active_visitor?).with(context).and_return(false)
18
+ feature.stub(:active_segment?).with(context).and_return(false)
19
+ feature.rollout?(context).should be_true
20
+ end
21
+
22
+ it "should be true if active_segment?" do
23
+ feature.should_receive(:active_segment?).with(context).and_return(true)
24
+ feature.stub(:active_visitor?).with(context).and_return(false)
25
+ feature.stub(:active_user?).with(context).and_return(false)
26
+ feature.rollout?(context).should be_true
27
+ end
28
+ end
29
+
30
+ describe "active_user?" do
31
+ context "with no logged in user" do
32
+ it "should be true if users_rate is 100" do
33
+ feature.users_rate = 100
34
+ context.stub(:user).and_return(nil)
35
+ feature.active_user?(context).should be_false
36
+ end
37
+ end
38
+
39
+ context "with logged in user" do
40
+ it "should be false if users_rate is 0" do
41
+ feature.users_rate = 0
42
+ context.stub(:user).and_return(double :id => 0)
43
+ feature.active_user?(context).should be_false
44
+ end
45
+
46
+ it "should be true if users_rate is 100" do
47
+ feature.users_rate = 100
48
+ context.stub(:user).and_return(double :id => 0)
49
+ feature.active_user?(context).should be_true
50
+ end
51
+
52
+ describe "by user_id modulus" do
53
+ [212,201,1,113,1008].each do |i|
54
+ it "should be true" do
55
+ feature.users_rate = 13
56
+ context.stub(:user).and_return(double :id => i)
57
+ context.stub(:user_id).and_return(i)
58
+ feature.active_user?(context).should be_true
59
+ end
60
+ end
61
+
62
+ [200,231,17,199,1018].each do |i|
63
+ it "should be true" do
64
+ feature.users_rate = 13
65
+ context.stub(:user).and_return(double :id => i)
66
+ context.stub(:user_id).and_return(i)
67
+ feature.active_user?(context).should be_false
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ describe "active_visitor?" do
75
+ it "should be false if rate is 0" do
76
+ feature.visitors_rate = 0
77
+ feature.active_visitor?(context).should be_false
78
+ end
79
+ it "should be true if rate is 100" do
80
+ feature.visitors_rate = 100
81
+ feature.active_visitor?(context).should be_true
82
+ end
83
+
84
+ describe "by visitor_id modulus" do
85
+ [212,201,1,113,1008].each do |i|
86
+ it "should be true" do
87
+ feature.visitors_rate = 13
88
+ context.stub(:visitor_id).and_return(i)
89
+ feature.active_visitor?(context).should be_true
90
+ end
91
+ end
92
+
93
+ [200,231,17,199,1018].each do |i|
94
+ it "should be true" do
95
+ feature.visitors_rate = 13
96
+ context.stub(:visitor_id).and_return(i)
97
+ feature.active_visitor?(context).should be_false
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ describe "active_segment?" do
104
+ it "should be false if there are no group segments" do
105
+ feature.active_segment?(context).should be_false
106
+ end
107
+ context "with group segments" do
108
+ before :each do
109
+ @group_a = Group.create(:a)
110
+ @group_b = Group.create(:b)
111
+ feature.set_segments_for(:a, ["on"])
112
+ feature.set_segments_for(:b, ["monkey", "chimp"])
113
+ feature.save
114
+ Group.stub(:find).with("a").and_return(@group_a)
115
+ Group.stub(:find).with("b").and_return(@group_b)
116
+ end
117
+
118
+ it "should be false if user is in no group segments" do
119
+ @group_a.should_receive(:segment_for).with(context).and_return(nil)
120
+ @group_b.should_receive(:segment_for).with(context).and_return("babboon")
121
+ feature.active_segment?(context).should be_false
122
+ end
123
+
124
+ it "should be true if user is in any group segment" do
125
+ @group_a.stub(:segment_for).with(context).and_return(nil)
126
+ @group_b.stub(:segment_for).with(context).and_return("monkey")
127
+ feature.active_segment?(context).should be_true
128
+ end
129
+
130
+ end
131
+ end
132
+
133
+ describe "id_to_int" do
134
+ it "should return an integer given an integer" do
135
+ feature.id_to_int(20).should eq(20)
136
+ end
137
+ it "should return an integer given a string" do
138
+ feature.id_to_int("happy").should be_a(Integer)
139
+ feature.id_to_int("monday").should be_a(Integer)
140
+ end
141
+ it "should return a different int for each string (last 8 chars)" do
142
+ feature.id_to_int("happy").should_not == feature.id_to_int("monday")
143
+ end
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,58 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module Blackbeard
4
+ describe Feature do
5
+ let(:feature){ Blackbeard::Feature.create('example') }
6
+
7
+ describe "segments_for and set_segments_for" do
8
+ it "should return an empty list if no segments" do
9
+ feature.segments_for(:nothing).should == []
10
+ end
11
+
12
+ it "should return the segments for the group" do
13
+ feature.set_segments_for(:hello, ["world", "goodbye"])
14
+ feature.set_segments_for(:foo, "bar")
15
+ feature.segments_for(:hello).should include("world","goodbye")
16
+ feature.segments_for(:hello).should_not include("bar")
17
+ feature.segments_for(:foo).should == ["bar"]
18
+ end
19
+ end
20
+
21
+ describe "#active_for?" do
22
+ let(:context){ double }
23
+
24
+ context "when status is nil" do
25
+ it "should be false" do
26
+ feature.active_for?(context).should be_false
27
+ end
28
+ end
29
+
30
+ context "when status is inactive" do
31
+ it "should be false" do
32
+ feature.status = :inactive
33
+ feature.active_for?(context).should be_false
34
+ end
35
+ end
36
+
37
+ context "when status is active" do
38
+ it "should be true" do
39
+ feature.status = :active
40
+ feature.active_for?(context).should be_true
41
+ end
42
+ end
43
+
44
+ context "when status is 'rollout'" do
45
+ it "should defer to rollout?" do
46
+ rollout_result = double
47
+ feature.status = :rollout
48
+ feature.should_receive(:rollout?).with(context).and_return(rollout_result)
49
+ feature.active_for?(context).should be(rollout_result)
50
+ end
51
+ end
52
+
53
+
54
+ end
55
+
56
+
57
+ end
58
+ end