scientist 1.6.0 → 1.6.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f03841ae9bccb8d7b979971d5f6a38482f57bbc81af82d0a710479ccc3f16fbb
4
- data.tar.gz: c0f81d696918e302462baf1bd918cd25179a99045d48082fb162e1fdb2fa2280
3
+ metadata.gz: eee7969d8abea0f0fa0a53dea180d2f786d67a589b8694b2ccf6c70bdcedcbb4
4
+ data.tar.gz: 87bb457307fb452731efed8413770abed9f3d687e9718dd54edce106d745d389
5
5
  SHA512:
6
- metadata.gz: f1680e6564a6e83b3dde55f6757f3e7c953a70b3d748aa0e7ca2a54b5a33a4b3a947a8f20aa52790a41e6a66e36627e4e851f627625c0aaf9fd4cf41b94ace8c
7
- data.tar.gz: 75ba381053b73e9418ed2ab5608c70c92a55990c938a1c25f4324169ff560a8feea6827ef909e50b9fc3baf5a5eb49f3b7b26ed4e3f9799007b80a71c7543c86
6
+ metadata.gz: 882c44d39b595fe4c660abd619b5889552fe5d48c8bf2a2ff57e8b62fa221853a3af8727d04318068da888b6a8dd59e23f78c0eda71237261e409cb4038a5524
7
+ data.tar.gz: a6397a830792dda87404c7775d804d19a8b229a488180bba633501ff80ca889b9e61d44cf06287e48059664f8d6dd5097bf82e2ac6c4d33e478e02ea5e88c78f
data/.travis.yml CHANGED
@@ -3,10 +3,9 @@ language: ruby
3
3
  cache: bundler
4
4
  script: script/test
5
5
  rvm:
6
- - 2.3.8
7
- - 2.4.5
8
- - 2.5.3
9
- - 2.6.1
6
+ - 2.6.7
7
+ - 2.7.3
8
+ - 3.0.1
10
9
  - truffleruby-head
11
10
  before_install: gem install bundler
12
11
  addons:
data/README.md CHANGED
@@ -109,6 +109,38 @@ class MyWidget
109
109
  end
110
110
  ```
111
111
 
112
+ If either the control block or candidate block raises an error, Scientist compares the two observations' classes and messages using `==`. To override this behavior, use `compare_error` to define how to compare observed errors instead:
113
+
114
+ ```ruby
115
+ class MyWidget
116
+ include Scientist
117
+
118
+ def slug_from_login(login)
119
+ science "slug_from_login" do |e|
120
+ e.use { User.slug_from_login login } # returns String instance or ArgumentError
121
+ e.try { UserService.slug_from_login login } # returns String instance or ArgumentError
122
+
123
+ compare_error_message_and_class = -> (control, candidate) do
124
+ control.class == candidate.class &&
125
+ control.message == candidate.message
126
+ end
127
+
128
+ compare_argument_errors = -> (control, candidate) do
129
+ control.class == ArgumentError &&
130
+ candidate.class == ArgumentError &&
131
+ control.message.start_with?("Input has invalid characters") &&
132
+ candidate.message.star_with?("Invalid characters in input")
133
+ end
134
+
135
+ e.compare_error do |control, candidate|
136
+ compare_error_message_and_class.call(control, candidate) ||
137
+ compare_argument_errors.call(control, candidate)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ ```
143
+
112
144
  ### Adding context
113
145
 
114
146
  Results aren't very useful without some way to identify them. Use the `context` method to add to or retrieve the context for an experiment:
@@ -228,7 +260,7 @@ def admin?(user)
228
260
  end
229
261
  ```
230
262
 
231
- The ignore blocks are only called if the *values* don't match. If one observation raises an exception and the other doesn't, it's always considered a mismatch. If both observations raise different exceptions, that is also considered a mismatch.
263
+ The ignore blocks are only called if the *values* don't match. Unless a `compare_error` comparator is defined, two cases are considered mismatches: a) one observation raising an exception and the other not, b) observations raising exceptions with different classes or messages.
232
264
 
233
265
  ### Enabling/disabling experiments
234
266
 
@@ -395,6 +427,8 @@ Scientist rescues and tracks _all_ exceptions raised in a `try` or `use` block,
395
427
  Scientist::Observation::RESCUES.replace [StandardError]
396
428
  ```
397
429
 
430
+ **Timeout ⏲️**: If you're introducing a candidate that could possibly timeout, use caution. ⚠️ While Scientist rescues all exceptions that occur in the candidate block, it *does not* protect you from timeouts, as doing so would be complicated. It would likely require running the candidate code in a background job and tracking the time of a request. We feel the cost of this complexity would outweigh the benefit, so make sure that your code doesn't cause timeouts. This risk can be reduced by running the experiment on a low percentage so that users can (most likely) bypass the experiment by refreshing the page if they hit a timeout. See [Ramping up experiments](#ramping-up-experiments) below for how details on how to set the percentage for your experiment.
431
+
398
432
  #### In a Scientist callback
399
433
 
400
434
  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:
@@ -551,6 +585,7 @@ Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs t
551
585
  - [junkpiano/scientist](https://github.com/junkpiano/scientist) (Swift)
552
586
  - [serverless scientist](http://serverlessscientist.com/) (AWS Lambda)
553
587
  - [fightmegg/scientist](https://github.com/fightmegg/scientist) (TypeScript, Browser / Node.js)
588
+ - [MisterSpex/misterspex-scientist](https://github.com/MisterSpex/misterspex-scientist) (Java, no dependencies)
554
589
 
555
590
  ## Maintainers
556
591
 
@@ -134,6 +134,16 @@ module Scientist::Experiment
134
134
  @_scientist_comparator = block
135
135
  end
136
136
 
137
+ # A block which compares two experimental errors.
138
+ #
139
+ # The block must take two arguments, the control Error and a candidate Error,
140
+ # and return true or false.
141
+ #
142
+ # Returns the block.
143
+ def compare_errors(*args, &block)
144
+ @_scientist_error_comparator = block
145
+ end
146
+
137
147
  # A Symbol-keyed Hash of extra experiment data.
138
148
  def context(context = nil)
139
149
  @_scientist_context ||= {}
@@ -177,13 +187,9 @@ module Scientist::Experiment
177
187
  "experiment"
178
188
  end
179
189
 
180
- # Internal: compare two observations, using the configured compare block if present.
190
+ # Internal: compare two observations, using the configured compare and compare_errors lambdas if present.
181
191
  def observations_are_equivalent?(a, b)
182
- if @_scientist_comparator
183
- a.equivalent_to?(b, &@_scientist_comparator)
184
- else
185
- a.equivalent_to? b
186
- end
192
+ a.equivalent_to? b, @_scientist_comparator, @_scientist_error_comparator
187
193
  rescue StandardError => ex
188
194
  raised :compare, ex
189
195
  false
@@ -45,25 +45,34 @@ class Scientist::Observation
45
45
 
46
46
  # Is this observation equivalent to another?
47
47
  #
48
- # other - the other Observation in question
49
- # comparator - an optional comparison block. This observation's value and the
50
- # other observation's value are yielded to this to determine
51
- # their equivalency. Block should return true/false.
48
+ # other - the other Observation in question
49
+ # comparator - an optional comparison proc. This observation's value and the
50
+ # other observation's value are passed to this to determine
51
+ # their equivalency. Proc should return true/false.
52
+ # error_comparator - an optional comparison proc. This observation's Error and the
53
+ # other observation's Error are passed to this to determine
54
+ # their equivalency. Proc should return true/false.
52
55
  #
53
56
  # Returns true if:
54
57
  #
55
58
  # * The values of the observation are equal (using `==`)
56
59
  # * The values of the observations are equal according to a comparison
57
- # block, if given
60
+ # proc, if given
61
+ # * The exceptions raised by the obeservations are equal according to the
62
+ # error comparison proc, if given.
58
63
  # * Both observations raised an exception with the same class and message.
59
64
  #
60
65
  # Returns false otherwise.
61
- def equivalent_to?(other, &comparator)
66
+ def equivalent_to?(other, comparator=nil, error_comparator=nil)
62
67
  return false unless other.is_a?(Scientist::Observation)
63
68
 
64
69
  if raised? || other.raised?
65
- return other.exception.class == exception.class &&
66
- other.exception.message == exception.message
70
+ if error_comparator
71
+ return error_comparator.call(exception, other.exception)
72
+ else
73
+ return other.exception.class == exception.class &&
74
+ other.exception.message == exception.message
75
+ end
67
76
  end
68
77
 
69
78
  if comparator
@@ -1,3 +1,3 @@
1
1
  module Scientist
2
- VERSION = "1.6.0"
2
+ VERSION = "1.6.1"
3
3
  end
@@ -201,6 +201,18 @@ describe Scientist::Experiment do
201
201
  assert @ex.published_result.matched?
202
202
  end
203
203
 
204
+ it "compares errors with an error comparator block if provided" do
205
+ @ex.compare_errors { |a, b| a.class == b.class }
206
+ @ex.use { raise "foo" }
207
+ @ex.try { raise "bar" }
208
+
209
+ resulting_error = assert_raises RuntimeError do
210
+ @ex.run
211
+ end
212
+ assert_equal "foo", resulting_error.message
213
+ assert @ex.published_result.matched?
214
+ end
215
+
204
216
  it "knows how to compare two experiments" do
205
217
  a = Scientist::Observation.new(@ex, "a") { 1 }
206
218
  b = Scientist::Observation.new(@ex, "b") { 2 }
@@ -80,7 +80,7 @@ describe Scientist::Observation do
80
80
  refute x.equivalent_to?(y)
81
81
  end
82
82
 
83
- FirstErrror = Class.new(StandardError)
83
+ FirstError = Class.new(StandardError)
84
84
  SecondError = Class.new(StandardError)
85
85
 
86
86
  it "compares exception classes" do
@@ -92,22 +92,46 @@ describe Scientist::Observation do
92
92
  refute x.equivalent_to?(y)
93
93
  end
94
94
 
95
- it "compares values using a comparator block" do
95
+ it "compares values using a comparator proc" do
96
96
  a = Scientist::Observation.new("test", @experiment) { 1 }
97
97
  b = Scientist::Observation.new("test", @experiment) { "1" }
98
98
 
99
99
  refute a.equivalent_to?(b)
100
- assert a.equivalent_to?(b) { |x, y| x.to_s == y.to_s }
100
+
101
+ compare_on_string = -> (x, y) { x.to_s == y.to_s }
102
+
103
+ assert a.equivalent_to?(b, compare_on_string)
101
104
 
102
105
  yielded = []
103
- a.equivalent_to?(b) do |x, y|
106
+ compare_appends = -> (x, y) do
104
107
  yielded << x
105
108
  yielded << y
106
109
  true
107
110
  end
111
+ a.equivalent_to?(b, compare_appends)
112
+
108
113
  assert_equal [a.value, b.value], yielded
109
114
  end
110
115
 
116
+ it "compares exceptions using an error comparator proc" do
117
+ x = Scientist::Observation.new("test", @experiment) { raise FirstError, "error" }
118
+ y = Scientist::Observation.new("test", @experiment) { raise SecondError, "error" }
119
+ z = Scientist::Observation.new("test", @experiment) { raise FirstError, "ERROR" }
120
+
121
+ refute x.equivalent_to?(z)
122
+ refute x.equivalent_to?(y)
123
+
124
+ compare_on_class = -> (error, other_error) {
125
+ error.class == other_error.class
126
+ }
127
+ compare_on_message = -> (error, other_error) {
128
+ error.message == other_error.message
129
+ }
130
+
131
+ assert x.equivalent_to?(z, nil, compare_on_class)
132
+ assert x.equivalent_to?(y, nil, compare_on_message)
133
+ end
134
+
111
135
  describe "#cleaned_value" do
112
136
  it "returns the observation's value by default" do
113
137
  a = Scientist::Observation.new("test", @experiment) { 1 }
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.6.0
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2021-03-08 00:00:00.000000000 Z
15
+ date: 2021-10-22 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: minitest
@@ -95,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  requirements: []
98
- rubygems_version: 3.1.4
98
+ rubygems_version: 3.2.22
99
99
  signing_key:
100
100
  specification_version: 4
101
101
  summary: Carefully test, measure, and track refactored code.