blackbeard 0.0.3.1 → 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 (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