vinted-ab 0.1.0 → 0.2.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/.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
|
+
[](https://codeclimate.com/github/vinted/ab)
|
4
|
+
[](http://badge.fury.io/rb/vinted-ab)
|
5
|
+
[](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
|
+

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