A proof of concept for exception testing in Ruby
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