betatest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ begin
2
+ require "rubygems"
3
+ gem "betatest"
4
+ rescue Gem::LoadError
5
+ # do nothing
6
+ end
7
+
8
+ require "betatest"
9
+ require "betatest/spec"
10
+ require "betatest/mock"
11
+
12
+ Betatest.autorun
@@ -0,0 +1,423 @@
1
+ require 'betatest/unit'
2
+ require 'betatest/spec'
3
+
4
+ module Betatest
5
+ ##
6
+ # Subclass Benchmark to create your own benchmark runs. Methods
7
+ # starting with "bench_" get executed on a per-class.
8
+ #
9
+ # See Betatest::Assertions
10
+
11
+ class Benchmark < Test
12
+ def self.io # :nodoc:
13
+ @io
14
+ end
15
+
16
+ def io # :nodoc:
17
+ self.class.io
18
+ end
19
+
20
+ def self.run reporter, options = {} # :nodoc:
21
+ # NOTE: this is truly horrible... but I don't see a way around this ATM.
22
+ @io = reporter.reporters.first.io
23
+ super
24
+ end
25
+
26
+ def self.runnable_methods # :nodoc:
27
+ methods_matching(/^bench_/)
28
+ end
29
+
30
+ ##
31
+ # Returns a set of ranges stepped exponentially from +min+ to
32
+ # +max+ by powers of +base+. Eg:
33
+ #
34
+ # bench_exp(2, 16, 2) # => [2, 4, 8, 16]
35
+
36
+ def self.bench_exp min, max, base = 10
37
+ min = (Math.log10(min) / Math.log10(base)).to_i
38
+ max = (Math.log10(max) / Math.log10(base)).to_i
39
+
40
+ (min..max).map { |m| base ** m }.to_a
41
+ end
42
+
43
+ ##
44
+ # Returns a set of ranges stepped linearly from +min+ to +max+ by
45
+ # +step+. Eg:
46
+ #
47
+ # bench_linear(20, 40, 10) # => [20, 30, 40]
48
+
49
+ def self.bench_linear min, max, step = 10
50
+ (min..max).step(step).to_a
51
+ rescue LocalJumpError # 1.8.6
52
+ r = []; (min..max).step(step) { |n| r << n }; r
53
+ end
54
+
55
+ ##
56
+ # Specifies the ranges used for benchmarking for that class.
57
+ # Defaults to exponential growth from 1 to 10k by powers of 10.
58
+ # Override if you need different ranges for your benchmarks.
59
+ #
60
+ # See also: ::bench_exp and ::bench_linear.
61
+
62
+ def self.bench_range
63
+ bench_exp 1, 10_000
64
+ end
65
+
66
+ ##
67
+ # Runs the given +work+, gathering the times of each run. Range
68
+ # and times are then passed to a given +validation+ proc. Outputs
69
+ # the benchmark name and times in tab-separated format, making it
70
+ # easy to paste into a spreadsheet for graphing or further
71
+ # analysis.
72
+ #
73
+ # Ranges are specified by ::bench_range.
74
+ #
75
+ # Eg:
76
+ #
77
+ # def bench_algorithm
78
+ # validation = proc { |x, y| ... }
79
+ # assert_performance validation do |n|
80
+ # @obj.algorithm(n)
81
+ # end
82
+ # end
83
+
84
+ def assert_performance validation, &work
85
+ range = self.class.bench_range
86
+
87
+ io.print "#{self.name}"
88
+
89
+ times = []
90
+
91
+ range.each do |x|
92
+ GC.start
93
+ t0 = Time.now
94
+ instance_exec(x, &work)
95
+ t = Time.now - t0
96
+
97
+ io.print "\t%9.6f" % t
98
+ times << t
99
+ end
100
+ io.puts
101
+
102
+ validation[range, times]
103
+ end
104
+
105
+ ##
106
+ # Runs the given +work+ and asserts that the times gathered fit to
107
+ # match a constant rate (eg, linear slope == 0) within a given
108
+ # +threshold+. Note: because we're testing for a slope of 0, R^2
109
+ # is not a good determining factor for the fit, so the threshold
110
+ # is applied against the slope itself. As such, you probably want
111
+ # to tighten it from the default.
112
+ #
113
+ # See http://www.graphpad.com/curvefit/goodness_of_fit.htm for
114
+ # more details.
115
+ #
116
+ # Fit is calculated by #fit_linear.
117
+ #
118
+ # Ranges are specified by ::bench_range.
119
+ #
120
+ # Eg:
121
+ #
122
+ # def bench_algorithm
123
+ # assert_performance_constant 0.9999 do |n|
124
+ # @obj.algorithm(n)
125
+ # end
126
+ # end
127
+
128
+ def assert_performance_constant threshold = 0.99, &work
129
+ validation = proc do |range, times|
130
+ a, b, rr = fit_linear range, times
131
+ assert_in_delta 0, b, 1 - threshold
132
+ [a, b, rr]
133
+ end
134
+
135
+ assert_performance validation, &work
136
+ end
137
+
138
+ ##
139
+ # Runs the given +work+ and asserts that the times gathered fit to
140
+ # match a exponential curve within a given error +threshold+.
141
+ #
142
+ # Fit is calculated by #fit_exponential.
143
+ #
144
+ # Ranges are specified by ::bench_range.
145
+ #
146
+ # Eg:
147
+ #
148
+ # def bench_algorithm
149
+ # assert_performance_exponential 0.9999 do |n|
150
+ # @obj.algorithm(n)
151
+ # end
152
+ # end
153
+
154
+ def assert_performance_exponential threshold = 0.99, &work
155
+ assert_performance validation_for_fit(:exponential, threshold), &work
156
+ end
157
+
158
+ ##
159
+ # Runs the given +work+ and asserts that the times gathered fit to
160
+ # match a logarithmic curve within a given error +threshold+.
161
+ #
162
+ # Fit is calculated by #fit_logarithmic.
163
+ #
164
+ # Ranges are specified by ::bench_range.
165
+ #
166
+ # Eg:
167
+ #
168
+ # def bench_algorithm
169
+ # assert_performance_logarithmic 0.9999 do |n|
170
+ # @obj.algorithm(n)
171
+ # end
172
+ # end
173
+
174
+ def assert_performance_logarithmic threshold = 0.99, &work
175
+ assert_performance validation_for_fit(:logarithmic, threshold), &work
176
+ end
177
+
178
+ ##
179
+ # Runs the given +work+ and asserts that the times gathered fit to
180
+ # match a straight line within a given error +threshold+.
181
+ #
182
+ # Fit is calculated by #fit_linear.
183
+ #
184
+ # Ranges are specified by ::bench_range.
185
+ #
186
+ # Eg:
187
+ #
188
+ # def bench_algorithm
189
+ # assert_performance_linear 0.9999 do |n|
190
+ # @obj.algorithm(n)
191
+ # end
192
+ # end
193
+
194
+ def assert_performance_linear threshold = 0.99, &work
195
+ assert_performance validation_for_fit(:linear, threshold), &work
196
+ end
197
+
198
+ ##
199
+ # Runs the given +work+ and asserts that the times gathered curve
200
+ # fit to match a power curve within a given error +threshold+.
201
+ #
202
+ # Fit is calculated by #fit_power.
203
+ #
204
+ # Ranges are specified by ::bench_range.
205
+ #
206
+ # Eg:
207
+ #
208
+ # def bench_algorithm
209
+ # assert_performance_power 0.9999 do |x|
210
+ # @obj.algorithm
211
+ # end
212
+ # end
213
+
214
+ def assert_performance_power threshold = 0.99, &work
215
+ assert_performance validation_for_fit(:power, threshold), &work
216
+ end
217
+
218
+ ##
219
+ # Takes an array of x/y pairs and calculates the general R^2 value.
220
+ #
221
+ # See: http://en.wikipedia.org/wiki/Coefficient_of_determination
222
+
223
+ def fit_error xys
224
+ y_bar = sigma(xys) { |x, y| y } / xys.size.to_f
225
+ ss_tot = sigma(xys) { |x, y| (y - y_bar) ** 2 }
226
+ ss_err = sigma(xys) { |x, y| (yield(x) - y) ** 2 }
227
+
228
+ 1 - (ss_err / ss_tot)
229
+ end
230
+
231
+ ##
232
+ # To fit a functional form: y = ae^(bx).
233
+ #
234
+ # Takes x and y values and returns [a, b, r^2].
235
+ #
236
+ # See: http://mathworld.wolfram.com/LeastSquaresFittingExponential.html
237
+
238
+ def fit_exponential xs, ys
239
+ n = xs.size
240
+ xys = xs.zip(ys)
241
+ sxlny = sigma(xys) { |x,y| x * Math.log(y) }
242
+ slny = sigma(xys) { |x,y| Math.log(y) }
243
+ sx2 = sigma(xys) { |x,y| x * x }
244
+ sx = sigma xs
245
+
246
+ c = n * sx2 - sx ** 2
247
+ a = (slny * sx2 - sx * sxlny) / c
248
+ b = ( n * sxlny - sx * slny ) / c
249
+
250
+ return Math.exp(a), b, fit_error(xys) { |x| Math.exp(a + b * x) }
251
+ end
252
+
253
+ ##
254
+ # To fit a functional form: y = a + b*ln(x).
255
+ #
256
+ # Takes x and y values and returns [a, b, r^2].
257
+ #
258
+ # See: http://mathworld.wolfram.com/LeastSquaresFittingLogarithmic.html
259
+
260
+ def fit_logarithmic xs, ys
261
+ n = xs.size
262
+ xys = xs.zip(ys)
263
+ slnx2 = sigma(xys) { |x,y| Math.log(x) ** 2 }
264
+ slnx = sigma(xys) { |x,y| Math.log(x) }
265
+ sylnx = sigma(xys) { |x,y| y * Math.log(x) }
266
+ sy = sigma(xys) { |x,y| y }
267
+
268
+ c = n * slnx2 - slnx ** 2
269
+ b = ( n * sylnx - sy * slnx ) / c
270
+ a = (sy - b * slnx) / n
271
+
272
+ return a, b, fit_error(xys) { |x| a + b * Math.log(x) }
273
+ end
274
+
275
+
276
+ ##
277
+ # Fits the functional form: a + bx.
278
+ #
279
+ # Takes x and y values and returns [a, b, r^2].
280
+ #
281
+ # See: http://mathworld.wolfram.com/LeastSquaresFitting.html
282
+
283
+ def fit_linear xs, ys
284
+ n = xs.size
285
+ xys = xs.zip(ys)
286
+ sx = sigma xs
287
+ sy = sigma ys
288
+ sx2 = sigma(xs) { |x| x ** 2 }
289
+ sxy = sigma(xys) { |x,y| x * y }
290
+
291
+ c = n * sx2 - sx**2
292
+ a = (sy * sx2 - sx * sxy) / c
293
+ b = ( n * sxy - sx * sy ) / c
294
+
295
+ return a, b, fit_error(xys) { |x| a + b * x }
296
+ end
297
+
298
+ ##
299
+ # To fit a functional form: y = ax^b.
300
+ #
301
+ # Takes x and y values and returns [a, b, r^2].
302
+ #
303
+ # See: http://mathworld.wolfram.com/LeastSquaresFittingPowerLaw.html
304
+
305
+ def fit_power xs, ys
306
+ n = xs.size
307
+ xys = xs.zip(ys)
308
+ slnxlny = sigma(xys) { |x, y| Math.log(x) * Math.log(y) }
309
+ slnx = sigma(xs) { |x | Math.log(x) }
310
+ slny = sigma(ys) { | y| Math.log(y) }
311
+ slnx2 = sigma(xs) { |x | Math.log(x) ** 2 }
312
+
313
+ b = (n * slnxlny - slnx * slny) / (n * slnx2 - slnx ** 2);
314
+ a = (slny - b * slnx) / n
315
+
316
+ return Math.exp(a), b, fit_error(xys) { |x| (Math.exp(a) * (x ** b)) }
317
+ end
318
+
319
+ ##
320
+ # Enumerates over +enum+ mapping +block+ if given, returning the
321
+ # sum of the result. Eg:
322
+ #
323
+ # sigma([1, 2, 3]) # => 1 + 2 + 3 => 7
324
+ # sigma([1, 2, 3]) { |n| n ** 2 } # => 1 + 4 + 9 => 14
325
+
326
+ def sigma enum, &block
327
+ enum = enum.map(&block) if block
328
+ enum.inject { |sum, n| sum + n }
329
+ end
330
+
331
+ ##
332
+ # Returns a proc that calls the specified fit method and asserts
333
+ # that the error is within a tolerable threshold.
334
+
335
+ def validation_for_fit msg, threshold
336
+ proc do |range, times|
337
+ a, b, rr = send "fit_#{msg}", range, times
338
+ assert_operator rr, :>=, threshold
339
+ [a, b, rr]
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ module Betatest
346
+ class BenchSpec < Benchmark
347
+ extend Betatest::Spec::DSL
348
+
349
+ ##
350
+ # This is used to define a new benchmark method. You usually don't
351
+ # use this directly and is intended for those needing to write new
352
+ # performance curve fits (eg: you need a specific polynomial fit).
353
+ #
354
+ # See ::bench_performance_linear for an example of how to use this.
355
+
356
+ def self.bench name, &block
357
+ define_method "bench_#{name.gsub(/\W+/, '_')}", &block
358
+ end
359
+
360
+ ##
361
+ # Specifies the ranges used for benchmarking for that class.
362
+ #
363
+ # bench_range do
364
+ # bench_exp(2, 16, 2)
365
+ # end
366
+ #
367
+ # See Betatest::Benchmark#bench_range for more details.
368
+
369
+ def self.bench_range &block
370
+ return super unless block
371
+
372
+ meta = (class << self; self; end)
373
+ meta.send :define_method, "bench_range", &block
374
+ end
375
+
376
+ ##
377
+ # Create a benchmark that verifies that the performance is linear.
378
+ #
379
+ # describe "my class Bench" do
380
+ # bench_performance_linear "fast_algorithm", 0.9999 do |n|
381
+ # @obj.fast_algorithm(n)
382
+ # end
383
+ # end
384
+
385
+ def self.bench_performance_linear name, threshold = 0.99, &work
386
+ bench name do
387
+ assert_performance_linear threshold, &work
388
+ end
389
+ end
390
+
391
+ ##
392
+ # Create a benchmark that verifies that the performance is constant.
393
+ #
394
+ # describe "my class Bench" do
395
+ # bench_performance_constant "zoom_algorithm!" do |n|
396
+ # @obj.zoom_algorithm!(n)
397
+ # end
398
+ # end
399
+
400
+ def self.bench_performance_constant name, threshold = 0.99, &work
401
+ bench name do
402
+ assert_performance_constant threshold, &work
403
+ end
404
+ end
405
+
406
+ ##
407
+ # Create a benchmark that verifies that the performance is exponential.
408
+ #
409
+ # describe "my class Bench" do
410
+ # bench_performance_exponential "algorithm" do |n|
411
+ # @obj.algorithm(n)
412
+ # end
413
+ # end
414
+
415
+ def self.bench_performance_exponential name, threshold = 0.99, &work
416
+ bench name do
417
+ assert_performance_exponential threshold, &work
418
+ end
419
+ end
420
+ end
421
+
422
+ Betatest::Spec.register_spec_type(/Bench(mark)?$/, Betatest::BenchSpec)
423
+ end
@@ -0,0 +1,281 @@
1
+ ##
2
+ # It's where you hide your "assertions".
3
+ #
4
+ # Please note, because of the way that expectations are implemented,
5
+ # all expectations (eg must_equal) are dependent upon a thread local
6
+ # variable +:current_spec+. If your specs rely on mixing threads into
7
+ # the specs themselves, you're better off using assertions. For
8
+ # example:
9
+ #
10
+ # it "should still work in threads" do
11
+ # my_threaded_thingy do
12
+ # (1+1).must_equal 2 # bad
13
+ # assert_equal 2, 1+1 # good
14
+ # end
15
+ # end
16
+
17
+ module Betatest::Expectations
18
+ ##
19
+ # See Betatest::Assertions#assert_empty.
20
+ #
21
+ # collection.must_be_empty
22
+ #
23
+ # :method: must_be_empty
24
+
25
+ infect_an_assertion :assert_empty, :must_be_empty, :unary
26
+
27
+ ##
28
+ # See Betatest::Assertions#assert_equal
29
+ #
30
+ # a.must_equal b
31
+ #
32
+ # :method: must_equal
33
+
34
+ infect_an_assertion :assert_equal, :must_equal
35
+
36
+ ##
37
+ # See Betatest::Assertions#assert_in_delta
38
+ #
39
+ # n.must_be_close_to m [, delta]
40
+ #
41
+ # :method: must_be_close_to
42
+
43
+ infect_an_assertion :assert_in_delta, :must_be_close_to
44
+
45
+ alias :must_be_within_delta :must_be_close_to # :nodoc:
46
+
47
+ ##
48
+ # See Betatest::Assertions#assert_in_epsilon
49
+ #
50
+ # n.must_be_within_epsilon m [, epsilon]
51
+ #
52
+ # :method: must_be_within_epsilon
53
+
54
+ infect_an_assertion :assert_in_epsilon, :must_be_within_epsilon
55
+
56
+ ##
57
+ # See Betatest::Assertions#assert_includes
58
+ #
59
+ # collection.must_include obj
60
+ #
61
+ # :method: must_include
62
+
63
+ infect_an_assertion :assert_includes, :must_include, :reverse
64
+
65
+ ##
66
+ # See Betatest::Assertions#assert_instance_of
67
+ #
68
+ # obj.must_be_instance_of klass
69
+ #
70
+ # :method: must_be_instance_of
71
+
72
+ infect_an_assertion :assert_instance_of, :must_be_instance_of
73
+
74
+ ##
75
+ # See Betatest::Assertions#assert_kind_of
76
+ #
77
+ # obj.must_be_kind_of mod
78
+ #
79
+ # :method: must_be_kind_of
80
+
81
+ infect_an_assertion :assert_kind_of, :must_be_kind_of
82
+
83
+ ##
84
+ # See Betatest::Assertions#assert_match
85
+ #
86
+ # a.must_match b
87
+ #
88
+ # :method: must_match
89
+
90
+ infect_an_assertion :assert_match, :must_match
91
+
92
+ ##
93
+ # See Betatest::Assertions#assert_nil
94
+ #
95
+ # obj.must_be_nil
96
+ #
97
+ # :method: must_be_nil
98
+
99
+ infect_an_assertion :assert_nil, :must_be_nil, :unary
100
+
101
+ ##
102
+ # See Betatest::Assertions#assert_operator
103
+ #
104
+ # n.must_be :<=, 42
105
+ #
106
+ # This can also do predicates:
107
+ #
108
+ # str.must_be :empty?
109
+ #
110
+ # :method: must_be
111
+
112
+ infect_an_assertion :assert_operator, :must_be, :reverse
113
+
114
+ ##
115
+ # See Betatest::Assertions#assert_output
116
+ #
117
+ # proc { ... }.must_output out_or_nil [, err]
118
+ #
119
+ # :method: must_output
120
+
121
+ infect_an_assertion :assert_output, :must_output
122
+
123
+ ##
124
+ # See Betatest::Assertions#assert_raises
125
+ #
126
+ # proc { ... }.must_raise exception
127
+ #
128
+ # :method: must_raise
129
+
130
+ infect_an_assertion :assert_raises, :must_raise
131
+
132
+ ##
133
+ # See Betatest::Assertions#assert_respond_to
134
+ #
135
+ # obj.must_respond_to msg
136
+ #
137
+ # :method: must_respond_to
138
+
139
+ infect_an_assertion :assert_respond_to, :must_respond_to, :reverse
140
+
141
+ ##
142
+ # See Betatest::Assertions#assert_same
143
+ #
144
+ # a.must_be_same_as b
145
+ #
146
+ # :method: must_be_same_as
147
+
148
+ infect_an_assertion :assert_same, :must_be_same_as
149
+
150
+ ##
151
+ # See Betatest::Assertions#assert_silent
152
+ #
153
+ # proc { ... }.must_be_silent
154
+ #
155
+ # :method: must_be_silent
156
+
157
+ infect_an_assertion :assert_silent, :must_be_silent
158
+
159
+ ##
160
+ # See Betatest::Assertions#assert_throws
161
+ #
162
+ # proc { ... }.must_throw sym
163
+ #
164
+ # :method: must_throw
165
+
166
+ infect_an_assertion :assert_throws, :must_throw
167
+
168
+ ##
169
+ # See Betatest::Assertions#refute_empty
170
+ #
171
+ # collection.wont_be_empty
172
+ #
173
+ # :method: wont_be_empty
174
+
175
+ infect_an_assertion :refute_empty, :wont_be_empty, :unary
176
+
177
+ ##
178
+ # See Betatest::Assertions#refute_equal
179
+ #
180
+ # a.wont_equal b
181
+ #
182
+ # :method: wont_equal
183
+
184
+ infect_an_assertion :refute_equal, :wont_equal
185
+
186
+ ##
187
+ # See Betatest::Assertions#refute_in_delta
188
+ #
189
+ # n.wont_be_close_to m [, delta]
190
+ #
191
+ # :method: wont_be_close_to
192
+
193
+ infect_an_assertion :refute_in_delta, :wont_be_close_to
194
+
195
+ alias :wont_be_within_delta :wont_be_close_to # :nodoc:
196
+
197
+ ##
198
+ # See Betatest::Assertions#refute_in_epsilon
199
+ #
200
+ # n.wont_be_within_epsilon m [, epsilon]
201
+ #
202
+ # :method: wont_be_within_epsilon
203
+
204
+ infect_an_assertion :refute_in_epsilon, :wont_be_within_epsilon
205
+
206
+ ##
207
+ # See Betatest::Assertions#refute_includes
208
+ #
209
+ # collection.wont_include obj
210
+ #
211
+ # :method: wont_include
212
+
213
+ infect_an_assertion :refute_includes, :wont_include, :reverse
214
+
215
+ ##
216
+ # See Betatest::Assertions#refute_instance_of
217
+ #
218
+ # obj.wont_be_instance_of klass
219
+ #
220
+ # :method: wont_be_instance_of
221
+
222
+ infect_an_assertion :refute_instance_of, :wont_be_instance_of
223
+
224
+ ##
225
+ # See Betatest::Assertions#refute_kind_of
226
+ #
227
+ # obj.wont_be_kind_of mod
228
+ #
229
+ # :method: wont_be_kind_of
230
+
231
+ infect_an_assertion :refute_kind_of, :wont_be_kind_of
232
+
233
+ ##
234
+ # See Betatest::Assertions#refute_match
235
+ #
236
+ # a.wont_match b
237
+ #
238
+ # :method: wont_match
239
+
240
+ infect_an_assertion :refute_match, :wont_match
241
+
242
+ ##
243
+ # See Betatest::Assertions#refute_nil
244
+ #
245
+ # obj.wont_be_nil
246
+ #
247
+ # :method: wont_be_nil
248
+
249
+ infect_an_assertion :refute_nil, :wont_be_nil, :unary
250
+
251
+ ##
252
+ # See Betatest::Assertions#refute_operator
253
+ #
254
+ # n.wont_be :<=, 42
255
+ #
256
+ # This can also do predicates:
257
+ #
258
+ # str.wont_be :empty?
259
+ #
260
+ # :method: wont_be
261
+
262
+ infect_an_assertion :refute_operator, :wont_be, :reverse
263
+
264
+ ##
265
+ # See Betatest::Assertions#refute_respond_to
266
+ #
267
+ # obj.wont_respond_to msg
268
+ #
269
+ # :method: wont_respond_to
270
+
271
+ infect_an_assertion :refute_respond_to, :wont_respond_to, :reverse
272
+
273
+ ##
274
+ # See Betatest::Assertions#refute_same
275
+ #
276
+ # a.wont_be_same_as b
277
+ #
278
+ # :method: wont_be_same_as
279
+
280
+ infect_an_assertion :refute_same, :wont_be_same_as
281
+ end