Skip to content

Instantly share code, notes, and snippets.

@jonathanpenn
Forked from joefiorini/rdingus.rb
Created December 12, 2008 02:36
Show Gist options
  • Save jonathanpenn/34995 to your computer and use it in GitHub Desktop.
Save jonathanpenn/34995 to your computer and use it in GitHub Desktop.
require 'rubygems'
require 'spec'
if not defined? BlankSlate
class BlankSlate
instance_methods.each { |m| undef_method m unless m =~ /^(__|instance_eval)/ }
end
end
class RDingus < BlankSlate
def initialize(parent = nil, invocation = nil)
@parent = parent
@invocation = invocation
@invocations = InvocationList.new
@invocation_expectations = InvocationResultHash.new
@children = []
@call_count = 0
end
def method_missing(name, *args, &block)
@call_count += 1
invocation = InvocationRecord.new(self, name, args)
@invocations << invocation
if @invocation_expectations.defined?(invocation)
result = @invocation_expectations[invocation]
begin
if result._dingus?
result
end
rescue NoMethodError
# We use this kludge, because it's the only way to test for a dingus
# We need to know it's not a dingus before calling other methods
if result.is_a?(Proc)
obj = Object.new
obj.extend(DingusProc)
obj.args = args
obj.method = name
obj.block = block
obj.instance_eval &result
else
result
end
end
else
dingus = RDingus.new(self, invocation)
@children << dingus
@invocation_expectations[invocation] = dingus
end
end
def _define(&block)
proxy = DefinitionProxy.new(self, @invocation_expectations)
if block
proxy.instance_eval &block
else
proxy
end
end
def _calls
@invocations
end
def _dingus?
true
end
def inspect
"#<RDingus #{__id__}, calls=#{@call_count}>"
end
def dup
self
end
def clone
dup
end
public
module DingusProc
attr_accessor :method, :args, :block
end
class DefinitionProxy < BlankSlate
def initialize(parent = nil, list = nil)
@parent = parent
@list = list || InvocationResultHash.new
end
def method_missing(m, *args, &block)
if block
invocation = InvocationRecord.new(@parent, m, args, block)
@list[invocation] = block
else
if args.empty?
invocation = InvocationRecord.new(@parent, m, args)
dingus = RDingus.new(@parent, invocation)
@list[invocation] = dingus
dingus._define
else
invocation = InvocationRecord.new(@parent, m, [], args.first)
@list[invocation] = args.first
end
end
end
def _define(&block)
self.instance_eval &block
end
def _list
@list
end
end
class InvocationRecord
attr_reader :dingus, :method, :args
attr_accessor :value
def initialize(dingus, method, args = [], *optional)
@dingus = dingus
@method = method
@args = args
@value = optional.first
@has_value = !optional.empty?
end
def value=(v)
@has_value = true
@value = v
end
def has_value?
@has_value
end
def eql?(b)
self.method == b.method && ((self.args == b.args) || self.any? || b.any?)
end
def ==(b)
self.eql?(b)
end
def any?
@args && @args.first == :any
end
def to_s
"#{method}(#{args.map{|i|i.inspect}.join(", ")})"
end
def inspect
retval = @has_value ? " => #{@value.inspect}" : ""
"#<Invocation \"#{self}\"#{retval}>"
end
end
class InvocationResultHash
def initialize
@list = []
end
def []=(invocation, value)
@list << [invocation, value]
value
end
def [](invocation)
@list.reverse.each do |inv, value|
return value if inv == invocation
end
nil
end
def defined?(invocation)
@list.any? {|inv, value| inv == invocation}
end
def empty?
@list.empty?
end
end
class InvocationList < Array
def include?(method, *args)
if method.is_a? Symbol
self.any? do |i|
i.method == method
end
else
raise "Haven't implemented include? for anything but Symbols (given #{method.class})"
end
end
alias_method :has_received?, :include?
def [](index)
case index
when Fixnum
self.at(index)
when Symbol
InvocationList.new self.select{|i| i.method == index }
else
raise "Unknown index type: #{index.class}"
end
end
def count
length
end
end
end
def should_be_a_dingus(object)
lambda {
object._dingus?
}.should_not raise_error(NoMethodError)
end
def should_not_be_a_dingus(object)
lambda {
object._dingus?
}.should raise_error(NoMethodError)
end
describe RDingus::DefinitionProxy do
before :each do
@rdingus = RDingus.new
@def = RDingus::DefinitionProxy.new(@rdingus)
end
describe "when given a sub defining block" do
before :each do
@def._define do
b_method :result
end
@invocation = RDingus::InvocationRecord.new(@rdingus, :b_method)
end
it "should record the method's result" do
should_not_be_a_dingus @def._list[@invocation]
@def._list[@invocation].should == :result
end
end
describe "when calling methods to record" do
before :each do
@def.a_method(:arguments) { :return_value }
@invocation = RDingus::InvocationRecord.new(@rdingus, :a_method)
end
it "should record them in an InvocationResultHash" do
@def._list.should be_kind_of(RDingus::InvocationResultHash)
end
it "should use the given InvocationResultHash if present" do
@hash = RDingus::InvocationResultHash.new
@def = RDingus::DefinitionProxy.new @rdingus, @hash
@def.a_method { :test }
@def._list[@invocation].should_not be_nil
end
describe "with a block" do
it "should store the block" do
@def.a_method { :block }
should_not_be_a_dingus @def._list[@invocation]
@def._list[@invocation].should be_instance_of(Proc)
end
end
describe "with just a return value" do
it "should store the value" do
@def.a_method :value
should_not_be_a_dingus @def._list[@invocation]
@def._list[@invocation].should == :value
end
end
describe "with no value or block" do
it "should return a new dingus' definition proxy" do
should_not_be_a_dingus @def.a_method._list
@def.a_method._list.should be_empty
end
end
end
end
describe RDingus do
before :each do
@rdingus = RDingus.new
end
it "should be recognized by the custom matcher 'be_a_dingus'" do
should_be_a_dingus(@rdingus)
end
it "should return an RDingus on method calls" do
should_be_a_dingus(@rdingus.a_method)
end
it "should return the same RDingus for the same method invocation" do
@rdingus.a_method.__id__.should == @rdingus.a_method.__id__
end
it "should return a different RDingus for a different method invocation" do
@rdingus.a_method.__id__.should_not == @rdingus.b_method.__id__
end
it "should return a different RDingus for the same method invocation but with different arguments" do
@rdingus.a_method.__id__.should_not == @rdingus.a_method(:argument).__id__
end
it "should return self on dup" do
@rdingus.dup.__id__.should == @rdingus.__id__
end
it "should return self on clone" do
@rdingus.clone.__id__.should == @rdingus.__id__
end
describe "when calling .inspect" do
it "should not return an RDingus" do
should_not_be_a_dingus @rdingus.inspect
end
it "should return a string" do
# This has to be done in reverse because "should" is not
# defined on a dingus
String.should === @rdingus.inspect
end
end
describe "when defining return values" do
# Comparisons with a dingus can be tricky. They will always be "true"
# (or technically "non nil/false") if compared with something else.
# You have to check to make sure it's *not* a dingus before making
# a comparison.
it "should return what's assigned" do
@rdingus._define.a_method { :shell }
should_not_be_a_dingus @rdingus.a_method
:shell.should == @rdingus.a_method
end
it "should return false if assigned" do
@rdingus._define.a_method { false }
should_not_be_a_dingus @rdingus.a_method
false.should == @rdingus.a_method
end
it "should return nil if assigned" do
@rdingus._define.a_method { nil }
should_not_be_a_dingus @rdingus.a_method
nil.should == @rdingus.a_method
end
it "should not remember as a method call" do
@rdingus._define.a_method { "something" }
@rdingus._calls.should be_empty
end
it "should not forget other method calls" do
@rdingus.remember_me
@rdingus._define.a_method "something"
@rdingus._calls.map{|i|i.method}.should == [:remember_me]
end
describe "and within the result proc" do
it "should provide access to method name" do
@rdingus._define.a_method { raise method.to_s }
lambda { @rdingus.a_method }.should raise_error("a_method")
end
it "should provide access to called arguments" do
@rdingus._define.a_method("arguments") { raise args.first }
lambda { @rdingus.a_method("arguments") }.should raise_error("arguments")
end
it "should provide access to passed block" do
@rdingus._define.a_method { raise block.call }
lambda {
@rdingus.a_method do
"raise me"
end
}.should raise_error("raise me")
end
end
describe "of nested method calls" do
it "should return what's assigned" do
@rdingus._define.a_method.b_method :stump
should_not_be_a_dingus @rdingus.a_method.b_method
:stump.should == @rdingus.a_method.b_method
end
it "should not remember as a method call" do
@rdingus._define.a_method.b_method "something"
@rdingus._calls.should be_empty
end
end
describe "with arbitrary arguments" do
before :each do
@rdingus._define.a_method(:any) { :stump }
end
it "should return what's assigned given any arguments" do
Symbol.should === @rdingus.a_method(:argument)
:stump.should == @rdingus.a_method(:argument)
end
it "should return what's assigned with no arguments" do
Symbol.should === @rdingus.a_method
:stump.should == @rdingus.a_method
end
end
describe "with specific arguments" do
before :each do
@rdingus._define.a_method(:argument) { :stump }
end
it "should return what's assigned" do
Symbol.should === @rdingus.a_method(:argument)
:stump.should == @rdingus.a_method(:argument)
end
it "should return an RDingus if arguments don't match" do
should_be_a_dingus @rdingus.a_method(:argument, :argument2)
end
end
describe "with block syntax" do
before :each do
@rdingus._define do
a_method { :stump }
end
end
it "should return what's assigned" do
should_not_be_a_dingus @rdingus.a_method
@rdingus.a_method.should == :stump
end
describe "within a block syntax" do
it "should return what's assigned" do
@rdingus._define do
a_method._define do
b_method :stump
end
end
should_not_be_a_dingus @rdingus.a_method.b_method
@rdingus.a_method.b_method.should == :stump
end
end
end
end
describe "when specifying exceptions" do
it "should raise given exception" do
@rdingus._define.raise_me { raise "Exception" }
lambda { @rdingus.raise_me }.should raise_error("Exception")
end
it "should not remember as a method call" do
@rdingus._define.raise_me {raise "Exception" }
lambda { @rdingus.raise_me }.should raise_error("Exception")
@rdingus._calls.map{|i|i.method}.should == [:raise_me]
end
it "should not forget other method calls" do
@rdingus.remember_me
@rdingus._define.raise_me { raise "Exception" }
lambda { @rdingus.raise_me }.should raise_error("Exception")
@rdingus._calls.map{|i|i.method}.should == [:remember_me, :raise_me]
end
end
describe "recording invocations" do
it "should remember methods invoked" do
@rdingus.sandwich
:sandwich.should == @rdingus._calls.first.method
end
it "should remember with arguments invoked" do
@rdingus.sandwich(:with_cheese)
:with_cheese.should == @rdingus._calls.first.args.first
end
it "should count method invocations" do
@rdingus.sandwich
@rdingus._calls.count.should == 1
end
end
end
describe RDingus::InvocationRecord do
before :each do
@rdingus = RDingus.new
@invocation_record = RDingus::InvocationRecord.new(@rdingus, :method, [:arg1, :arg2])
end
it "should provide access to it's owning RDingus" do
@invocation_record.dingus.should == @rdingus
end
it "should be equal for same method and arguments" do
RDingus::InvocationRecord.new(@rdingus, :method, [:arg1, :arg2]).should == @invocation_record
end
it "should not be equal for different method" do
RDingus::InvocationRecord.new(@rdingus, :method2, [:arg1, :arg2]).should_not == @invocation_record
end
it "should not be equal for different arguments" do
RDingus::InvocationRecord.new(@rdingus, :method, [:arg2, :arg3]).should_not == @invocation_record
end
it "should define == the same as eql?" do
RDingus::InvocationRecord.new(@rdingus, :method, [:arg1, :arg2]).should be_eql(@invocation_record)
end
describe "when told to accept any arguments" do
before :each do
@invocation_record = RDingus::InvocationRecord.new(@rdingus, :method, [:any])
end
it "should be equal for same method and same arguments" do
RDingus::InvocationRecord.new(@rdingus, :method, [:any]).should == @invocation_record
end
it "should be equal for same method and different arguments" do
RDingus::InvocationRecord.new(@rdingus, :method, [:other, :args]).should == @invocation_record
end
it "should not be equal for different method" do
RDingus::InvocationRecord.new(@rdingus, :method2, [:arg1, :arg2]).should_not == @invocation_record
end
end
it "should convert to a useful string" do
@invocation_record.to_s.should == "method(:arg1, :arg2)"
end
it "should have a useful inspect result" do
@invocation_record.value = :arg3
@invocation_record.inspect.should == "#<Invocation \"method(:arg1, :arg2)\" => :arg3>"
end
end
describe RDingus::InvocationResultHash do
before :each do
@rdingus = RDingus.new
@invocation = RDingus::InvocationRecord.new(@rdingus, :method, [:arg1, :arg2])
@invocation_list = RDingus::InvocationResultHash.new
end
it "should store and retrieve for same invocation" do
@invocation_list[@invocation] = "test"
@invocation_list[@invocation].should == "test"
end
it "should store and retrieve for equivalent invocations" do
@invocation_list[RDingus::InvocationRecord.new(@rdingus, :method, [:any])] = 'test'
@invocation_list[@invocation].should == "test"
end
it "should not store and retrieve for non equivalent invocations" do
@invocation_list[RDingus::InvocationRecord.new(@rdingus, :method2)] = "test"
@invocation_list[@invocation].should_not == "test"
end
it "should know if a value is defined" do
@invocation_list[@invocation] = "test"
@invocation_list.defined?(@invocation).should be_true
end
it "should know it has a value even if value is nil" do
@invocation_list[@invocation] = false
@invocation_list[@invocation].should == false
@invocation_list.defined?(@invocation).should be_true
end
it "should know it has a value even if value is false" do
@invocation_list[@invocation] = nil
@invocation_list[@invocation].should == nil
@invocation_list.defined?(@invocation).should be_true
end
describe "when redefining invocation" do
it "should use the last one" do
@invocation_list[@invocation] = "first"
@invocation_list[RDingus::InvocationRecord.new(@rdingus, :method, [:any])] = "second"
@invocation_list[@invocation].should == "second"
end
end
end
describe RDingus::InvocationList do
before :each do
@rdingus = RDingus.new
@list = RDingus::InvocationList.new
end
describe "when adding method invocations" do
it "should allow append" do
@list << :test
@list.to_a.should == [:test]
end
end
describe "when counting method invocations" do
before :each do
@list << RDingus::InvocationRecord.new(@rdingus, :some_method)
@list << RDingus::InvocationRecord.new(@rdingus, :some_method_2)
end
describe "without sub calls" do
it "should be equal to length" do
@list.count.should == @list.length
end
end
end
describe "when querying using include?" do
describe "with just a method name symbol" do
before :each do
@invocation = RDingus::InvocationRecord.new(@rdingus, :some_method)
end
describe "that is in the list" do
before :each do
@list << @invocation
end
it "should find the invocation" do
@list.should include(:some_method)
end
end
describe "that is not in the list" do
it "should not find the invocation" do
@list.should_not include(:some_method)
end
end
end
it "should alias has_received? to include?" do
@list << RDingus::InvocationRecord.new(@rdingus, :some_method)
@list.should have_received(:some_method)
end
end
describe "when indexing via []" do
before :each do
@invocation1 = RDingus::InvocationRecord.new(@rdingus, :some_method)
@invocation2 = RDingus::InvocationRecord.new(@rdingus, :method_2, [:arg1, :arg2])
@invocation3 = RDingus::InvocationRecord.new(@rdingus, :method_3)
@list << @invocation1; @list << @invocation2; @list << @invocation3
end
describe "with integers" do
it "should return the requested invocation in the sequence" do
@list[2].method.should == :method_3
end
end
describe "with a method name symbol" do
before :each do
@list << @invocation2
end
it "should return a new list" do
@list[:method_2].should be_kind_of(RDingus::InvocationList)
end
it "should have proper number of elements" do
@list[:method_2].length.should == 2
end
it "should only have invocations of the given method" do
@list[:method_2].each {|i| i.method.should == :method_2 }
end
end
end
end
class Person
attr_accessor :name
attr_accessor :shoe_size
def eat(sandwich)
@sandwich = sandwich
@sandwich.cut :in => 2
@sandwich.take_a_bite
end
def drop(sandwich)
sandwich.drop
end
def opinion_of_sandwich
if @sandwich.cheese.spicy?
"yummy!"
else
"bland"
end
end
end
describe Person do
before :each do
@person = Person.new
end
describe "when specifying a shoe size" do
it "should save the shoe size" do
@person.shoe_size = 4
@person.shoe_size.should == 4
end
end
describe "when using a sandwich" do
before :each do
@sandwich = RDingus.new
@person.eat(@sandwich)
end
it "should be able to cut a sandwhich" do
@sandwich._calls.first.method.should == :cut
end
it "should cut into two slices" do
@sandwich._calls[0].args[0].should == {:in => 2}
end
it "should take a bite" do
@sandwich._calls.should have_received(:take_a_bite)
end
it "should not have been dropped" do
@sandwich._calls.should_not have_received(:dropped)
end
it "should raise an exception if it drops the sandwich" do
@sandwich._define.drop { raise "Dropped Sandwich!" }
lambda { @person.drop(@sandwich) }.should raise_error("Dropped Sandwich!")
end
describe "that has spicy cheese" do
before :each do
@sandwich._define.cheese.spicy? true
end
it "should have an opinion of 'yummy!'" do
@person.opinion_of_sandwich.should == "yummy!"
end
end
describe "that does not have spicy cheese (block syntax)" do
before :each do
@sandwich._define do
cheese.spicy? false
end
end
it "should have an opinion of 'bland'" do
@person.opinion_of_sandwich.should == "bland"
end
end
describe "that does not have spicy cheese" do
before :each do
@sandwich._define.cheese.spicy? false
end
it "should have an opinion of 'bland'" do
@person.opinion_of_sandwich.should == "bland"
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment