scientist 1.4.0 → 1.6.2

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
  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: