vinted-ab 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +11 -0
- data/README.md +70 -66
- data/ab.gemspec +1 -0
- data/config.json +74 -0
- data/lib/ab/assigned_test.rb +4 -4
- data/lib/ab/null_test.rb +0 -1
- data/lib/ab/test.rb +14 -3
- data/lib/ab/tests.rb +13 -14
- data/lib/ab/version.rb +1 -1
- data/spec/assigned_test_spec.rb +21 -27
- data/spec/examples/all_buckets/input.json +22 -0
- data/spec/examples/all_buckets/output.json +7 -0
- data/spec/examples/already_finished/input.json +20 -0
- data/spec/examples/already_finished/output.json +6 -0
- data/spec/examples/big_weights/input.json +22 -0
- data/spec/examples/big_weights/output.json +7 -0
- data/spec/examples/explicit_times/input.json +20 -0
- data/spec/examples/explicit_times/output.json +6 -0
- data/spec/examples/few_buckets/input.json +22 -0
- data/spec/examples/few_buckets/output.json +8 -0
- data/spec/examples/has_not_started/input.json +19 -0
- data/spec/examples/has_not_started/output.json +6 -0
- data/spec/examples/multiple_tests/input.json +30 -0
- data/spec/examples/multiple_tests/output.json +6 -0
- data/spec/examples/multiple_variants/input.json +30 -0
- data/spec/examples/multiple_variants/output.json +9 -0
- data/spec/examples/no_buckets/input.json +17 -0
- data/spec/examples/no_buckets/output.json +6 -0
- data/spec/examples/no_variants/input.json +13 -0
- data/spec/examples/no_variants/output.json +6 -0
- data/spec/examples/zero_buckets/input.json +18 -0
- data/spec/examples/zero_buckets/output.json +6 -0
- data/spec/examples/zero_weight/input.json +22 -0
- data/spec/examples/zero_weight/output.json +6 -0
- data/spec/integration_spec.rb +28 -0
- data/spec/null_test_spec.rb +4 -2
- data/spec/test_spec.rb +19 -2
- data/spec/tests_spec.rb +47 -21
- metadata +49 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73b2901ba92e371c5f79d864db755df119cadd6a
|
4
|
+
data.tar.gz: 14bfef36209d2fd22a70ec59d7741812a18c67a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ae98d8cd54d30db32c7a54aee1377a9b782f4ca9d38c7e5d00cfea6f63f7e616faba5309601698eea4045ba6497612f12d4a47ff3a9f90cdfefbe45d1a8df62
|
7
|
+
data.tar.gz: 7c709e4b04db27be43ddc6b03fa37aca36d6e31dab8625abe153b2b68305c52ec8a8bbb138384295603ecccafec54eef421b8c65ee10c5937576761cc7fe2e61
|
data/.rubocop.yml
ADDED
data/README.md
CHANGED
@@ -1,8 +1,48 @@
|
|
1
1
|
# ab
|
2
2
|
|
3
|
+
[![Code Climate](https://codeclimate.com/github/vinted/ab.png)](https://codeclimate.com/github/vinted/ab)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/vinted-ab.png)](http://badge.fury.io/rb/vinted-ab)
|
5
|
+
[![Dependency Status](https://gemnasium.com/vinted/ab.png)](https://gemnasium.com/vinted/ab)
|
6
|
+
|
7
|
+
vinted-ab is used to determine whether an identifier belongs to a particular ab test and which variant of that ab test. Identifiers will usually represent users, but other scenario are possible. There are two parts to that: [Configuration](#configuration) and [Algorithm](#algorithm).
|
8
|
+
|
9
|
+
High-level description: Identifiers are divided into some number of buckets, using hashing. Before a test is started, buckets are chosen for that test. That gives the ability to pick the needed level of isolation. Each test also has a seed, which is used to randomise how users are divided among test variants.
|
10
|
+
|
11
|
+
![users](https://cloud.githubusercontent.com/assets/54526/2971326/0535267a-db69-11e3-9878-e2b6a5d5505d.png)
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
ab = Ab::Tests.new(configuration, identifier)
|
17
|
+
|
18
|
+
# defining callbacks
|
19
|
+
Ab::Tests.before_picking_variant { |test| puts "picking variant for #{test}" }
|
20
|
+
Ab::Tests.after_picking_variant { |test, variant| puts "#{variant_name}" }
|
21
|
+
|
22
|
+
# ab.test never returns nil
|
23
|
+
# but if you don't belong to any of the buckets, variant will be nil
|
24
|
+
case ab.test.variant
|
25
|
+
when 'red_button'
|
26
|
+
red_button
|
27
|
+
when 'green_button'
|
28
|
+
green_button
|
29
|
+
else
|
30
|
+
blue_button
|
31
|
+
end
|
32
|
+
|
33
|
+
# calls #variant underneath, results of that call are cached
|
34
|
+
puts 'red button' if ab.test.red_button?
|
35
|
+
```
|
36
|
+
|
3
37
|
## Configuration
|
4
38
|
|
5
|
-
|
39
|
+
Configuration is expected to be in JSON, for which you can find the Schema [here](https://github.com/vinted/ab/blob/master/config.json). The provided schema is compatible with JSON Schema Draft 3. If you'd like to validate your JSON against this schema, in Ruby, you can do it using `json-schema` gem:
|
40
|
+
|
41
|
+
```
|
42
|
+
JSON::Validator.validate('/path/to/schema/config.json', json, version: :draft3)
|
43
|
+
```
|
44
|
+
|
45
|
+
An example config:
|
6
46
|
|
7
47
|
```json
|
8
48
|
{
|
@@ -15,13 +55,7 @@ For this lib to work, it requires a configuration json, which looks like this:
|
|
15
55
|
"start_at": "2014-05-21T11:06:30+03:00",
|
16
56
|
"end_at": "2014-05-28T11:06:30+03:00",
|
17
57
|
"seed": "aaaa1111",
|
18
|
-
"buckets": [
|
19
|
-
1,
|
20
|
-
2,
|
21
|
-
3,
|
22
|
-
4,
|
23
|
-
5
|
24
|
-
],
|
58
|
+
"buckets": [1, 2, 3, 4, 5],
|
25
59
|
"variants": [
|
26
60
|
{
|
27
61
|
"name": "green_button",
|
@@ -37,71 +71,41 @@ For this lib to work, it requires a configuration json, which looks like this:
|
|
37
71
|
}
|
38
72
|
]
|
39
73
|
},
|
40
|
-
{
|
41
|
-
"id": 44,
|
42
|
-
"name": "red_shirts",
|
43
|
-
"start_at": "2014-05-24T11:09:30+03:00",
|
44
|
-
"end_at": "2099-05-24T11:09:30+03:00",
|
45
|
-
"seed": "bbbb4444",
|
46
|
-
"buckets": [
|
47
|
-
4,
|
48
|
-
5,
|
49
|
-
6,
|
50
|
-
7
|
51
|
-
],
|
52
|
-
"variants": [
|
53
|
-
{
|
54
|
-
"name": "red_shirt",
|
55
|
-
"chance_weight": 1
|
56
|
-
},
|
57
|
-
{
|
58
|
-
"name": "control",
|
59
|
-
"chance_weight": 1
|
60
|
-
}
|
61
|
-
]
|
62
|
-
},
|
63
|
-
{
|
64
|
-
"id": 47,
|
65
|
-
"name": "feed",
|
66
|
-
"start_at": "1999-03-31T00:00:00+03:00",
|
67
|
-
"end_at": "2099-03-31T00:00:00+03:00",
|
68
|
-
"seed": "cccc8888",
|
69
|
-
"buckets": "all",
|
70
|
-
"variants": [
|
71
|
-
{
|
72
|
-
"name": "enabled",
|
73
|
-
"chance_weight": 1
|
74
|
-
}
|
75
|
-
]
|
76
|
-
},
|
77
74
|
]
|
78
75
|
}
|
79
76
|
```
|
80
77
|
|
81
|
-
|
78
|
+
Short explanation for a couple of config parameters:
|
82
79
|
|
83
|
-
|
84
|
-
configuration = retrieve_from_svc_abs
|
85
|
-
ab = Ab::Tests.new(configuration, user_id)
|
80
|
+
`salt`: used to salt every identifier, before determining to which bucket that identifier belongs.
|
86
81
|
|
87
|
-
|
88
|
-
Ab::Tests.before_picking_variant { |test| puts 'magic' }
|
89
|
-
Ab::Tests.after_picking_variant { |test, variant| puts "#{variant_name}" }
|
82
|
+
`bucket_count`: the total number of buckets.
|
90
83
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
84
|
+
`all_buckets`: optional boolean which tells that all buckets are used in this test. Checking `buckets` is not required in that case.
|
85
|
+
|
86
|
+
`ab_tests.start_at`: the start date time for ab test, in ISO 8601 format. Is not required, in which case, test has already started.
|
87
|
+
|
88
|
+
`ab_tests.end_at`: the end date time for ab test, in ISO 8601 format. Is not required, in which case, there's no predetermined date when test will end.
|
89
|
+
|
90
|
+
`ab_tests.buckets`: which buckets should be used for this ab test, represented as bucket ids. If the total number of buckets is 1000, values of this arrays are expected to be in 1..1000 range.
|
91
|
+
|
92
|
+
`ab_tests.variants`: tests can have multiple variants, each with a name and a weight.
|
93
|
+
|
94
|
+
More examples can be found in [spec/examples](https://github.com/vinted/ab/tree/master/spec/examples). Those examples are part of the test suite, which is run using [this code](https://github.com/vinted/ab/blob/master/spec/integration_spec.rb). We strongly recommend using those examples if you're reimplementing this library in another language.
|
95
|
+
|
96
|
+
## Algorithm
|
97
|
+
|
98
|
+
Most of the logic, is in `AssignedTest` class, which can be used as an [example implementation](https://github.com/vinted/ab/blob/master/lib/ab/assigned_test.rb).
|
99
|
+
|
100
|
+
Here's some procedural pseudo code to serve as a reference:
|
101
|
+
|
102
|
+
```pseudo
|
103
|
+
bucket_id = SHA256(salt + identifier.to_string).to_integer % bucket_count
|
101
104
|
|
102
|
-
|
103
|
-
|
104
|
-
puts 'magic' if ab.experiment.red_button?
|
105
|
+
return if not (test.all_buckets? or test.buckets.include?(bucket_id))
|
106
|
+
return if not DateTime.now.between?(test.start_at, test.end_at)
|
105
107
|
|
106
|
-
|
108
|
+
chance_weight_sum = chance_weight_sum > 0 ? test.chance_weight_sum : 1
|
109
|
+
weight_id = SHA256(test.seed + identifier.to_string).to_integer % chance_weight_sum
|
110
|
+
test.variants.find { |variant| variant.accumulated_chance_weight > weight_id }
|
107
111
|
```
|
data/ab.gemspec
CHANGED
@@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
24
24
|
spec.add_development_dependency 'rake', '~> 10.1', '>= 10.1.0'
|
25
25
|
spec.add_development_dependency 'rspec', '~> 2.14', '>= 2.14.0'
|
26
|
+
spec.add_development_dependency 'json-schema', '~> 2.2.2', '>= 2.2.2'
|
26
27
|
end
|
data/config.json
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
{
|
2
|
+
"type": "object",
|
3
|
+
"required": true,
|
4
|
+
"properties": {
|
5
|
+
"bucket_count": {
|
6
|
+
"type": "number",
|
7
|
+
"required": true
|
8
|
+
},
|
9
|
+
"salt": {
|
10
|
+
"type": "string",
|
11
|
+
"required": true
|
12
|
+
},
|
13
|
+
"ab_tests": {
|
14
|
+
"type": "array",
|
15
|
+
"required": false,
|
16
|
+
"items": [
|
17
|
+
{
|
18
|
+
"type": "object",
|
19
|
+
"properties": {
|
20
|
+
"all_buckets": {
|
21
|
+
"type": "boolean",
|
22
|
+
"required": false
|
23
|
+
},
|
24
|
+
"buckets": {
|
25
|
+
"type": "array",
|
26
|
+
"required": false,
|
27
|
+
"items": {
|
28
|
+
"type": "number",
|
29
|
+
"required": false
|
30
|
+
}
|
31
|
+
},
|
32
|
+
"start_at": {
|
33
|
+
"type": "string",
|
34
|
+
"required": false
|
35
|
+
},
|
36
|
+
"end_at": {
|
37
|
+
"type": "string",
|
38
|
+
"required": false
|
39
|
+
},
|
40
|
+
"id": {
|
41
|
+
"type": "number",
|
42
|
+
"required": true
|
43
|
+
},
|
44
|
+
"name": {
|
45
|
+
"type": "string",
|
46
|
+
"required": true
|
47
|
+
},
|
48
|
+
"seed": {
|
49
|
+
"type": "string",
|
50
|
+
"required": true
|
51
|
+
},
|
52
|
+
"variants": {
|
53
|
+
"type": "array",
|
54
|
+
"required": true,
|
55
|
+
"items": {
|
56
|
+
"type": "object",
|
57
|
+
"properties": {
|
58
|
+
"chance_weight": {
|
59
|
+
"type": "number",
|
60
|
+
"required": true
|
61
|
+
},
|
62
|
+
"name": {
|
63
|
+
"type": "string",
|
64
|
+
"required": true
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
]
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
data/lib/ab/assigned_test.rb
CHANGED
@@ -27,16 +27,16 @@ module Ab
|
|
27
27
|
private
|
28
28
|
|
29
29
|
def part_of_test?
|
30
|
-
@test.
|
30
|
+
@test.all_buckets? ||
|
31
|
+
@test.buckets && @test.buckets.include?(bucket_id)
|
31
32
|
end
|
32
33
|
|
33
34
|
def bucket_id
|
34
|
-
@bucket_id ||= digest(@test.salt
|
35
|
+
@bucket_id ||= digest("#{@test.salt}#{@id}") % @test.bucket_count
|
35
36
|
end
|
36
37
|
|
37
38
|
def running?
|
38
|
-
now
|
39
|
-
now.between?(@test.start_at, @test.end_at)
|
39
|
+
DateTime.now.between?(@test.start_at, @test.end_at)
|
40
40
|
end
|
41
41
|
|
42
42
|
def weight_id
|
data/lib/ab/null_test.rb
CHANGED
data/lib/ab/test.rb
CHANGED
@@ -4,6 +4,10 @@ module Ab
|
|
4
4
|
hash['buckets']
|
5
5
|
end
|
6
6
|
|
7
|
+
def all_buckets?
|
8
|
+
hash['all_buckets']
|
9
|
+
end
|
10
|
+
|
7
11
|
def name
|
8
12
|
hash['name']
|
9
13
|
end
|
@@ -12,7 +16,7 @@ module Ab
|
|
12
16
|
@variants ||= begin
|
13
17
|
accumulated = 0
|
14
18
|
hash['variants'].map do |variant_hash|
|
15
|
-
Variant.new(variant_hash, accumulated += variant_hash['chance_weight']
|
19
|
+
Variant.new(variant_hash, accumulated += variant_hash['chance_weight'])
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -22,15 +26,22 @@ module Ab
|
|
22
26
|
end
|
23
27
|
|
24
28
|
def start_at
|
25
|
-
@start_at ||=
|
29
|
+
@start_at ||= parse_time('start_at', 0)
|
26
30
|
end
|
27
31
|
|
28
32
|
def end_at
|
29
|
-
@end_at ||=
|
33
|
+
@end_at ||= parse_time('end_at', 3000)
|
30
34
|
end
|
31
35
|
|
32
36
|
def weight_sum
|
33
37
|
variants.map(&:chance_weight).inject(:+)
|
34
38
|
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def parse_time(name, default)
|
43
|
+
value = hash[name]
|
44
|
+
value.nil? ? DateTime.new(default) : DateTime.parse(value)
|
45
|
+
end
|
35
46
|
end
|
36
47
|
end
|
data/lib/ab/tests.rb
CHANGED
@@ -10,24 +10,23 @@ module Ab
|
|
10
10
|
Ab::Tests.run_hook :after_picking_variant, test, variant
|
11
11
|
end
|
12
12
|
|
13
|
-
def initialize(
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
13
|
+
def initialize(json, id)
|
14
|
+
config = json.is_a?(Hash) ? json : JSON.parse(json)
|
15
|
+
|
16
|
+
salt = config['salt']
|
17
|
+
bucket_count = config['bucket_count']
|
18
|
+
|
19
|
+
tests = (config['ab_tests'] || []).map { |test| Test.new(test, salt, bucket_count) }
|
20
|
+
|
21
|
+
@assigned_tests = tests.map do |test|
|
22
|
+
assigned_test = AssignedTest.new(test, id)
|
23
|
+
define_singleton_method(test.name) { assigned_test }
|
24
|
+
[test.name, assigned_test]
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def all
|
27
|
-
|
28
|
-
[name, send(name).variant]
|
29
|
-
end
|
30
|
-
Hash[result]
|
29
|
+
Hash[@assigned_tests.map { |name, assigned_test| [name, assigned_test.variant] }]
|
31
30
|
end
|
32
31
|
|
33
32
|
def method_missing(meth, *args, &block)
|
data/lib/ab/version.rb
CHANGED
data/spec/assigned_test_spec.rb
CHANGED
@@ -1,52 +1,46 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
2
3
|
|
3
4
|
module Ab
|
4
5
|
describe AssignedTest do
|
5
6
|
let(:assigned_test) { AssignedTest.new(test, id) }
|
6
|
-
let(:test)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
'end_at' => end_at,
|
12
|
-
'buckets' => buckets,
|
13
|
-
'seed' => seed,
|
14
|
-
'variants' => variants
|
15
|
-
}
|
16
|
-
}
|
7
|
+
let(:test) do
|
8
|
+
OpenStruct.new(name: name, start_at: start_at, end_at: end_at,
|
9
|
+
buckets: buckets, seed: 'cccc8888', variants: variants,
|
10
|
+
weight_sum: 2, salt: 'e131bfcfcab06c65d633d0266c7e62a4918', bucket_count: 1000)
|
11
|
+
end
|
17
12
|
let(:id) { 1 }
|
18
13
|
let(:name) { 'feed' }
|
19
|
-
let(:start_at) { DateTime.now.prev_year
|
20
|
-
let(:end_at) { DateTime.now.next_year
|
21
|
-
let(:
|
22
|
-
let(:buckets) { 'all' }
|
14
|
+
let(:start_at) { DateTime.now.prev_year }
|
15
|
+
let(:end_at) { DateTime.now.next_year }
|
16
|
+
let(:buckets) { 1..1000 }
|
23
17
|
let(:thousand_variants) { 1.upto(1000).map { |i| AssignedTest.new(test, i).variant } }
|
24
18
|
|
25
19
|
describe '#variant' do
|
26
20
|
subject { assigned_test.variant }
|
27
21
|
|
28
22
|
context 'single variant' do
|
29
|
-
let(:variants) { [
|
23
|
+
let(:variants) { [OpenStruct.new(name: 'enabled', accumulated_chance_weight: accumulated_chance_weight)] }
|
30
24
|
|
31
25
|
context 'that is turned off' do
|
32
|
-
let(:
|
26
|
+
let(:accumulated_chance_weight) { 0 }
|
33
27
|
it { should be_nil }
|
34
28
|
end
|
35
29
|
|
36
|
-
context 'that is turned on with
|
37
|
-
let(:
|
30
|
+
context 'that is turned on with 2' do
|
31
|
+
let(:accumulated_chance_weight) { 2 }
|
38
32
|
it { should == 'enabled' }
|
39
33
|
end
|
40
34
|
|
41
35
|
context 'that is turned on with 111' do
|
42
|
-
let(:
|
36
|
+
let(:accumulated_chance_weight) { 111 }
|
43
37
|
it { should == 'enabled' }
|
44
38
|
end
|
45
39
|
end
|
46
40
|
|
47
41
|
context 'single variant with buckets' do
|
48
42
|
let(:buckets) { (1..100) }
|
49
|
-
let(:variants) { [
|
43
|
+
let(:variants) { [OpenStruct.new(name: 'enabled', accumulated_chance_weight: 2)] }
|
50
44
|
|
51
45
|
specify 'half the ids fall under one, other under other' do
|
52
46
|
enabled = thousand_variants.select { |variant| variant == 'enabled' }
|
@@ -57,25 +51,25 @@ module Ab
|
|
57
51
|
context 'test that has not started yet' do
|
58
52
|
let(:start_at) { DateTime.now.next_year.to_s }
|
59
53
|
let(:buckets) { [1, 2, 3] }
|
60
|
-
let(:variants) { [
|
54
|
+
let(:variants) { [OpenStruct.new(name: 'enabled', accumulated_chance_weight: 2)] }
|
61
55
|
it { should be_nil }
|
62
56
|
end
|
63
57
|
|
64
58
|
context 'test that has already ended' do
|
65
59
|
let(:end_at) { DateTime.now.prev_year.to_s }
|
66
60
|
let(:buckets) { [1, 2, 3] }
|
67
|
-
let(:variants) { [
|
61
|
+
let(:variants) { [OpenStruct.new(name: 'enabled', accumulated_chance_weight: 2)] }
|
68
62
|
it { should be_nil }
|
69
63
|
end
|
70
64
|
|
71
65
|
context 'two variants' do
|
72
66
|
let(:name) { 'button' }
|
73
|
-
let(:variants)
|
67
|
+
let(:variants) do
|
74
68
|
[
|
75
|
-
|
76
|
-
|
69
|
+
OpenStruct.new(name: 'red', accumulated_chance_weight: 1),
|
70
|
+
OpenStruct.new(name: 'blue', accumulated_chance_weight: 2)
|
77
71
|
]
|
78
|
-
|
72
|
+
end
|
79
73
|
|
80
74
|
specify 'half the ids fall under one, other under other' do
|
81
75
|
reds = thousand_variants.select { |variant| variant == 'red' }
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"salt": "aaaa1111",
|
3
|
+
"bucket_count": 1000,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 42,
|
7
|
+
"name": "all_buckets",
|
8
|
+
"seed": "bbbb2222",
|
9
|
+
"all_buckets": true,
|
10
|
+
"variants": [
|
11
|
+
{
|
12
|
+
"name": "green",
|
13
|
+
"chance_weight": 1
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"name": "red",
|
17
|
+
"chance_weight": 1
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
21
|
+
]
|
22
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
{
|
2
|
+
"test": "all_buckets",
|
3
|
+
"variants": {
|
4
|
+
"green": [1, 2, 3, 4, 6, 10, 13, 16, 18, 19, 21, 22, 23, 25, 26, 30, 33, 34, 38, 41, 42, 43, 44, 47, 48, 51, 52, 53, 55, 57, 58, 59, 61, 65, 66, 72, 73, 74, 75, 79, 81, 83, 88, 89, 91, 92, 97, 98],
|
5
|
+
"red": [5, 7, 8, 9, 11, 12, 14, 15, 17, 20, 24, 27, 28, 29, 31, 32, 35, 36, 37, 39, 40, 45, 46, 49, 50, 54, 56, 60, 62, 63, 64, 67, 68, 69, 70, 71, 76, 77, 78, 80, 82, 84, 85, 86, 87, 90, 93, 94, 95, 96, 99, 100]
|
6
|
+
}
|
7
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"salt": "9a38d3194fc4c1b43c500c551d3c80a0",
|
3
|
+
"bucket_count": 4,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 2,
|
7
|
+
"name": "already_finished",
|
8
|
+
"seed": "792e3b32a6dd59d3f300b2ac8b4caa09",
|
9
|
+
"start_at": "1605-11-05T00:00:00+01:00",
|
10
|
+
"end_at": "2014-02-01T02:15:00+03:00",
|
11
|
+
"all_buckets": true,
|
12
|
+
"variants": [
|
13
|
+
{
|
14
|
+
"name": "green",
|
15
|
+
"chance_weight": 1
|
16
|
+
}
|
17
|
+
]
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "already_finished",
|
3
|
+
"variants": {
|
4
|
+
"": [291605, 304029, 260623, 330108, 579320, 530360, 372392, 700813, 244200, 541818, 432400, 943631, 437817, 226191, 569129, 436127, 490360, 929904, 603358, 437705, 878118, 807422, 972099, 404677, 67694, 384045, 48608, 518304, 830501, 902758, 915681, 211550, 748688, 606226, 309490, 12909, 762, 560680, 945245, 795680, 358793, 850366, 278017, 765934, 378015, 156624, 878811, 814470, 830857, 712438]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"salt": "1fd3eca97a86c4458bf3b6308ed4d990",
|
3
|
+
"bucket_count": 1000000,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 11,
|
7
|
+
"name": "big_weights",
|
8
|
+
"seed": "5aae80d25c5ba1931cbd1bfabada7628",
|
9
|
+
"all_buckets": true,
|
10
|
+
"variants": [
|
11
|
+
{
|
12
|
+
"name": "green",
|
13
|
+
"chance_weight": 1111111
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"name": "red",
|
17
|
+
"chance_weight": 2222222
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
21
|
+
]
|
22
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
{
|
2
|
+
"test": "big_weights",
|
3
|
+
"variants": {
|
4
|
+
"green": [4, 6, 8, 10, 20, 21, 26, 27, 33, 35, 37, 38, 41, 43, 45, 48, 54, 63, 68, 72, 74, 75, 77, 80, 82, 84, 89, 92, 94, 96, 97, 98, 100000, 222222, 3333333],
|
5
|
+
"red": [1, 2, 3, 5, 7, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 22, 23, 24, 25, 28, 29, 30, 31, 32, 34, 36, 39, 40, 42, 44, 46, 47, 49, 50, 51, 52, 53, 55, 56, 57, 58, 59, 60, 61, 62, 64, 65, 66, 67, 69, 70, 71, 73, 76, 78, 79, 81, 83, 85, 86, 87, 88, 90, 91, 93, 95, 99, 100, 44444444, 5555555555, 66666666666666, 7777777777777777777, 888888888888888]
|
6
|
+
}
|
7
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"salt": "26c9eb8052e063c0c90ccfc2b2b8d3f9",
|
3
|
+
"bucket_count": 10,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 5,
|
7
|
+
"name": "explicit_times",
|
8
|
+
"seed": "2a14d8c5a30618799547e428dfa4a807",
|
9
|
+
"start_at": "2013-01-01T00:00:00+00:00",
|
10
|
+
"end_at": "2222-02-02T02:02:02+02:00",
|
11
|
+
"all_buckets": true,
|
12
|
+
"variants": [
|
13
|
+
{
|
14
|
+
"name": "green",
|
15
|
+
"chance_weight": 1
|
16
|
+
}
|
17
|
+
]
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "explicit_times",
|
3
|
+
"variants": {
|
4
|
+
"green": [246634, 189232, 261124, 258312, 143345, 61526, 563378, 183884, 292018, 85369, 241636, 797666, 364106, 514650, 185138, 881918, 349238, 605224, 290364, 517050, 923044, 522072, 745441, 194585, 18954, 836159, 733972, 847258, 330674, 547290, 614244, 230881, 994375, 933292, 184046, 954399, 951499, 986197, 302526, 245495, 406331, 162540, 782412, 502991, 926435, 439917, 949962, 161191, 435941, 809921]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"salt": "61b81d7844c40c4057ebe6fc1a7a4a54",
|
3
|
+
"bucket_count": 10,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 7,
|
7
|
+
"name": "few_buckets",
|
8
|
+
"seed": "f63eb232778ee3b0ad7b8f515dea18a2",
|
9
|
+
"buckets": [1, 2, 3, 4],
|
10
|
+
"variants": [
|
11
|
+
{
|
12
|
+
"name": "green",
|
13
|
+
"chance_weight": 1
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"name": "red",
|
17
|
+
"chance_weight": 1
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
21
|
+
]
|
22
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
{
|
2
|
+
"salt": "2",
|
3
|
+
"bucket_count": 333,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 2,
|
7
|
+
"name": "has_not_started",
|
8
|
+
"seed": "2",
|
9
|
+
"start_at": "2444-01-01T00:00:00+00:00",
|
10
|
+
"all_buckets": true,
|
11
|
+
"variants": [
|
12
|
+
{
|
13
|
+
"name": "green",
|
14
|
+
"chance_weight": 1
|
15
|
+
}
|
16
|
+
]
|
17
|
+
}
|
18
|
+
]
|
19
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "has_not_started",
|
3
|
+
"variants": {
|
4
|
+
"": [183045, 691488, 793455, 522272, 961068, 778995, 753374, 995071, 219, 9, 765648, 519309, 581785, 419174, 62315, 901724, 739080, 528967, 744073, 751906, 886404, 457847, 817084, 160980, 302650, 911722, 447343, 871078, 450743, 879249, 276816, 826730, 697169, 344382, 511211, 154169, 55093, 648220, 964453, 862546, 208317, 16098, 341078, 111032, 277598, 876165, 650530, 446612, 583245, 7724]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
{
|
2
|
+
"salt": "61b81d7844c40c4057ebe6fc1a7a4a54",
|
3
|
+
"bucket_count": 22222,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 8,
|
7
|
+
"name": "one",
|
8
|
+
"seed": "f63eb232778ee3b0ad7b8f515dea18a2",
|
9
|
+
"all_buckets": true,
|
10
|
+
"variants": [
|
11
|
+
{
|
12
|
+
"name": "green",
|
13
|
+
"chance_weight": 1
|
14
|
+
}
|
15
|
+
]
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"id": 9,
|
19
|
+
"name": "two",
|
20
|
+
"seed": "f63eb232778ee3b0ad7b8f515dea18a2",
|
21
|
+
"all_buckets": true,
|
22
|
+
"variants": [
|
23
|
+
{
|
24
|
+
"name": "red",
|
25
|
+
"chance_weight": 1
|
26
|
+
}
|
27
|
+
]
|
28
|
+
}
|
29
|
+
]
|
30
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "two",
|
3
|
+
"variants": {
|
4
|
+
"red": [376108, 78463, 987379, 883214, 484448, 91173, 404534, 192288, 422282, 409742, 56641, 738643, 314843, 970353, 47323, 702285, 574459, 545482, 359063, 972527, 83465, 779284, 632264, 888819, 18155, 863241, 451140, 396046, 622149, 206291, 129760, 357063, 749273, 46561, 18731, 237123, 486940, 911556, 919840, 235874, 290879, 480711, 738111, 266919, 532775, 323640, 313300, 820330, 532474, 431884]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
{
|
2
|
+
"salt": "61b81d7844c40c4057ebe6fc1a7a4a54",
|
3
|
+
"bucket_count": 20,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 6,
|
7
|
+
"name": "multiple_variants",
|
8
|
+
"seed": "1995f6808c018b6dd91a3bc927fd7e92",
|
9
|
+
"all_buckets": true,
|
10
|
+
"variants": [
|
11
|
+
{
|
12
|
+
"name": "green",
|
13
|
+
"chance_weight": 1
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"name": "red",
|
17
|
+
"chance_weight": 1
|
18
|
+
},
|
19
|
+
{
|
20
|
+
"name": "blue",
|
21
|
+
"chance_weight": 1
|
22
|
+
},
|
23
|
+
{
|
24
|
+
"name": "orange",
|
25
|
+
"chance_weight": 1
|
26
|
+
}
|
27
|
+
]
|
28
|
+
}
|
29
|
+
]
|
30
|
+
}
|
@@ -0,0 +1,9 @@
|
|
1
|
+
{
|
2
|
+
"test": "multiple_variants",
|
3
|
+
"variants": {
|
4
|
+
"green": [1, 2, 3, 7, 8, 9, 12, 19, 31, 34, 37, 43, 49, 60, 62, 64, 65, 68, 71, 75, 78, 79, 87, 91, 94, 95],
|
5
|
+
"orange": [4, 11, 15, 21, 25, 32, 33, 35, 38, 40, 46, 48, 56, 57, 59, 61, 63, 74, 77, 84, 88],
|
6
|
+
"blue": [5, 10, 14, 16, 17, 20, 24, 28, 30, 42, 44, 45, 53, 55, 67, 69, 76, 81, 86, 90, 92, 93, 98, 100],
|
7
|
+
"red": [6, 13, 18, 22, 23, 26, 27, 29, 36, 39, 41, 47, 50, 51, 52, 54, 58, 66, 70, 72, 73, 80, 82, 83, 85, 89, 96, 97, 99]
|
8
|
+
}
|
9
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "no_buckets",
|
3
|
+
"variants": {
|
4
|
+
"": [221661, 512005, 429796, 860028, 10044, 249906, 491257, 188883, 793187, 86200, 155542, 257569, 534172, 506476, 461784, 123346, 882492, 933085, 105147, 575351, 756286, 293415, 946852, 894051, 109248, 285169, 499834, 422777, 793, 353968, 470821, 300689, 994515, 352756, 527621, 811497, 12865, 405584, 783387, 23868, 1577, 402157, 645302, 921879, 743877, 65532, 729269, 704958, 473143, 621616]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "no_variants",
|
3
|
+
"variants": {
|
4
|
+
"": [622251, 272484, 204185, 253530, 568849, 707722, 716486, 995162, 824709, 767433, 492812, 248413, 167496, 656567, 151008, 922024, 329511, 720392, 736955, 661974, 97247, 543083, 639996, 699049, 157179, 307863, 889578, 95851, 677312, 337342, 324874, 566262, 625613, 394275, 363587, 441650, 117251, 689288, 313266, 732267, 263893, 27783, 357379, 673605, 127934, 940549, 254671, 732244, 628961, 87713]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
{
|
2
|
+
"salt": "61b81d7844c40c4057ebe6fc1a7a4a54",
|
3
|
+
"bucket_count": 2,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 4,
|
7
|
+
"name": "zero_buckets",
|
8
|
+
"seed": "1995f6808c018b6dd91a3bc927fd7e92",
|
9
|
+
"buckets": [],
|
10
|
+
"variants": [
|
11
|
+
{
|
12
|
+
"name": "green",
|
13
|
+
"chance_weight": 1
|
14
|
+
}
|
15
|
+
]
|
16
|
+
}
|
17
|
+
]
|
18
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "zero_buckets",
|
3
|
+
"variants": {
|
4
|
+
"": [407157, 680726, 413486, 225300, 884397, 493575, 77961, 191171, 540935, 504825, 276950, 750121, 964532, 987549, 250191, 644840, 906385, 584061, 56659, 397052, 544631, 107797, 485280, 418972, 547068, 726896, 224553, 369481, 713741, 739533, 639550, 895066, 397180, 829280, 581343, 566108, 438772, 173296, 99849, 106325, 308317, 135582, 82913, 994233, 882570, 72915, 699850, 161517, 420297, 836392]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"salt": "199c13b89551aa4127370496ee557892",
|
3
|
+
"bucket_count": 44,
|
4
|
+
"ab_tests": [
|
5
|
+
{
|
6
|
+
"id": 6,
|
7
|
+
"name": "zero_weights",
|
8
|
+
"seed": "2631740ab92aa67dd64172e4f740896d",
|
9
|
+
"all_buckets": true,
|
10
|
+
"variants": [
|
11
|
+
{
|
12
|
+
"name": "zero",
|
13
|
+
"chance_weight": 0
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"name": "weight",
|
17
|
+
"chance_weight": 0
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
21
|
+
]
|
22
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"test": "zero_weight",
|
3
|
+
"variants": {
|
4
|
+
"": [407157, 680726, 413486, 225300, 884397, 493575, 77961, 191171, 540935, 504825, 276950, 750121, 964532, 987549, 250191, 644840, 906385, 584061, 56659, 397052, 544631, 107797, 485280, 418972, 547068, 726896, 224553, 369481, 713741, 739533, 639550, 895066, 397180, 829280, 581343, 566108, 438772, 173296, 99849, 106325, 308317, 135582, 82913, 994233, 882570, 72915, 699850, 161517, 420297, 836392]
|
5
|
+
}
|
6
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json-schema'
|
3
|
+
|
4
|
+
describe 'ab' do
|
5
|
+
path_to_schema = "#{File.dirname(__FILE__)}/../config.json"
|
6
|
+
|
7
|
+
Dir.glob("#{File.dirname(__FILE__)}/examples/**").each do |name|
|
8
|
+
context "#{name} example" do
|
9
|
+
let(:input) { IO.read("#{name}/input.json") }
|
10
|
+
let(:output) { JSON.parse(IO.read("#{name}/output.json")) }
|
11
|
+
|
12
|
+
specify 'validates against schema' do
|
13
|
+
result = JSON::Validator.validate(path_to_schema, input, version: :draft3)
|
14
|
+
result.should be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
specify 'correctly assigns variants' do
|
18
|
+
cases = []
|
19
|
+
output['variants'].map { |variant, ids| ids.each { |id| cases << [id, variant] } }
|
20
|
+
|
21
|
+
cases.each do |id, variant|
|
22
|
+
tests = Ab::Tests.new(input, id)
|
23
|
+
tests.send(output['test']).variant.to_s.should == variant
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/null_test_spec.rb
CHANGED
@@ -4,12 +4,14 @@ module Ab
|
|
4
4
|
describe NullTest do
|
5
5
|
subject { NullTest.new }
|
6
6
|
|
7
|
+
its(:variant) { should be_nil }
|
8
|
+
|
7
9
|
specify 'does not raise for method ending in question mark' do
|
8
|
-
expect{ subject.bla? }.to_not raise_error
|
10
|
+
expect { subject.bla? }.to_not raise_error
|
9
11
|
end
|
10
12
|
|
11
13
|
specify 'raises for method not ending in question mark' do
|
12
|
-
expect{ subject.bla }.to raise_error
|
14
|
+
expect { subject.bla }.to raise_error
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
data/spec/test_spec.rb
CHANGED
@@ -16,10 +16,27 @@ module Ab
|
|
16
16
|
it { should == 'test' }
|
17
17
|
end
|
18
18
|
|
19
|
+
context '#weight_sum' do
|
20
|
+
subject { test.weight_sum }
|
21
|
+
let(:test_hash) do
|
22
|
+
{ 'variants' => [{ 'chance_weight' => 1 },
|
23
|
+
{ 'chance_weight' => 2 }] }
|
24
|
+
end
|
25
|
+
it { should == 3 }
|
26
|
+
end
|
27
|
+
|
19
28
|
context '#start_at' do
|
20
29
|
subject { test.start_at }
|
21
|
-
|
22
|
-
|
30
|
+
|
31
|
+
context 'nil' do
|
32
|
+
let(:test_hash) { {} }
|
33
|
+
it { should < DateTime.new(1977) }
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'april fools' do
|
37
|
+
let(:test_hash) { { 'start_at' => '2014-04-01T12:00:00+00:00' } }
|
38
|
+
it { should == DateTime.new(2014, 4, 1, 12) }
|
39
|
+
end
|
23
40
|
end
|
24
41
|
|
25
42
|
context '#end_at' do
|
data/spec/tests_spec.rb
CHANGED
@@ -2,36 +2,62 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module Ab
|
4
4
|
describe Tests do
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
let(:tests) { Tests.new(config, id) }
|
6
|
+
let(:config) { {} }
|
7
|
+
let(:id) { 1 }
|
8
|
+
|
9
|
+
shared_context 'simple config with feed' do
|
10
|
+
let(:config) do
|
11
|
+
{
|
12
|
+
'salt' => 'anything',
|
13
|
+
'bucket_count' => 1000,
|
14
|
+
'ab_tests' => [{
|
15
|
+
'name' => 'feed',
|
16
|
+
'all_buckets' => true,
|
17
|
+
'variants' => [{ 'name' => 'enabled', 'chance_weight' => 1 }]
|
18
|
+
}]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
8
22
|
|
9
|
-
|
10
|
-
|
23
|
+
describe '#respond_to?' do
|
24
|
+
subject { tests.respond_to?(method_name) }
|
11
25
|
|
12
|
-
|
13
|
-
|
26
|
+
1.upto(10).each do |i|
|
27
|
+
context "random method name of #{i} length" do
|
28
|
+
let(:method_name) { SecureRandom.hex(i) }
|
29
|
+
it { should be_true }
|
14
30
|
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#method_missing' do
|
35
|
+
subject { tests.send(method_name) }
|
15
36
|
|
16
|
-
|
17
|
-
|
37
|
+
1.upto(10).each do |i|
|
38
|
+
context "random method name of #{i} length" do
|
39
|
+
let(:method_name) { SecureRandom.hex(i) }
|
40
|
+
it { should be_kind_of(NullTest) }
|
18
41
|
end
|
19
42
|
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#all' do
|
46
|
+
include_context 'simple config with feed'
|
47
|
+
subject { tests.all }
|
48
|
+
it { should == { 'feed' => 'enabled' } }
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '.new' do
|
52
|
+
subject { tests }
|
53
|
+
|
54
|
+
specify 'has no public methods' do
|
55
|
+
(subject.public_methods(false) - [:method_missing, :respond_to?, :all]).count.should == 0
|
56
|
+
end
|
20
57
|
|
21
58
|
context 'single experiment with single variant' do
|
22
|
-
|
23
|
-
{
|
24
|
-
'salt' => 'anything',
|
25
|
-
'bucket_count' => 1000,
|
26
|
-
'ab_tests' => [{
|
27
|
-
'name' => 'feed',
|
28
|
-
'buckets' => 'all',
|
29
|
-
'variants' => [{ 'name' => 'enabled', 'chance_weight' => 1 }]
|
30
|
-
}]
|
31
|
-
}
|
32
|
-
}
|
59
|
+
include_context 'simple config with feed'
|
33
60
|
its(:feed) { should be_kind_of AssignedTest }
|
34
|
-
its(:all) { should == { 'feed' => 'enabled' } }
|
35
61
|
end
|
36
62
|
end
|
37
63
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vinted-ab
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mindaugas Mozūras
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-06-
|
12
|
+
date: 2014-06-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: hooks
|
@@ -85,6 +85,26 @@ dependencies:
|
|
85
85
|
- - '>='
|
86
86
|
- !ruby/object:Gem::Version
|
87
87
|
version: 2.14.0
|
88
|
+
- !ruby/object:Gem::Dependency
|
89
|
+
name: json-schema
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ~>
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: 2.2.2
|
95
|
+
- - '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 2.2.2
|
98
|
+
type: :development
|
99
|
+
prerelease: false
|
100
|
+
version_requirements: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ~>
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 2.2.2
|
105
|
+
- - '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: 2.2.2
|
88
108
|
description:
|
89
109
|
email:
|
90
110
|
- mindaugas.mozuras@gmail.com
|
@@ -94,11 +114,13 @@ extensions: []
|
|
94
114
|
extra_rdoc_files: []
|
95
115
|
files:
|
96
116
|
- .gitignore
|
117
|
+
- .rubocop.yml
|
97
118
|
- Gemfile
|
98
119
|
- LICENSE
|
99
120
|
- README.md
|
100
121
|
- Rakefile
|
101
122
|
- ab.gemspec
|
123
|
+
- config.json
|
102
124
|
- lib/ab.rb
|
103
125
|
- lib/ab/assigned_test.rb
|
104
126
|
- lib/ab/null_test.rb
|
@@ -107,6 +129,31 @@ files:
|
|
107
129
|
- lib/ab/variant.rb
|
108
130
|
- lib/ab/version.rb
|
109
131
|
- spec/assigned_test_spec.rb
|
132
|
+
- spec/examples/all_buckets/input.json
|
133
|
+
- spec/examples/all_buckets/output.json
|
134
|
+
- spec/examples/already_finished/input.json
|
135
|
+
- spec/examples/already_finished/output.json
|
136
|
+
- spec/examples/big_weights/input.json
|
137
|
+
- spec/examples/big_weights/output.json
|
138
|
+
- spec/examples/explicit_times/input.json
|
139
|
+
- spec/examples/explicit_times/output.json
|
140
|
+
- spec/examples/few_buckets/input.json
|
141
|
+
- spec/examples/few_buckets/output.json
|
142
|
+
- spec/examples/has_not_started/input.json
|
143
|
+
- spec/examples/has_not_started/output.json
|
144
|
+
- spec/examples/multiple_tests/input.json
|
145
|
+
- spec/examples/multiple_tests/output.json
|
146
|
+
- spec/examples/multiple_variants/input.json
|
147
|
+
- spec/examples/multiple_variants/output.json
|
148
|
+
- spec/examples/no_buckets/input.json
|
149
|
+
- spec/examples/no_buckets/output.json
|
150
|
+
- spec/examples/no_variants/input.json
|
151
|
+
- spec/examples/no_variants/output.json
|
152
|
+
- spec/examples/zero_buckets/input.json
|
153
|
+
- spec/examples/zero_buckets/output.json
|
154
|
+
- spec/examples/zero_weight/input.json
|
155
|
+
- spec/examples/zero_weight/output.json
|
156
|
+
- spec/integration_spec.rb
|
110
157
|
- spec/null_test_spec.rb
|
111
158
|
- spec/spec_helper.rb
|
112
159
|
- spec/test_spec.rb
|