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