Skip to content

Instantly share code, notes, and snippets.

@avdi
Created January 10, 2011 04:28
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save avdi/772356 to your computer and use it in GitHub Desktop.
Save avdi/772356 to your computer and use it in GitHub Desktop.
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
@avdi
Copy link
Author

avdi commented Apr 30, 2011

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

@akostrikov
Copy link

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