planout 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +4 -0
- data/LICENSE +30 -0
- data/README.md +74 -0
- data/Rakefile +8 -0
- data/examples/planout/voting_experiment.rb +31 -0
- data/lib/planout.rb +16 -0
- data/lib/planout/assignment.rb +41 -0
- data/lib/planout/bernoulli_trial.rb +11 -0
- data/lib/planout/experiment.rb +112 -0
- data/lib/planout/op_random.rb +28 -0
- data/lib/planout/op_simple.rb +20 -0
- data/lib/planout/operator.rb +14 -0
- data/lib/planout/random_float.rb +11 -0
- data/lib/planout/random_integer.rb +11 -0
- data/lib/planout/simple_experiment.rb +17 -0
- data/lib/planout/uniform_choice.rb +12 -0
- data/lib/planout/version.rb +3 -0
- data/lib/planout/weighted_choice.rb +26 -0
- data/planout.gemspec +24 -0
- data/test/planout/assignment_test.rb +42 -0
- data/test/planout/experiment_test.rb +33 -0
- data/test/planout/operator_test.rb +16 -0
- data/test/test_helper.rb +2 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 81d07087a7e4796827f84836ff455d897bb2cc9d
|
4
|
+
data.tar.gz: e859012c622ca1377b99005011209b9086216fda
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b9c798846c42b70a9e58b34817f0637086e06c0327e17e337faf5296ecf19b72fdc6682e622ac4fb1c4ad37f3245346e978839ce2de255cb5e44c68e53a63d7
|
7
|
+
data.tar.gz: f53ebe3dd097c406ecb793a70b82f8f22e7c51b525c8e2386e021d023e4ab1719535607386334382e52c98e8800378c082cbbe0c7ca8a3377808cb3a145f1306
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
BSD License
|
2
|
+
|
3
|
+
For PlanOut software
|
4
|
+
|
5
|
+
Copyright (c) 2014, Facebook, Inc. All rights reserved.
|
6
|
+
|
7
|
+
Redistribution and use in source and binary forms, with or without modification,
|
8
|
+
are permitted provided that the following conditions are met:
|
9
|
+
|
10
|
+
* Redistributions of source code must retain the above copyright notice, this
|
11
|
+
list of conditions and the following disclaimer.
|
12
|
+
|
13
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
15
|
+
and/or other materials provided with the distribution.
|
16
|
+
|
17
|
+
* Neither the name Facebook nor the names of its contributors may be used to
|
18
|
+
endorse or promote products derived from this software without specific
|
19
|
+
prior written permission.
|
20
|
+
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
22
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
23
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
25
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
26
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
27
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
28
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
29
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
30
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
## Overview
|
2
|
+
This is a rough implementation of the Experiment / logging infrasture for running PlanOut experiments, with all the random assignment operators available in Python. This port is nearly a line-by-line port, and produces assignments that are completely consistent with those produced by the Python reference implementation.
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem 'planout'
|
9
|
+
```
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
```bash
|
13
|
+
$ bundle
|
14
|
+
```
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
```bash
|
18
|
+
$ gem install planout
|
19
|
+
```
|
20
|
+
|
21
|
+
## How it works
|
22
|
+
|
23
|
+
This defines a simple experiment that randomly assigns three variables, foo, bar, and baz.
|
24
|
+
`foo` and `baz` use `userid` as input, while `bar` uses a pair, namely `userid` combined with the value of `foo` from the prior step.
|
25
|
+
```Ruby
|
26
|
+
class VotingExperiment < SimpleExperiment
|
27
|
+
# Experiment#assign takes params and an input array
|
28
|
+
def assign(params, **inputs)
|
29
|
+
userid = inputs[:userid]
|
30
|
+
|
31
|
+
params[:button_color] = UniformChoice.new({
|
32
|
+
choices: ['ff0000', '#00ff00'],
|
33
|
+
unit: userid
|
34
|
+
})
|
35
|
+
|
36
|
+
params[:button_text] = UniformChoice.new({
|
37
|
+
choices: ["I'm voting", "I'm a voter"],
|
38
|
+
unit: userid,
|
39
|
+
salt:'x'
|
40
|
+
})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
Then, we can examine the assignments produced for a few input userids. Note that since exposure logging is enabled by default, all of the experiments' inputs, configuration information, timestamp, and parameter assignments are pooped out via the Logger class.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
my_exp = VotingExperiment.new(userid: 14)
|
49
|
+
my_button_color = my_exp.get(:button_color)
|
50
|
+
button_text = my_exp.get(:button_text)
|
51
|
+
puts "button color is #{my_button_color} and button text is #{button_text}."
|
52
|
+
```
|
53
|
+
|
54
|
+
The output of the Ruby script looks something like this:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
logged data: {"name":"VotingExperiment","time":1404944726,"salt":"VotingExperiment","inputs":{"userid":14},"params":{"button_color":"ff0000","button_text":"I'm a voter"},"event":"exposure"}
|
58
|
+
|
59
|
+
button color is ff0000 and button text is I'm a voter.
|
60
|
+
```
|
61
|
+
## Examples
|
62
|
+
|
63
|
+
For examples please refer to the `examples` directory.
|
64
|
+
|
65
|
+
## Running the tests
|
66
|
+
Make sure you're in the ruby implementation directory of PlanOut and run
|
67
|
+
|
68
|
+
`rake` or `rake test`
|
69
|
+
|
70
|
+
to run the entire test suite.
|
71
|
+
|
72
|
+
If you wish to run a specific test, run
|
73
|
+
|
74
|
+
`rake test TEST=test/testname.rb` or even better `ruby test/testname.rb`
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative '../../lib/planout/simple_experiment'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class VotingExperiment < SimpleExperiment
|
5
|
+
def setup; end
|
6
|
+
|
7
|
+
def assign(params, **inputs)
|
8
|
+
userid = inputs[:userid]
|
9
|
+
params[:button_color] = UniformChoice.new({
|
10
|
+
choices: ['ff0000', '00ff00'],
|
11
|
+
unit: userid
|
12
|
+
})
|
13
|
+
|
14
|
+
params[:button_text] = UniformChoice.new({
|
15
|
+
choices: ["I'm voting", "I'm a voter"],
|
16
|
+
unit: userid,
|
17
|
+
salt:'x'
|
18
|
+
})
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
if __FILE__ == $0
|
23
|
+
(14..16).each do |i|
|
24
|
+
my_exp = VotingExperiment.new(userid:i)
|
25
|
+
# toggling the above disables or re-enables auto-logging
|
26
|
+
#my_exp.auto_exposure_log = false
|
27
|
+
puts "\ngetting assignment for user #{i} note: first time triggers a log event"
|
28
|
+
puts "button color is #{my_exp.get(:button_color)} and button text is #{my_exp.get(:button_text)}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/planout.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'planout/operator'
|
2
|
+
require_relative 'planout/op_simple'
|
3
|
+
require_relative 'planout/op_random'
|
4
|
+
require_relative 'planout/random_float'
|
5
|
+
require_relative 'planout/random_integer'
|
6
|
+
require_relative 'planout/bernoulli_trial'
|
7
|
+
require_relative 'planout/weighted_choice'
|
8
|
+
require_relative 'planout/uniform_choice'
|
9
|
+
require_relative 'planout/assignment'
|
10
|
+
require_relative 'planout/experiment'
|
11
|
+
require_relative 'planout/simple_experiment'
|
12
|
+
require_relative 'planout/version'
|
13
|
+
|
14
|
+
module Planout
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Planout
|
2
|
+
class Assignment
|
3
|
+
attr_accessor :experiment_salt, :data
|
4
|
+
|
5
|
+
def initialize(experiment_salt)
|
6
|
+
@experiment_salt = experiment_salt
|
7
|
+
@data = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def evaluate(data)
|
11
|
+
data
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(var, default = nil)
|
15
|
+
@data[var.to_sym] || default
|
16
|
+
end
|
17
|
+
|
18
|
+
# in python this would be defined as __setattr__ or __setitem__
|
19
|
+
# not sure how to do this in Ruby.
|
20
|
+
def set(name, value)
|
21
|
+
if value.is_a? Operator
|
22
|
+
value.args[:salt] = name if !value.args.has_key?(:salt)
|
23
|
+
@data[name.to_sym] = value.execute(self)
|
24
|
+
else
|
25
|
+
@data[name.to_sym] = value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](x)
|
30
|
+
get(x)
|
31
|
+
end
|
32
|
+
|
33
|
+
def []=(x,y)
|
34
|
+
set(x,y)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_params
|
38
|
+
@data
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Planout
|
5
|
+
class Experiment
|
6
|
+
attr_accessor :auto_exposure_log
|
7
|
+
|
8
|
+
def initialize(**inputs)
|
9
|
+
@inputs = inputs
|
10
|
+
@exposure_logged = false
|
11
|
+
@_salt = nil
|
12
|
+
@in_experiment = true
|
13
|
+
@name = self.class.name
|
14
|
+
@auto_exposure_log = true
|
15
|
+
|
16
|
+
setup # sets name, salt, etc.
|
17
|
+
|
18
|
+
@assignment = Assignment.new(salt)
|
19
|
+
@assigned = false
|
20
|
+
|
21
|
+
@logger = nil
|
22
|
+
setup
|
23
|
+
end
|
24
|
+
|
25
|
+
def _assign
|
26
|
+
configure_logger
|
27
|
+
assign(@assignment, **@inputs)
|
28
|
+
@in_experiment = @assignment.get(:in_experiment, @in_experiment)
|
29
|
+
@assigned = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def salt=(value)
|
37
|
+
@_salt = value
|
38
|
+
end
|
39
|
+
|
40
|
+
def salt
|
41
|
+
@_salt || @name
|
42
|
+
end
|
43
|
+
|
44
|
+
def auto_exposure_log=(value)
|
45
|
+
@auto_exposure_log = value
|
46
|
+
end
|
47
|
+
|
48
|
+
def configure_logger
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def requires_assignment
|
53
|
+
_assign if !@assigned
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_logged?
|
57
|
+
@logged
|
58
|
+
end
|
59
|
+
|
60
|
+
def requires_exposure_logging
|
61
|
+
log_exposure if @auto_exposure_log && @in_experiment && !@exposure_logged
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_params
|
65
|
+
requires_assignment
|
66
|
+
requires_exposure_logging
|
67
|
+
@assignment.get_params
|
68
|
+
end
|
69
|
+
|
70
|
+
def get(name, default = nil)
|
71
|
+
requires_assignment
|
72
|
+
requires_exposure_logging
|
73
|
+
@assignment.get(name, default)
|
74
|
+
end
|
75
|
+
|
76
|
+
def assign(params, *inputs)
|
77
|
+
# up to child class to implement
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def log_event(event_type, extras = nil)
|
82
|
+
if extras.nil?
|
83
|
+
extra_payload = {event: event_type}
|
84
|
+
else
|
85
|
+
extra_payload = {
|
86
|
+
event: event_type,
|
87
|
+
extra_data: extras.clone
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
log(as_blob(extra_payload))
|
92
|
+
end
|
93
|
+
|
94
|
+
def log_exposure(extras = nil)
|
95
|
+
@exposure_logged = true
|
96
|
+
log_event(:exposure, extras)
|
97
|
+
end
|
98
|
+
|
99
|
+
def as_blob(extras = {})
|
100
|
+
d = {
|
101
|
+
name: @name,
|
102
|
+
time: Time.now.to_i,
|
103
|
+
salt: salt,
|
104
|
+
inputs: @inputs,
|
105
|
+
params: @assignment.data
|
106
|
+
}
|
107
|
+
|
108
|
+
d.merge!(extras)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'op_simple'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class OpRandom < OpSimple
|
5
|
+
LongScale = Float(0xFFFFFFFFFFFFFFF)
|
6
|
+
|
7
|
+
def get_unit(appended_unit = nil)
|
8
|
+
unit = @parameters[:unit]
|
9
|
+
unit = [unit] if !unit.is_a? Array
|
10
|
+
unit += appended_unit if appended_unit != nil
|
11
|
+
unit
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_hash(appended_unit = nil)
|
15
|
+
salt = @parameters[:salt]
|
16
|
+
salty = "#{@mapper.experiment_salt}.#{salt}"
|
17
|
+
unit_str = get_unit(appended_unit).join('.')
|
18
|
+
x = "#{salty}.#{unit_str}"
|
19
|
+
last_hex = (Digest::SHA1.hexdigest(x))[0..14]
|
20
|
+
last_hex.to_i(16)
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_uniform(min_val = 0.0, max_val = 1.0, appended_unit = nil)
|
24
|
+
zero_to_one = self.get_hash(appended_unit)/LongScale
|
25
|
+
min_val + (max_val-min_val) * zero_to_one
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'operator'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class OpSimple < Operator
|
5
|
+
def execute(mapper)
|
6
|
+
@mapper = mapper
|
7
|
+
@parameters = {}
|
8
|
+
|
9
|
+
@args.each do |key, value|
|
10
|
+
@parameters[key] = mapper.evaluate(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
simple_execute
|
14
|
+
end
|
15
|
+
|
16
|
+
def simple_execute
|
17
|
+
-1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'experiment'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class SimpleExperiment < Experiment
|
5
|
+
def configure_logger
|
6
|
+
@logger = Logger.new(STDOUT)
|
7
|
+
#@loger.level = Logger::WARN
|
8
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
9
|
+
"logged data: #{msg}\n"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def log(data)
|
14
|
+
@logger.info(JSON.dump(data))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative 'op_random'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class UniformChoice < OpRandom
|
5
|
+
def simple_execute
|
6
|
+
choices = @parameters[:choices]
|
7
|
+
return [] if choices.length() == 0
|
8
|
+
rand_index = get_hash() % choices.length()
|
9
|
+
choices[rand_index]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'op_random'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class WeightedChoice < OpRandom
|
5
|
+
def simple_execute
|
6
|
+
choices = @parameters[:choices]
|
7
|
+
weights = @parameters[:weights]
|
8
|
+
|
9
|
+
return [] if choices.length() == 0
|
10
|
+
|
11
|
+
cum_weights = choices.zip(weights)
|
12
|
+
cum_sum = 0.0
|
13
|
+
|
14
|
+
cum_weights.each do |choice, weight|
|
15
|
+
cum_sum += weight
|
16
|
+
cum_weights[choice] = cum_sum
|
17
|
+
end
|
18
|
+
|
19
|
+
stop_value = get_uniform(0.0, cum_sum)
|
20
|
+
|
21
|
+
cum_weights.each do |choice, cum_weight|
|
22
|
+
choice if stop_value <= cum_weight
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/planout.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'planout/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "planout"
|
8
|
+
spec.version = Planout::VERSION
|
9
|
+
spec.authors = ["Mohnish Thallavajhula"]
|
10
|
+
spec.email = ["i@mohni.sh"]
|
11
|
+
spec.summary = %q{PlanOut is a framework and programming language for online field experimentation.}
|
12
|
+
spec.description = %q{PlanOut is a framework and programming language for online field experimentation. PlanOut was created to make it easy to run and iterate on sophisticated experiments, while satisfying the constraints of deployed Internet services with many users.}
|
13
|
+
spec.homepage = "https://facebook.github.io/planout"
|
14
|
+
spec.license = "BSD"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "minitest", "~> 5.5"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class AssignmentTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@assignment = Assignment.new('mtsalt')
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_salt
|
10
|
+
assert_equal('mtsalt', @assignment.experiment_salt)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_evaluate
|
14
|
+
assert_equal(1, @assignment.evaluate(1))
|
15
|
+
assert_equal(2, @assignment.evaluate(2))
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_set
|
19
|
+
@assignment.set(:color, 'green')
|
20
|
+
@assignment.set('platform', 'ios')
|
21
|
+
@assignment.set('foo', UniformChoice.new({ unit: 1, choices: ['x', 'y'] }))
|
22
|
+
assert_equal('green', @assignment.data[:color])
|
23
|
+
assert_equal('ios', @assignment.data[:platform])
|
24
|
+
assert_equal('y', @assignment.data[:foo])
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_get
|
28
|
+
@assignment.set(:button_text, 'Click Me!')
|
29
|
+
@assignment.set(:gender, 'f')
|
30
|
+
assert_equal('Click Me!', @assignment.get('button_text'))
|
31
|
+
assert_equal('f', @assignment.get('gender'))
|
32
|
+
assert_equal(10, @assignment.get('missing_key', 10))
|
33
|
+
assert_nil(@assignment.get('missing_key'))
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_get_params
|
37
|
+
@assignment.set('foo', 'bar')
|
38
|
+
@assignment.set(:baz, 'qux')
|
39
|
+
assert_equal({ foo: 'bar', baz: 'qux' }, @assignment.get_params)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require_relative '../../examples/planout/voting_experiment'
|
3
|
+
|
4
|
+
module Planout
|
5
|
+
class ExperimentTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@voting_experiment = VotingExperiment.new(userid: 14)
|
8
|
+
@voting_experiment2 = VotingExperiment.new(userid: 15)
|
9
|
+
@voting_experiment.auto_exposure_log = false
|
10
|
+
@voting_experiment2.auto_exposure_log = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_get_attributes
|
14
|
+
assert_equal('ff0000', @voting_experiment.get(:button_color))
|
15
|
+
assert_equal(1, @voting_experiment.get(:missing_key, 1))
|
16
|
+
assert_equal('ff0000', @voting_experiment2.get(:button_color))
|
17
|
+
assert_equal("I'm voting", @voting_experiment.get(:button_text))
|
18
|
+
assert_equal("I'm a voter", @voting_experiment2.get(:button_text))
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_get_params
|
22
|
+
assert_equal({ button_color: 'ff0000', button_text: "I'm voting" }, @voting_experiment.get_params)
|
23
|
+
assert_equal({ button_color: 'ff0000', button_text: "I'm a voter" }, @voting_experiment2.get_params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_as_blob
|
27
|
+
result = @voting_experiment.as_blob
|
28
|
+
assert_equal('Planout::VotingExperiment', result[:name])
|
29
|
+
assert_equal('Planout::VotingExperiment', result[:salt])
|
30
|
+
assert_equal({ userid: 14 }, result[:inputs])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
module Planout
|
4
|
+
class OperatorTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@operator = Operator.new({ foo: 'bar' })
|
7
|
+
@op_simple = OpSimple.new({ bar: 'qux' })
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_execute
|
11
|
+
a = Assignment.new('mtsalt')
|
12
|
+
assert_equal('mtsalt', @operator.execute(a))
|
13
|
+
assert_equal(-1, @op_simple.execute(a))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: planout
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mohnish Thallavajhula
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-12-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description: PlanOut is a framework and programming language for online field experimentation.
|
56
|
+
PlanOut was created to make it easy to run and iterate on sophisticated experiments,
|
57
|
+
while satisfying the constraints of deployed Internet services with many users.
|
58
|
+
email:
|
59
|
+
- i@mohni.sh
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- ".gitignore"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- examples/planout/voting_experiment.rb
|
70
|
+
- lib/planout.rb
|
71
|
+
- lib/planout/assignment.rb
|
72
|
+
- lib/planout/bernoulli_trial.rb
|
73
|
+
- lib/planout/experiment.rb
|
74
|
+
- lib/planout/op_random.rb
|
75
|
+
- lib/planout/op_simple.rb
|
76
|
+
- lib/planout/operator.rb
|
77
|
+
- lib/planout/random_float.rb
|
78
|
+
- lib/planout/random_integer.rb
|
79
|
+
- lib/planout/simple_experiment.rb
|
80
|
+
- lib/planout/uniform_choice.rb
|
81
|
+
- lib/planout/version.rb
|
82
|
+
- lib/planout/weighted_choice.rb
|
83
|
+
- planout.gemspec
|
84
|
+
- test/planout/assignment_test.rb
|
85
|
+
- test/planout/experiment_test.rb
|
86
|
+
- test/planout/operator_test.rb
|
87
|
+
- test/test_helper.rb
|
88
|
+
homepage: https://facebook.github.io/planout
|
89
|
+
licenses:
|
90
|
+
- BSD
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.2.2
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: PlanOut is a framework and programming language for online field experimentation.
|
112
|
+
test_files:
|
113
|
+
- test/planout/assignment_test.rb
|
114
|
+
- test/planout/experiment_test.rb
|
115
|
+
- test/planout/operator_test.rb
|
116
|
+
- test/test_helper.rb
|