-
-
Save jonathanpenn/34995 to your computer and use it in GitHub Desktop.
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 '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