abnormal 0.0.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.
- data/MIT-LICENSE +20 -0
- data/README.md +1 -0
- data/lib/abnormal.rb +95 -0
- data/lib/abnormal.rb~ +20 -0
- data/lib/abnormal/version.rb +4 -0
- data/lib/abnormal/version.rb~ +0 -0
- data/spec/abnormal_spec.rb +240 -0
- data/spec/abnormal_spec.rb~ +77 -0
- data/test/test_abnormal.rb +13 -0
- data/test/test_abnormal.rb~ +13 -0
- metadata +99 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Patrick McKenzie
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
A/B testing for Ruby
|
data/lib/abnormal.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'abnormal/version'
|
2
|
+
|
3
|
+
class Abnormal
|
4
|
+
def self.db; @@db; end
|
5
|
+
def self.db=(db)
|
6
|
+
@@db = db
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.ab_test(identity, test_name, alternatives, conversions)
|
10
|
+
conversions = [conversions] unless conversions.is_a? Array
|
11
|
+
|
12
|
+
test_id = Digest::MD5.hexdigest(test_name)
|
13
|
+
db['tests'].update(
|
14
|
+
{:name => test_name, :_id => test_id},
|
15
|
+
{
|
16
|
+
:$set => {
|
17
|
+
:alternatives => alternatives,
|
18
|
+
},
|
19
|
+
:$addToSet => {
|
20
|
+
:conversions => {:$each => conversions}
|
21
|
+
}
|
22
|
+
},
|
23
|
+
:upsert => true
|
24
|
+
)
|
25
|
+
|
26
|
+
conversions.each do |conversion|
|
27
|
+
db['participations'].update(
|
28
|
+
{
|
29
|
+
:participant => identity,
|
30
|
+
:test_id => test_id,
|
31
|
+
:conversion => conversion
|
32
|
+
},
|
33
|
+
{
|
34
|
+
:$set => {:conversions => 0}
|
35
|
+
},
|
36
|
+
:upsert => true
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
chose_alternative(identity, test_name, alternatives)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.convert!(identity, conversion)
|
44
|
+
db['participations'].update(
|
45
|
+
{
|
46
|
+
:participant => identity,
|
47
|
+
:conversion => conversion
|
48
|
+
},
|
49
|
+
{
|
50
|
+
:$inc => {:conversions => 1}
|
51
|
+
},
|
52
|
+
:multi => true
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.get_test(test_id)
|
57
|
+
db['tests'].find_one(:_id => test_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.tests
|
61
|
+
db['tests'].find.to_a
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.get_participation(id, test_name, conversion)
|
65
|
+
db['participations'].find_one(
|
66
|
+
:participant => id,
|
67
|
+
:test_id => Digest::MD5.hexdigest(test_name),
|
68
|
+
:conversion => conversion
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.participations
|
73
|
+
db['participations'].find.to_a
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.chose_alternative(identity, test_name, alternatives)
|
77
|
+
alternatives_array = normalize_alternatives(alternatives)
|
78
|
+
index = Digest::MD5.hexdigest(test_name + identity).to_i(16) % alternatives_array.size
|
79
|
+
alternatives_array[index]
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.normalize_alternatives(alternatives)
|
83
|
+
case alternatives
|
84
|
+
when Array
|
85
|
+
alternatives
|
86
|
+
when Hash
|
87
|
+
alternatives_array = []
|
88
|
+
idx = 0
|
89
|
+
alternatives.each{|k,v| alternatives_array.fill(k, idx, v); idx += v}
|
90
|
+
alternatives_array
|
91
|
+
when Range
|
92
|
+
alternatives.to_a
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/abnormal.rb~
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'abnormal/version'
|
2
|
+
|
3
|
+
class Abnormal
|
4
|
+
def self.db; @@db; end
|
5
|
+
def self.db=(db)
|
6
|
+
@@db = db
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.ab_test(identity, test_name, alternatives, conversions)
|
10
|
+
db['tests'].update({:name => test_name}, {:$set => {:alternatives => alternatives, :id => Digest::MD5.hexdigest(test_name)}}, :upsert => true)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.get_test(test_id)
|
14
|
+
db['tests'].find_one(:id => test_id)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.tests
|
18
|
+
db['tests'].find.to_a
|
19
|
+
end
|
20
|
+
end
|
File without changes
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
7
|
+
require 'abnormal'
|
8
|
+
|
9
|
+
Bundler.require(:development)
|
10
|
+
|
11
|
+
describe Abnormal do
|
12
|
+
before(:all) do
|
13
|
+
Abnormal.db = Mongo::Connection.new['abnormal_test']
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
Abnormal.db['tests'].drop
|
18
|
+
Abnormal.db['participations'].drop
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'ab_test' do
|
22
|
+
describe 'the first call for a new test' do
|
23
|
+
it "adds that test to the test table" do
|
24
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
25
|
+
Abnormal.get_test(Digest::MD5.hexdigest('test'))['name'].should == 'test'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "multiple calls for the same test" do
|
30
|
+
it "doesn't add a duplicate test" do
|
31
|
+
Abnormal.ab_test('id1', 'test', [1, 2], 'conversion')
|
32
|
+
Abnormal.ab_test('id2', 'test', [1, 2], 'conversion')
|
33
|
+
Abnormal.should have(1).tests
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "multiple calls for multiple tests" do
|
38
|
+
it "creates multiple tests" do
|
39
|
+
Abnormal.ab_test('id', 'test1', [1, 2], 'conversion')
|
40
|
+
Abnormal.ab_test('id', 'test2', [1, 2], 'conversion')
|
41
|
+
Abnormal.should have(2).tests
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "a call with one conversion" do
|
46
|
+
it "sets that conversion on the test" do
|
47
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
48
|
+
Abnormal.get_test(Digest::MD5.hexdigest('test'))['conversions'].to_set.should == %w[conversion].to_set
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "multiple calls with the same conversion" do
|
53
|
+
it "doesn't add a duplicate conversions" do
|
54
|
+
Abnormal.ab_test('id1', 'test', [1, 2], 'conversion')
|
55
|
+
Abnormal.ab_test('id2', 'test', [1, 2], 'conversion')
|
56
|
+
Abnormal.get_test(Digest::MD5.hexdigest('test'))['conversions'].should have(1).items
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "a call with multiple conversions" do
|
61
|
+
it "sets all of the conversions" do
|
62
|
+
Abnormal.ab_test('id', 'test', [1, 2], %w[conversion1 conversion2])
|
63
|
+
Abnormal.get_test(Digest::MD5.hexdigest('test'))['conversions'].to_set.should == %w[conversion1 conversion2].to_set
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "multiple calls with different conversions" do
|
68
|
+
it "sets all of the conversions" do
|
69
|
+
Abnormal.ab_test('id1', 'test', [1, 2], 'conversion1')
|
70
|
+
Abnormal.ab_test('id2', 'test', [1, 2], 'conversion2')
|
71
|
+
Abnormal.get_test(Digest::MD5.hexdigest('test'))['conversions'].to_set.should == %w[conversion1 conversion2].to_set
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "the first call by an identity that has not participated in the test with the conversion" do
|
76
|
+
it "records in the participation" do
|
77
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
78
|
+
Abnormal.get_participation('id', 'test', 'conversion').should be
|
79
|
+
end
|
80
|
+
|
81
|
+
it "has 0 conversions" do
|
82
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
83
|
+
Abnormal.get_participation('id', 'test', 'conversion')['conversions'].should == 0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "subsequent calls by an identity that has participated in the test with the conversion" do
|
88
|
+
it "records in the participation" do
|
89
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
90
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
91
|
+
Abnormal.should have(1).participations
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "a call with multiple conversions" do
|
96
|
+
it "makes multiple participations" do
|
97
|
+
Abnormal.ab_test('id1', 'test', [1, 2], %w[conversion1 conversion2])
|
98
|
+
Abnormal.should have(2).participations
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "different calls by different identities" do
|
103
|
+
it "makes different participations" do
|
104
|
+
Abnormal.ab_test('id1', 'test', [1, 2], 'conversion')
|
105
|
+
Abnormal.ab_test('id2', 'test', [1, 2], 'conversion')
|
106
|
+
Abnormal.should have(2).participations
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "different calls for different tests" do
|
111
|
+
it "makes different participations" do
|
112
|
+
Abnormal.ab_test('id', 'test1', [1, 2], 'conversion')
|
113
|
+
Abnormal.ab_test('id', 'test2', [1, 2], 'conversion')
|
114
|
+
Abnormal.should have(2).participations
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "different calls with different conversions" do
|
119
|
+
it "makes different participations" do
|
120
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion1')
|
121
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion2')
|
122
|
+
Abnormal.should have(2).participations
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it "returns the correct alternative" do
|
127
|
+
Abnormal.stub(:chose_alternative){ 3 }
|
128
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion').should == 3
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# I don't like any of these... is there a better way to do this?
|
133
|
+
describe "choose_alternative" do
|
134
|
+
it "returns the same result for the same user and test" do
|
135
|
+
alt1 = Abnormal.chose_alternative('id', 'test', [1, 2])
|
136
|
+
alt2 = Abnormal.chose_alternative('id', 'test', [1, 2])
|
137
|
+
alt1.should == alt2
|
138
|
+
end
|
139
|
+
|
140
|
+
it "returns different results for different users" do
|
141
|
+
alt1 = Abnormal.chose_alternative('id1', 'test', [1, 2])
|
142
|
+
alt2 = Abnormal.chose_alternative('id2', 'test', [1, 2])
|
143
|
+
alt1.should_not == alt2
|
144
|
+
end
|
145
|
+
|
146
|
+
it "returns different results for different tests" do
|
147
|
+
alt1 = Abnormal.chose_alternative('id', 'test_1', [1, 2])
|
148
|
+
alt2 = Abnormal.chose_alternative('id', 'test2', [1, 2])
|
149
|
+
alt1.should_not == alt2
|
150
|
+
end
|
151
|
+
|
152
|
+
it "returns all possible alternatives" do
|
153
|
+
alternatives = [1, 2]
|
154
|
+
actual_alternatives = (1..10).map{|i| Abnormal.chose_alternative("id#{i}", 'test', alternatives) }
|
155
|
+
|
156
|
+
actual_alternatives.to_set.should == alternatives.to_set
|
157
|
+
end
|
158
|
+
|
159
|
+
it "uses normalize_alternatives" do
|
160
|
+
Abnormal.stub(:normalize_alternatives){ [3] }
|
161
|
+
Abnormal.chose_alternative('id', 'test1', [1, 2]).should == 3
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "convert!" do
|
166
|
+
describe "with an identity/conversion pair that does not exist" do
|
167
|
+
it "does nothing" do
|
168
|
+
Abnormal.convert!('id', 'conversion')
|
169
|
+
Abnormal.should have(0).participations
|
170
|
+
end
|
171
|
+
|
172
|
+
it "does not apply to future participations" do
|
173
|
+
identity = 'id'
|
174
|
+
test_name = 'test'
|
175
|
+
conversion = 'conversion'
|
176
|
+
|
177
|
+
Abnormal.convert!(identity, conversion)
|
178
|
+
Abnormal.ab_test(identity, test_name, [1, 2], conversion)
|
179
|
+
Abnormal.get_participation(identity, test_name, conversion)['conversions'].should == 0
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "with an identity/conversion pair that does exist" do
|
184
|
+
it "records the conversions" do
|
185
|
+
identity = 'id'
|
186
|
+
test_name = 'test'
|
187
|
+
conversion = 'conversion'
|
188
|
+
|
189
|
+
Abnormal.ab_test(identity, test_name, [1, 2], conversion)
|
190
|
+
Abnormal.convert!(identity, conversion)
|
191
|
+
Abnormal.get_participation(identity, test_name, conversion)['conversions'].should == 1
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "an identity participating in multiple tests with the conversions" do
|
196
|
+
it "records the conversion for the each test" do
|
197
|
+
identity = 'id'
|
198
|
+
conversion = 'conversion'
|
199
|
+
|
200
|
+
Abnormal.ab_test(identity, 'test1', [1, 2], conversion)
|
201
|
+
Abnormal.ab_test(identity, 'test2', [1, 2], conversion)
|
202
|
+
Abnormal.convert!(identity, conversion)
|
203
|
+
Abnormal.get_participation(identity, 'test1', conversion)['conversions'].should == 1
|
204
|
+
Abnormal.get_participation(identity, 'test2', conversion)['conversions'].should == 1
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe "an identity participating in multiple tests, not all of which have the conversion" do
|
209
|
+
it "only records a conversion for the test listening to that conversion" do
|
210
|
+
identity = 'id'
|
211
|
+
|
212
|
+
Abnormal.ab_test(identity, 'test1', [1, 2], 'conversion1')
|
213
|
+
Abnormal.ab_test(identity, 'test2', [1, 2], 'conversion2')
|
214
|
+
Abnormal.convert!(identity, 'conversion1')
|
215
|
+
Abnormal.get_participation(identity, 'test1', 'conversion1')['conversions'].should == 1
|
216
|
+
Abnormal.get_participation(identity, 'test2', 'conversion2')['conversions'].should == 0
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe "normalize_alternatives" do
|
222
|
+
describe "given an array" do
|
223
|
+
it "returns the array" do
|
224
|
+
Abnormal.normalize_alternatives([1, 2, 7]).should == [1, 2, 7]
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe "given a hash" do
|
229
|
+
it "expands the hash into an array" do
|
230
|
+
Abnormal.normalize_alternatives({:a => 1, :b => 9}).should == [:a, :b, :b, :b, :b, :b, :b, :b, :b, :b]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
describe "given a range" do
|
235
|
+
it "converts the range into a hash" do
|
236
|
+
Abnormal.normalize_alternatives(1..10).should == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
7
|
+
require 'abnormal'
|
8
|
+
|
9
|
+
Bundler.require(:development)
|
10
|
+
|
11
|
+
describe Abnormal do
|
12
|
+
before(:all) do
|
13
|
+
Abnormal.db = Mongo::Connection.new['abnormal_test']
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
Abnormal.db['tests'].drop
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'ab_test' do
|
21
|
+
describe 'the first call for a new test' do
|
22
|
+
it "adds that test to the test table" do
|
23
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
24
|
+
Abnormal.get_test(Digest::MD5.hexdigest('test'))['name'].should == 'test'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "multiple calls for the same test" do
|
29
|
+
it "doesn't add a duplicate test" do
|
30
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
31
|
+
Abnormal.ab_test('id2', 'test', [1, 2], 'conversion')
|
32
|
+
Abnormal.should have(1).tests
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "multiple calls for multiple tests" do
|
37
|
+
it "should create multiple tests" do
|
38
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion')
|
39
|
+
Abnormal.ab_test('id', 'test2', [1, 2], 'conversion')
|
40
|
+
Abnormal.should have(2).tests
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "returns the correct alternative" do
|
45
|
+
Abnormal.stub(:chose_alternative){ 3 }
|
46
|
+
Abnormal.ab_test('id', 'test', [1, 2], 'conversion').should == 3
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# I don't like any of these... is there a better way to do this?
|
51
|
+
describe "choose_alternative" do
|
52
|
+
it "returns the same result for the same user and test" do
|
53
|
+
alt1 = Abnormal.chose_alternative('id', 'test', [1, 2])
|
54
|
+
alt2 = Abnormal.chose_alternative('id', 'test', [1, 2])
|
55
|
+
alt1.should == alt2
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns different results for different users" do
|
59
|
+
alt1 = Abnormal.chose_alternative('id1', 'test', [1, 2])
|
60
|
+
alt2 = Abnormal.chose_alternative('id2', 'test', [1, 2])
|
61
|
+
alt1.should_not == alt2
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns different results for different tests" do
|
65
|
+
alt1 = Abnormal.chose_alternative('id', 'test_1', [1, 2])
|
66
|
+
alt2 = Abnormal.chose_alternative('id', 'test2', [1, 2])
|
67
|
+
alt1.should_not == alt2
|
68
|
+
end
|
69
|
+
|
70
|
+
it "returns all possible alternatives" do
|
71
|
+
alternatives = [1, 2]
|
72
|
+
actual_alternatives = (1..10).map{|i| Abnormal.chose_alternative("id#{i}", 'test', alternatives) }
|
73
|
+
|
74
|
+
actual_alternatives.to_set.should == alternatives.to_set
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: abnormal
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Fairley
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-28 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: mongo
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: rspec
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
description:
|
50
|
+
email:
|
51
|
+
- michaelfairley@gmail.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- lib/abnormal/version.rb
|
60
|
+
- lib/abnormal/version.rb~
|
61
|
+
- lib/abnormal.rb
|
62
|
+
- lib/abnormal.rb~
|
63
|
+
- test/test_abnormal.rb
|
64
|
+
- test/test_abnormal.rb~
|
65
|
+
- spec/abnormal_spec.rb
|
66
|
+
- spec/abnormal_spec.rb~
|
67
|
+
- MIT-LICENSE
|
68
|
+
- README.md
|
69
|
+
has_rdoc: true
|
70
|
+
homepage: http://github.com/michaelfairley/abnormal
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: "0"
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
requirements: []
|
91
|
+
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 1.6.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 3
|
96
|
+
summary: Ruby A/B testing
|
97
|
+
test_files:
|
98
|
+
- test/test_abnormal.rb
|
99
|
+
- spec/abnormal_spec.rb
|