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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c0c4363232ca282aeab9af989a6499a4aaa8eea8
4
- data.tar.gz: 781901ba02fe3c35bc0ab79ed9a969998834a92a
3
+ metadata.gz: 5c22232b66f7eeb5a35e4f6a308042bdbed6a42b
4
+ data.tar.gz: 18765e089ded1443d66ece9a606bea76df53fc51
5
5
  SHA512:
6
- metadata.gz: 5d27e2fc8dc16c73253dcf66f22c21c73a589cdfd6f8cbf88337f88f8ae6cd40fa047e701c16440f20eca36aa644ca940fe6f321269a62d5e0808fb3e79bc43f
7
- data.tar.gz: 063961175f72b277982ce4eb1bd9060a65a902a75c4a4735689202030534fe0fedeede28e6e67815c8d7e8a69af7bd979c7fcb41758790683045a73da21d90de
6
+ metadata.gz: fe1306268ecef4790dc0acc9c9d8783c3ba4088d0bc7aabda95725cf805cbee06ac058cbbbd2783e5f785236e60288146091ac6794e2ba7b912c3695ac3415b4
7
+ data.tar.gz: 262e07ce96dee85a475c962ba3de7e5f2eafb3199ba036d7a6cd727693baf27271cef7f84b310bcaad1d187a98317edcc2aeee9026dae6b1dc8028b8b6aeecc3
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  /.bundle
3
3
  /.ruby-version
4
4
  /Gemfile.lock
5
+ /coverage
data/.travis.yml CHANGED
@@ -7,4 +7,10 @@ rvm:
7
7
  - 2.0.0
8
8
  - 2.1.8
9
9
  - 2.2.4
10
+ - 2.3.0
11
+ - 2.4.0
10
12
  before_install: gem install bundler
13
+ addons:
14
+ apt:
15
+ packages:
16
+ - libgmp-dev
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 |e|
44
- e.use { model.check_user(user).valid? } # old way
45
- e.try { user.can?(:read, model) } # new way
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
- super
71
+ true
69
72
  end
70
73
 
71
74
  def publish(result)
72
75
  # see "Publishing results" below
73
- super
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 < ActiveRecord::Base
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 < ActiveRecord::Base
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
- 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:
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
@@ -23,7 +23,7 @@ module Scientist
23
23
  class BehaviorNotUnique < BadBehavior
24
24
  def initialize(experiment, name)
25
25
  super experiment, name,
26
- "#{experiment.name} alread has #{name} behavior"
26
+ "#{experiment.name} already has #{name} behavior"
27
27
  end
28
28
  end
29
29
 
@@ -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) if !context.nil?
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 Object => e
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
- if value
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
- values_are_equal = false
65
- both_raised = other.raised? && raised?
66
- neither_raised = !other.raised? && !raised?
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
- exceptions_are_equivalent = # backtraces will differ, natch
77
- both_raised &&
78
- other.exception.class == exception.class &&
79
- other.exception.message == exception.message
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
@@ -1,3 +1,3 @@
1
1
  module Scientist
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/scientist.gemspec CHANGED
@@ -16,4 +16,5 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
 
18
18
  gem.add_development_dependency "minitest", "~> 5.8"
19
+ gem.add_development_dependency "coveralls", "~> 0.8"
19
20
  end
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.0.0
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: 2016-02-03 00:00:00.000000000 Z
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
- - ".gitignore"
43
- - ".travis.yml"
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.2.3
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.