scientist 1.4.0 → 1.6.2

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: 49ab4f74d43836220fbadd4692445a3cc61a13387cf87f533f39db9c58ba8544
4
- data.tar.gz: d1648b2e74411130e36ddcac2f9e9bece7c7db3d65ca37deb1d7d6cf7a28096e
3
+ metadata.gz: 1e623b9acbede185a6ee8ca0ae2227a52707499c28bdf04e4fe8a2afcbd0f051
4
+ data.tar.gz: 560b1d3353829f6be217229546297fcd4102e6c47dc87fc12ffa963c5e2fa3f7
5
5
  SHA512:
6
- metadata.gz: 67f99e2906499d736c4ab002273d0bf9dbea07a3413b387c12a239afbdc65a542d658f7f1d087cdf482a261698e02c439ecb673a73a6497ca2ce4e07940e1a25
7
- data.tar.gz: 60479759bc916ae2b0d1c1b9bf575de872fbb5adfda098ee6fea4802384ceeed30caee4973583e751de9c5b3dce686acc1602aa7f6fade034d22b4f3ae5e7180
6
+ metadata.gz: 593c2807ff16f1381a0189e4babdad3ab366ad66057e4af1b083611fb204d55ae9434bf989550f8371de7586be55fe4f8a760f84f2424323a9189803583e9243
7
+ data.tar.gz: 1239bb86dfad9fcee2b0dc4dc2fc8458306e877afd2aa1e722b9e4db7072b71658469bf48b425959c52111fac2cc8bb5dbbb11435263a199f036ad2ee4697228
data/.travis.yml CHANGED
@@ -3,10 +3,10 @@ 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
9
+ - truffleruby-head
10
10
  before_install: gem install bundler
11
11
  addons:
12
12
  apt:
data/CONTRIBUTING.md CHANGED
@@ -10,9 +10,9 @@ Contributions to this project are [released](https://help.github.com/articles/gi
10
10
  ## Submitting a pull request
11
11
 
12
12
  0. [Fork][fork] and clone the repository
13
- 0. Create a new branch: `git checkout -b my-branch-name`
14
- 0. Make your change, push to your fork and [submit a pull request][pr]
15
- 0. Pat your self on the back and wait for your pull request to be reviewed and merged.
13
+ 1. Create a new branch: `git checkout -b my-branch-name`
14
+ 2. Make your change, push to your fork and [submit a pull request][pr]
15
+ 3. Pat your self on the back and wait for your pull request to be reviewed and merged.
16
16
 
17
17
  Here are a few things you can do that will increase the likelihood of your pull request being accepted:
18
18
 
data/README.md CHANGED
@@ -24,9 +24,9 @@ Wrap a `use` block around the code's original behavior, and wrap `try` around th
24
24
 
25
25
  * It decides whether or not to run the `try` block,
26
26
  * Randomizes the order in which `use` and `try` blocks are run,
27
- * Measures the durations of all behaviors,
27
+ * Measures the durations of all behaviors in seconds,
28
28
  * Compares the result of `try` to the result of `use`,
29
- * Swallows (but records) any exceptions raised in the `try` block, and
29
+ * Swallow and record exceptions raised in the `try` block when overriding `raised`, and
30
30
  * Publishes all this information.
31
31
 
32
32
  The `use` block is called the **control**. The `try` block is called the **candidate**.
@@ -62,7 +62,7 @@ class MyExperiment
62
62
 
63
63
  attr_accessor :name
64
64
 
65
- def initialize(name:)
65
+ def initialize(name)
66
66
  @name = name
67
67
  end
68
68
 
@@ -71,20 +71,21 @@ class MyExperiment
71
71
  true
72
72
  end
73
73
 
74
+ def raised(operation, error)
75
+ # see "In a Scientist callback" below
76
+ p "Operation '#{operation}' failed with error '#{error.inspect}'"
77
+ super # will re-raise
78
+ end
79
+
74
80
  def publish(result)
75
81
  # see "Publishing results" below
76
82
  p result
77
83
  end
78
84
  end
79
-
80
- # replace `Scientist::Default` as the default implementation
81
- module Scientist::Experiment
82
- def self.new(name)
83
- MyExperiment.new(name: name)
84
- end
85
- end
86
85
  ```
87
86
 
87
+ When `Scientist::Experiment` is included in a class, it automatically sets it as the default implementation via `Scientist::Experiment.set_default`. This `set_default` call is skipped if you include `Scientist::Experiment` in a module.
88
+
88
89
  Now calls to the `science` helper will load instances of `MyExperiment`.
89
90
 
90
91
  ### Controlling comparison
@@ -108,6 +109,38 @@ class MyWidget
108
109
  end
109
110
  ```
110
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.start_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
+
111
144
  ### Adding context
112
145
 
113
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:
@@ -227,7 +260,7 @@ def admin?(user)
227
260
  end
228
261
  ```
229
262
 
230
- 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.
231
264
 
232
265
  ### Enabling/disabling experiments
233
266
 
@@ -256,7 +289,7 @@ class MyExperiment
256
289
 
257
290
  attr_accessor :name, :percent_enabled
258
291
 
259
- def initialize(name:)
292
+ def initialize(name)
260
293
  @name = name
261
294
  @percent_enabled = 100
262
295
  end
@@ -394,6 +427,8 @@ Scientist rescues and tracks _all_ exceptions raised in a `try` or `use` block,
394
427
  Scientist::Observation::RESCUES.replace [StandardError]
395
428
  ```
396
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
+
397
432
  #### In a Scientist callback
398
433
 
399
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:
@@ -537,6 +572,8 @@ Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs t
537
572
  - [tomiaijo/scientist](https://github.com/tomiaijo/scientist) (C++)
538
573
  - [trello/scientist](https://github.com/trello/scientist) (node.js)
539
574
  - [ziyasal/scientist.js](https://github.com/ziyasal/scientist.js) (node.js, ES6)
575
+ - [TrueWill/tzientist](https://github.com/TrueWill/tzientist) (node.js, TypeScript)
576
+ - [TrueWill/paleontologist](https://github.com/TrueWill/paleontologist) (Deno, TypeScript)
540
577
  - [yeller/laboratory](https://github.com/yeller/laboratory) (Clojure)
541
578
  - [lancew/Scientist](https://github.com/lancew/Scientist) (Perl 5)
542
579
  - [lancew/ScientistP6](https://github.com/lancew/ScientistP6) (Perl 6)
@@ -546,6 +583,9 @@ Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs t
546
583
  - [jelmersnoeck/experiment](https://github.com/jelmersnoeck/experiment) (Go)
547
584
  - [spoptchev/scientist](https://github.com/spoptchev/scientist) (Kotlin / Java)
548
585
  - [junkpiano/scientist](https://github.com/junkpiano/scientist) (Swift)
586
+ - [serverless scientist](http://serverlessscientist.com/) (AWS Lambda)
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)
549
589
 
550
590
  ## Maintainers
551
591
 
@@ -9,12 +9,22 @@ module Scientist::Experiment
9
9
  # If this is nil, raise_on_mismatches class attribute is used instead.
10
10
  attr_accessor :raise_on_mismatches
11
11
 
12
- # Create a new instance of a class that implements the Scientist::Experiment
13
- # interface.
14
- #
15
- # Override this method directly to change the default implementation.
12
+ def self.included(base)
13
+ self.set_default(base) if base.instance_of?(Class)
14
+ base.extend RaiseOnMismatch
15
+ end
16
+
17
+ # Instantiate a new experiment (using the class given to the .set_default method).
16
18
  def self.new(name)
17
- Scientist::Default.new(name)
19
+ (@experiment_klass || Scientist::Default).new(name)
20
+ end
21
+
22
+ # Configure Scientist to use the given class for all future experiments
23
+ # (must implement the Scientist::Experiment interface).
24
+ #
25
+ # Called automatically when new experiments are defined.
26
+ def self.set_default(klass)
27
+ @experiment_klass = klass
18
28
  end
19
29
 
20
30
  # A mismatch, raised when raise_on_mismatches is enabled.
@@ -67,10 +77,6 @@ module Scientist::Experiment
67
77
  end
68
78
  end
69
79
 
70
- def self.included(base)
71
- base.extend RaiseOnMismatch
72
- end
73
-
74
80
  # Define a block of code to run before an experiment begins, if the experiment
75
81
  # is enabled.
76
82
  #
@@ -78,6 +84,7 @@ module Scientist::Experiment
78
84
  #
79
85
  # Returns the configured block.
80
86
  def before_run(&block)
87
+ marshalize(block)
81
88
  @_scientist_before_run = block
82
89
  end
83
90
 
@@ -93,6 +100,7 @@ module Scientist::Experiment
93
100
  #
94
101
  # Returns the configured block.
95
102
  def clean(&block)
103
+ marshalize(block)
96
104
  @_scientist_cleaner = block
97
105
  end
98
106
 
@@ -125,9 +133,21 @@ module Scientist::Experiment
125
133
  #
126
134
  # Returns the block.
127
135
  def compare(*args, &block)
136
+ marshalize(block)
128
137
  @_scientist_comparator = block
129
138
  end
130
139
 
140
+ # A block which compares two experimental errors.
141
+ #
142
+ # The block must take two arguments, the control Error and a candidate Error,
143
+ # and return true or false.
144
+ #
145
+ # Returns the block.
146
+ def compare_errors(*args, &block)
147
+ marshalize(block)
148
+ @_scientist_error_comparator = block
149
+ end
150
+
131
151
  # A Symbol-keyed Hash of extra experiment data.
132
152
  def context(context = nil)
133
153
  @_scientist_context ||= {}
@@ -143,6 +163,7 @@ module Scientist::Experiment
143
163
  #
144
164
  # This can be called more than once with different blocks to use.
145
165
  def ignore(&block)
166
+ marshalize(block)
146
167
  @_scientist_ignores ||= []
147
168
  @_scientist_ignores << block
148
169
  end
@@ -171,13 +192,9 @@ module Scientist::Experiment
171
192
  "experiment"
172
193
  end
173
194
 
174
- # Internal: compare two observations, using the configured compare block if present.
195
+ # Internal: compare two observations, using the configured compare and compare_errors lambdas if present.
175
196
  def observations_are_equivalent?(a, b)
176
- if @_scientist_comparator
177
- a.equivalent_to?(b, &@_scientist_comparator)
178
- else
179
- a.equivalent_to? b
180
- end
197
+ a.equivalent_to? b, @_scientist_comparator, @_scientist_error_comparator
181
198
  rescue StandardError => ex
182
199
  raised :compare, ex
183
200
  false
@@ -216,17 +233,7 @@ module Scientist::Experiment
216
233
  @_scientist_before_run.call
217
234
  end
218
235
 
219
- observations = []
220
-
221
- behaviors.keys.shuffle.each do |key|
222
- block = behaviors[key]
223
- fabricated_duration = @_scientist_fabricated_durations && @_scientist_fabricated_durations[key]
224
- observations << Scientist::Observation.new(key, self, fabricated_duration: fabricated_duration, &block)
225
- end
226
-
227
- control = observations.detect { |o| o.name == name }
228
-
229
- result = Scientist::Result.new self, observations, control
236
+ result = generate_result(name)
230
237
 
231
238
  begin
232
239
  publish(result)
@@ -242,15 +249,14 @@ module Scientist::Experiment
242
249
  end
243
250
  end
244
251
 
245
- if control.raised?
246
- raise control.exception
247
- else
248
- control.value
249
- end
252
+ control = result.control
253
+ raise control.exception if control.raised?
254
+ control.value
250
255
  end
251
256
 
252
257
  # Define a block that determines whether or not the experiment should run.
253
258
  def run_if(&block)
259
+ marshalize(block)
254
260
  @_scientist_run_if_block = block
255
261
  end
256
262
 
@@ -276,6 +282,7 @@ module Scientist::Experiment
276
282
 
277
283
  # Register a named behavior for this experiment, default "candidate".
278
284
  def try(name = nil, &block)
285
+ marshalize(block)
279
286
  name = (name || "candidate").to_s
280
287
 
281
288
  if behaviors.include?(name)
@@ -287,6 +294,7 @@ module Scientist::Experiment
287
294
 
288
295
  # Register the control behavior for this experiment.
289
296
  def use(&block)
297
+ marshalize(block)
290
298
  try "control", &block
291
299
  end
292
300
 
@@ -304,4 +312,33 @@ module Scientist::Experiment
304
312
  def fabricate_durations_for_testing_purposes(fabricated_durations = {})
305
313
  @_scientist_fabricated_durations = fabricated_durations
306
314
  end
315
+
316
+ # Internal: Generate the observations and create the result from those and the control.
317
+ def generate_result(name)
318
+ observations = []
319
+
320
+ behaviors.keys.shuffle.each do |key|
321
+ block = behaviors[key]
322
+ fabricated_duration = @_scientist_fabricated_durations && @_scientist_fabricated_durations[key]
323
+ observations << Scientist::Observation.new(key, self, fabricated_duration: fabricated_duration, &block)
324
+ end
325
+
326
+ control = observations.detect { |o| o.name == name }
327
+ Scientist::Result.new(self, observations, control)
328
+ end
329
+
330
+ private
331
+
332
+ # In order to support marshaling, we have to make the procs marshalable. Some
333
+ # CI providers attempt to marshal Scientist mismatch errors so that they can
334
+ # be sent out to different places (logs, etc.) The mismatch errors contain
335
+ # code from the experiment. This code contains Procs - which can't be
336
+ # marshaled until we run the following code.
337
+ def marshalize(block)
338
+ unless block.respond_to?(:_dump) || block.respond_to?(:_dump_data)
339
+ def block._dump(_)
340
+ to_s
341
+ end
342
+ end
343
+ end
307
344
  end
@@ -8,9 +8,6 @@ class Scientist::Observation
8
8
  # The experiment this observation is for
9
9
  attr_reader :experiment
10
10
 
11
- # The instant observation began.
12
- attr_reader :now
13
-
14
11
  # The String name of the behavior.
15
12
  attr_reader :name
16
13
 
@@ -26,7 +23,6 @@ class Scientist::Observation
26
23
  def initialize(name, experiment, fabricated_duration: nil, &block)
27
24
  @name = name
28
25
  @experiment = experiment
29
- @now = Time.now
30
26
 
31
27
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) unless fabricated_duration
32
28
  begin
@@ -49,25 +45,34 @@ class Scientist::Observation
49
45
 
50
46
  # Is this observation equivalent to another?
51
47
  #
52
- # other - the other Observation in question
53
- # comparator - an optional comparison block. This observation's value and the
54
- # other observation's value are yielded to this to determine
55
- # 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.
56
55
  #
57
56
  # Returns true if:
58
57
  #
59
58
  # * The values of the observation are equal (using `==`)
60
59
  # * The values of the observations are equal according to a comparison
61
- # 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.
62
63
  # * Both observations raised an exception with the same class and message.
63
64
  #
64
65
  # Returns false otherwise.
65
- def equivalent_to?(other, &comparator)
66
+ def equivalent_to?(other, comparator=nil, error_comparator=nil)
66
67
  return false unless other.is_a?(Scientist::Observation)
67
68
 
68
69
  if raised? || other.raised?
69
- return other.exception.class == exception.class &&
70
- 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
71
76
  end
72
77
 
73
78
  if comparator
@@ -1,3 +1,3 @@
1
1
  module Scientist
2
- VERSION = "1.4.0"
2
+ VERSION = "1.6.2"
3
3
  end
data/script/release CHANGED
@@ -12,10 +12,10 @@ cd $(dirname "$0")/..
12
12
  rm -rf scientist-*.gem
13
13
  gem build -q scientist.gemspec
14
14
 
15
- # Make sure we're on the master branch.
15
+ # Make sure we're on the main branch.
16
16
 
17
- (git branch --no-color | grep -q '* master') || {
18
- echo "Only release from the master branch."
17
+ (git branch --no-color | grep -q '* main') || {
18
+ echo "Only release from the main branch."
19
19
  exit 1
20
20
  }
21
21
 
@@ -35,4 +35,4 @@ git fetch -t origin
35
35
  # Tag it and bag it.
36
36
 
37
37
  gem push scientist-*.gem && git tag "$tag" &&
38
- git push origin master && git push origin "$tag"
38
+ git push origin main && git push origin "$tag"
@@ -2,6 +2,9 @@ describe Scientist::Experiment do
2
2
  class Fake
3
3
  include Scientist::Experiment
4
4
 
5
+ # Undo auto-config magic / preserve default behavior of Scientist::Experiment.new
6
+ Scientist::Experiment.set_default(nil)
7
+
5
8
  def initialize(*args)
6
9
  end
7
10
 
@@ -28,6 +31,24 @@ describe Scientist::Experiment do
28
31
  @ex = Fake.new
29
32
  end
30
33
 
34
+ it "sets the default on inclusion" do
35
+ klass = Class.new do
36
+ include Scientist::Experiment
37
+
38
+ def initialize(name)
39
+ end
40
+ end
41
+
42
+ assert_kind_of klass, Scientist::Experiment.new("hello")
43
+
44
+ Scientist::Experiment.set_default(nil)
45
+ end
46
+
47
+ it "doesn't set the default on inclusion when it's a module" do
48
+ Module.new { include Scientist::Experiment }
49
+ assert_kind_of Scientist::Default, Scientist::Experiment.new("hello")
50
+ end
51
+
31
52
  it "has a default implementation" do
32
53
  ex = Scientist::Experiment.new("hello")
33
54
  assert_kind_of Scientist::Default, ex
@@ -180,6 +201,18 @@ describe Scientist::Experiment do
180
201
  assert @ex.published_result.matched?
181
202
  end
182
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
+
183
216
  it "knows how to compare two experiments" do
184
217
  a = Scientist::Observation.new(@ex, "a") { 1 }
185
218
  b = Scientist::Observation.new(@ex, "b") { 2 }
@@ -458,6 +491,27 @@ describe Scientist::Experiment do
458
491
  assert_raises(Scientist::Experiment::MismatchError) { runner.call }
459
492
  end
460
493
 
494
+ it "can be marshaled" do
495
+ Fake.raise_on_mismatches = true
496
+ @ex.before_run { "some block" }
497
+ @ex.clean { "some block" }
498
+ @ex.compare_errors { "some block" }
499
+ @ex.ignore { false }
500
+ @ex.run_if { "some block" }
501
+ @ex.try { "candidate" }
502
+ @ex.use { "control" }
503
+ @ex.compare { |control, candidate| control == candidate }
504
+
505
+ mismatch = nil
506
+ begin
507
+ @ex.run
508
+ rescue Scientist::Experiment::MismatchError => e
509
+ mismatch = e
510
+ end
511
+
512
+ assert_kind_of(String, Marshal.dump(mismatch))
513
+ end
514
+
461
515
  describe "#raise_on_mismatches?" do
462
516
  it "raises when there is a mismatch if the experiment instance's raise on mismatches is enabled" do
463
517
  Fake.raise_on_mismatches = false
@@ -546,7 +600,7 @@ candidate:
546
600
  assert_equal " \"value\"", lines[2]
547
601
  assert_equal "candidate:", lines[3]
548
602
  assert_equal " #<RuntimeError: error>", lines[4]
549
- assert_match %r( test/scientist/experiment_test.rb:\d+:in `block), lines[5]
603
+ assert_match %r(test/scientist/experiment_test.rb:\d+:in `block), lines[5]
550
604
  end
551
605
  end
552
606
  end
@@ -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.4.0
4
+ version: 1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
@@ -9,10 +9,10 @@ authors:
9
9
  - Rick Bradley
10
10
  - Jesse Toth
11
11
  - Nathan Witmer
12
- autorequire:
12
+ autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2019-09-20 00:00:00.000000000 Z
15
+ date: 2021-11-04 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: minitest
@@ -80,7 +80,7 @@ homepage: https://github.com/github/scientist
80
80
  licenses:
81
81
  - MIT
82
82
  metadata: {}
83
- post_install_message:
83
+ post_install_message:
84
84
  rdoc_options: []
85
85
  require_paths:
86
86
  - lib
@@ -95,8 +95,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  requirements: []
98
- rubygems_version: 3.0.3
99
- signing_key:
98
+ rubygems_version: 3.2.22
99
+ signing_key:
100
100
  specification_version: 4
101
101
  summary: Carefully test, measure, and track refactored code.
102
102
  test_files: