public
Last active

A proof of concept for exception testing in Ruby

  • Download Gist
exception_tester.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
require 'set'
 
class ExceptionTester
class TestException < Exception
end
 
# Accepts a block containing the code you want to make exception safety
# assertions about.
def initialize(&exercise)
@exercise = exercise
end
 
 
# Accepts a block containing a predicate which will prove or disprove the
# exception safety of the exercised code
def assert(&invariant)
recording = record(&@exercise)
recording.size.times do |n|
playback(recording, n, &invariant)
unless invariant.call
raise "Assertion failed on call #{n}: #{@signature.inspect}"
end
end
end
 
private
 
# Makes a recording - which is a Set of codepoint tuples - of a given block of code.
def record(&block)
recording = Set.new
recorder = lambda do |event, file, line, id, binding, classname|
recording.add([event, file, line, id, classname])
end
set_trace_func(recorder)
block.call
set_trace_func(nil)
# We only care about method calls
recording.reject!{|event| !%w[call c-call].include?(event[0])}
 
# Get rid of calls outside of the block
recording.delete_if{|sig|
sig[0] == "c-call" &&
sig[1] == __FILE__ &&
sig[3] == :call &&
sig[4] == Proc
}
recording.delete_if{|sig|
sig[0] == "c-call" &&
sig[1] == __FILE__ &&
sig[3] == :set_trace_func &&
sig[4] == Kernel
}
 
recording
end
 
# Playback the given recording, and raise TestException once it reaches
# fail_index
def playback(recording, fail_index, &invariant)
recording = recording.dup
recording_size = recording.size
call_count = 0
player = lambda do |event, file, line, id, binding, classname|
signature = [event, file, line, id, classname]
if recording.member?(signature)
@signature = signature
call_count = recording_size - recording.size
recording.delete(signature)
if fail_index == call_count
raise TestException
end
end
end
set_trace_func(player)
begin
@exercise.call
rescue TestException
# do nothing
ensure
set_trace_func(nil)
end
end
 
end
 
if __FILE__ == $0
def swap_keys(hash, x_key, y_key)
temp = hash[x_key]
hash[x_key] = hash[y_key]
hash[y_key] = temp
end
h = {:a => 42, :b => 23}
 
tester = ExceptionTester.new{ swap_keys(h, :a, :b) }
tester.assert{
# Assert the keys are either fully swapped or not swapped at all
(h == {:a => 42, :b => 23}) ||
(h == {:a => 23, :b => 42})
}
end

Note: this code is a companion to the eBook "Exceptional Ruby", which can be found at http://exceptionalruby.com.

Avdi, this tester is truly inspiring on writing functional code in ruby.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.