betatest 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,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