scientist 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/README.md +55 -11
- data/doc/changelog.md +10 -0
- data/lib/scientist/errors.rb +1 -1
- data/lib/scientist/experiment.rb +1 -1
- data/lib/scientist/observation.rb +14 -21
- data/lib/scientist/version.rb +1 -1
- data/scientist.gemspec +1 -0
- data/script/test +1 -0
- data/test/scientist/observation_test.rb +48 -0
- metadata +24 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c22232b66f7eeb5a35e4f6a308042bdbed6a42b
|
4
|
+
data.tar.gz: 18765e089ded1443d66ece9a606bea76df53fc51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe1306268ecef4790dc0acc9c9d8783c3ba4088d0bc7aabda95725cf805cbee06ac058cbbbd2783e5f785236e60288146091ac6794e2ba7b912c3695ac3415b4
|
7
|
+
data.tar.gz: 262e07ce96dee85a475c962ba3de7e5f2eafb3199ba036d7a6cd727693baf27271cef7f84b310bcaad1d187a98317edcc2aeee9026dae6b1dc8028b8b6aeecc3
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Scientist!
|
2
2
|
|
3
|
-
A Ruby library for carefully refactoring critical paths. [![Build Status](https://travis-ci.org/github/scientist.svg?branch=master)](https://travis-ci.org/github/scientist)
|
3
|
+
A Ruby library for carefully refactoring critical paths. [![Build Status](https://travis-ci.org/github/scientist.svg?branch=master)](https://travis-ci.org/github/scientist) [![Coverage Status](https://coveralls.io/repos/github/github/scientist/badge.svg?branch=master)](https://coveralls.io/github/github/scientist?branch=master)
|
4
4
|
|
5
5
|
## How do I science?
|
6
6
|
|
@@ -40,9 +40,9 @@ class MyWidget
|
|
40
40
|
include Scientist
|
41
41
|
|
42
42
|
def allows?(user)
|
43
|
-
science "widget-permissions" do |
|
44
|
-
|
45
|
-
|
43
|
+
science "widget-permissions" do |experiment|
|
44
|
+
experiment.use { model.check_user(user).valid? } # old way
|
45
|
+
experiment.try { user.can?(:read, model) } # new way
|
46
46
|
end # returns the control value
|
47
47
|
end
|
48
48
|
end
|
@@ -55,22 +55,25 @@ If you don't declare any `try` blocks, none of the Scientist machinery is invoke
|
|
55
55
|
The examples above will run, but they're not really *doing* anything. The `try` blocks run every time and none of the results get published. Replace the default experiment implementation to control execution and reporting:
|
56
56
|
|
57
57
|
```ruby
|
58
|
-
require "scientist"
|
58
|
+
require "scientist/experiment"
|
59
59
|
|
60
60
|
class MyExperiment
|
61
|
-
include ActiveModel::Model
|
62
61
|
include Scientist::Experiment
|
63
62
|
|
64
63
|
attr_accessor :name
|
65
64
|
|
65
|
+
def initialize(name:)
|
66
|
+
@name = name
|
67
|
+
end
|
68
|
+
|
66
69
|
def enabled?
|
67
70
|
# see "Ramping up experiments" below
|
68
|
-
|
71
|
+
true
|
69
72
|
end
|
70
73
|
|
71
74
|
def publish(result)
|
72
75
|
# see "Publishing results" below
|
73
|
-
|
76
|
+
p result
|
74
77
|
end
|
75
78
|
end
|
76
79
|
|
@@ -191,9 +194,11 @@ end
|
|
191
194
|
And this cleaned value is available in observations in the final published result:
|
192
195
|
|
193
196
|
```ruby
|
194
|
-
class MyExperiment
|
197
|
+
class MyExperiment
|
195
198
|
include Scientist::Experiment
|
196
199
|
|
200
|
+
# ...
|
201
|
+
|
197
202
|
def publish(result)
|
198
203
|
result.control.value # [<User alice>, <User bob>, <User carol>]
|
199
204
|
result.control.cleaned_value # ["alice", "bob", "carol"]
|
@@ -244,11 +249,22 @@ end
|
|
244
249
|
As a scientist, you know it's always important to be able to turn your experiment off, lest it run amok and result in villagers with pitchforks on your doorstep. In order to control whether or not an experiment is enabled, you must include the `enabled?` method in your `Scientist::Experiment` implementation.
|
245
250
|
|
246
251
|
```ruby
|
247
|
-
class MyExperiment
|
252
|
+
class MyExperiment
|
248
253
|
include Scientist::Experiment
|
254
|
+
|
255
|
+
attr_accessor :name, :percent_enabled
|
256
|
+
|
257
|
+
def initialize(name:)
|
258
|
+
@name = name
|
259
|
+
@percent_enabled = 100
|
260
|
+
end
|
261
|
+
|
249
262
|
def enabled?
|
250
263
|
percent_enabled > 0 && rand(100) < percent_enabled
|
251
264
|
end
|
265
|
+
|
266
|
+
# ...
|
267
|
+
|
252
268
|
end
|
253
269
|
```
|
254
270
|
|
@@ -338,7 +354,18 @@ Scientist will raise a `Scientist::Experiment::MismatchError` exception if any o
|
|
338
354
|
|
339
355
|
### Handling errors
|
340
356
|
|
341
|
-
|
357
|
+
#### In candidate code
|
358
|
+
|
359
|
+
Scientist rescues and tracks _all_ exceptions raised in a `try` or `use` block, including some where rescuing may cause unexpected behavior (like `SystemExit` or `ScriptError`). To rescue a more restrictive set of exceptions, modify the `RESCUES` list:
|
360
|
+
|
361
|
+
```ruby
|
362
|
+
# default is [Exception]
|
363
|
+
Scientist::Observation::RESCUES.replace [StandardError]
|
364
|
+
```
|
365
|
+
|
366
|
+
#### In a Scientist callback
|
367
|
+
|
368
|
+
If an exception is raised within any of Scientist's internal helpers, like `publish`, `compare`, or `clean`, the `raised` method is called with the symbol name of the internal operation that failed and the exception that was raised. The default behavior of `Scientist::Default` is to simply re-raise the exception. Since this halts the experiment entirely, it's often a better idea to handle this error and continue so the experiment as a whole isn't canceled entirely:
|
342
369
|
|
343
370
|
```ruby
|
344
371
|
class MyExperiment
|
@@ -435,6 +462,23 @@ end
|
|
435
462
|
|
436
463
|
Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs the unit tests. All development dependencies are installed automatically. Science requires Ruby 1.9 or newer.
|
437
464
|
|
465
|
+
## Alternatives
|
466
|
+
|
467
|
+
- [daylerees/scientist](https://github.com/daylerees/scientist) (PHP)
|
468
|
+
- [github/scientist.net](https://github.com/github/scientist.net) (.NET)
|
469
|
+
- [joealcorn/laboratory](https://github.com/joealcorn/laboratory) (Python)
|
470
|
+
- [rawls238/Scientist4J](https://github.com/rawls238/Scientist4J) (Java)
|
471
|
+
- [tomiaijo/scientist](https://github.com/tomiaijo/scientist) (C++)
|
472
|
+
- [trello/scientist](https://github.com/trello/scientist) (node.js)
|
473
|
+
- [ziyasal/scientist.js](https://github.com/ziyasal/scientist.js) (node.js, ES6)
|
474
|
+
- [yeller/laboratory](https://github.com/yeller/laboratory) (Clojure)
|
475
|
+
- [lancew/Scientist](https://github.com/lancew/Scientist) (Perl 5)
|
476
|
+
- [lancew/ScientistP6](https://github.com/lancew/ScientistP6) (Perl 6)
|
477
|
+
- [MadcapJake/Test-Lab](https://github.com/MadcapJake/Test-Lab) (Perl 6)
|
478
|
+
- [cwbriones/scientist](https://github.com/cwbriones/scientist) (Elixir)
|
479
|
+
- [calavera/go-scientist](https://github.com/calavera/go-scientist) (Go)
|
480
|
+
|
481
|
+
|
438
482
|
## Maintainers
|
439
483
|
|
440
484
|
[@jbarnette](https://github.com/jbarnette),
|
data/doc/changelog.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Changes
|
2
|
+
|
3
|
+
## v1.1.0 (29 August 2017)
|
4
|
+
|
5
|
+
- New: [Specify which exception types to rescue](https://github.com/github/scientist#in-candidate-code)
|
6
|
+
- New: List [alternative implementations](https://github.com/github/scientist#alternatives) in `README.md`
|
7
|
+
- New: Code coverage via [coveralls.io](https://coveralls.io/github/github/scientist)
|
8
|
+
- New: Run CI on Ruby 2.3 and 2.4
|
9
|
+
- Fix: `README` typos, examples, and lies
|
10
|
+
- Fix: `false` results are now passed to the cleaner
|
data/lib/scientist/errors.rb
CHANGED
data/lib/scientist/experiment.rb
CHANGED
@@ -124,7 +124,7 @@ module Scientist::Experiment
|
|
124
124
|
# A Symbol-keyed Hash of extra experiment data.
|
125
125
|
def context(context = nil)
|
126
126
|
@_scientist_context ||= {}
|
127
|
-
@_scientist_context.merge!(context)
|
127
|
+
@_scientist_context.merge!(context) unless context.nil?
|
128
128
|
@_scientist_context
|
129
129
|
end
|
130
130
|
|
@@ -1,6 +1,10 @@
|
|
1
1
|
# What happened when this named behavior was executed? Immutable.
|
2
2
|
class Scientist::Observation
|
3
3
|
|
4
|
+
# An Array of Exception types to rescue when initializing an observation.
|
5
|
+
# NOTE: This Array will change to `[StandardError]` in the next major release.
|
6
|
+
RESCUES = [Exception]
|
7
|
+
|
4
8
|
# The experiment this observation is for
|
5
9
|
attr_reader :experiment
|
6
10
|
|
@@ -26,7 +30,7 @@ class Scientist::Observation
|
|
26
30
|
|
27
31
|
begin
|
28
32
|
@value = block.call
|
29
|
-
rescue
|
33
|
+
rescue *RESCUES => e
|
30
34
|
@exception = e
|
31
35
|
end
|
32
36
|
|
@@ -38,9 +42,7 @@ class Scientist::Observation
|
|
38
42
|
# Return a cleaned value suitable for publishing. Uses the experiment's
|
39
43
|
# defined cleaner block to clean the observed value.
|
40
44
|
def cleaned_value
|
41
|
-
|
42
|
-
experiment.clean_value value
|
43
|
-
end
|
45
|
+
experiment.clean_value value unless value.nil?
|
44
46
|
end
|
45
47
|
|
46
48
|
# Is this observation equivalent to another?
|
@@ -61,25 +63,16 @@ class Scientist::Observation
|
|
61
63
|
def equivalent_to?(other, &comparator)
|
62
64
|
return false unless other.is_a?(Scientist::Observation)
|
63
65
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
if neither_raised
|
69
|
-
if block_given?
|
70
|
-
values_are_equal = yield value, other.value
|
71
|
-
else
|
72
|
-
values_are_equal = value == other.value
|
73
|
-
end
|
66
|
+
if raised? || other.raised?
|
67
|
+
return other.exception.class == exception.class &&
|
68
|
+
other.exception.message == exception.message
|
74
69
|
end
|
75
70
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
(neither_raised && values_are_equal) ||
|
82
|
-
(both_raised && exceptions_are_equivalent)
|
71
|
+
if comparator
|
72
|
+
comparator.call(value, other.value)
|
73
|
+
else
|
74
|
+
value == other.value
|
75
|
+
end
|
83
76
|
end
|
84
77
|
|
85
78
|
def hash
|
data/lib/scientist/version.rb
CHANGED
data/scientist.gemspec
CHANGED
data/script/test
CHANGED
@@ -6,6 +6,7 @@ set -e
|
|
6
6
|
cd $(dirname "$0")/..
|
7
7
|
script/bootstrap && ruby -I lib \
|
8
8
|
-e 'require "bundler/setup"' \
|
9
|
+
-e 'require "coveralls"; Coveralls.wear!{ add_filter ".bundle" }' \
|
9
10
|
-e 'require "minitest/autorun"' \
|
10
11
|
-e 'require "scientist"' \
|
11
12
|
-e '(ARGV.empty? ? Dir["test/**/*_test.rb"] : ARGV).each { |f| load f }' -- "$@"
|
@@ -25,6 +25,37 @@ describe Scientist::Observation do
|
|
25
25
|
assert_nil ob.value
|
26
26
|
end
|
27
27
|
|
28
|
+
describe "::RESCUES" do
|
29
|
+
before do
|
30
|
+
@original = Scientist::Observation::RESCUES.dup
|
31
|
+
end
|
32
|
+
|
33
|
+
after do
|
34
|
+
Scientist::Observation::RESCUES.replace(@original)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "includes all exception types by default" do
|
38
|
+
ob = Scientist::Observation.new("test", @experiment) do
|
39
|
+
raise Exception.new("not a StandardError")
|
40
|
+
end
|
41
|
+
|
42
|
+
assert ob.raised?
|
43
|
+
assert_instance_of Exception, ob.exception
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can customize rescued types" do
|
47
|
+
Scientist::Observation::RESCUES.replace [StandardError]
|
48
|
+
|
49
|
+
ex = assert_raises Exception do
|
50
|
+
Scientist::Observation.new("test", @experiment) do
|
51
|
+
raise Exception.new("not a StandardError")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
assert_equal "not a StandardError", ex.message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
28
59
|
it "compares values" do
|
29
60
|
a = Scientist::Observation.new("test", @experiment) { 1 }
|
30
61
|
b = Scientist::Observation.new("test", @experiment) { 1 }
|
@@ -88,6 +119,23 @@ describe Scientist::Observation do
|
|
88
119
|
a = Scientist::Observation.new("test", @experiment) { "test" }
|
89
120
|
assert_equal "TEST", a.cleaned_value
|
90
121
|
end
|
122
|
+
|
123
|
+
it "doesn't clean nil values" do
|
124
|
+
@experiment.clean { |value| "foo" }
|
125
|
+
a = Scientist::Observation.new("test", @experiment) { nil }
|
126
|
+
assert_nil a.cleaned_value
|
127
|
+
end
|
128
|
+
|
129
|
+
it "returns false boolean values" do
|
130
|
+
a = Scientist::Observation.new("test", @experiment) { false }
|
131
|
+
assert_equal false, a.cleaned_value
|
132
|
+
end
|
133
|
+
|
134
|
+
it "cleans false values" do
|
135
|
+
@experiment.clean { |value| value.to_s.upcase }
|
136
|
+
a = Scientist::Observation.new("test", @experiment) { false }
|
137
|
+
assert_equal "FALSE", a.cleaned_value
|
138
|
+
end
|
91
139
|
end
|
92
140
|
|
93
141
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scientist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub Open Source
|
@@ -12,22 +12,36 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date:
|
15
|
+
date: 2017-08-29 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: minitest
|
19
19
|
requirement: !ruby/object:Gem::Requirement
|
20
20
|
requirements:
|
21
|
-
- -
|
21
|
+
- - ~>
|
22
22
|
- !ruby/object:Gem::Version
|
23
23
|
version: '5.8'
|
24
24
|
type: :development
|
25
25
|
prerelease: false
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
27
27
|
requirements:
|
28
|
-
- -
|
28
|
+
- - ~>
|
29
29
|
- !ruby/object:Gem::Version
|
30
30
|
version: '5.8'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: coveralls
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0.8'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0.8'
|
31
45
|
description: A Ruby library for carefully refactoring critical paths
|
32
46
|
email:
|
33
47
|
- opensource+scientist@github.com
|
@@ -39,11 +53,12 @@ executables: []
|
|
39
53
|
extensions: []
|
40
54
|
extra_rdoc_files: []
|
41
55
|
files:
|
42
|
-
-
|
43
|
-
-
|
56
|
+
- .gitignore
|
57
|
+
- .travis.yml
|
44
58
|
- Gemfile
|
45
59
|
- LICENSE.txt
|
46
60
|
- README.md
|
61
|
+
- doc/changelog.md
|
47
62
|
- lib/scientist.rb
|
48
63
|
- lib/scientist/default.rb
|
49
64
|
- lib/scientist/errors.rb
|
@@ -70,17 +85,17 @@ require_paths:
|
|
70
85
|
- lib
|
71
86
|
required_ruby_version: !ruby/object:Gem::Requirement
|
72
87
|
requirements:
|
73
|
-
- -
|
88
|
+
- - '>='
|
74
89
|
- !ruby/object:Gem::Version
|
75
90
|
version: '0'
|
76
91
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
92
|
requirements:
|
78
|
-
- -
|
93
|
+
- - '>='
|
79
94
|
- !ruby/object:Gem::Version
|
80
95
|
version: '0'
|
81
96
|
requirements: []
|
82
97
|
rubyforge_project:
|
83
|
-
rubygems_version: 2.
|
98
|
+
rubygems_version: 2.0.14.1
|
84
99
|
signing_key:
|
85
100
|
specification_version: 4
|
86
101
|
summary: Carefully test, measure, and track refactored code.
|