scientist 1.5.0 → 1.6.3

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: c2831c4fc4f76eb1e8e822a21e613a07e33229bdfe47528cd4555b72ecb05a7c
4
- data.tar.gz: e0e003dbbe7ecb82749c41aff85ff6b4174e6169dce34081d20f325f62959f8d
3
+ metadata.gz: 53663e0ed4b522336c69b73073aa144a4b6c8178e3fcd716c0e4519a66e308ba
4
+ data.tar.gz: b60d0c55dce9e1e9a23eb7e6ce4c445bf01340dcfd459d50716d7cd85e405b93
5
5
  SHA512:
6
- metadata.gz: 07f99bcd0d9619205f42f15f16cc00b31fff3fab127344ca70360c51eea28d3eb15bc21e9471edbccfb06ddf2f07bc93ac0024af3899371486757fb53dbb5710
7
- data.tar.gz: cb7a3cf609e501a5d4a7894cf62400dacde1dc654ec7caf5d9beb8d115bb4f0c1e217cdeddd7c72863385459bfd7bb929221df74725312c3dfe21af315598486
6
+ metadata.gz: 6e1baf64882c857abdd3fd02b77785a783a8694854358c78a9da3be213b42ac88e36805ebf7f10f0d87fb14d7808302e1afffd2793c5ffc21addec5326e5f969
7
+ data.tar.gz: e5a65140ca8704c7443f88404cfb9e971f4a2925d756eed18d7482be74aab3243315fda727dfe8fed13d0732d76ca442a12cb7af200ca6ae3e4de4b0f1bab1a2
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,7 +24,7 @@ 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.
@@ -84,6 +84,8 @@ class MyExperiment
84
84
  end
85
85
  ```
86
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
+
87
89
  Now calls to the `science` helper will load instances of `MyExperiment`.
88
90
 
89
91
  ### Controlling comparison
@@ -107,6 +109,38 @@ class MyWidget
107
109
  end
108
110
  ```
109
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
+
110
144
  ### Adding context
111
145
 
112
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:
@@ -226,7 +260,7 @@ def admin?(user)
226
260
  end
227
261
  ```
228
262
 
229
- 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.
230
264
 
231
265
  ### Enabling/disabling experiments
232
266
 
@@ -393,6 +427,8 @@ Scientist rescues and tracks _all_ exceptions raised in a `try` or `use` block,
393
427
  Scientist::Observation::RESCUES.replace [StandardError]
394
428
  ```
395
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
+
396
432
  #### In a Scientist callback
397
433
 
398
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:
@@ -548,6 +584,8 @@ Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs t
548
584
  - [spoptchev/scientist](https://github.com/spoptchev/scientist) (Kotlin / Java)
549
585
  - [junkpiano/scientist](https://github.com/junkpiano/scientist) (Swift)
550
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)
551
589
 
552
590
  ## Maintainers
553
591
 
@@ -10,7 +10,7 @@ module Scientist::Experiment
10
10
  attr_accessor :raise_on_mismatches
11
11
 
12
12
  def self.included(base)
13
- self.set_default(base)
13
+ self.set_default(base) if base.instance_of?(Class)
14
14
  base.extend RaiseOnMismatch
15
15
  end
16
16
 
@@ -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
@@ -312,4 +318,20 @@ module Scientist::Experiment
312
318
  control = observations.detect { |o| o.name == name }
313
319
  Scientist::Result.new(self, observations, control)
314
320
  end
321
+
322
+ private
323
+
324
+ # In order to support marshaling, we have to make the procs marshalable. Some
325
+ # CI providers attempt to marshal Scientist mismatch errors so that they can
326
+ # be sent out to different places (logs, etc.) The mismatch errors contain
327
+ # code from the experiment. This code contains procs. These procs prevent the
328
+ # error from being marshaled. To fix this, we simple exclude the procs from
329
+ # the data that we marshal.
330
+ def marshal_dump
331
+ [@name, @result, @raise_on_mismatches]
332
+ end
333
+
334
+ def marshal_load
335
+ @name, @result, @raise_on_mismatches = array
336
+ end
315
337
  end
@@ -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.5.0"
2
+ VERSION = "1.6.3"
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"
@@ -31,6 +31,24 @@ describe Scientist::Experiment do
31
31
  @ex = Fake.new
32
32
  end
33
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
+
34
52
  it "has a default implementation" do
35
53
  ex = Scientist::Experiment.new("hello")
36
54
  assert_kind_of Scientist::Default, ex
@@ -183,6 +201,18 @@ describe Scientist::Experiment do
183
201
  assert @ex.published_result.matched?
184
202
  end
185
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
+
186
216
  it "knows how to compare two experiments" do
187
217
  a = Scientist::Observation.new(@ex, "a") { 1 }
188
218
  b = Scientist::Observation.new(@ex, "b") { 2 }
@@ -461,6 +491,27 @@ describe Scientist::Experiment do
461
491
  assert_raises(Scientist::Experiment::MismatchError) { runner.call }
462
492
  end
463
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
+
464
515
  describe "#raise_on_mismatches?" do
465
516
  it "raises when there is a mismatch if the experiment instance's raise on mismatches is enabled" do
466
517
  Fake.raise_on_mismatches = false
@@ -549,7 +600,7 @@ candidate:
549
600
  assert_equal " \"value\"", lines[2]
550
601
  assert_equal "candidate:", lines[3]
551
602
  assert_equal " #<RuntimeError: error>", lines[4]
552
- 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]
553
604
  end
554
605
  end
555
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.5.0
4
+ version: 1.6.3
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: 2020-09-08 00:00:00.000000000 Z
15
+ date: 2021-12-09 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.2
98
+ rubygems_version: 3.2.32
99
99
  signing_key:
100
100
  specification_version: 4
101
101
  summary: Carefully test, measure, and track refactored code.