dat-science 0.0.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|