dat-science 0.0.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +105 -31
- data/dat-science.gemspec +1 -1
- data/lib/dat/science/experiment.rb +38 -31
- data/lib/dat/science/result.rb +3 -3
- data/script/release +38 -0
- data/test/dat_science_experiment_test.rb +217 -2
- metadata +2 -1
data/README.md
CHANGED
@@ -1,26 +1,33 @@
|
|
1
|
-
#
|
1
|
+
# Science!
|
2
2
|
|
3
|
-
A Ruby library for carefully refactoring critical paths. Science isn't
|
4
|
-
|
5
|
-
|
3
|
+
A Ruby library for carefully refactoring critical paths. Science isn't a feature
|
4
|
+
flipper or an A/B testing tool, it's a pattern that helps measure and validate
|
5
|
+
large code changes without altering behavior.
|
6
6
|
|
7
7
|
## How do I do science?
|
8
8
|
|
9
|
+
Let's pretend you're changing the way you handle permissions in a large web app.
|
10
|
+
Tests can help guide your refactoring, but you really want to compare the
|
11
|
+
current and new behaviors live, under load.
|
12
|
+
|
9
13
|
```ruby
|
10
14
|
require "dat/science"
|
11
15
|
|
12
|
-
|
16
|
+
class MyApp::Widget
|
17
|
+
def allows?(user)
|
18
|
+
experiment = Dat::Science::Experiment.new "widget-permissions" do |e|
|
19
|
+
e.control { model.check_user(user).valid? } # old way
|
20
|
+
e.candidate { user.can? :read, model } # new way
|
21
|
+
end
|
13
22
|
|
14
|
-
|
15
|
-
|
16
|
-
experiment.candidate { user.can? :read, model }
|
23
|
+
experiment.run
|
24
|
+
end
|
17
25
|
end
|
18
26
|
```
|
19
27
|
|
20
|
-
Wrap a `control` block around the code's original behavior, and wrap
|
21
|
-
|
22
|
-
|
23
|
-
behind the scenes:
|
28
|
+
Wrap a `control` block around the code's original behavior, and wrap `candidate`
|
29
|
+
around the new behavior. `experiment.run` will always return whatever the
|
30
|
+
`control` block returns, but it does a bunch of stuff behind the scenes:
|
24
31
|
|
25
32
|
* Decides whether or not to run `candidate`,
|
26
33
|
* Runs `candidate` before `control` 50% of the time,
|
@@ -29,32 +36,60 @@ behind the scenes:
|
|
29
36
|
* Swallows any exceptions raised by the candidate behavior, and
|
30
37
|
* Publishes all this information for tracking and reporting.
|
31
38
|
|
32
|
-
|
39
|
+
If you'd like a bit less verbosity, the `Dat::Science#science` helper
|
40
|
+
instantiates an experiment and calls `run`:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require "dat/science"
|
33
44
|
|
34
|
-
|
35
|
-
|
45
|
+
class MyApp::Widget
|
46
|
+
include Dat::Science
|
47
|
+
|
48
|
+
def allows?(user)
|
49
|
+
science "widget-permissions" do |e|
|
50
|
+
e.control { model.check_user(user).valid? } # old way
|
51
|
+
e.candidate { user.can? :read, model } # new way
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
## Making science useful
|
58
|
+
|
59
|
+
The examples above will run, but they're not particularly helpful. The
|
60
|
+
`candidate` block runs every time, and none of the results get
|
61
|
+
published. Let's fix that by creating an app-specific sublass of
|
62
|
+
`Dat::Science::Experiment`. This makes it easy to add custom behavior
|
63
|
+
for enabling/disabling/throttling experiments and publishing results.
|
36
64
|
|
37
65
|
```ruby
|
38
66
|
require "dat/science"
|
39
67
|
|
40
|
-
module
|
68
|
+
module MyApp
|
41
69
|
class Experiment < Dat::Science::Experiment
|
42
70
|
def enabled?
|
43
|
-
# See "Ramping up
|
71
|
+
# See "Ramping up experiments" below.
|
44
72
|
end
|
45
73
|
|
46
74
|
def publish(name, payload)
|
47
|
-
# See "Publishing
|
75
|
+
# See "Publishing results" below.
|
48
76
|
end
|
49
77
|
end
|
50
78
|
end
|
51
79
|
```
|
52
80
|
|
81
|
+
After creating a subclass, tell `Dat::Science` to instantiate it any time the
|
82
|
+
`science` helper is called:
|
83
|
+
|
53
84
|
```ruby
|
54
|
-
Dat::Science.experiment =
|
85
|
+
Dat::Science.experiment = MyApp::Experiment
|
55
86
|
```
|
56
87
|
|
57
|
-
### Ramping up
|
88
|
+
### Ramping up experiments
|
89
|
+
|
90
|
+
By default the `candidate` block of an experiment will run 100% of the time.
|
91
|
+
This is often a really bad idea when testing live. `Experiment#enabled?` can be
|
92
|
+
overridden to run all candidates, say, 10% of the time:
|
58
93
|
|
59
94
|
```ruby
|
60
95
|
def enabled?
|
@@ -62,22 +97,39 @@ def enabled?
|
|
62
97
|
end
|
63
98
|
```
|
64
99
|
|
100
|
+
Or, even better, use a feature flag library like [Flipper][]. Delegating the
|
101
|
+
decision makes it easy to define different rules for each experiment, and can
|
102
|
+
help keep all your entropy concerns in one place.
|
103
|
+
|
104
|
+
[Flipper]: https://github.com/jnunemaker/flipper
|
105
|
+
|
65
106
|
```ruby
|
66
107
|
def enabled?
|
67
|
-
|
108
|
+
MyApp.flipper[name].enabled?
|
68
109
|
end
|
69
110
|
```
|
70
111
|
|
71
|
-
### Publishing
|
112
|
+
### Publishing results
|
113
|
+
|
114
|
+
By default the results of an experiment are discarded. This isn't very useful.
|
115
|
+
`Experiment#publish` can be overridden to publish results via any
|
116
|
+
instrumentation mechansim, which makes it easy to graph durations or
|
117
|
+
matches/mismatches and store results. The only two events published by an
|
118
|
+
experiment are `match` when the result of the control and candidate behaviors
|
119
|
+
are the same, and `mismatch` when they aren't.
|
72
120
|
|
73
121
|
```ruby
|
74
|
-
def publish(
|
75
|
-
|
122
|
+
def publish(event, payload)
|
123
|
+
MyApp.instrument "science.#{event}", payload
|
76
124
|
end
|
77
125
|
```
|
78
126
|
|
127
|
+
The published `payload` is a Symbol-keyed Hash:
|
128
|
+
|
79
129
|
```ruby
|
80
130
|
{
|
131
|
+
:experiment => "widget-permissions",
|
132
|
+
|
81
133
|
:candidate => {
|
82
134
|
:duration => 2.5,
|
83
135
|
:exception => nil,
|
@@ -94,13 +146,35 @@ end
|
|
94
146
|
}
|
95
147
|
```
|
96
148
|
|
97
|
-
|
149
|
+
The `:candidate` and `:control` Hashes have the same keys:
|
150
|
+
|
151
|
+
* `:duration` is the execution in ms, expressed as a float.
|
152
|
+
* `:exception` is a reference to any raised exception or `nil`.
|
153
|
+
* `:value` is the result of the block.
|
154
|
+
|
155
|
+
`:first` is either `:candidate` or `:control`, depending on which block was run
|
156
|
+
first during the experiment. `:experiment` is the name of the experiment.
|
157
|
+
|
158
|
+
#### Adding context
|
159
|
+
|
160
|
+
It's often useful to add more information to your results, and
|
161
|
+
`Experiment#context` makes it easy:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
science "widget-permissions" do |experiment|
|
165
|
+
experiment.context :user => user
|
166
|
+
|
167
|
+
experiment.control { model.check_user(user).valid? } # old way
|
168
|
+
experiment.candidate { user.can? :read, model } # new way
|
169
|
+
end
|
170
|
+
```
|
98
171
|
|
99
|
-
|
172
|
+
`context` takes a Symbol-keyed Hash of additional information to publish and
|
173
|
+
merges it with the default payload.
|
100
174
|
|
101
|
-
## Hacking on
|
175
|
+
## Hacking on science
|
102
176
|
|
103
|
-
Make sure a modern Bundler is available
|
104
|
-
tests. All development dependencies will be installed automatically if
|
105
|
-
they're not available. Dat science happens primarily on Ruby 1.9.3 and
|
106
|
-
|
177
|
+
Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs
|
178
|
+
the unit tests. All development dependencies will be installed automatically if
|
179
|
+
they're not available. Dat science happens primarily on Ruby 1.9.3 and 1.8.7,
|
180
|
+
but science should be universal.
|
data/dat-science.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |gem|
|
2
2
|
gem.name = "dat-science"
|
3
|
-
gem.version = "
|
3
|
+
gem.version = "1.0.0"
|
4
4
|
gem.authors = ["John Barnette", "Rick Bradley"]
|
5
5
|
gem.email = ["jbarnette@github.com"]
|
6
6
|
gem.description = "Gradually test, measure, and track refactored code."
|
@@ -9,8 +9,8 @@ module Dat
|
|
9
9
|
# Public: The name of this experiment.
|
10
10
|
attr_reader :name
|
11
11
|
|
12
|
-
# Public: Create a new experiment instance. `self` is yielded to
|
13
|
-
#
|
12
|
+
# Public: Create a new experiment instance. `self` is yielded to an
|
13
|
+
# optional `block` if it's provided.
|
14
14
|
def initialize(name, &block)
|
15
15
|
@candidate = nil
|
16
16
|
@context = { :experiment => name }
|
@@ -20,37 +20,37 @@ module Dat
|
|
20
20
|
yield self if block_given?
|
21
21
|
end
|
22
22
|
|
23
|
-
# Public:
|
24
|
-
#
|
25
|
-
def
|
26
|
-
@
|
23
|
+
# Public: Declare the candidate behavior `block` for this experiment.
|
24
|
+
# Returns `block`.
|
25
|
+
def candidate(&block)
|
26
|
+
@candidate = block if block
|
27
|
+
@candidate
|
27
28
|
end
|
28
29
|
|
29
|
-
# Public:
|
30
|
-
#
|
31
|
-
def
|
32
|
-
@
|
30
|
+
# Public: Add a Hash of `payload` data to be included when events are
|
31
|
+
# published or returns the current context if `payload` is `nil`.
|
32
|
+
def context(payload = nil)
|
33
|
+
@context.merge! payload if payload
|
34
|
+
@context
|
33
35
|
end
|
34
36
|
|
35
|
-
# Public: Declare the
|
36
|
-
#
|
37
|
-
def
|
38
|
-
@
|
37
|
+
# Public: Declare the control behavior `block` for this experiment.
|
38
|
+
# Returns `block`.
|
39
|
+
def control(&block)
|
40
|
+
@control = block if block
|
41
|
+
@control
|
39
42
|
end
|
40
43
|
|
41
44
|
# Public: Run the control and candidate behaviors, timing each and
|
42
|
-
# comparing the results. The run order is randomized. Returns the
|
43
|
-
#
|
45
|
+
# comparing the results. The run order is randomized. Returns the control
|
46
|
+
# behavior's result.
|
44
47
|
#
|
45
|
-
# If the experiment is disabled or candidate behavior isn't
|
46
|
-
#
|
47
|
-
# immediately.
|
48
|
+
# If the experiment is disabled or candidate behavior isn't provided the
|
49
|
+
# control behavior's result will be returned immediately.
|
48
50
|
def run
|
49
51
|
return run_control unless candidate? && enabled?
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
-
if control_goes_first
|
53
|
+
if control_runs_first?
|
54
54
|
control = observe_control
|
55
55
|
candidate = observe_candidate
|
56
56
|
else
|
@@ -61,10 +61,10 @@ module Dat
|
|
61
61
|
payload = {
|
62
62
|
:candidate => candidate.payload,
|
63
63
|
:control => control.payload,
|
64
|
-
:first =>
|
64
|
+
:first => control_runs_first? ? :control : :candidate
|
65
65
|
}
|
66
66
|
|
67
|
-
kind = control == candidate ?
|
67
|
+
kind = control == candidate ? :match : :mismatch
|
68
68
|
publish_with_context kind, payload
|
69
69
|
|
70
70
|
raise control.exception if control.raised?
|
@@ -76,7 +76,13 @@ module Dat
|
|
76
76
|
|
77
77
|
# Internal: Does this experiment have candidate behavior?
|
78
78
|
def candidate?
|
79
|
-
|
79
|
+
!!candidate
|
80
|
+
end
|
81
|
+
|
82
|
+
# Internal: Should the control behavior run first?
|
83
|
+
def control_runs_first?
|
84
|
+
return @control_runs_first if defined? @control_runs_first
|
85
|
+
@control_runs_first = rand(2) == 0
|
80
86
|
end
|
81
87
|
|
82
88
|
# Internal: Is this experiment enabled? More specifically, should
|
@@ -112,23 +118,24 @@ module Dat
|
|
112
118
|
observe { run_control }
|
113
119
|
end
|
114
120
|
|
115
|
-
# Internal: Broadcast an event
|
121
|
+
# Internal: Broadcast an `event` String and `payload` Hash. The
|
116
122
|
# default implementation is a no-op. Returns nothing.
|
117
|
-
def publish(
|
123
|
+
def publish(event, payload)
|
118
124
|
end
|
119
125
|
|
120
126
|
# Internal: Call `publish`, merging the `payload` with `context`.
|
121
|
-
def publish_with_context(
|
122
|
-
publish
|
127
|
+
def publish_with_context(event, payload)
|
128
|
+
publish event, context.merge(payload)
|
123
129
|
end
|
130
|
+
|
124
131
|
# Internal: Run the candidate behavior and return its result.
|
125
132
|
def run_candidate
|
126
|
-
|
133
|
+
candidate.call
|
127
134
|
end
|
128
135
|
|
129
136
|
# Internal: Run the control behavior and return its result.
|
130
137
|
def run_control
|
131
|
-
|
138
|
+
control.call
|
132
139
|
end
|
133
140
|
end
|
134
141
|
end
|
data/lib/dat/science/result.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Dat
|
2
|
-
|
3
|
-
# Internal. The value of a watched behavior.
|
4
2
|
module Science
|
3
|
+
|
4
|
+
# Internal. The output of running of an observed behavior.
|
5
5
|
class Result
|
6
6
|
attr_reader :duration
|
7
7
|
attr_reader :exception
|
@@ -14,7 +14,7 @@ module Dat
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def ==(other)
|
17
|
-
return false unless other.is_a? Science::Result
|
17
|
+
return false unless other.is_a? Dat::Science::Result
|
18
18
|
|
19
19
|
values_are_equal = other.value == value
|
20
20
|
both_raised = other.raised? && raised?
|
data/script/release
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# Tag and push a release.
|
3
|
+
|
4
|
+
set -e
|
5
|
+
|
6
|
+
# Make sure we're in the project root.
|
7
|
+
|
8
|
+
cd $(dirname "$0")/..
|
9
|
+
|
10
|
+
# Build a new gem archive.
|
11
|
+
|
12
|
+
rm -rf dat-science-*.gem
|
13
|
+
gem build -q dat-science.gemspec
|
14
|
+
|
15
|
+
# Make sure we're on the master branch.
|
16
|
+
|
17
|
+
(git branch | grep -q '* master') || {
|
18
|
+
echo "Only release from the master branch."
|
19
|
+
exit 1
|
20
|
+
}
|
21
|
+
|
22
|
+
# Figure out what version we're releasing.
|
23
|
+
|
24
|
+
tag=v`ls dat-science-*.gem | sed 's/^dat-science-\(.*\)\.gem$/\1/'`
|
25
|
+
|
26
|
+
# Make sure we haven't released this version before.
|
27
|
+
|
28
|
+
git fetch -t origin
|
29
|
+
|
30
|
+
(git tag -l | grep -q "$tag") && {
|
31
|
+
echo "Whoops, there's already a '${tag}' tag."
|
32
|
+
exit 1
|
33
|
+
}
|
34
|
+
|
35
|
+
# Tag it and bag it.
|
36
|
+
|
37
|
+
gem push dat-science-*.gem && git tag "$tag" &&
|
38
|
+
git push origin master && git push origin "$tag"
|
@@ -2,7 +2,222 @@ require "minitest/autorun"
|
|
2
2
|
require "dat/science/experiment"
|
3
3
|
|
4
4
|
class DatScienceExperimentTest < MiniTest::Unit::TestCase
|
5
|
-
|
6
|
-
|
5
|
+
class Experiment < Dat::Science::Experiment
|
6
|
+
def self.published
|
7
|
+
@published ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def publish(name, payload)
|
11
|
+
Experiment.published << [name, payload]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup
|
16
|
+
Experiment.published.clear
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_initialize
|
20
|
+
in_block = nil
|
21
|
+
experiment = Experiment.new("foo") { |e| in_block = e }
|
22
|
+
|
23
|
+
assert_equal "foo", experiment.name
|
24
|
+
assert_equal experiment, in_block
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_candidate_default
|
28
|
+
assert_nil Experiment.new("foo").candidate
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_candidate
|
32
|
+
e = Experiment.new "foo"
|
33
|
+
b = lambda {}
|
34
|
+
|
35
|
+
e.candidate &b
|
36
|
+
assert_same b, e.candidate
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_context_default
|
40
|
+
e = Experiment.new "foo"
|
41
|
+
|
42
|
+
expected = { :experiment => "foo" }
|
43
|
+
assert_equal expected, e.context
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_context
|
47
|
+
e = Experiment.new "foo"
|
48
|
+
e.context :bar => :baz
|
49
|
+
|
50
|
+
assert_equal :baz, e.context[:bar]
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_control_default
|
54
|
+
assert_nil Experiment.new("foo").control
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_control
|
58
|
+
e = Experiment.new "foo"
|
59
|
+
b = lambda {}
|
60
|
+
|
61
|
+
e.control &b
|
62
|
+
assert_same b, e.control
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_run_with_no_candidate
|
66
|
+
e = Experiment.new "foo"
|
67
|
+
e.control { :foo }
|
68
|
+
|
69
|
+
assert_equal :foo, e.run
|
70
|
+
assert Experiment.published.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_run_disabled
|
74
|
+
e = Experiment.new "foo"
|
75
|
+
e.control { :foo }
|
76
|
+
e.candidate { :bar }
|
77
|
+
|
78
|
+
def e.enabled?
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
assert_equal :foo, e.run
|
83
|
+
assert Experiment.published.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_run
|
87
|
+
e = Experiment.new "foo"
|
88
|
+
e.control { :foo }
|
89
|
+
|
90
|
+
candidate_run = false
|
91
|
+
e.candidate { candidate_run = true; :bar }
|
92
|
+
|
93
|
+
def e.control_runs_first?
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
assert_equal :foo, e.run
|
98
|
+
assert candidate_run
|
99
|
+
|
100
|
+
event, payload = Experiment.published.first
|
101
|
+
refute_nil event
|
102
|
+
refute_nil payload
|
103
|
+
|
104
|
+
assert_equal :mismatch, event
|
105
|
+
|
106
|
+
assert_equal "foo", payload[:experiment]
|
107
|
+
assert_equal :control, payload[:first]
|
108
|
+
|
109
|
+
assert payload[:control][:duration]
|
110
|
+
assert_nil payload[:control][:exception]
|
111
|
+
assert_equal :foo, payload[:control][:value]
|
112
|
+
|
113
|
+
assert payload[:candidate][:duration]
|
114
|
+
assert_nil payload[:candidate][:exception]
|
115
|
+
assert_equal :bar, payload[:candidate][:value]
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_run_candidate_first
|
119
|
+
e = Experiment.new "foo"
|
120
|
+
e.control { :foo }
|
121
|
+
e.candidate { :bar }
|
122
|
+
|
123
|
+
def e.control_runs_first?
|
124
|
+
false
|
125
|
+
end
|
126
|
+
|
127
|
+
assert_equal :foo, e.run
|
128
|
+
|
129
|
+
event, payload = Experiment.published.first
|
130
|
+
refute_nil event
|
131
|
+
refute_nil payload
|
132
|
+
|
133
|
+
assert_equal :mismatch, event
|
134
|
+
assert_equal :candidate, payload[:first]
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_run_match
|
138
|
+
e = Experiment.new "foo"
|
139
|
+
e.control { :foo }
|
140
|
+
e.candidate { :foo }
|
141
|
+
|
142
|
+
assert_equal :foo, e.run
|
143
|
+
|
144
|
+
event, payload = Experiment.published.first
|
145
|
+
refute_nil event
|
146
|
+
refute_nil payload
|
147
|
+
|
148
|
+
assert_equal :match, event
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_run_passes_control_exceptions_through
|
152
|
+
e = Experiment.new "foo"
|
153
|
+
e.control { raise "bar" }
|
154
|
+
|
155
|
+
candidate_run = false
|
156
|
+
e.candidate { candidate_run = true }
|
157
|
+
|
158
|
+
ex = assert_raises RuntimeError do
|
159
|
+
e.run
|
160
|
+
end
|
161
|
+
|
162
|
+
assert candidate_run
|
163
|
+
assert_equal "bar", ex.message
|
164
|
+
|
165
|
+
event, payload = Experiment.published.first
|
166
|
+
refute_nil event
|
167
|
+
refute_nil payload
|
168
|
+
|
169
|
+
assert_equal :mismatch, event
|
170
|
+
refute_nil payload[:control][:exception]
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_run_swallows_candidate_exceptions
|
174
|
+
e = Experiment.new "foo"
|
175
|
+
e.control { :foo }
|
176
|
+
e.candidate { raise "bar" }
|
177
|
+
|
178
|
+
assert_equal :foo, e.run
|
179
|
+
|
180
|
+
event, payload = Experiment.published.first
|
181
|
+
refute_nil event
|
182
|
+
refute_nil payload
|
183
|
+
|
184
|
+
assert_equal :mismatch, event
|
185
|
+
refute_nil payload[:candidate][:exception]
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_run_similar_exceptions_are_a_match
|
189
|
+
e = Experiment.new "foo"
|
190
|
+
e.control { raise "foo" }
|
191
|
+
e.candidate { raise "foo" }
|
192
|
+
|
193
|
+
assert_raises RuntimeError do
|
194
|
+
e.run
|
195
|
+
end
|
196
|
+
|
197
|
+
event, payload = Experiment.published.first
|
198
|
+
refute_nil event
|
199
|
+
refute_nil payload
|
200
|
+
|
201
|
+
assert_equal :match, event
|
202
|
+
refute_nil payload[:control][:exception]
|
203
|
+
refute_nil payload[:candidate][:exception]
|
204
|
+
end
|
205
|
+
|
206
|
+
def test_run_dissimilar_exceptions_are_a_mismatch
|
207
|
+
e = Experiment.new "foo"
|
208
|
+
e.control { raise "foo" }
|
209
|
+
e.candidate { raise "bar" }
|
210
|
+
|
211
|
+
assert_raises RuntimeError do
|
212
|
+
e.run
|
213
|
+
end
|
214
|
+
|
215
|
+
event, payload = Experiment.published.first
|
216
|
+
refute_nil event
|
217
|
+
refute_nil payload
|
218
|
+
|
219
|
+
assert_equal :mismatch, event
|
220
|
+
refute_nil payload[:control][:exception]
|
221
|
+
refute_nil payload[:candidate][:exception]
|
7
222
|
end
|
8
223
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dat-science
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -60,6 +60,7 @@ files:
|
|
60
60
|
- lib/dat/science/experiment.rb
|
61
61
|
- lib/dat/science/result.rb
|
62
62
|
- script/bootstrap
|
63
|
+
- script/release
|
63
64
|
- script/test
|
64
65
|
- test/dat_science_experiment_test.rb
|
65
66
|
- test/dat_science_test.rb
|