scientist 0.0.0 → 0.0.1

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.
@@ -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