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 +4 -4
- data/.travis.yml +4 -4
- data/CONTRIBUTING.md +3 -3
- data/README.md +52 -12
- data/lib/scientist/experiment.rb +68 -31
- data/lib/scientist/observation.rb +17 -12
- data/lib/scientist/version.rb +1 -1
- data/script/release +4 -4
- data/test/scientist/experiment_test.rb +55 -1
- data/test/scientist/observation_test.rb +28 -4
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e623b9acbede185a6ee8ca0ae2227a52707499c28bdf04e4fe8a2afcbd0f051
|
4
|
+
data.tar.gz: 560b1d3353829f6be217229546297fcd4102e6c47dc87fc12ffa963c5e2fa3f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 593c2807ff16f1381a0189e4babdad3ab366ad66057e4af1b083611fb204d55ae9434bf989550f8371de7586be55fe4f8a760f84f2424323a9189803583e9243
|
7
|
+
data.tar.gz: 1239bb86dfad9fcee2b0dc4dc2fc8458306e877afd2aa1e722b9e4db7072b71658469bf48b425959c52111fac2cc8bb5dbbb11435263a199f036ad2ee4697228
|
data/.travis.yml
CHANGED
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
*
|
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.
|
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
|
|
data/lib/scientist/experiment.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
195
|
+
# Internal: compare two observations, using the configured compare and compare_errors lambdas if present.
|
175
196
|
def observations_are_equivalent?(a, b)
|
176
|
-
|
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
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
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
|
53
|
-
# comparator
|
54
|
-
#
|
55
|
-
#
|
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
|
-
#
|
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,
|
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
|
-
|
70
|
-
|
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
|
data/lib/scientist/version.rb
CHANGED
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
|
15
|
+
# Make sure we're on the main branch.
|
16
16
|
|
17
|
-
(git branch --no-color | grep -q '*
|
18
|
-
echo "Only release from the
|
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
|
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(
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
+
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:
|
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.
|
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:
|