Skip to content

Commit 3fa27b8

Browse files
authored
Merge pull request #77 from cdwort/support_error_comparison
Support error comparison
2 parents ef7fa3f + 556aaf4 commit 3fa27b8

File tree

5 files changed

+102
-19
lines changed

5 files changed

+102
-19
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,38 @@ class MyWidget
109109
end
110110
```
111111

112+
If either the control block or candidate block raises an error, Scientist compares the two observations' classes and messages using `==`. To override this behavior, use `compare_error` to define how to compare observed errors instead:
113+
114+
```ruby
115+
class MyWidget
116+
include Scientist
117+
118+
def slug_from_login(login)
119+
science "slug_from_login" do |e|
120+
e.use { User.slug_from_login login } # returns String instance or ArgumentError
121+
e.try { UserService.slug_from_login login } # returns String instance or ArgumentError
122+
123+
compare_error_message_and_class = -> (control, candidate) do
124+
control.class == candidate.class &&
125+
control.message == candidate.message
126+
end
127+
128+
compare_argument_errors = -> (control, candidate) do
129+
control.class == ArgumentError &&
130+
candidate.class == ArgumentError &&
131+
control.message.start_with?("Input has invalid characters") &&
132+
candidate.message.star_with?("Invalid characters in input")
133+
end
134+
135+
e.compare_error do |control, candidate|
136+
compare_error_message_and_class.call(control, candidate) ||
137+
compare_argument_errors.call(control, candidate)
138+
end
139+
end
140+
end
141+
end
142+
```
143+
112144
### Adding context
113145

114146
Results aren't very useful without some way to identify them. Use the `context` method to add to or retrieve the context for an experiment:
@@ -228,7 +260,7 @@ def admin?(user)
228260
end
229261
```
230262

231-
The ignore blocks are only called if the *values* don't match. If one observation raises an exception and the other doesn't, it's always considered a mismatch. If both observations raise different exceptions, that is also considered a mismatch.
263+
The ignore blocks are only called if the *values* don't match. Unless a `compare_error` comparator is defined, two cases are considered mismatches: a) one observation raising an exception and the other not, b) observations raising exceptions with different classes or messages.
232264

233265
### Enabling/disabling experiments
234266

lib/scientist/experiment.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ def compare(*args, &block)
134134
@_scientist_comparator = block
135135
end
136136

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+
137147
# A Symbol-keyed Hash of extra experiment data.
138148
def context(context = nil)
139149
@_scientist_context ||= {}
@@ -177,13 +187,9 @@ def name
177187
"experiment"
178188
end
179189

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.
181191
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
187193
rescue StandardError => ex
188194
raised :compare, ex
189195
false

lib/scientist/observation.rb

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,34 @@ def cleaned_value
4545

4646
# Is this observation equivalent to another?
4747
#
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.
5255
#
5356
# Returns true if:
5457
#
5558
# * The values of the observation are equal (using `==`)
5659
# * 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.
5863
# * Both observations raised an exception with the same class and message.
5964
#
6065
# Returns false otherwise.
61-
def equivalent_to?(other, &comparator)
66+
def equivalent_to?(other, comparator=nil, error_comparator=nil)
6267
return false unless other.is_a?(Scientist::Observation)
6368

6469
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
6776
end
6877

6978
if comparator

test/scientist/experiment_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ def @ex.publish(result)
201201
assert @ex.published_result.matched?
202202
end
203203

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+
204216
it "knows how to compare two experiments" do
205217
a = Scientist::Observation.new(@ex, "a") { 1 }
206218
b = Scientist::Observation.new(@ex, "b") { 2 }

test/scientist/observation_test.rb

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
refute x.equivalent_to?(y)
8181
end
8282

83-
FirstErrror = Class.new(StandardError)
83+
FirstError = Class.new(StandardError)
8484
SecondError = Class.new(StandardError)
8585

8686
it "compares exception classes" do
@@ -92,22 +92,46 @@
9292
refute x.equivalent_to?(y)
9393
end
9494

95-
it "compares values using a comparator block" do
95+
it "compares values using a comparator proc" do
9696
a = Scientist::Observation.new("test", @experiment) { 1 }
9797
b = Scientist::Observation.new("test", @experiment) { "1" }
9898

9999
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)
101104

102105
yielded = []
103-
a.equivalent_to?(b) do |x, y|
106+
compare_appends = -> (x, y) do
104107
yielded << x
105108
yielded << y
106109
true
107110
end
111+
a.equivalent_to?(b, compare_appends)
112+
108113
assert_equal [a.value, b.value], yielded
109114
end
110115

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+
111135
describe "#cleaned_value" do
112136
it "returns the observation's value by default" do
113137
a = Scientist::Observation.new("test", @experiment) { 1 }

0 commit comments

Comments
 (0)