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.
- checksums.yaml +4 -4
- data/README.md +31 -20
- data/dashboard/public/stylesheets/application.css +4 -0
- data/dashboard/routes/features.rb +31 -0
- data/dashboard/routes/groups.rb +2 -2
- data/dashboard/routes/metrics.rb +6 -6
- data/dashboard/routes/tests.rb +2 -2
- data/dashboard/views/features/index.erb +21 -0
- data/dashboard/views/features/show.erb +163 -0
- data/dashboard/views/groups/show.erb +1 -1
- data/dashboard/views/layout.erb +2 -1
- data/lib/blackbeard.rb +18 -7
- data/lib/blackbeard/configuration.rb +3 -2
- data/lib/blackbeard/context.rb +11 -7
- data/lib/blackbeard/dashboard.rb +2 -0
- data/lib/blackbeard/errors.rb +4 -0
- data/lib/blackbeard/feature.rb +45 -0
- data/lib/blackbeard/feature_rollout.rb +47 -0
- data/lib/blackbeard/metric.rb +22 -4
- data/lib/blackbeard/pirate.rb +11 -5
- data/lib/blackbeard/redis_store.rb +4 -0
- data/lib/blackbeard/storable.rb +72 -6
- data/lib/blackbeard/storable_attributes.rb +50 -3
- data/lib/blackbeard/storable_has_many.rb +1 -0
- data/lib/blackbeard/storable_has_set.rb +1 -0
- data/lib/blackbeard/version.rb +1 -1
- data/spec/blackbeard_spec.rb +13 -0
- data/spec/configuration_spec.rb +6 -0
- data/spec/context_spec.rb +12 -12
- data/spec/dashboard/features_spec.rb +52 -0
- data/spec/dashboard/groups_spec.rb +4 -4
- data/spec/dashboard/metrics_spec.rb +6 -6
- data/spec/dashboard/tests_spec.rb +4 -4
- data/spec/feature_rollout_spec.rb +147 -0
- data/spec/feature_spec.rb +58 -0
- data/spec/group_spec.rb +1 -1
- data/spec/metric_data/total_spec.rb +1 -1
- data/spec/metric_data/unique_spec.rb +1 -1
- data/spec/metric_spec.rb +5 -5
- data/spec/pirate_spec.rb +16 -12
- data/spec/redis_store_spec.rb +6 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/storable_attributes_spec.rb +58 -6
- data/spec/storable_has_many_spec.rb +2 -2
- data/spec/storable_has_set_spec.rb +1 -1
- data/spec/storable_spec.rb +119 -23
- data/spec/test_spec.rb +1 -1
- metadata +13 -2
data/lib/blackbeard/version.rb
CHANGED
data/spec/blackbeard_spec.rb
CHANGED
@@ -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
|
data/spec/configuration_spec.rb
CHANGED
@@ -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
|
data/spec/context_spec.rb
CHANGED
@@ -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.
|
10
|
-
let(:unique_metric) { Metric.
|
11
|
-
let(:test) { 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 "#
|
80
|
-
let(:
|
81
|
-
let(:
|
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(:
|
85
|
-
pirate.stub(:
|
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
|
-
|
90
|
-
context.
|
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
|
-
|
95
|
-
context.
|
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.
|
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.
|
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.
|
41
|
+
group = Group.create("jostling")
|
42
42
|
post "/groups/#{group.id}", :name => 'hello'
|
43
43
|
|
44
44
|
last_response.should be_ok
|
45
|
-
Group.
|
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.
|
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.
|
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.
|
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.
|
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.
|
45
|
-
group = Group.
|
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.
|
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.
|
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.
|
34
|
+
test = Test.create("jostling")
|
35
35
|
post "/tests/#{test.id}", :name => 'hello'
|
36
36
|
|
37
37
|
last_response.should be_ok
|
38
|
-
|
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
|