Created
January 10, 2011 04:28
-
-
Save avdi/772356 to your computer and use it in GitHub Desktop.
A proof of concept for exception testing in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
Avdi, this tester is truly inspiring on writing functional code in ruby.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note: this code is a companion to the eBook "Exceptional Ruby", which can be found at http://exceptionalruby.com.