scientist 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,398 @@
1
+ describe Scientist::Experiment do
2
+ class Fake
3
+ include Scientist::Experiment
4
+
5
+ def initialize(*args)
6
+ end
7
+
8
+ def enabled?
9
+ true
10
+ end
11
+
12
+ attr_reader :published_result
13
+
14
+ def exceptions
15
+ @exceptions ||= []
16
+ end
17
+
18
+ def raised(op, exception)
19
+ exceptions << [op, exception]
20
+ end
21
+
22
+ def publish(result)
23
+ @published_result = result
24
+ end
25
+ end
26
+
27
+ before do
28
+ @ex = Fake.new
29
+ end
30
+
31
+ it "has a default implementation" do
32
+ ex = Scientist::Experiment.new("hello")
33
+ assert_kind_of Scientist::Default, ex
34
+ assert_equal "hello", ex.name
35
+ end
36
+
37
+ it "provides a static default name" do
38
+ assert_equal "experiment", Fake.new.name
39
+ end
40
+
41
+ it "requires includers to implement enabled?" do
42
+ obj = Object.new
43
+ obj.extend Scientist::Experiment
44
+
45
+ assert_raises NoMethodError do
46
+ obj.enabled?
47
+ end
48
+ end
49
+
50
+ it "requires includers to implement publish" do
51
+ obj = Object.new
52
+ obj.extend Scientist::Experiment
53
+
54
+ assert_raises NoMethodError do
55
+ obj.publish("result")
56
+ end
57
+ end
58
+
59
+ it "can't be run without a control behavior" do
60
+ e = assert_raises Scientist::BehaviorMissing do
61
+ @ex.run
62
+ end
63
+
64
+ assert_equal "control", e.name
65
+ end
66
+
67
+ it "is a straight pass-through with only a control behavior" do
68
+ @ex.use { "control" }
69
+ assert_equal "control", @ex.run
70
+ end
71
+
72
+ it "runs other behaviors but always returns the control" do
73
+ @ex.use { "control" }
74
+ @ex.try { "candidate" }
75
+
76
+ assert_equal "control", @ex.run
77
+ end
78
+
79
+ it "complains about duplicate behavior names" do
80
+ @ex.use { "control" }
81
+
82
+ e = assert_raises Scientist::BehaviorNotUnique do
83
+ @ex.use { "control-again" }
84
+ end
85
+
86
+ assert_equal @ex, e.experiment
87
+ assert_equal "control", e.name
88
+ end
89
+
90
+ it "swallows exceptions raised by candidate behaviors" do
91
+ @ex.use { "control" }
92
+ @ex.try { raise "candidate" }
93
+
94
+ assert_equal "control", @ex.run
95
+ end
96
+
97
+ it "passes through exceptions raised by the control behavior" do
98
+ @ex.use { raise "control" }
99
+ @ex.try { "candidate" }
100
+
101
+ exception = assert_raises RuntimeError do
102
+ @ex.run
103
+ end
104
+
105
+ assert_equal "control", exception.message
106
+ end
107
+
108
+ it "shuffles behaviors before running" do
109
+ last = nil
110
+ runs = []
111
+
112
+ @ex.use { last = "control" }
113
+ @ex.try { last = "candidate" }
114
+
115
+ 10000.times do
116
+ @ex.run
117
+ runs << last
118
+ end
119
+
120
+ assert runs.uniq.size > 1
121
+ end
122
+
123
+ it "re-raises exceptions raised during publish by default" do
124
+ ex = Scientist::Experiment.new("hello")
125
+ assert_kind_of Scientist::Default, ex
126
+ def ex.publish(result)
127
+ raise "boomtown"
128
+ end
129
+
130
+ ex.use { "control" }
131
+ ex.try { "candidate" }
132
+
133
+ exception = assert_raises RuntimeError do
134
+ ex.run
135
+ end
136
+
137
+ assert_equal "boomtown", exception.message
138
+ end
139
+
140
+ it "reports publishing errors" do
141
+ def @ex.publish(result)
142
+ raise "boomtown"
143
+ end
144
+
145
+ @ex.use { "control" }
146
+ @ex.try { "candidate" }
147
+
148
+ assert_equal "control", @ex.run
149
+
150
+ op, exception = @ex.exceptions.pop
151
+
152
+ assert_equal :publish, op
153
+ assert_equal "boomtown", exception.message
154
+ end
155
+
156
+ it "publishes results" do
157
+ @ex.use { 1 }
158
+ @ex.try { 1 }
159
+ assert_equal 1, @ex.run
160
+ assert @ex.published_result
161
+ end
162
+
163
+ it "does not publish results when there is only a control value" do
164
+ @ex.use { 1 }
165
+ assert_equal 1, @ex.run
166
+ assert_nil @ex.published_result
167
+ end
168
+
169
+ it "compares results with a comparator block if provided" do
170
+ @ex.compare { |a, b| a == b.to_s }
171
+ @ex.use { "1" }
172
+ @ex.try { 1 }
173
+
174
+ assert_equal "1", @ex.run
175
+ assert @ex.published_result.matched?
176
+ end
177
+
178
+ it "knows how to compare two experiments" do
179
+ a = Scientist::Observation.new(@ex, "a") { 1 }
180
+ b = Scientist::Observation.new(@ex, "b") { 2 }
181
+
182
+ assert @ex.observations_are_equivalent?(a, a)
183
+ refute @ex.observations_are_equivalent?(a, b)
184
+ end
185
+
186
+ it "uses a compare block to determine if observations are equivalent" do
187
+ a = Scientist::Observation.new(@ex, "a") { "1" }
188
+ b = Scientist::Observation.new(@ex, "b") { 1 }
189
+ @ex.compare { |x, y| x == y.to_s }
190
+ assert @ex.observations_are_equivalent?(a, b)
191
+ end
192
+
193
+ it "reports errors in a compare block" do
194
+ @ex.compare { raise "boomtown" }
195
+ @ex.use { "control" }
196
+ @ex.try { "candidate" }
197
+
198
+ assert_equal "control", @ex.run
199
+
200
+ op, exception = @ex.exceptions.pop
201
+
202
+ assert_equal :compare, op
203
+ assert_equal "boomtown", exception.message
204
+ end
205
+
206
+ it "reports errors in the enabled? method" do
207
+ def @ex.enabled?
208
+ raise "kaboom"
209
+ end
210
+
211
+ @ex.use { "control" }
212
+ @ex.try { "candidate" }
213
+ assert_equal "control", @ex.run
214
+
215
+ op, exception = @ex.exceptions.pop
216
+
217
+ assert_equal :enabled, op
218
+ assert_equal "kaboom", exception.message
219
+ end
220
+
221
+ it "reports errors in a run_if block" do
222
+ @ex.run_if { raise "kaboom" }
223
+ @ex.use { "control" }
224
+ @ex.try { "candidate" }
225
+ assert_equal "control", @ex.run
226
+
227
+ op, exception = @ex.exceptions.pop
228
+
229
+ assert_equal :run_if, op
230
+ assert_equal "kaboom", exception.message
231
+ end
232
+
233
+ it "returns the given value when no clean block is configured" do
234
+ assert_equal 10, @ex.clean_value(10)
235
+ end
236
+
237
+ it "calls the configured clean block with a value when configured" do
238
+ @ex.clean do |value|
239
+ value.upcase
240
+ end
241
+
242
+ assert_equal "TEST", @ex.clean_value("test")
243
+ end
244
+
245
+ it "reports an error and returns the original value when an error is raised in a clean block" do
246
+ @ex.clean { |value| raise "kaboom" }
247
+
248
+ @ex.use { "control" }
249
+ @ex.try { "candidate" }
250
+ assert_equal "control", @ex.run
251
+
252
+ assert_equal "control", @ex.published_result.control.cleaned_value
253
+
254
+ op, exception = @ex.exceptions.pop
255
+
256
+ assert_equal :clean, op
257
+ assert_equal "kaboom", exception.message
258
+ end
259
+
260
+ describe "#run_if" do
261
+ it "does not run the experiment if the given block returns false" do
262
+ candidate_ran = false
263
+ run_check_ran = false
264
+
265
+ @ex.use { 1 }
266
+ @ex.try { candidate_ran = true; 1 }
267
+
268
+ @ex.run_if { run_check_ran = true; false }
269
+
270
+ @ex.run
271
+
272
+ assert run_check_ran
273
+ refute candidate_ran
274
+ end
275
+
276
+ it "runs the experiment if the given block returns true" do
277
+ candidate_ran = false
278
+ run_check_ran = false
279
+
280
+ @ex.use { true }
281
+ @ex.try { candidate_ran = true }
282
+
283
+ @ex.run_if { run_check_ran = true }
284
+
285
+ @ex.run
286
+
287
+ assert run_check_ran
288
+ assert candidate_ran
289
+ end
290
+ end
291
+
292
+ describe "#ignore_mismatched_observation?" do
293
+ before do
294
+ @a = Scientist::Observation.new(@ex, "a") { 1 }
295
+ @b = Scientist::Observation.new(@ex, "b") { 2 }
296
+ end
297
+
298
+ it "does not ignore an observation if no ignores are configured" do
299
+ refute @ex.ignore_mismatched_observation?(@a, @b)
300
+ end
301
+
302
+ it "calls a configured ignore block with the given observed values" do
303
+ called = false
304
+ @ex.ignore do |a, b|
305
+ called = true
306
+ assert_equal @a.value, a
307
+ assert_equal @b.value, b
308
+ true
309
+ end
310
+
311
+ assert @ex.ignore_mismatched_observation?(@a, @b)
312
+ assert called
313
+ end
314
+
315
+ it "calls multiple ignore blocks to see if any match" do
316
+ called_one = called_two = called_three = false
317
+ @ex.ignore { |a, b| called_one = true; false }
318
+ @ex.ignore { |a, b| called_two = true; false }
319
+ @ex.ignore { |a, b| called_three = true; false }
320
+ refute @ex.ignore_mismatched_observation?(@a, @b)
321
+ assert called_one
322
+ assert called_two
323
+ assert called_three
324
+ end
325
+
326
+ it "only calls ignore blocks until one matches" do
327
+ called_one = called_two = called_three = false
328
+ @ex.ignore { |a, b| called_one = true; false }
329
+ @ex.ignore { |a, b| called_two = true; true }
330
+ @ex.ignore { |a, b| called_three = true; false }
331
+ assert @ex.ignore_mismatched_observation?(@a, @b)
332
+ assert called_one
333
+ assert called_two
334
+ refute called_three
335
+ end
336
+
337
+ it "reports exceptions raised in an ignore block and returns false" do
338
+ def @ex.exceptions
339
+ @exceptions ||= []
340
+ end
341
+
342
+ def @ex.raised(op, exception)
343
+ exceptions << [op, exception]
344
+ end
345
+
346
+ @ex.ignore { raise "kaboom" }
347
+
348
+ refute @ex.ignore_mismatched_observation?(@a, @b)
349
+
350
+ op, exception = @ex.exceptions.pop
351
+ assert_equal :ignore, op
352
+ assert_equal "kaboom", exception.message
353
+ end
354
+
355
+ it "skips ignore blocks that raise and tests any remaining blocks if an exception is swallowed" do
356
+ def @ex.exceptions
357
+ @exceptions ||= []
358
+ end
359
+
360
+ # this swallows the exception rather than re-raising
361
+ def @ex.raised(op, exception)
362
+ exceptions << [op, exception]
363
+ end
364
+
365
+ @ex.ignore { raise "kaboom" }
366
+ @ex.ignore { true }
367
+
368
+ assert @ex.ignore_mismatched_observation?(@a, @b)
369
+ assert_equal 1, @ex.exceptions.size
370
+ end
371
+ end
372
+
373
+ describe "raising on mismatches" do
374
+ before do
375
+ @old_raise_on_mismatches = Fake.raise_on_mismatches?
376
+ end
377
+
378
+ after do
379
+ Fake.raise_on_mismatches = @old_raise_on_mismatches
380
+ end
381
+
382
+ it "raises when there is a mismatch if raise on mismatches is enabled" do
383
+ Fake.raise_on_mismatches = true
384
+ @ex.use { "fine" }
385
+ @ex.try { "not fine" }
386
+
387
+ assert_raises(Scientist::Experiment::MismatchError) { @ex.run }
388
+ end
389
+
390
+ it "doesn't raise when there is a mismatch if raise on mismatches is disabled" do
391
+ Fake.raise_on_mismatches = false
392
+ @ex.use { "fine" }
393
+ @ex.try { "not fine" }
394
+
395
+ @ex.run
396
+ end
397
+ end
398
+ end
@@ -0,0 +1,93 @@
1
+ describe Scientist::Observation do
2
+
3
+ before do
4
+ @experiment = Scientist::Experiment.new "test"
5
+ end
6
+
7
+ it "observes and records the execution of a block" do
8
+ ob = Scientist::Observation.new("test", @experiment) do
9
+ sleep 0.1
10
+ "ret"
11
+ end
12
+
13
+ assert_equal "ret", ob.value
14
+ refute ob.raised?
15
+ assert_in_delta 0.1, ob.duration, 0.01
16
+ end
17
+
18
+ it "stashes exceptions" do
19
+ ob = Scientist::Observation.new("test", @experiment) do
20
+ raise "exception"
21
+ end
22
+
23
+ assert ob.raised?
24
+ assert_equal "exception", ob.exception.message
25
+ assert_nil ob.value
26
+ end
27
+
28
+ it "compares values" do
29
+ a = Scientist::Observation.new("test", @experiment) { 1 }
30
+ b = Scientist::Observation.new("test", @experiment) { 1 }
31
+
32
+ assert a.equivalent_to?(b)
33
+
34
+ x = Scientist::Observation.new("test", @experiment) { 1 }
35
+ y = Scientist::Observation.new("test", @experiment) { 2 }
36
+
37
+ refute x.equivalent_to?(y)
38
+ end
39
+
40
+ it "compares exception messages" do
41
+ a = Scientist::Observation.new("test", @experiment) { raise "error" }
42
+ b = Scientist::Observation.new("test", @experiment) { raise "error" }
43
+
44
+ assert a.equivalent_to?(b)
45
+
46
+ x = Scientist::Observation.new("test", @experiment) { raise "error" }
47
+ y = Scientist::Observation.new("test", @experiment) { raise "ERROR" }
48
+
49
+ refute x.equivalent_to?(y)
50
+ end
51
+
52
+ FirstErrror = Class.new(StandardError)
53
+ SecondError = Class.new(StandardError)
54
+
55
+ it "compares exception classes" do
56
+ x = Scientist::Observation.new("test", @experiment) { raise FirstError, "error" }
57
+ y = Scientist::Observation.new("test", @experiment) { raise SecondError, "error" }
58
+ z = Scientist::Observation.new("test", @experiment) { raise FirstError, "error" }
59
+
60
+ assert x.equivalent_to?(z)
61
+ refute x.equivalent_to?(y)
62
+ end
63
+
64
+ it "compares values using a comparator block" do
65
+ a = Scientist::Observation.new("test", @experiment) { 1 }
66
+ b = Scientist::Observation.new("test", @experiment) { "1" }
67
+
68
+ refute a.equivalent_to?(b)
69
+ assert a.equivalent_to?(b) { |x, y| x.to_s == y.to_s }
70
+
71
+ yielded = []
72
+ a.equivalent_to?(b) do |x, y|
73
+ yielded << x
74
+ yielded << y
75
+ true
76
+ end
77
+ assert_equal [a.value, b.value], yielded
78
+ end
79
+
80
+ describe "#cleaned_value" do
81
+ it "returns the observation's value by default" do
82
+ a = Scientist::Observation.new("test", @experiment) { 1 }
83
+ assert_equal 1, a.cleaned_value
84
+ end
85
+
86
+ it "uses the experiment's clean block to clean a value when configured" do
87
+ @experiment.clean { |value| value.upcase }
88
+ a = Scientist::Observation.new("test", @experiment) { "test" }
89
+ assert_equal "TEST", a.cleaned_value
90
+ end
91
+ end
92
+
93
+ end
@@ -0,0 +1,111 @@
1
+ describe Scientist::Result do
2
+ before do
3
+ @experiment = Scientist::Experiment.new "experiment"
4
+ end
5
+
6
+ it "is immutable" do
7
+ control = Scientist::Observation.new("control", @experiment)
8
+ candidate = Scientist::Observation.new("candidate", @experiment)
9
+
10
+ result = Scientist::Result.new @experiment,
11
+ observations: [control, candidate], control: control
12
+
13
+ assert result.frozen?
14
+ end
15
+
16
+ it "evaluates its observations" do
17
+ a = Scientist::Observation.new("a", @experiment) { 1 }
18
+ b = Scientist::Observation.new("b", @experiment) { 1 }
19
+
20
+ assert a.equivalent_to?(b)
21
+
22
+ result = Scientist::Result.new @experiment, observations: [a, b], control: a
23
+ assert result.matched?
24
+ refute result.mismatched?
25
+ assert_equal [], result.mismatched
26
+
27
+ x = Scientist::Observation.new("x", @experiment) { 1 }
28
+ y = Scientist::Observation.new("y", @experiment) { 2 }
29
+ z = Scientist::Observation.new("z", @experiment) { 3 }
30
+
31
+ result = Scientist::Result.new @experiment, observations: [x, y, z], control: x
32
+ refute result.matched?
33
+ assert result.mismatched?
34
+ assert_equal [y, z], result.mismatched
35
+ end
36
+
37
+ it "has no mismatches if there is only a control observation" do
38
+ a = Scientist::Observation.new("a", @experiment) { 1 }
39
+ result = Scientist::Result.new @experiment, observations: [a], control: a
40
+ assert result.matched?
41
+ end
42
+
43
+ it "evaluates observations using the experiment's compare block" do
44
+ a = Scientist::Observation.new("a", @experiment) { "1" }
45
+ b = Scientist::Observation.new("b", @experiment) { 1 }
46
+
47
+ @experiment.compare { |x, y| x == y.to_s }
48
+
49
+ result = Scientist::Result.new @experiment, observations: [a, b], control: a
50
+
51
+ assert result.matched?, result.mismatched
52
+ end
53
+
54
+ it "does not ignore any mismatches when nothing's ignored" do
55
+ x = Scientist::Observation.new("x", @experiment) { 1 }
56
+ y = Scientist::Observation.new("y", @experiment) { 2 }
57
+
58
+ result = Scientist::Result.new @experiment, observations: [x, y], control: x
59
+
60
+ assert result.mismatched?
61
+ refute result.ignored?
62
+ end
63
+
64
+ it "uses the experiment's ignore block to ignore mismatched observations" do
65
+ x = Scientist::Observation.new("x", @experiment) { 1 }
66
+ y = Scientist::Observation.new("y", @experiment) { 2 }
67
+ called = false
68
+ @experiment.ignore { called = true }
69
+
70
+ result = Scientist::Result.new @experiment, observations: [x, y], control: x
71
+
72
+ refute result.mismatched?
73
+ assert result.ignored?
74
+ assert_equal [], result.mismatched
75
+ assert_equal [y], result.ignored
76
+ assert called
77
+ end
78
+
79
+ it "partitions observations into mismatched and ignored when applicable" do
80
+ x = Scientist::Observation.new("x", @experiment) { :x }
81
+ y = Scientist::Observation.new("y", @experiment) { :y }
82
+ z = Scientist::Observation.new("z", @experiment) { :z }
83
+
84
+ @experiment.ignore { |control, candidate| candidate == :y }
85
+
86
+ result = Scientist::Result.new @experiment, observations: [x, y, z], control: x
87
+
88
+ assert result.mismatched?
89
+ assert result.ignored?
90
+ assert_equal [y], result.ignored
91
+ assert_equal [z], result.mismatched
92
+ end
93
+
94
+ it "knows the experiment's name" do
95
+ a = Scientist::Observation.new("a", @experiment) { 1 }
96
+ b = Scientist::Observation.new("b", @experiment) { 1 }
97
+ result = Scientist::Result.new @experiment, observations: [a, b], control: a
98
+
99
+ assert_equal @experiment.name, result.experiment_name
100
+ end
101
+
102
+ it "has the context from an experiment" do
103
+ @experiment.context :foo => :bar
104
+ a = Scientist::Observation.new("a", @experiment) { 1 }
105
+ b = Scientist::Observation.new("b", @experiment) { 1 }
106
+ result = Scientist::Result.new @experiment, observations: [a, b], control: a
107
+
108
+ assert_equal({:foo => :bar}, result.context)
109
+ end
110
+
111
+ end
@@ -1,7 +1,45 @@
1
- require "minitest/autorun"
2
-
3
1
  describe Scientist do
4
- it "exists" do
5
- assert Scientist
2
+ it "has a version or whatever" do
3
+ assert Scientist::VERSION
4
+ end
5
+
6
+ it "provides a helper to instantiate and run experiments" do
7
+ obj = Object.new
8
+ obj.extend(Scientist)
9
+
10
+ r = obj.science "test" do |e|
11
+ e.use { :control }
12
+ e.try { :candidate }
13
+ end
14
+
15
+ assert_equal :control, r
16
+ end
17
+
18
+ it "provides an empty default_scientist_context" do
19
+ obj = Object.new
20
+ obj.extend(Scientist)
21
+
22
+ assert_equal Hash.new, obj.default_scientist_context
23
+ end
24
+
25
+ it "respects default_scientist_context" do
26
+ obj = Object.new
27
+ obj.extend(Scientist)
28
+
29
+ def obj.default_scientist_context
30
+ { :default => true }
31
+ end
32
+
33
+ experiment = nil
34
+
35
+ obj.science "test" do |e|
36
+ experiment = e
37
+ e.context :inline => true
38
+ e.use { }
39
+ end
40
+
41
+ refute_nil experiment
42
+ assert_equal true, experiment.context[:default]
43
+ assert_equal true, experiment.context[:inline]
6
44
  end
7
45
  end