expected
and target
, passes them
# to the differ to append a diff message to the failure message.
def fail_with(message, expected=nil, target=nil) # :nodoc:
if Array === message && message.length == 3
message, expected, target = message[0], message[1], message[2]
end
unless (differ.nil? || expected.nil? || target.nil?)
if expected.is_a?(String)
message << "\nDiff:" << self.differ.diff_as_string(target.to_s, expected)
elsif !target.is_a?(Proc)
message << "\nDiff:" << self.differ.diff_as_object(target, expected)
end
end
Kernel::raise(Spec::Expectations::ExpectationNotMetError.new(message))
end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/expectations/differs/default.rb b/test/lib/spec/expectations/differs/default.rb
index e08325728..87e59b3a6 100644
--- a/test/lib/spec/expectations/differs/default.rb
+++ b/test/lib/spec/expectations/differs/default.rb
@@ -1,62 +1,61 @@
begin
require 'rubygems'
require 'diff/lcs' #necessary due to loading bug on some machines - not sure why - DaC
require 'diff/lcs/hunk'
rescue LoadError ; raise "You must gem install diff-lcs to use diffing" ; end
require 'pp'
module Spec
module Expectations
module Differs
- # TODO add colour support
# TODO add some rdoc
class Default
def initialize(format=:unified,context_lines=nil,colour=nil)
context_lines ||= 3
colour ||= false
@format,@context_lines,@colour = format,context_lines,colour
end
# This is snagged from diff/lcs/ldiff.rb (which is a commandline tool)
def diff_as_string(data_old, data_new)
data_old = data_old.split(/\n/).map! { |e| e.chomp }
data_new = data_new.split(/\n/).map! { |e| e.chomp }
output = ""
diffs = Diff::LCS.diff(data_old, data_new)
return output if diffs.empty?
oldhunk = hunk = nil
file_length_difference = 0
diffs.each do |piece|
begin
hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, @context_lines,
file_length_difference)
file_length_difference = hunk.file_length_difference
next unless oldhunk
# Hunks may overlap, which is why we need to be careful when our
# diff includes lines of context. Otherwise, we might print
# redundant lines.
if (@context_lines > 0) and hunk.overlaps?(oldhunk)
hunk.unshift(oldhunk)
else
output << oldhunk.diff(@format)
end
ensure
oldhunk = hunk
output << "\n"
end
end
#Handle the last remaining hunk
output << oldhunk.diff(@format) << "\n"
end
def diff_as_object(target,expected)
diff_as_string(PP.pp(target,""), PP.pp(expected,""))
end
end
end
end
end
diff --git a/test/lib/spec/expectations/extensions.rb b/test/lib/spec/expectations/extensions.rb
index 0381dc7f3..60c9b9e7d 100644
--- a/test/lib/spec/expectations/extensions.rb
+++ b/test/lib/spec/expectations/extensions.rb
@@ -1,3 +1,2 @@
require 'spec/expectations/extensions/object'
-require 'spec/expectations/extensions/proc'
require 'spec/expectations/extensions/string_and_symbol'
diff --git a/test/lib/spec/expectations/extensions/object.rb b/test/lib/spec/expectations/extensions/object.rb
index dd5498fdd..f59af722e 100644
--- a/test/lib/spec/expectations/extensions/object.rb
+++ b/test/lib/spec/expectations/extensions/object.rb
@@ -1,109 +1,66 @@
module Spec
module Expectations
# rspec adds #should and #should_not to every Object (and,
# implicitly, every Class).
module ObjectExpectations
# :call-seq:
# should(matcher)
# should == expected
+ # should === expected
# should =~ expected
#
# receiver.should(matcher)
# => Passes if matcher.matches?(receiver)
#
# receiver.should == expected #any value
# => Passes if (receiver == expected)
#
+ # receiver.should === expected #any value
+ # => Passes if (receiver === expected)
+ #
# receiver.should =~ regexp
# => Passes if (receiver =~ regexp)
#
# See Spec::Matchers for more information about matchers
#
# == Warning
#
# NOTE that this does NOT support receiver.should != expected.
# Instead, use receiver.should_not == expected
def should(matcher=nil, &block)
return ExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher
- Should::Should.new(self)
+ Spec::Matchers::PositiveOperatorMatcher.new(self)
end
# :call-seq:
# should_not(matcher)
# should_not == expected
+ # should_not === expected
# should_not =~ expected
#
# receiver.should_not(matcher)
# => Passes unless matcher.matches?(receiver)
#
# receiver.should_not == expected
# => Passes unless (receiver == expected)
#
+ # receiver.should_not === expected
+ # => Passes unless (receiver === expected)
+ #
# receiver.should_not =~ regexp
# => Passes unless (receiver =~ regexp)
#
# See Spec::Matchers for more information about matchers
def should_not(matcher=nil, &block)
return NegativeExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher
- should.not
+ Spec::Matchers::NegativeOperatorMatcher.new(self)
end
- deprecated do
- # Deprecated: use should have(n).items (see Spec::Matchers)
- # This will be removed in 0.9
- def should_have(expected)
- should.have(expected)
- end
- alias_method :should_have_exactly, :should_have
-
- # Deprecated: use should have_at_least(n).items (see Spec::Matchers)
- # This will be removed in 0.9
- def should_have_at_least(expected)
- should.have.at_least(expected)
- end
-
- # Deprecated: use should have_at_most(n).items (see Spec::Matchers)
- # This will be removed in 0.9
- def should_have_at_most(expected)
- should.have.at_most(expected)
- end
-
- # Deprecated: use should include(expected) (see Spec::Matchers)
- # This will be removed in 0.9
- def should_include(expected)
- should.include(expected)
- end
-
- # Deprecated: use should_not include(expected) (see Spec::Matchers)
- # This will be removed in 0.9
- def should_not_include(expected)
- should.not.include(expected)
- end
-
- # Deprecated: use should be(expected) (see Spec::Matchers)
- # This will be removed in 0.9
- def should_be(expected = :___no_arg)
- should.be(expected)
- end
-
- # Deprecated: use should_not be(expected) (see Spec::Matchers)
- # This will be removed in 0.9
- def should_not_be(expected = :___no_arg)
- should_not.be(expected)
- end
- end
end
end
end
class Object
include Spec::Expectations::ObjectExpectations
- deprecated do
- include Spec::Expectations::UnderscoreSugar
- end
end
-
-deprecated do
- Object.handle_underscores_for_rspec!
-end
\ No newline at end of file
diff --git a/test/lib/spec/expectations/extensions/string_and_symbol.rb b/test/lib/spec/expectations/extensions/string_and_symbol.rb
index 30f60d4d0..29cfbddfa 100644
--- a/test/lib/spec/expectations/extensions/string_and_symbol.rb
+++ b/test/lib/spec/expectations/extensions/string_and_symbol.rb
@@ -1,17 +1,17 @@
module Spec
module Expectations
module StringHelpers
def starts_with?(prefix)
- to_s[0..(prefix.length - 1)] == prefix
+ to_s[0..(prefix.to_s.length - 1)] == prefix.to_s
end
end
end
end
class String
include Spec::Expectations::StringHelpers
end
class Symbol
include Spec::Expectations::StringHelpers
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/expectations/handler.rb b/test/lib/spec/expectations/handler.rb
index 9d3fd1f88..4caa321e4 100644
--- a/test/lib/spec/expectations/handler.rb
+++ b/test/lib/spec/expectations/handler.rb
@@ -1,47 +1,43 @@
module Spec
module Expectations
module MatcherHandlerHelper
def describe(matcher)
matcher.respond_to?(:description) ? matcher.description : "[#{matcher.class.name} does not provide a description]"
end
end
class ExpectationMatcherHandler
class << self
include MatcherHandlerHelper
def handle_matcher(actual, matcher, &block)
- unless matcher.nil?
- match = matcher.matches?(actual, &block)
- ::Spec::Matchers.generated_description = "should #{describe(matcher)}"
- Spec::Expectations.fail_with(matcher.failure_message) unless match
- end
+ match = matcher.matches?(actual, &block)
+ ::Spec::Matchers.generated_description = "should #{describe(matcher)}"
+ Spec::Expectations.fail_with(matcher.failure_message) unless match
end
end
end
class NegativeExpectationMatcherHandler
class << self
include MatcherHandlerHelper
def handle_matcher(actual, matcher, &block)
- unless matcher.nil?
- unless matcher.respond_to?(:negative_failure_message)
- Spec::Expectations.fail_with(
- <<-EOF
- Matcher does not support should_not.
- See Spec::Matchers for more information
- about matchers.
- EOF
- )
- end
- match = matcher.matches?(actual, &block)
- ::Spec::Matchers.generated_description = "should not #{describe(matcher)}"
- Spec::Expectations.fail_with(matcher.negative_failure_message) if match
+ unless matcher.respond_to?(:negative_failure_message)
+ Spec::Expectations.fail_with(
+<<-EOF
+Matcher does not support should_not.
+See Spec::Matchers for more information
+about matchers.
+EOF
+)
end
+ match = matcher.matches?(actual, &block)
+ ::Spec::Matchers.generated_description = "should not #{describe(matcher)}"
+ Spec::Expectations.fail_with(matcher.negative_failure_message) if match
end
end
end
end
end
diff --git a/test/lib/spec/extensions.rb b/test/lib/spec/extensions.rb
new file mode 100755
index 000000000..824f03bfb
--- /dev/null
+++ b/test/lib/spec/extensions.rb
@@ -0,0 +1 @@
+require 'spec/extensions/object'
diff --git a/test/lib/spec/extensions/object.rb b/test/lib/spec/extensions/object.rb
new file mode 100755
index 000000000..6218aa770
--- /dev/null
+++ b/test/lib/spec/extensions/object.rb
@@ -0,0 +1,6 @@
+class Object
+ def args_and_options(*args)
+ options = Hash === args.last ? args.pop : {}
+ return args, options
+ end
+end
diff --git a/test/lib/spec/matchers.rb b/test/lib/spec/matchers.rb
index 9db24d486..fd208d628 100644
--- a/test/lib/spec/matchers.rb
+++ b/test/lib/spec/matchers.rb
@@ -1,160 +1,166 @@
-require 'spec/deprecated'
-require 'spec/callback'
require 'spec/matchers/be'
require 'spec/matchers/be_close'
require 'spec/matchers/change'
require 'spec/matchers/eql'
require 'spec/matchers/equal'
require 'spec/matchers/has'
require 'spec/matchers/have'
require 'spec/matchers/include'
require 'spec/matchers/match'
require 'spec/matchers/raise_error'
require 'spec/matchers/respond_to'
require 'spec/matchers/satisfy'
require 'spec/matchers/throw_symbol'
+require 'spec/matchers/operator_matcher'
module Spec
# RSpec ships with a number of useful Expression Matchers. An Expression Matcher
# is any object that responds to the following methods:
#
# matches?(actual)
# failure_message
# negative_failure_message #optional
# description #optional
#
# See Spec::Expectations to learn how to use these as Expectation Matchers.
# See Spec::Mocks to learn how to use them as Mock Argument Constraints.
#
# == Predicates
#
# In addition to those Expression Matchers that are defined explicitly, RSpec will
# create custom Matchers on the fly for any arbitrary predicate, giving your specs
# a much more natural language feel.
#
# A Ruby predicate is a method that ends with a "?" and returns true or false.
# Common examples are +empty?+, +nil?+, and +instance_of?+.
#
# All you need to do is write +should be_+ followed by the predicate without
# the question mark, and RSpec will figure it out from there. For example:
#
# [].should be_empty => [].empty? #passes
# [].should_not be_empty => [].empty? #fails
#
# In addtion to prefixing the predicate matchers with "be_", you can also use "be_a_"
# and "be_an_", making your specs read much more naturally:
#
# "a string".should be_an_instance_of(String) =>"a string".instance_of?(String) #passes
#
# 3.should be_a_kind_of(Fixnum) => 3.kind_of?(Numeric) #passes
# 3.should be_a_kind_of(Numeric) => 3.kind_of?(Numeric) #passes
# 3.should be_an_instance_of(Fixnum) => 3.instance_of?(Fixnum) #passes
# 3.should_not be_instance_of(Numeric) => 3.instance_of?(Numeric) #fails
#
# RSpec will also create custom matchers for predicates like +has_key?+. To
# use this feature, just state that the object should have_key(:key) and RSpec will
# call has_key?(:key) on the target. For example:
#
# {:a => "A"}.should have_key(:a) => {:a => "A"}.has_key?(:a) #passes
# {:a => "A"}.should have_key(:b) => {:a => "A"}.has_key?(:b) #fails
#
# You can use this feature to invoke any predicate that begins with "has_", whether it is
# part of the Ruby libraries (like +Hash#has_key?+) or a method you wrote on your own class.
#
- # == Custom Expression Matchers
+ # == Custom Expectation Matchers
#
- # When you find that none of the stock Expression Matchers provide a natural
+ # When you find that none of the stock Expectation Matchers provide a natural
# feeling expectation, you can very easily write your own.
#
# For example, imagine that you are writing a game in which players can
# be in various zones on a virtual board. To specify that bob should
# be in zone 4, you could say:
#
# bob.current_zone.should eql(Zone.new("4"))
#
# But you might find it more expressive to say:
#
# bob.should be_in_zone("4")
#
# and/or
#
# bob.should_not be_in_zone("3")
#
# To do this, you would need to write a class like this:
#
# class BeInZone
# def initialize(expected)
# @expected = expected
# end
- # def matches?(actual)
- # @actual = actual
- # bob.current_zone.eql?(Zone.new(@expected))
+ # def matches?(target)
+ # @target = target
+ # @target.current_zone.eql?(Zone.new(@expected))
# end
# def failure_message
- # "expected #{@actual.inspect} to be in Zone #{@expected}"
+ # "expected #{@target.inspect} to be in Zone #{@expected}"
# end
# def negative_failure_message
- # "expected #{@actual.inspect} not to be in Zone #{@expected}"
+ # "expected #{@target.inspect} not to be in Zone #{@expected}"
# end
# end
#
# ... and a method like this:
#
# def be_in_zone(expected)
# BeInZone.new(expected)
# end
#
# And then expose the method to your specs. This is normally done
# by including the method and the class in a module, which is then
# included in your spec:
#
# module CustomGameMatchers
# class BeInZone
# ...
# end
#
# def be_in_zone(expected)
# ...
# end
# end
#
- # context "Player behaviour" do
+ # describe "Player behaviour" do
# include CustomGameMatchers
# ...
# end
+ #
+ # or you can include in globally in a spec_helper.rb file required
+ # from your spec file(s):
+ #
+ # Spec::Runner.configure do |config|
+ # config.include(CustomGameMatchers)
+ # end
+ #
module Matchers
-
- class << self
- callback_events :description_generated
+ module ModuleMethods
+ def description_generated(callback)
+ description_generated_callbacks << callback
+ end
+
+ def unregister_description_generated(callback)
+ description_generated_callbacks.delete(callback)
+ end
+
def generated_description=(name)
- notify_callbacks(:description_generated, name)
+ description_generated_callbacks.each do |callback|
+ callback.call(name)
+ end
+ end
+
+ private
+ def description_generated_callbacks
+ @description_generated_callbacks ||= []
end
end
+ extend ModuleMethods
def method_missing(sym, *args, &block) # :nodoc:
return Matchers::Be.new(sym, *args) if sym.starts_with?("be_")
return Matchers::Has.new(sym, *args) if sym.starts_with?("have_")
super
end
- deprecated do
- # This supports sugar delegating to Matchers
- class Matcher #:nodoc:
- include Matchers
-
- def respond_to?(sym)
- if sym.to_s[0..2] == "be_"
- return true
- else
- super
- end
- end
- end
- end
-
class MatcherError < StandardError
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/matchers/be.rb b/test/lib/spec/matchers/be.rb
index 957f23de8..0eb1629a6 100644
--- a/test/lib/spec/matchers/be.rb
+++ b/test/lib/spec/matchers/be.rb
@@ -1,161 +1,206 @@
module Spec
module Matchers
class Be #:nodoc:
- def initialize(expected=nil, *args)
- @expected = parse_expected(expected)
+ def initialize(*args)
+ @expected = parse_expected(args.shift)
@args = args
@comparison = ""
end
def matches?(actual)
@actual = actual
return true if match_or_compare unless handling_predicate?
if handling_predicate?
begin
return @result = actual.__send__(predicate, *@args)
rescue => predicate_error
# This clause should be empty, but rcov will not report it as covered
# unless something (anything) is executed within the clause
rcov_error_report = "http://eigenclass.org/hiki.rb?rcov-0.8.0"
end
# This supports should_exist > target.exists? in the old world.
# We should consider deprecating that ability as in the new world
# you can't write "should exist" unless you have your own custom matcher.
begin
return @result = actual.__send__(present_tense_predicate, *@args)
rescue
raise predicate_error
end
end
return false
end
def failure_message
return "expected #{@comparison}#{expected}, got #{@actual.inspect}" unless handling_predicate?
return "expected #{predicate}#{args_to_s} to return true, got #{@result.inspect}"
end
def negative_failure_message
return "expected not #{expected}, got #{@actual.inspect}" unless handling_predicate?
return "expected #{predicate}#{args_to_s} to return false, got #{@result.inspect}"
end
def expected
return true if @expected == :true
return false if @expected == :false
return "nil" if @expected == :nil
return @expected.inspect
end
def match_or_compare
return @actual == true if @expected == :true
return @actual == false if @expected == :false
return @actual.nil? if @expected == :nil
return @actual < @expected if @less_than
return @actual <= @expected if @less_than_or_equal
return @actual >= @expected if @greater_than_or_equal
return @actual > @expected if @greater_than
+ return @actual == @expected if @double_equal
+ return @actual === @expected if @triple_equal
return @actual.equal?(@expected)
end
+
+ def ==(expected)
+ @double_equal = true
+ @comparison = "== "
+ @expected = expected
+ self
+ end
+
+ def ===(expected)
+ @triple_equal = true
+ @comparison = "=== "
+ @expected = expected
+ self
+ end
def <(expected)
@less_than = true
@comparison = "< "
@expected = expected
self
end
def <=(expected)
@less_than_or_equal = true
@comparison = "<= "
@expected = expected
self
end
def >=(expected)
@greater_than_or_equal = true
@comparison = ">= "
@expected = expected
self
end
def >(expected)
@greater_than = true
@comparison = "> "
@expected = expected
self
end
def description
- "be #{@comparison}#{@expected}"
+ "#{prefix_to_sentence}#{comparison}#{expected_to_sentence}#{args_to_sentence}"
end
private
def parse_expected(expected)
if Symbol === expected
- ["be_an_","be_a_","be_"].each do |prefix|
- @handling_predicate = true
- return "#{expected.to_s.sub(prefix,"")}".to_sym if expected.starts_with?(prefix)
+ @handling_predicate = true
+ ["be_an_","be_a_","be_"].each do |@prefix|
+ return "#{expected.to_s.sub(@prefix,"")}".to_sym if expected.starts_with?(@prefix)
end
end
+ @prefix = "be "
return expected
end
+
+ def handling_predicate?
+ return false if [:true, :false, :nil].include?(@expected)
+ return @handling_predicate
+ end
def predicate
"#{@expected.to_s}?".to_sym
end
def present_tense_predicate
"#{@expected.to_s}s?".to_sym
end
def args_to_s
return "" if @args.empty?
- transformed_args = @args.collect{|a| a.inspect}
- return "(#{transformed_args.join(', ')})"
+ inspected_args = @args.collect{|a| a.inspect}
+ return "(#{inspected_args.join(', ')})"
end
- def handling_predicate?
- return false if [:true, :false, :nil].include?(@expected)
- return @handling_predicate
+ def comparison
+ @comparison
+ end
+
+ def expected_to_sentence
+ split_words(@expected)
+ end
+
+ def prefix_to_sentence
+ split_words(@prefix)
end
+
+ def split_words(sym)
+ sym.to_s.gsub(/_/,' ')
+ end
+
+ def args_to_sentence
+ case @args.length
+ when 0
+ ""
+ when 1
+ " #{@args[0]}"
+ else
+ " #{@args[0...-1].join(', ')} and #{@args[-1]}"
+ end
+ end
+
end
# :call-seq:
# should be_true
# should be_false
# should be_nil
# should be_arbitrary_predicate(*args)
# should_not be_nil
# should_not be_arbitrary_predicate(*args)
#
# Given true, false, or nil, will pass if actual is
# true, false or nil (respectively).
#
# Predicates are any Ruby method that ends in a "?" and returns true or false.
# Given be_ followed by arbitrary_predicate (without the "?"), RSpec will match
# convert that into a query against the target object.
#
# The arbitrary_predicate feature will handle any predicate
# prefixed with "be_an_" (e.g. be_an_instance_of), "be_a_" (e.g. be_a_kind_of)
# or "be_" (e.g. be_empty), letting you choose the prefix that best suits the predicate.
#
# == Examples
#
# target.should be_true
# target.should be_false
# target.should be_nil
# target.should_not be_nil
#
# collection.should be_empty #passes if target.empty?
# "this string".should be_an_intance_of(String)
#
# target.should_not be_empty #passes unless target.empty?
# target.should_not be_old_enough(16) #passes unless target.old_enough?(16)
def be(*args)
Matchers::Be.new(*args)
end
end
end
diff --git a/test/lib/spec/matchers/be_close.rb b/test/lib/spec/matchers/be_close.rb
index b09e3fd2f..7763eb97e 100644
--- a/test/lib/spec/matchers/be_close.rb
+++ b/test/lib/spec/matchers/be_close.rb
@@ -1,37 +1,37 @@
module Spec
module Matchers
class BeClose #:nodoc:
def initialize(expected, delta)
@expected = expected
@delta = delta
end
def matches?(actual)
@actual = actual
(@actual - @expected).abs < @delta
end
def failure_message
- "expected #{@expected} +/- (<#{@delta}), got #{@actual}"
+ "expected #{@expected} +/- (< #{@delta}), got #{@actual}"
end
def description
- "be close to #{@expected} (+- #{@delta})"
+ "be close to #{@expected} (within +- #{@delta})"
end
end
# :call-seq:
# should be_close(expected, delta)
# should_not be_close(expected, delta)
#
# Passes if actual == expected +/- delta
#
# == Example
#
# result.should be_close(3.0, 0.5)
def be_close(expected, delta)
Matchers::BeClose.new(expected, delta)
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/matchers/eql.rb b/test/lib/spec/matchers/eql.rb
index caca1f7c6..280ca5454 100644
--- a/test/lib/spec/matchers/eql.rb
+++ b/test/lib/spec/matchers/eql.rb
@@ -1,43 +1,43 @@
module Spec
module Matchers
class Eql #:nodoc:
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.eql?(@expected)
end
def failure_message
return "expected #{@expected.inspect}, got #{@actual.inspect} (using .eql?)", @expected, @actual
end
def negative_failure_message
return "expected #{@actual.inspect} not to equal #{@expected.inspect} (using .eql?)", @expected, @actual
end
def description
"eql #{@expected.inspect}"
end
end
# :call-seq:
# should eql(expected)
# should_not eql(expected)
#
# Passes if actual and expected are of equal value, but not necessarily the same object.
#
# See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more information about equality in Ruby.
#
# == Examples
#
# 5.should eql(5)
# 5.should_not eql(3)
def eql(expected)
Matchers::Eql.new(expected)
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/matchers/equal.rb b/test/lib/spec/matchers/equal.rb
index e987e73cb..4bfc74951 100644
--- a/test/lib/spec/matchers/equal.rb
+++ b/test/lib/spec/matchers/equal.rb
@@ -1,43 +1,43 @@
module Spec
module Matchers
class Equal #:nodoc:
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.equal?(@expected)
end
def failure_message
return "expected #{@expected.inspect}, got #{@actual.inspect} (using .equal?)", @expected, @actual
end
def negative_failure_message
return "expected #{@actual.inspect} not to equal #{@expected.inspect} (using .equal?)", @expected, @actual
end
def description
"equal #{@expected.inspect}"
end
end
# :call-seq:
# should equal(expected)
# should_not equal(expected)
#
# Passes if actual and expected are the same object (object identity).
#
# See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more information about equality in Ruby.
#
# == Examples
#
# 5.should equal(5) #Fixnums are equal
# "5".should_not equal("5") #Strings that look the same are not the same object
def equal(expected)
Matchers::Equal.new(expected)
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/matchers/have.rb b/test/lib/spec/matchers/have.rb
index 81f9af3e3..f28b86ad3 100644
--- a/test/lib/spec/matchers/have.rb
+++ b/test/lib/spec/matchers/have.rb
@@ -1,140 +1,142 @@
module Spec
module Matchers
class Have #:nodoc:
def initialize(expected, relativity=:exactly)
@expected = (expected == :no ? 0 : expected)
@relativity = relativity
end
def relativities
@relativities ||= {
:exactly => "",
:at_least => "at least ",
:at_most => "at most "
}
end
def method_missing(sym, *args, &block)
@collection_name = sym
@args = args
@block = block
self
end
def matches?(collection_owner)
- if collection_owner.respond_to?(collection_name)
- collection = collection_owner.send(collection_name, *@args, &@block)
+ if collection_owner.respond_to?(@collection_name)
+ collection = collection_owner.send(@collection_name, *@args, &@block)
elsif (collection_owner.respond_to?(:length) || collection_owner.respond_to?(:size))
collection = collection_owner
else
- collection_owner.send(collection_name, *@args, &@block)
+ collection_owner.send(@collection_name, *@args, &@block)
end
- @actual = collection.length if collection.respond_to?(:length)
@actual = collection.size if collection.respond_to?(:size)
+ @actual = collection.length if collection.respond_to?(:length)
+ raise not_a_collection if @actual.nil?
return @actual >= @expected if @relativity == :at_least
return @actual <= @expected if @relativity == :at_most
return @actual == @expected
end
+
+ def not_a_collection
+ "expected #{@collection_name} to be a collection but it does not respond to #length or #size"
+ end
def failure_message
- "expected #{relative_expectation} #{collection_name}, got #{@actual}"
+ "expected #{relative_expectation} #{@collection_name}, got #{@actual}"
end
def negative_failure_message
if @relativity == :exactly
- return "expected target not to have #{@expected} #{collection_name}, got #{@actual}"
+ return "expected target not to have #{@expected} #{@collection_name}, got #{@actual}"
elsif @relativity == :at_most
return <<-EOF
Isn't life confusing enough?
Instead of having to figure out the meaning of this:
- should_not have_at_most(#{@expected}).#{collection_name}
+ should_not have_at_most(#{@expected}).#{@collection_name}
We recommend that you use this instead:
- should have_at_least(#{@expected + 1}).#{collection_name}
+ should have_at_least(#{@expected + 1}).#{@collection_name}
EOF
elsif @relativity == :at_least
return <<-EOF
Isn't life confusing enough?
Instead of having to figure out the meaning of this:
- should_not have_at_least(#{@expected}).#{collection_name}
+ should_not have_at_least(#{@expected}).#{@collection_name}
We recommend that you use this instead:
- should have_at_most(#{@expected - 1}).#{collection_name}
+ should have_at_most(#{@expected - 1}).#{@collection_name}
EOF
end
end
def description
- "have #{relative_expectation} #{collection_name}"
+ "have #{relative_expectation} #{@collection_name}"
end
private
- def collection_name
- @collection_name
- end
def relative_expectation
"#{relativities[@relativity]}#{@expected}"
end
end
# :call-seq:
# should have(number).named_collection__or__sugar
# should_not have(number).named_collection__or__sugar
#
# Passes if receiver is a collection with the submitted
# number of items OR if the receiver OWNS a collection
# with the submitted number of items.
#
# If the receiver OWNS the collection, you must use the name
# of the collection. So if a Team instance has a
# collection named #players, you must use that name
# to set the expectation.
#
# If the receiver IS the collection, you can use any name
# you like for named_collection. We'd recommend using
# either "elements", "members", or "items" as these are all
# standard ways of describing the things IN a collection.
#
# This also works for Strings, letting you set an expectation
# about its length
#
# == Examples
#
# # Passes if team.players.size == 11
# team.should have(11).players
#
# # Passes if [1,2,3].length == 3
# [1,2,3].should have(3).items #"items" is pure sugar
#
# # Passes if "this string".length == 11
# "this string".should have(11).characters #"characters" is pure sugar
def have(n)
Matchers::Have.new(n)
end
alias :have_exactly :have
# :call-seq:
# should have_at_least(number).items
#
# Exactly like have() with >=.
#
# == Warning
#
# +should_not+ +have_at_least+ is not supported
def have_at_least(n)
Matchers::Have.new(n, :at_least)
end
# :call-seq:
# should have_at_most(number).items
#
# Exactly like have() with <=.
#
# == Warning
#
# +should_not+ +have_at_most+ is not supported
def have_at_most(n)
Matchers::Have.new(n, :at_most)
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/matchers/include.rb b/test/lib/spec/matchers/include.rb
index 0d387f323..5476f97d8 100644
--- a/test/lib/spec/matchers/include.rb
+++ b/test/lib/spec/matchers/include.rb
@@ -1,50 +1,70 @@
module Spec
module Matchers
class Include #:nodoc:
- def initialize(expected)
- @expected = expected
+ def initialize(*expecteds)
+ @expecteds = expecteds
end
def matches?(actual)
@actual = actual
- actual.include?(@expected)
+ @expecteds.each do |expected|
+ return false unless actual.include?(expected)
+ end
+ true
end
def failure_message
_message
end
def negative_failure_message
_message("not ")
end
def description
- "include #{@expected.inspect}"
+ "include #{_pretty_print(@expecteds)}"
end
private
def _message(maybe_not="")
- "expected #{@actual.inspect} #{maybe_not}to include #{@expected.inspect}"
+ "expected #{@actual.inspect} #{maybe_not}to include #{_pretty_print(@expecteds)}"
+ end
+
+ def _pretty_print(array)
+ result = ""
+ array.each_with_index do |item, index|
+ if index < (array.length - 2)
+ result << "#{item.inspect}, "
+ elsif index < (array.length - 1)
+ result << "#{item.inspect} and "
+ else
+ result << "#{item.inspect}"
+ end
+ end
+ result
end
end
# :call-seq:
# should include(expected)
# should_not include(expected)
#
# Passes if actual includes expected. This works for
- # collections and Strings
+ # collections and Strings. You can also pass in multiple args
+ # and it will only pass if all args are found in collection.
#
# == Examples
#
# [1,2,3].should include(3)
+ # [1,2,3].should include(2,3) #would pass
+ # [1,2,3].should include(2,3,4) #would fail
# [1,2,3].should_not include(4)
# "spread".should include("read")
# "spread".should_not include("red")
- def include(expected)
- Matchers::Include.new(expected)
+ def include(*expected)
+ Matchers::Include.new(*expected)
end
end
end
diff --git a/test/lib/spec/matchers/operator_matcher.rb b/test/lib/spec/matchers/operator_matcher.rb
new file mode 100755
index 000000000..2d47ea85a
--- /dev/null
+++ b/test/lib/spec/matchers/operator_matcher.rb
@@ -0,0 +1,72 @@
+module Spec
+ module Matchers
+ class BaseOperatorMatcher
+
+ def initialize(target)
+ @target = target
+ end
+
+ def ==(expected)
+ @expected = expected
+ __delegate_method_missing_to_target("==", expected)
+ end
+
+ def ===(expected)
+ @expected = expected
+ __delegate_method_missing_to_target("===", expected)
+ end
+
+ def =~(expected)
+ @expected = expected
+ __delegate_method_missing_to_target("=~", expected)
+ end
+
+ def >(expected)
+ @expected = expected
+ __delegate_method_missing_to_target(">", expected)
+ end
+
+ def >=(expected)
+ @expected = expected
+ __delegate_method_missing_to_target(">=", expected)
+ end
+
+ def <(expected)
+ @expected = expected
+ __delegate_method_missing_to_target("<", expected)
+ end
+
+ def <=(expected)
+ @expected = expected
+ __delegate_method_missing_to_target("<=", expected)
+ end
+
+ def fail_with_message(message)
+ Spec::Expectations.fail_with(message, @expected, @target)
+ end
+
+ end
+
+ class PositiveOperatorMatcher < BaseOperatorMatcher #:nodoc:
+
+ def __delegate_method_missing_to_target(operator, expected)
+ ::Spec::Matchers.generated_description = "should #{operator} #{expected.inspect}"
+ return if @target.send(operator, expected)
+ return fail_with_message("expected: #{expected.inspect},\n got: #{@target.inspect} (using #{operator})") if ['==','===', '=~'].include?(operator)
+ return fail_with_message("expected: #{operator} #{expected.inspect},\n got: #{operator.gsub(/./, ' ')} #{@target.inspect}")
+ end
+
+ end
+
+ class NegativeOperatorMatcher < BaseOperatorMatcher #:nodoc:
+
+ def __delegate_method_missing_to_target(operator, expected)
+ ::Spec::Matchers.generated_description = "should not #{operator} #{expected.inspect}"
+ return unless @target.send(operator, expected)
+ return fail_with_message("expected not: #{operator} #{expected.inspect},\n got: #{operator.gsub(/./, ' ')} #{@target.inspect}")
+ end
+
+ end
+
+ end
+end
diff --git a/test/lib/spec/matchers/raise_error.rb b/test/lib/spec/matchers/raise_error.rb
index 95e82ad5e..b45dcf65c 100644
--- a/test/lib/spec/matchers/raise_error.rb
+++ b/test/lib/spec/matchers/raise_error.rb
@@ -1,100 +1,105 @@
module Spec
module Matchers
class RaiseError #:nodoc:
- def initialize(exception=Exception, message=nil)
- @expected_error = exception
- @expected_message = message
+ def initialize(error_or_message=Exception, message=nil)
+ if String === error_or_message
+ @expected_error = Exception
+ @expected_message = error_or_message
+ else
+ @expected_error = error_or_message
+ @expected_message = message
+ end
end
def matches?(proc)
@raised_expected_error = false
@raised_other = false
begin
proc.call
rescue @expected_error => @actual_error
if @expected_message.nil?
@raised_expected_error = true
else
case @expected_message
when Regexp
if @expected_message =~ @actual_error.message
@raised_expected_error = true
else
@raised_other = true
end
else
- if @actual_error.message == @expected_message
+ if @expected_message == @actual_error.message
@raised_expected_error = true
else
@raised_other = true
end
end
end
rescue => @actual_error
@raised_other = true
ensure
return @raised_expected_error
end
end
def failure_message
return "expected #{expected_error}#{actual_error}" if @raised_other || !@raised_expected_error
end
def negative_failure_message
"expected no #{expected_error}#{actual_error}"
end
def description
"raise #{expected_error}"
end
private
def expected_error
case @expected_message
when nil
@expected_error
when Regexp
"#{@expected_error} with message matching #{@expected_message.inspect}"
else
"#{@expected_error} with #{@expected_message.inspect}"
end
end
def actual_error
@actual_error.nil? ? " but nothing was raised" : ", got #{@actual_error.inspect}"
end
end
# :call-seq:
# should raise_error()
# should raise_error(NamedError)
# should raise_error(NamedError, String)
# should raise_error(NamedError, Regexp)
# should_not raise_error()
# should_not raise_error(NamedError)
# should_not raise_error(NamedError, String)
# should_not raise_error(NamedError, Regexp)
#
# With no args, matches if any error is raised.
# With a named error, matches only if that specific error is raised.
# With a named error and messsage specified as a String, matches only if both match.
# With a named error and messsage specified as a Regexp, matches only if both match.
#
# == Examples
#
# lambda { do_something_risky }.should raise_error
# lambda { do_something_risky }.should raise_error(PoorRiskDecisionError)
# lambda { do_something_risky }.should raise_error(PoorRiskDecisionError, "that was too risky")
# lambda { do_something_risky }.should raise_error(PoorRiskDecisionError, /oo ri/)
#
# lambda { do_something_risky }.should_not raise_error
# lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError)
# lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError, "that was too risky")
# lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError, /oo ri/)
def raise_error(error=Exception, message=nil)
Matchers::RaiseError.new(error, message)
end
end
end
diff --git a/test/lib/spec/matchers/respond_to.rb b/test/lib/spec/matchers/respond_to.rb
index 013a36f1d..3d23422aa 100644
--- a/test/lib/spec/matchers/respond_to.rb
+++ b/test/lib/spec/matchers/respond_to.rb
@@ -1,35 +1,45 @@
module Spec
module Matchers
class RespondTo #:nodoc:
- def initialize(sym)
- @sym = sym
+ def initialize(*names)
+ @names = names
+ @names_not_responded_to = []
end
def matches?(target)
- return target.respond_to?(@sym)
+ @names.each do |name|
+ unless target.respond_to?(name)
+ @names_not_responded_to << name
+ end
+ end
+ return @names_not_responded_to.empty?
end
def failure_message
- "expected target to respond to #{@sym.inspect}"
+ "expected target to respond to #{@names_not_responded_to.collect {|name| name.inspect }.join(', ')}"
end
def negative_failure_message
- "expected target not to respond to #{@sym.inspect}"
+ "expected target not to respond to #{@names.collect {|name| name.inspect }.join(', ')}"
end
def description
- "respond to ##{@sym.to_s}"
+ "respond to ##{@names.to_s}"
end
end
# :call-seq:
- # should respond_to(:sym)
- # should_not respond_to(:sym)
+ # should respond_to(*names)
+ # should_not respond_to(*names)
#
- # Matches if the target object responds to :sym
- def respond_to(sym)
- Matchers::RespondTo.new(sym)
+ # Matches if the target object responds to all of the names
+ # provided. Names can be Strings or Symbols.
+ #
+ # == Examples
+ #
+ def respond_to(*names)
+ Matchers::RespondTo.new(*names)
end
end
end
diff --git a/test/lib/spec/matchers/throw_symbol.rb b/test/lib/spec/matchers/throw_symbol.rb
index 6732f6fed..6d047bc39 100644
--- a/test/lib/spec/matchers/throw_symbol.rb
+++ b/test/lib/spec/matchers/throw_symbol.rb
@@ -1,75 +1,72 @@
module Spec
module Matchers
class ThrowSymbol #:nodoc:
def initialize(expected=nil)
@expected = expected
end
def matches?(proc)
begin
proc.call
rescue NameError => e
- @actual = extract_sym_from_name_error(e)
+ @actual = e.name.to_sym
ensure
if @expected.nil?
return @actual.nil? ? false : true
else
return @actual == @expected
end
end
end
def failure_message
if @actual
"expected #{expected}, got #{@actual.inspect}"
else
"expected #{expected} but nothing was thrown"
end
end
def negative_failure_message
if @expected
"expected #{expected} not to be thrown"
else
"expected no Symbol, got :#{@actual}"
end
end
def description
"throw #{expected}"
end
private
def expected
@expected.nil? ? "a Symbol" : @expected.inspect
end
- def extract_sym_from_name_error(error)
- return "#{error.message.split("`").last.split("'").first}".to_sym
- end
end
# :call-seq:
# should throw_symbol()
# should throw_symbol(:sym)
# should_not throw_symbol()
# should_not throw_symbol(:sym)
#
# Given a Symbol argument, matches if a proc throws the specified Symbol.
#
# Given no argument, matches if a proc throws any Symbol.
#
# == Examples
#
# lambda { do_something_risky }.should throw_symbol
# lambda { do_something_risky }.should throw_symbol(:that_was_risky)
#
# lambda { do_something_risky }.should_not throw_symbol
# lambda { do_something_risky }.should_not throw_symbol(:that_was_risky)
def throw_symbol(sym=nil)
Matchers::ThrowSymbol.new(sym)
end
end
end
diff --git a/test/lib/spec/mocks.rb b/test/lib/spec/mocks.rb
index d0a5d0299..66cbafb3c 100644
--- a/test/lib/spec/mocks.rb
+++ b/test/lib/spec/mocks.rb
@@ -1,232 +1,208 @@
require 'spec/mocks/methods'
-require 'spec/mocks/mock_handler'
+require 'spec/mocks/argument_constraint_matchers'
+require 'spec/mocks/spec_methods'
+require 'spec/mocks/proxy'
require 'spec/mocks/mock'
require 'spec/mocks/argument_expectation'
require 'spec/mocks/message_expectation'
require 'spec/mocks/order_group'
require 'spec/mocks/errors'
require 'spec/mocks/error_generator'
require 'spec/mocks/extensions/object'
+require 'spec/mocks/space'
+
module Spec
# == Mocks and Stubs
#
# RSpec will create Mock Objects and Stubs for you at runtime, or attach stub/mock behaviour
# to any of your real objects (Partial Mock/Stub). Because the underlying implementation
# for mocks and stubs is the same, you can intermingle mock and stub
# behaviour in either dynamically generated mocks or your pre-existing classes.
# There is a semantic difference in how they are created, however,
# which can help clarify the role it is playing within a given spec.
#
# == Mock Objects
#
# Mocks are objects that allow you to set and verify expectations that they will
# receive specific messages during run time. They are very useful for specifying how the subject of
# the spec interacts with its collaborators. This approach is widely known as "interaction
# testing".
#
# Mocks are also very powerful as a design tool. As you are
# driving the implementation of a given class, Mocks provide an anonymous
# collaborator that can change in behaviour as quickly as you can write an expectation in your
# spec. This flexibility allows you to design the interface of a collaborator that often
# does not yet exist. As the shape of the class being specified becomes more clear, so do the
# requirements for its collaborators - often leading to the discovery of new types that are
# needed in your system.
#
# Read Endo-Testing[http://www.mockobjects.com/files/endotesting.pdf] for a much
# more in depth description of this process.
#
# == Stubs
#
# Stubs are objects that allow you to set "stub" responses to
# messages. As Martin Fowler points out on his site,
# mocks_arent_stubs[http://www.martinfowler.com/articles/mocksArentStubs.html].
# Paraphrasing Fowler's paraphrasing
# of Gerard Meszaros: Stubs provide canned responses to messages they might receive in a test, while
# mocks allow you to specify and, subsquently, verify that certain messages should be received during
# the execution of a test.
#
# == Partial Mocks/Stubs
#
# RSpec also supports partial mocking/stubbing, allowing you to add stub/mock behaviour
# to instances of your existing classes. This is generally
# something to be avoided, because changes to the class can have ripple effects on
# seemingly unrelated specs. When specs fail due to these ripple effects, the fact
# that some methods are being mocked can make it difficult to understand why a
# failure is occurring.
#
# That said, partials do allow you to expect and
# verify interactions with class methods such as +#find+ and +#create+
# on Ruby on Rails model classes.
#
# == Further Reading
#
# There are many different viewpoints about the meaning of mocks and stubs. If you are interested
# in learning more, here is some recommended reading:
#
# * Mock Objects: http://www.mockobjects.com/
# * Endo-Testing: http://www.mockobjects.com/files/endotesting.pdf
# * Mock Roles, Not Objects: http://www.mockobjects.com/files/mockrolesnotobjects.pdf
# * Test Double Patterns: http://xunitpatterns.com/Test%20Double%20Patterns.html
# * Mocks aren't stubs: http://www.martinfowler.com/articles/mocksArentStubs.html
#
# == Creating a Mock
#
# You can create a mock in any specification (or setup) using:
#
# mock(name, options={})
#
# The optional +options+ argument is a +Hash+. Currently the only supported
# option is +:null_object+. Setting this to true instructs the mock to ignore
# any messages it hasn’t been told to expect – and quietly return itself. For example:
#
# mock("person", :null_object => true)
#
# == Creating a Stub
#
# You can create a stub in any specification (or setup) using:
#
# stub(name, stub_methods_and_values_hash)
#
# For example, if you wanted to create an object that always returns
# "More?!?!?!" to "please_sir_may_i_have_some_more" you would do this:
#
# stub("Mr Sykes", :please_sir_may_i_have_some_more => "More?!?!?!")
#
# == Creating a Partial Mock
#
# You don't really "create" a partial mock, you simply add method stubs and/or
# mock expectations to existing classes and objects:
#
# Factory.should_receive(:find).with(id).and_return(value)
# obj.stub!(:to_i).and_return(3)
# etc ...
#
# == Expecting Messages
#
# my_mock.should_receive(:sym)
# my_mock.should_not_receive(:sym)
#
# == Expecting Arguments
#
# my_mock.should_receive(:sym).with(*args)
# my_mock.should_not_receive(:sym).with(*args)
#
# == Argument Constraints using Expression Matchers
#
# Arguments that are passed to #with are compared with actual arguments received
# using == by default. In cases in which you want to specify things about the arguments
# rather than the arguments themselves, you can use any of the Expression Matchers.
# They don't all make syntactic sense (they were primarily designed for use with
# Spec::Expectations), but you are free to create your own custom Spec::Matchers.
#
# Spec::Mocks does provide one additional Matcher method named #ducktype.
#
# In addition, Spec::Mocks adds some keyword Symbols that you can use to
# specify certain kinds of arguments:
#
- # my_mock.should_receive(:sym).with(:no_args)
- # my_mock.should_receive(:sym).with(:any_args)
- # my_mock.should_receive(:sym).with(1, :numeric, "b") #2nd argument can any type of Numeric
- # my_mock.should_receive(:sym).with(1, :boolean, "b") #2nd argument can true or false
- # my_mock.should_receive(:sym).with(1, :string, "b") #2nd argument can be any String
+ # my_mock.should_receive(:sym).with(no_args())
+ # my_mock.should_receive(:sym).with(any_args())
+ # my_mock.should_receive(:sym).with(1, an_instance_of(Numeric), "b") #2nd argument can any type of Numeric
+ # my_mock.should_receive(:sym).with(1, boolean(), "b") #2nd argument can true or false
# my_mock.should_receive(:sym).with(1, /abc/, "b") #2nd argument can be any String matching the submitted Regexp
- # my_mock.should_receive(:sym).with(1, :anything, "b") #2nd argument can be anything at all
+ # my_mock.should_receive(:sym).with(1, anything(), "b") #2nd argument can be anything at all
# my_mock.should_receive(:sym).with(1, ducktype(:abs, :div), "b")
# #2nd argument can be object that responds to #abs and #div
#
# == Receive Counts
#
# my_mock.should_receive(:sym).once
# my_mock.should_receive(:sym).twice
# my_mock.should_receive(:sym).exactly(n).times
# my_mock.should_receive(:sym).at_least(:once)
# my_mock.should_receive(:sym).at_least(:twice)
# my_mock.should_receive(:sym).at_least(n).times
# my_mock.should_receive(:sym).at_most(:once)
# my_mock.should_receive(:sym).at_most(:twice)
# my_mock.should_receive(:sym).at_most(n).times
# my_mock.should_receive(:sym).any_number_of_times
#
# == Ordering
#
# my_mock.should_receive(:sym).ordered
# my_mock.should_receive(:other_sym).ordered
# #This will fail if the messages are received out of order
#
# == Setting Reponses
#
# Whether you are setting a mock expectation or a simple stub, you can tell the
# object precisely how to respond:
#
# my_mock.should_receive(:sym).and_return(value)
# my_mock.should_receive(:sym).exactly(3).times.and_return(value1, value2, value3)
# # returns value1 the first time, value2 the second, etc
# my_mock.should_receive(:sym).and_return { ... } #returns value returned by the block
# my_mock.should_receive(:sym).and_raise(error)
# #error can be an instantiated object or a class
# #if it is a class, it must be instantiable with no args
# my_mock.should_receive(:sym).and_throw(:sym)
# my_mock.should_receive(:sym).and_yield([array,of,values,to,yield])
#
# Any of these responses can be applied to a stub as well, but stubs do
# not support any qualifiers about the message received (i.e. you can't specify arguments
# or receive counts):
#
# my_mock.stub!(:sym).and_return(value)
# my_mock.stub!(:sym).and_return(value1, value2, value3)
# my_mock.stub!(:sym).and_raise(error)
# my_mock.stub!(:sym).and_throw(:sym)
# my_mock.stub!(:sym).and_yield([array,of,values,to,yield])
#
# == Arbitrary Handling
#
# Once in a while you'll find that the available expectations don't solve the
# particular problem you are trying to solve. Imagine that you expect the message
# to come with an Array argument that has a specific length, but you don't care
# what is in it. You could do this:
#
# my_mock.should_receive(:sym) do |arg|
# arg.should be_an_istance_of(Array)
# arg.length.should == 7
# end
#
# Note that this would fail if the number of arguments received was different from
# the number of block arguments (in this case 1).
#
# == Combining Expectation Details
#
# Combining the message name with specific arguments, receive counts and responses
# you can get quite a bit of detail in your expectations:
#
# my_mock.should_receive(:<<).with("illegal value").once.and_raise(ArgumentError)
module Mocks
- # Shortcut for creating an instance of Spec::Mocks::Mock.
- def mock(name, options={})
- Spec::Mocks::Mock.new(name, options)
- end
-
- # Shortcut for creating an instance of Spec::Mocks::Mock with
- # predefined method stubs.
- #
- # == Examples
- #
- # stub_thing = stub("thing", :a => "A")
- # stub_thing.a == "A" => true
- #
- # stub_person = stub("thing", :name => "Joe", :email => "joe@domain.com")
- # stub_person.name => "Joe"
- # stub_person.email => "joe@domain.com"
- def stub(name, stubs={})
- object_stub = mock(name)
- stubs.each { |key, value| object_stub.stub!(key).and_return(value) }
- object_stub
- end
-
- # Shortcut for creating an instance of Spec::Mocks::DuckTypeArgConstraint
- def duck_type(*args)
- return Spec::Mocks::DuckTypeArgConstraint.new(*args)
- end
-
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/mocks/argument_constraint_matchers.rb b/test/lib/spec/mocks/argument_constraint_matchers.rb
new file mode 100644
index 000000000..0e4777082
--- /dev/null
+++ b/test/lib/spec/mocks/argument_constraint_matchers.rb
@@ -0,0 +1,27 @@
+module Spec
+ module Mocks
+ module ArgumentConstraintMatchers
+
+ # Shortcut for creating an instance of Spec::Mocks::DuckTypeArgConstraint
+ def duck_type(*args)
+ DuckTypeArgConstraint.new(*args)
+ end
+
+ def any_args
+ AnyArgsConstraint.new
+ end
+
+ def anything
+ AnyArgConstraint.new(nil)
+ end
+
+ def boolean
+ BooleanArgConstraint.new(nil)
+ end
+
+ def no_args
+ NoArgsConstraint.new
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/mocks/argument_expectation.rb b/test/lib/spec/mocks/argument_expectation.rb
index a4870e767..5da069b87 100644
--- a/test/lib/spec/mocks/argument_expectation.rb
+++ b/test/lib/spec/mocks/argument_expectation.rb
@@ -1,132 +1,183 @@
module Spec
module Mocks
class MatcherConstraint
def initialize(matcher)
@matcher = matcher
end
def matches?(value)
@matcher.matches?(value)
end
end
class LiteralArgConstraint
def initialize(literal)
@literal_value = literal
end
def matches?(value)
@literal_value == value
end
end
class RegexpArgConstraint
def initialize(regexp)
@regexp = regexp
end
def matches?(value)
return value =~ @regexp unless value.is_a?(Regexp)
value == @regexp
end
end
class AnyArgConstraint
def initialize(ignore)
end
+ def ==(other)
+ true
+ end
+
+ # TODO - need this?
def matches?(value)
true
end
end
+ class AnyArgsConstraint
+ def description
+ "any args"
+ end
+ end
+
+ class NoArgsConstraint
+ def description
+ "no args"
+ end
+
+ def ==(args)
+ args == []
+ end
+ end
+
class NumericArgConstraint
def initialize(ignore)
end
def matches?(value)
value.is_a?(Numeric)
end
end
class BooleanArgConstraint
def initialize(ignore)
end
+ def ==(value)
+ matches?(value)
+ end
+
def matches?(value)
return true if value.is_a?(TrueClass)
return true if value.is_a?(FalseClass)
false
end
end
class StringArgConstraint
def initialize(ignore)
end
def matches?(value)
value.is_a?(String)
end
end
class DuckTypeArgConstraint
- def initialize(*methods_to_respond_do)
- @methods_to_respond_do = methods_to_respond_do
+ def initialize(*methods_to_respond_to)
+ @methods_to_respond_to = methods_to_respond_to
end
def matches?(value)
- @methods_to_respond_do.all? { |sym| value.respond_to?(sym) }
+ @methods_to_respond_to.all? { |sym| value.respond_to?(sym) }
+ end
+
+ def description
+ "duck_type"
end
end
class ArgumentExpectation
attr_reader :args
@@constraint_classes = Hash.new { |hash, key| LiteralArgConstraint}
@@constraint_classes[:anything] = AnyArgConstraint
@@constraint_classes[:numeric] = NumericArgConstraint
@@constraint_classes[:boolean] = BooleanArgConstraint
@@constraint_classes[:string] = StringArgConstraint
def initialize(args)
@args = args
- if [:any_args] == args then @expected_params = nil
- elsif [:no_args] == args then @expected_params = []
+ if [:any_args] == args
+ @expected_params = nil
+ warn_deprecated(:any_args.inspect, "any_args()")
+ elsif args.length == 1 && args[0].is_a?(AnyArgsConstraint) then @expected_params = nil
+ elsif [:no_args] == args
+ @expected_params = []
+ warn_deprecated(:no_args.inspect, "no_args()")
+ elsif args.length == 1 && args[0].is_a?(NoArgsConstraint) then @expected_params = []
else @expected_params = process_arg_constraints(args)
end
end
def process_arg_constraints(constraints)
constraints.collect do |constraint|
convert_constraint(constraint)
end
end
+ def warn_deprecated(deprecated_method, instead)
+ STDERR.puts "The #{deprecated_method} constraint is deprecated. Use #{instead} instead."
+ end
+
def convert_constraint(constraint)
- return @@constraint_classes[constraint].new(constraint) if constraint.is_a?(Symbol)
- return constraint if constraint.is_a?(DuckTypeArgConstraint)
+ if [:anything, :numeric, :boolean, :string].include?(constraint)
+ case constraint
+ when :anything
+ instead = "anything()"
+ when :boolean
+ instead = "boolean()"
+ when :numeric
+ instead = "an_instance_of(Numeric)"
+ when :string
+ instead = "an_instance_of(String)"
+ end
+ warn_deprecated(constraint.inspect, instead)
+ return @@constraint_classes[constraint].new(constraint)
+ end
return MatcherConstraint.new(constraint) if is_matcher?(constraint)
return RegexpArgConstraint.new(constraint) if constraint.is_a?(Regexp)
return LiteralArgConstraint.new(constraint)
end
def is_matcher?(obj)
return obj.respond_to?(:matches?) && obj.respond_to?(:description)
end
def check_args(args)
return true if @expected_params.nil?
return true if @expected_params == args
return constraints_match?(args)
end
def constraints_match?(args)
return false if args.length != @expected_params.length
@expected_params.each_index { |i| return false unless @expected_params[i].matches?(args[i]) }
return true
end
end
end
end
diff --git a/test/lib/spec/mocks/error_generator.rb b/test/lib/spec/mocks/error_generator.rb
index 950864a7a..01d8f720d 100644
--- a/test/lib/spec/mocks/error_generator.rb
+++ b/test/lib/spec/mocks/error_generator.rb
@@ -1,85 +1,84 @@
module Spec
module Mocks
class ErrorGenerator
attr_writer :opts
def initialize(target, name)
@target = target
@name = name
end
def opts
@opts ||= {}
end
def raise_unexpected_message_error(sym, *args)
__raise "#{intro} received unexpected message :#{sym}#{arg_message(*args)}"
end
def raise_unexpected_message_args_error(expectation, *args)
- #this is either :no_args or an Array
- expected_args = (expectation.expected_args == :no_args ? "(no args)" : format_args(*expectation.expected_args))
+ expected_args = format_args(*expectation.expected_args)
actual_args = args.empty? ? "(no args)" : format_args(*args)
__raise "#{intro} expected #{expectation.sym.inspect} with #{expected_args} but received it with #{actual_args}"
end
def raise_expectation_error(sym, expected_received_count, actual_received_count, *args)
__raise "#{intro} expected :#{sym}#{arg_message(*args)} #{count_message(expected_received_count)}, but received it #{count_message(actual_received_count)}"
end
def raise_out_of_order_error(sym)
__raise "#{intro} received :#{sym} out of order"
end
def raise_block_failed_error(sym, detail)
__raise "#{intro} received :#{sym} but passed block failed with: #{detail}"
end
def raise_missing_block_error(args_to_yield)
__raise "#{intro} asked to yield |#{arg_list(*args_to_yield)}| but no block was passed"
end
def raise_wrong_arity_error(args_to_yield, arity)
__raise "#{intro} yielded |#{arg_list(*args_to_yield)}| to block with arity of #{arity}"
end
private
def intro
- @name ? "Mock '#{@name}'" : @target.to_s
+ @name ? "Mock '#{@name}'" : @target.inspect
end
def __raise(message)
message = opts[:message] unless opts[:message].nil?
Kernel::raise(Spec::Mocks::MockExpectationError, message)
end
def arg_message(*args)
" with " + format_args(*args)
end
def format_args(*args)
return "(no args)" if args.empty? || args == [:no_args]
return "(any args)" if args == [:any_args]
"(" + arg_list(*args) + ")"
end
def arg_list(*args)
args.collect do |arg|
arg.respond_to?(:description) ? arg.description : arg.inspect
end.join(", ")
end
def count_message(count)
return "at least #{pretty_print(count.abs)}" if count < 0
return pretty_print(count)
end
def pretty_print(count)
return "once" if count == 1
return "twice" if count == 2
return "#{count} times"
end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/mocks/message_expectation.rb b/test/lib/spec/mocks/message_expectation.rb
index 152e65a47..74ade3c58 100644
--- a/test/lib/spec/mocks/message_expectation.rb
+++ b/test/lib/spec/mocks/message_expectation.rb
@@ -1,231 +1,242 @@
module Spec
module Mocks
class BaseExpectation
attr_reader :sym
def initialize(error_generator, expectation_ordering, expected_from, sym, method_block, expected_received_count=1, opts={})
@error_generator = error_generator
@error_generator.opts = opts
@expected_from = expected_from
@sym = sym
@method_block = method_block
@return_block = lambda {}
@received_count = 0
@expected_received_count = expected_received_count
- @args_expectation = ArgumentExpectation.new([:any_args])
+ @args_expectation = ArgumentExpectation.new([AnyArgsConstraint.new])
@consecutive = false
@exception_to_raise = nil
@symbol_to_throw = nil
@order_group = expectation_ordering
@at_least = nil
@at_most = nil
@args_to_yield = nil
end
def expected_args
@args_expectation.args
end
def and_return(*values, &return_block)
Kernel::raise AmbiguousReturnError unless @method_block.nil?
if values.size == 0
value = nil
elsif values.size == 1
value = values[0]
else
value = values
@consecutive = true
@expected_received_count = values.size if @expected_received_count != :any &&
@expected_received_count < values.size
end
@return_block = block_given? ? return_block : lambda { value }
end
+ # :call-seq:
+ # and_raise()
+ # and_raise(Exception) #any exception class
+ # and_raise(exception) #any exception object
+ #
+ # == Warning
+ #
+ # When you pass an exception class, the MessageExpectation will
+ # raise an instance of it, creating it with +new+. If the exception
+ # class initializer requires any parameters, you must pass in an
+ # instance and not the class.
def and_raise(exception=Exception)
@exception_to_raise = exception
end
def and_throw(symbol)
@symbol_to_throw = symbol
end
def and_yield(*args)
@args_to_yield = args
end
def matches(sym, args)
@sym == sym and @args_expectation.check_args(args)
end
def invoke(args, block)
@order_group.handle_order_constraint self
begin
if @exception_to_raise.class == Class
@exception_instance_to_raise = @exception_to_raise.new
else
@exception_instance_to_raise = @exception_to_raise
end
Kernel::raise @exception_to_raise unless @exception_to_raise.nil?
Kernel::throw @symbol_to_throw unless @symbol_to_throw.nil?
if !@method_block.nil?
return invoke_method_block(args)
elsif !@args_to_yield.nil?
return invoke_with_yield(block)
elsif @consecutive
return invoke_consecutive_return_block(args, block)
else
return invoke_return_block(args, block)
end
ensure
@received_count += 1
end
end
protected
def invoke_method_block(args)
begin
@method_block.call(*args)
rescue => detail
@error_generator.raise_block_failed_error @sym, detail.message
end
end
def invoke_with_yield(block)
if block.nil?
@error_generator.raise_missing_block_error @args_to_yield
end
if block.arity > -1 && @args_to_yield.length != block.arity
@error_generator.raise_wrong_arity_error @args_to_yield, block.arity
end
block.call(*@args_to_yield)
end
def invoke_consecutive_return_block(args, block)
args << block unless block.nil?
value = @return_block.call(*args)
index = [@received_count, value.size-1].min
value[index]
end
def invoke_return_block(args, block)
args << block unless block.nil?
value = @return_block.call(*args)
value
end
end
class MessageExpectation < BaseExpectation
def matches_name_but_not_args(sym, args)
@sym == sym and not @args_expectation.check_args(args)
end
def verify_messages_received
return if @expected_received_count == :any
return if (@at_least) && (@received_count >= @expected_received_count)
return if (@at_most) && (@received_count <= @expected_received_count)
return if @expected_received_count == @received_count
begin
@error_generator.raise_expectation_error(@sym, @expected_received_count, @received_count, *@args_expectation.args)
rescue => error
error.backtrace.insert(0, @expected_from)
Kernel::raise error
end
end
def with(*args, &block)
@method_block = block if block
@args_expectation = ArgumentExpectation.new(args)
self
end
def exactly(n)
set_expected_received_count :exactly, n
self
end
def at_least(n)
set_expected_received_count :at_least, n
self
end
def at_most(n)
set_expected_received_count :at_most, n
self
end
def times(&block)
@method_block = block if block
self
end
def any_number_of_times(&block)
@method_block = block if block
@expected_received_count = :any
self
end
def never
@expected_received_count = 0
self
end
def once(&block)
@method_block = block if block
@expected_received_count = 1
self
end
def twice(&block)
@method_block = block if block
@expected_received_count = 2
self
end
def ordered(&block)
@method_block = block if block
@order_group.register(self)
@ordered = true
self
end
def negative_expectation_for?(sym)
return false
end
protected
def set_expected_received_count(relativity, n)
@at_least = (relativity == :at_least)
@at_most = (relativity == :at_most)
@expected_received_count = 1 if n == :once
@expected_received_count = 2 if n == :twice
@expected_received_count = n if n.kind_of? Numeric
end
end
class NegativeMessageExpectation < MessageExpectation
def initialize(message, expectation_ordering, expected_from, sym, method_block)
super(message, expectation_ordering, expected_from, sym, method_block, 0)
end
def negative_expectation_for?(sym)
return @sym == sym
end
end
class MethodStub < BaseExpectation
def initialize(message, expectation_ordering, expected_from, sym, method_block)
super(message, expectation_ordering, expected_from, sym, method_block, 0)
@expected_received_count = :any
end
end
end
end
diff --git a/test/lib/spec/mocks/methods.rb b/test/lib/spec/mocks/methods.rb
index a5f102fcf..3d898cf31 100644
--- a/test/lib/spec/mocks/methods.rb
+++ b/test/lib/spec/mocks/methods.rb
@@ -1,40 +1,39 @@
module Spec
module Mocks
module Methods
def should_receive(sym, opts={}, &block)
- __mock_handler.add_message_expectation(opts[:expected_from] || caller(1)[0], sym, opts, &block)
+ __mock_proxy.add_message_expectation(opts[:expected_from] || caller(1)[0], sym.to_sym, opts, &block)
end
def should_not_receive(sym, &block)
- __mock_handler.add_negative_message_expectation(caller(1)[0], sym, &block)
+ __mock_proxy.add_negative_message_expectation(caller(1)[0], sym.to_sym, &block)
end
def stub!(sym)
- __mock_handler.add_stub(caller(1)[0], sym)
+ __mock_proxy.add_stub(caller(1)[0], sym.to_sym)
end
def received_message?(sym, *args, &block) #:nodoc:
- __mock_handler.received_message?(sym, *args, &block)
+ __mock_proxy.received_message?(sym.to_sym, *args, &block)
end
- def __verify #:nodoc:
- __mock_handler.verify
+ def rspec_verify #:nodoc:
+ __mock_proxy.verify
end
- def __reset_mock #:nodoc:
- __mock_handler.reset
+ def rspec_reset #:nodoc:
+ __mock_proxy.reset
end
- def method_missing(sym, *args, &block) #:nodoc:
- __mock_handler.instance_eval {@messages_received << [sym, args, block]}
- super(sym, *args, &block)
- end
-
- private
+ private
- def __mock_handler
- @mock_handler ||= MockHandler.new(self, @name, @options)
+ def __mock_proxy
+ if Mock === self
+ @mock_proxy ||= Proxy.new(self, @name, @options)
+ else
+ @mock_proxy ||= Proxy.new(self, self.class.name)
+ end
end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/mocks/mock.rb b/test/lib/spec/mocks/mock.rb
index 68de11ff4..aa380e0af 100644
--- a/test/lib/spec/mocks/mock.rb
+++ b/test/lib/spec/mocks/mock.rb
@@ -1,26 +1,29 @@
module Spec
module Mocks
class Mock
include Methods
# Creates a new mock with a +name+ (that will be used in error messages only)
# == Options:
# * :null_object - if true, the mock object acts as a forgiving null object allowing any message to be sent to it.
def initialize(name, options={})
@name = name
@options = options
end
def method_missing(sym, *args, &block)
- __mock_handler.instance_eval {@messages_received << [sym, args, block]}
+ __mock_proxy.instance_eval {@messages_received << [sym, args, block]}
begin
- return self if __mock_handler.null_object?
+ return self if __mock_proxy.null_object?
super(sym, *args, &block)
rescue NoMethodError
- __mock_handler.raise_unexpected_message_error sym, *args
+ __mock_proxy.raise_unexpected_message_error sym, *args
end
end
+ def inspect
+ "#<#{self.class}:#{sprintf '0x%x', self.object_id} @name=#{@name.inspect}>"
+ end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/mocks/proxy.rb b/test/lib/spec/mocks/proxy.rb
new file mode 100644
index 000000000..6c79d1068
--- /dev/null
+++ b/test/lib/spec/mocks/proxy.rb
@@ -0,0 +1,167 @@
+module Spec
+ module Mocks
+ class Proxy
+ DEFAULT_OPTIONS = {
+ :null_object => false,
+ }
+
+ def initialize(target, name, options={})
+ @target = target
+ @name = name
+ @error_generator = ErrorGenerator.new target, name
+ @expectation_ordering = OrderGroup.new @error_generator
+ @expectations = []
+ @messages_received = []
+ @stubs = []
+ @proxied_methods = []
+ @options = options ? DEFAULT_OPTIONS.dup.merge(options) : DEFAULT_OPTIONS
+ end
+
+ def null_object?
+ @options[:null_object]
+ end
+
+ def add_message_expectation(expected_from, sym, opts={}, &block)
+ __add sym, block
+ @expectations << MessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil, 1, opts)
+ @expectations.last
+ end
+
+ def add_negative_message_expectation(expected_from, sym, &block)
+ __add sym, block
+ @expectations << NegativeMessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil)
+ @expectations.last
+ end
+
+ def add_stub(expected_from, sym)
+ __add sym, nil
+ @stubs.unshift MethodStub.new(@error_generator, @expectation_ordering, expected_from, sym, nil)
+ @stubs.first
+ end
+
+ def verify #:nodoc:
+ begin
+ verify_expectations
+ ensure
+ reset
+ end
+ end
+
+ def reset
+ clear_expectations
+ clear_stubs
+ reset_proxied_methods
+ clear_proxied_methods
+ end
+
+ def received_message?(sym, *args, &block)
+ return true if @messages_received.find {|array| array == [sym, args, block]}
+ return false
+ end
+
+ def has_negative_expectation?(sym)
+ @expectations.detect {|expectation| expectation.negative_expectation_for?(sym)}
+ end
+
+ def message_received(sym, *args, &block)
+ if expectation = find_matching_expectation(sym, *args)
+ expectation.invoke(args, block)
+ elsif stub = find_matching_method_stub(sym)
+ stub.invoke([], block)
+ elsif expectation = find_almost_matching_expectation(sym, *args)
+ raise_unexpected_message_args_error(expectation, *args) unless has_negative_expectation?(sym) unless null_object?
+ else
+ @target.send :method_missing, sym, *args, &block
+ end
+ end
+
+ def raise_unexpected_message_args_error(expectation, *args)
+ @error_generator.raise_unexpected_message_args_error expectation, *args
+ end
+
+ def raise_unexpected_message_error(sym, *args)
+ @error_generator.raise_unexpected_message_error sym, *args
+ end
+
+ private
+
+ def __add(sym, block)
+ $rspec_mocks.add(@target) unless $rspec_mocks.nil?
+ define_expected_method(sym)
+ end
+
+ def define_expected_method(sym)
+ if target_responds_to?(sym) && !@proxied_methods.include?(sym)
+ metaclass.__send__(:alias_method, munge(sym), sym) if metaclass.instance_methods.include?(sym.to_s)
+ @proxied_methods << sym
+ end
+
+ metaclass_eval(<<-EOF, __FILE__, __LINE__)
+ def #{sym}(*args, &block)
+ __mock_proxy.message_received :#{sym}, *args, &block
+ end
+ EOF
+ end
+
+ def target_responds_to?(sym)
+ return @target.send(munge(:respond_to?),sym) if @already_proxied_respond_to
+ return @already_proxied_respond_to = true if sym == :respond_to?
+ return @target.respond_to?(sym)
+ end
+
+ def munge(sym)
+ "proxied_by_rspec__#{sym.to_s}".to_sym
+ end
+
+ def clear_expectations
+ @expectations.clear
+ end
+
+ def clear_stubs
+ @stubs.clear
+ end
+
+ def clear_proxied_methods
+ @proxied_methods.clear
+ end
+
+ def metaclass_eval(str, filename, lineno)
+ metaclass.class_eval(str, filename, lineno)
+ end
+
+ def metaclass
+ (class << @target; self; end)
+ end
+
+ def verify_expectations
+ @expectations.each do |expectation|
+ expectation.verify_messages_received
+ end
+ end
+
+ def reset_proxied_methods
+ @proxied_methods.each do |sym|
+ if metaclass.instance_methods.include?(munge(sym).to_s)
+ metaclass.__send__(:alias_method, sym, munge(sym))
+ metaclass.__send__(:undef_method, munge(sym))
+ else
+ metaclass.__send__(:undef_method, sym)
+ end
+ end
+ end
+
+ def find_matching_expectation(sym, *args)
+ @expectations.find {|expectation| expectation.matches(sym, args)}
+ end
+
+ def find_almost_matching_expectation(sym, *args)
+ @expectations.find {|expectation| expectation.matches_name_but_not_args(sym, args)}
+ end
+
+ def find_matching_method_stub(sym)
+ @stubs.find {|stub| stub.matches(sym, [])}
+ end
+
+ end
+ end
+end
diff --git a/test/lib/spec/mocks/space.rb b/test/lib/spec/mocks/space.rb
new file mode 100644
index 000000000..e04bc5ccb
--- /dev/null
+++ b/test/lib/spec/mocks/space.rb
@@ -0,0 +1,28 @@
+module Spec
+ module Mocks
+ class Space
+ def add(obj)
+ mocks << obj unless mocks.include?(obj)
+ end
+
+ def verify_all
+ mocks.each do |mock|
+ mock.rspec_verify
+ end
+ end
+
+ def reset_all
+ mocks.each do |mock|
+ mock.rspec_reset
+ end
+ mocks.clear
+ end
+
+ private
+
+ def mocks
+ @mocks ||= []
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/mocks/spec_methods.rb b/test/lib/spec/mocks/spec_methods.rb
new file mode 100644
index 000000000..fd67fd210
--- /dev/null
+++ b/test/lib/spec/mocks/spec_methods.rb
@@ -0,0 +1,30 @@
+module Spec
+ module Mocks
+ module SpecMethods
+ include Spec::Mocks::ArgumentConstraintMatchers
+
+ # Shortcut for creating an instance of Spec::Mocks::Mock.
+ def mock(name, options={})
+ Spec::Mocks::Mock.new(name, options)
+ end
+
+ # Shortcut for creating an instance of Spec::Mocks::Mock with
+ # predefined method stubs.
+ #
+ # == Examples
+ #
+ # stub_thing = stub("thing", :a => "A")
+ # stub_thing.a == "A" => true
+ #
+ # stub_person = stub("thing", :name => "Joe", :email => "joe@domain.com")
+ # stub_person.name => "Joe"
+ # stub_person.email => "joe@domain.com"
+ def stub(name, stubs={})
+ object_stub = mock(name)
+ stubs.each { |key, value| object_stub.stub!(key).and_return(value) }
+ object_stub
+ end
+
+ end
+ end
+end
diff --git a/test/lib/spec/rake/spectask.rb b/test/lib/spec/rake/spectask.rb
index 5c9b365c1..f8c6809a9 100644
--- a/test/lib/spec/rake/spectask.rb
+++ b/test/lib/spec/rake/spectask.rb
@@ -1,173 +1,217 @@
#!/usr/bin/env ruby
# Define a task library for running RSpec contexts.
require 'rake'
require 'rake/tasklib'
module Spec
module Rake
# A Rake task that runs a set of RSpec contexts.
#
# Example:
#
# Spec::Rake::SpecTask.new do |t|
# t.warning = true
# t.rcov = true
# end
#
# This will create a task that can be run with:
#
# rake spec
#
+ # If rake is invoked with a "SPEC=filename" command line option,
+ # then the list of spec files will be overridden to include only the
+ # filename specified on the command line. This provides an easy way
+ # to run just one spec.
+ #
+ # If rake is invoked with a "SPEC_OPTS=options" command line option,
+ # then the given options will override the value of the +spec_opts+
+ # attribute.
+ #
+ # If rake is invoked with a "RCOV_OPTS=options" command line option,
+ # then the given options will override the value of the +rcov_opts+
+ # attribute.
+ #
+ # Examples:
+ #
+ # rake spec # run specs normally
+ # rake spec SPEC=just_one_file.rb # run just one spec file.
+ # rake spec SPEC_OPTS="--diff" # enable diffing
+ # rake spec RCOV_OPTS="--aggregate myfile.txt" # see rcov --help for details
+ #
+ # Each attribute of this task may be a proc. This allows for lazy evaluation,
+ # which is sometimes handy if you want to defer the evaluation of an attribute value
+ # until the task is run (as opposed to when it is defined).
class SpecTask < ::Rake::TaskLib
+ class << self
+ def attr_accessor(*names)
+ super(*names)
+ names.each do |name|
+ module_eval "def #{name}() evaluate(@#{name}) end" # Allows use of procs
+ end
+ end
+ end
# Name of spec task. (default is :spec)
attr_accessor :name
# Array of directories to be added to $LOAD_PATH before running the
# specs. Defaults to ['setup
- # and after the last teardown
. The use of these is generally discouraged, because it
- # introduces dependencies between the specs. Still, it might prove useful for very expensive operations
+ # It is also possible to specify a before :all and after :all
+ # block that will run only once for each behaviour, respectively before the first before :each
+ # and after the last after :each
. The use of these is generally discouraged, because it
+ # introduces dependencies between the examples. Still, it might prove useful for very expensive operations
# if you know what you are doing.
#
# == Local helper methods
#
# You can include local helper methods by simply expressing them within a context:
#
- # context "..." do
+ # describe "..." do
#
- # specify "..." do
+ # it "..." do
# helper_method
# end
#
# def helper_method
# ...
# end
#
# end
#
# == Included helper methods
#
# You can include helper methods in multiple contexts by expressing them within
# a module, and then including that module in your context:
#
# module AccountExampleHelperMethods
# def helper_method
# ...
# end
# end
#
- # context "A new account" do
+ # describe "A new account" do
# include AccountExampleHelperMethods
- # setup do
+ # before do
# @account = Account.new
# end
#
- # specify "should have a balance of $0" do
+ # it "should have a balance of $0" do
# helper_method
# @account.balance.should eql(Money.new(0, :dollars))
# end
# end
+ #
+ # == Shared behaviour
+ #
+ # You can define a shared behaviour, that may be used on other behaviours
+ #
+ # describe "All Editions", :shared => true do
+ # it "all editions behaviour" ...
+ # end
+ #
+ # describe SmallEdition do
+ # it_should_behave_like "All Editions"
+ #
+ # it "should do small edition stuff" do
+ # ...
+ # end
+ # end
module Runner
+ class << self
+ def configuration # :nodoc:
+ @configuration ||= Spec::DSL::Configuration.new
+ end
+
+ # Use this to configure various configurable aspects of
+ # RSpec:
+ #
+ # Spec::Runner.configure do |configuration|
+ # # Configure RSpec here
+ # end
+ #
+ # The yielded configuration object is a
+ # Spec::DSL::Configuration instance. See its RDoc
+ # for details about what you can do with it.
+ #
+ def configure
+ yield configuration if @configuration.nil?
+ end
+ end
end
end
diff --git a/test/lib/spec/runner/backtrace_tweaker.rb b/test/lib/spec/runner/backtrace_tweaker.rb
index 7300b36b8..aacc2c8b8 100644
--- a/test/lib/spec/runner/backtrace_tweaker.rb
+++ b/test/lib/spec/runner/backtrace_tweaker.rb
@@ -1,55 +1,57 @@
module Spec
module Runner
class BacktraceTweaker
def clean_up_double_slashes(line)
line.gsub!('//','/')
end
end
class NoisyBacktraceTweaker < BacktraceTweaker
def tweak_backtrace(error, spec_name)
return if error.backtrace.nil?
error.backtrace.each do |line|
clean_up_double_slashes(line)
end
end
end
# Tweaks raised Exceptions to mask noisy (unneeded) parts of the backtrace
class QuietBacktraceTweaker < BacktraceTweaker
unless defined?(IGNORE_PATTERNS)
root_dir = File.expand_path(File.join(__FILE__, '..', '..', '..', '..'))
spec_files = Dir["#{root_dir}/lib/spec/*"].map do |path|
subpath = path[root_dir.length..-1]
/#{subpath}/
end
IGNORE_PATTERNS = spec_files + [
/\/lib\/ruby\//,
/bin\/spec:/,
/bin\/rcov:/,
/lib\/rspec_on_rails/,
/vendor\/rails/,
# TextMate's Ruby and RSpec plugins
/Ruby\.tmbundle\/Support\/tmruby.rb:/,
/RSpec\.tmbundle\/Support\/lib/,
- /temp_textmate\./
+ /temp_textmate\./,
+ /mock_frameworks\/rspec/,
+ /spec_server/
]
end
def tweak_backtrace(error, spec_name)
return if error.backtrace.nil?
error.backtrace.collect! do |line|
clean_up_double_slashes(line)
IGNORE_PATTERNS.each do |ignore|
if line =~ ignore
line = nil
break
end
end
line
end
error.backtrace.compact!
end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/runner/behaviour_runner.rb b/test/lib/spec/runner/behaviour_runner.rb
new file mode 100644
index 000000000..1ac891f3c
--- /dev/null
+++ b/test/lib/spec/runner/behaviour_runner.rb
@@ -0,0 +1,123 @@
+module Spec
+ module Runner
+ class BehaviourRunner
+
+ def initialize(options, arg=nil)
+ @behaviours = []
+ @options = options
+ end
+
+ def add_behaviour(behaviour)
+ if !specified_examples.nil? && !specified_examples.empty?
+ behaviour.retain_examples_matching!(specified_examples)
+ end
+ @behaviours << behaviour if behaviour.number_of_examples != 0 && !behaviour.shared?
+ end
+
+ # Runs all behaviours and returns the number of failures.
+ def run(paths, exit_when_done)
+ prepare!(paths)
+ begin
+ run_behaviours
+ rescue Interrupt
+ ensure
+ report_end
+ end
+ failure_count = report_dump
+
+ heckle if(failure_count == 0 && !@options.heckle_runner.nil?)
+
+ if(exit_when_done)
+ exit_code = (failure_count == 0) ? 0 : 1
+ exit(exit_code)
+ end
+ failure_count
+ end
+
+ def report_end
+ @options.reporter.end
+ end
+
+ def report_dump
+ @options.reporter.dump
+ end
+
+ def prepare!(paths)
+ unless paths.nil? # It's nil when running single specs with ruby
+ paths = find_paths(paths)
+ sorted_paths = sort_paths(paths)
+ load_specs(sorted_paths) # This will populate @behaviours via callbacks to add_behaviour
+ end
+ @options.reporter.start(number_of_examples)
+ @behaviours.reverse! if @options.reverse
+ set_sequence_numbers
+ end
+
+ def run_behaviours
+ @behaviours.each do |behaviour|
+ behaviour.run(@options.reporter, @options.dry_run, @options.reverse, @options.timeout)
+ end
+ end
+
+ def number_of_examples
+ @behaviours.inject(0) {|sum, behaviour| sum + behaviour.number_of_examples}
+ end
+
+ FILE_SORTERS = {
+ 'mtime' => lambda {|file_a, file_b| File.mtime(file_b) <=> File.mtime(file_a)}
+ }
+
+ def sorter(paths)
+ FILE_SORTERS[@options.loadby]
+ end
+
+ def sort_paths(paths)
+ sorter = sorter(paths)
+ paths = paths.sort(&sorter) unless sorter.nil?
+ paths
+ end
+
+ private
+
+ # Sets the #number on each Example
+ def set_sequence_numbers
+ number = 0
+ @behaviours.each do |behaviour|
+ number = behaviour.set_sequence_numbers(number, @options.reverse)
+ end
+ end
+
+ def find_paths(paths)
+ result = []
+ paths.each do |path|
+ if File.directory?(path)
+ result += Dir["#{path}/**/*.rb"]
+ elsif File.file?(path)
+ result << path
+ else
+ raise "File or directory not found: #{path}"
+ end
+ end
+ result
+ end
+
+ def load_specs(paths)
+ paths.each do |path|
+ load path
+ end
+ end
+
+ def specified_examples
+ @options.examples
+ end
+
+ def heckle
+ heckle_runner = @options.heckle_runner
+ @options.heckle_runner = nil
+ behaviour_runner = self.class.new(@options)
+ behaviour_runner.instance_variable_set(:@behaviours, @behaviours)
+ heckle_runner.heckle_with(behaviour_runner)
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/command_line.rb b/test/lib/spec/runner/command_line.rb
index db928ad9b..0d70337e1 100644
--- a/test/lib/spec/runner/command_line.rb
+++ b/test/lib/spec/runner/command_line.rb
@@ -1,34 +1,22 @@
require 'spec/runner/option_parser'
module Spec
module Runner
# Facade to run specs without having to fork a new ruby process (using `spec ...`)
class CommandLine
# Runs specs. +argv+ is the commandline args as per the spec commandline API, +err+
# and +out+ are the streams output will be written to. +exit+ tells whether or
# not a system exit should be called after the specs are run and
# +warn_if_no_files+ tells whether or not a warning (the help message)
# should be printed to +err+ in case no files are specified.
def self.run(argv, err, out, exit=true, warn_if_no_files=true)
- old_context_runner = defined?($context_runner) ? $context_runner : nil
- $context_runner = OptionParser.new.create_context_runner(argv, err, out, warn_if_no_files)
- return if $context_runner.nil? # This is the case if we use --drb
+ old_behaviour_runner = defined?($behaviour_runner) ? $behaviour_runner : nil
+ $behaviour_runner = OptionParser.new.create_behaviour_runner(argv, err, out, warn_if_no_files)
+ return if $behaviour_runner.nil? # This is the case if we use --drb
- # If ARGV is a glob, it will actually each over each one of the matching files.
- argv.each do |file_or_dir|
- if File.directory?(file_or_dir)
- Dir["#{file_or_dir}/**/*.rb"].each do |file|
- load file
- end
- elsif File.file?(file_or_dir)
- load file_or_dir
- else
- raise "File or directory not found: #{file_or_dir}"
- end
- end
- $context_runner.run(exit)
- $context_runner = old_context_runner
+ $behaviour_runner.run(argv, exit)
+ $behaviour_runner = old_behaviour_runner
end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/runner/drb_command_line.rb b/test/lib/spec/runner/drb_command_line.rb
index d4c7d937d..7e745fb71 100644
--- a/test/lib/spec/runner/drb_command_line.rb
+++ b/test/lib/spec/runner/drb_command_line.rb
@@ -1,21 +1,21 @@
require "drb/drb"
module Spec
module Runner
# Facade to run specs by connecting to a DRB server
class DrbCommandLine
# Runs specs on a DRB server. Note that this API is similar to that of
# CommandLine - making it possible for clients to use both interchangeably.
def self.run(argv, stderr, stdout, exit=true, warn_if_no_files=true)
begin
DRb.start_service
- rails_spec_server = DRbObject.new_with_uri("druby://localhost:8989")
- rails_spec_server.run(argv, stderr, stdout)
+ spec_server = DRbObject.new_with_uri("druby://localhost:8989")
+ spec_server.run(argv, stderr, stdout)
rescue DRb::DRbConnError
stderr.puts "No server is running"
exit 1 if exit
end
end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/lib/spec/runner/extensions/kernel.rb b/test/lib/spec/runner/extensions/kernel.rb
index f060ec859..75f2c335e 100644
--- a/test/lib/spec/runner/extensions/kernel.rb
+++ b/test/lib/spec/runner/extensions/kernel.rb
@@ -1,17 +1,50 @@
module Kernel
- def context(name, &block)
- context = Spec::Runner::Context.new(name, &block)
- context_runner.add_context(context)
+ # Creates and registers an instance of a Spec::DSL::Behaviour (or a subclass).
+ # The instantiated behaviour class depends on the directory of the file
+ # calling this method. For example, Spec::Rails will use different
+ # classes for specs living in spec/models, spec/helpers,
+ # spec/views and spec/controllers.
+ #
+ # It is also possible to override autodiscovery of the behaviour class
+ # with an options Hash as the last argument:
+ #
+ # describe "name", :behaviour_type => :something_special do ...
+ #
+ # The reason for using different behaviour classes is to have
+ # different matcher methods available from within the describe
+ # block.
+ #
+ # See Spec::DSL::BehaviourFactory#add_behaviour_class for details about
+ # how to register special Spec::DSL::Behaviour implementations.
+ #
+ def describe(*args, &block)
+ raise ArgumentError if args.empty?
+ args << {} unless Hash === args.last
+ args.last[:spec_path] = caller(0)[1]
+ register_behaviour(Spec::DSL::BehaviourFactory.create(*args, &block))
end
-
+ alias :context :describe
+
+ def respond_to(*names)
+ Spec::Matchers::RespondTo.new(*names)
+ end
+
private
- def context_runner
+ def register_behaviour(behaviour)
+ if behaviour.shared?
+ Spec::DSL::Behaviour.add_shared_behaviour(behaviour)
+ else
+ behaviour_runner.add_behaviour(behaviour)
+ end
+ end
+
+ def behaviour_runner
# TODO: Figure out a better way to get this considered "covered" and keep this statement on multiple lines
- unless $context_runner; \
- $context_runner = ::Spec::Runner::OptionParser.new.create_context_runner(ARGV.dup, STDERR, STDOUT, false); \
- at_exit { $context_runner.run(false) }; \
+ unless $behaviour_runner; \
+ $behaviour_runner = ::Spec::Runner::OptionParser.new.create_behaviour_runner(ARGV.dup, STDERR, STDOUT, false); \
+ at_exit { $behaviour_runner.run(nil, false) }; \
end
- $context_runner
+ $behaviour_runner
end
end
diff --git a/test/lib/spec/runner/formatter.rb b/test/lib/spec/runner/formatter.rb
index f62e81733..17512d958 100644
--- a/test/lib/spec/runner/formatter.rb
+++ b/test/lib/spec/runner/formatter.rb
@@ -1,5 +1,9 @@
+require 'spec/runner/formatter/base_formatter'
require 'spec/runner/formatter/base_text_formatter'
require 'spec/runner/formatter/progress_bar_formatter'
require 'spec/runner/formatter/rdoc_formatter'
require 'spec/runner/formatter/specdoc_formatter'
require 'spec/runner/formatter/html_formatter'
+require 'spec/runner/formatter/failing_examples_formatter'
+require 'spec/runner/formatter/failing_behaviours_formatter'
+require 'spec/runner/formatter/snippet_extractor'
diff --git a/test/lib/spec/runner/formatter/base_formatter.rb b/test/lib/spec/runner/formatter/base_formatter.rb
new file mode 100644
index 000000000..7cc43ef0e
--- /dev/null
+++ b/test/lib/spec/runner/formatter/base_formatter.rb
@@ -0,0 +1,76 @@
+module Spec
+ module Runner
+ module Formatter
+ # Baseclass for formatters that implements all required methods as no-ops.
+ class BaseFormatter
+ def initialize(where)
+ @where = where
+ end
+
+ # This method is invoked before any examples are run, right after
+ # they have all been collected. This can be useful for special
+ # formatters that need to provide progress on feedback (graphical ones)
+ #
+ # This method will only be invoked once, and the next one to be invoked
+ # is #add_behaviour
+ def start(example_count)
+ end
+
+ # This method is invoked at the beginning of the execution of each behaviour.
+ # +name+ is the name of the behaviour and +first+ is true if it is the
+ # first behaviour - otherwise it's false.
+ #
+ # The next method to be invoked after this is #example_failed or #example_finished
+ def add_behaviour(name)
+ end
+
+ # This method is invoked when an +example+ starts.
+ def example_started(example)
+ end
+
+ # This method is invoked when an +example+ passes.
+ def example_passed(example)
+ end
+
+ # This method is invoked when an +example+ fails, i.e. an exception occurred
+ # inside it (such as a failed should or other exception). +counter+ is the
+ # sequence number of the failure (starting at 1) and +failure+ is the associated
+ # Failure object.
+ def example_failed(example, counter, failure)
+ end
+
+ # This method is invoked when an example is not yet implemented (i.e. has not
+ # been provided a block), or when an ExamplePendingError is raised.
+ # +name+ is the name of the example.
+ # +message+ is the message from the ExamplePendingError, if it exists, or the
+ # default value of "Not Yet Implemented"
+ def example_pending(behaviour_name, example_name, message)
+ end
+
+ # This method is invoked after all of the examples have executed. The next method
+ # to be invoked after this one is #dump_failure (once for each failed example),
+ def start_dump
+ end
+
+ # Dumps detailed information about an example failure.
+ # This method is invoked for each failed example after all examples have run. +counter+ is the sequence number
+ # of the associated example. +failure+ is a Failure object, which contains detailed
+ # information about the failure.
+ def dump_failure(counter, failure)
+ end
+
+ # This method is invoked after the dumping of examples and failures.
+ def dump_summary(duration, example_count, failure_count, pending_count)
+ end
+
+ # This gets invoked after the summary if option is set to do so.
+ def dump_pending
+ end
+
+ # This method is invoked at the very end. Allows the formatter to clean up, like closing open streams.
+ def close
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/formatter/base_text_formatter.rb b/test/lib/spec/runner/formatter/base_text_formatter.rb
index 31d1c3132..c3cf01b76 100644
--- a/test/lib/spec/runner/formatter/base_text_formatter.rb
+++ b/test/lib/spec/runner/formatter/base_text_formatter.rb
@@ -1,118 +1,130 @@
module Spec
module Runner
module Formatter
# Baseclass for text-based formatters. Can in fact be used for
# non-text based ones too - just ignore the +output+ constructor
# argument.
- class BaseTextFormatter
- def initialize(output, dry_run=false, colour=false)
- @output = output
- @dry_run = dry_run
- @colour = colour
- begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end
- end
-
- # This method is invoked before any specs are run, right after
- # they have all been collected. This can be useful for special
- # formatters that need to provide progress on feedback (graphical ones)
- #
- # This method will only be invoked once, and the next one to be invoked
- # is #add_context
- def start(spec_count)
- end
-
- # This method is invoked at the beginning of the execution of each context.
- # +name+ is the name of the context and +first+ is true if it is the
- # first context - otherwise it's false.
- #
- # The next method to be invoked after this is #spec_started
- def add_context(name, first)
- end
-
- # This method is invoked right before a spec is executed.
- # The next method to be invoked after this one is one of #spec_failed
- # or #spec_passed.
- def spec_started(name)
- end
-
- # This method is invoked when a spec fails, i.e. an exception occurred
- # inside it (such as a failed should or other exception). +name+ is the name
- # of the specification. +counter+ is the sequence number of the failure
- # (starting at 1) and +failure+ is the associated Failure object.
- def spec_failed(name, counter, failure)
+ class BaseTextFormatter < BaseFormatter
+ attr_writer :dry_run
+
+ # Creates a new instance that will write to +where+. If +where+ is a
+ # String, output will be written to the File with that name, otherwise
+ # +where+ is exected to be an IO (or an object that responds to #puts and #write).
+ def initialize(where)
+ super(where)
+ if where.is_a?(String)
+ @output = File.open(where, 'w')
+ elsif where == STDOUT
+ @output = Kernel
+ def @output.flush
+ STDOUT.flush
+ end
+ else
+ @output = where
+ end
+ @colour = false
+ @dry_run = false
+ @snippet_extractor = SnippetExtractor.new
+ @pending_examples = []
end
-
- # This method is invoked when a spec passes. +name+ is the name of the
- # specification.
- def spec_passed(name)
+
+ def example_pending(behaviour_name, example_name, message)
+ @pending_examples << ["#{behaviour_name} #{example_name}", message]
end
-
- # This method is invoked after all of the specs have executed. The next method
- # to be invoked after this one is #dump_failure (once for each failed spec),
- def start_dump
+
+ def colour=(colour)
+ @colour = colour
+ begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end
end
- # Dumps detailed information about a spec failure.
- # This method is invoked for each failed spec after all specs have run. +counter+ is the sequence number
- # of the associated spec. +failure+ is a Failure object, which contains detailed
- # information about the failure.
def dump_failure(counter, failure)
@output.puts
@output.puts "#{counter.to_s})"
+ @output.puts colourise("#{failure.header}\n#{failure.exception.message}", failure)
+ @output.puts format_backtrace(failure.exception.backtrace)
+ @output.flush
+ end
+
+ def colourise(s, failure)
if(failure.expectation_not_met?)
- @output.puts red(failure.header)
- @output.puts red(failure.exception.message)
+ red(s)
+ elsif(failure.pending_fixed?)
+ blue(s)
else
- @output.puts magenta(failure.header)
- @output.puts magenta(failure.exception.message)
+ magenta(s)
end
- @output.puts format_backtrace(failure.exception.backtrace)
- STDOUT.flush
end
- # This method is invoked at the very end.
- def dump_summary(duration, spec_count, failure_count)
+ def dump_summary(duration, example_count, failure_count, pending_count)
return if @dry_run
@output.puts
@output.puts "Finished in #{duration} seconds"
@output.puts
- summary = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
+
+ summary = "#{example_count} example#{'s' unless example_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
+ summary << ", #{pending_count} pending" if pending_count > 0
+
if failure_count == 0
- @output.puts green(summary)
+ if pending_count > 0
+ @output.puts yellow(summary)
+ else
+ @output.puts green(summary)
+ end
else
@output.puts red(summary)
end
+ @output.flush
+ dump_pending
end
+ def dump_pending
+ unless @pending_examples.empty?
+ @output.puts
+ @output.puts "Pending:"
+ @pending_examples.each do |pending_example|
+ @output.puts "#{pending_example[0]} (#{pending_example[1]})"
+ end
+ end
+ @output.flush
+ end
+
+ def close
+ if IO === @output
+ @output.close
+ end
+ end
+
def format_backtrace(backtrace)
return "" if backtrace.nil?
backtrace.map { |line| backtrace_line(line) }.join("\n")
end
protected
def backtrace_line(line)
line.sub(/\A([^:]+:\d+)$/, '\\1:')
end
def colour(text, colour_code)
return text unless @colour && output_to_tty?
"#{colour_code}#{text}\e[0m"
end
def output_to_tty?
begin
@output == Kernel || @output.tty?
rescue NoMethodError
false
end
end
- def red(text); colour(text, "\e[31m"); end
def green(text); colour(text, "\e[32m"); end
+ def red(text); colour(text, "\e[31m"); end
def magenta(text); colour(text, "\e[35m"); end
+ def yellow(text); colour(text, "\e[33m"); end
+ def blue(text); colour(text, "\e[34m"); end
end
end
end
end
diff --git a/test/lib/spec/runner/formatter/failing_behaviours_formatter.rb b/test/lib/spec/runner/formatter/failing_behaviours_formatter.rb
new file mode 100644
index 000000000..2b3940fd3
--- /dev/null
+++ b/test/lib/spec/runner/formatter/failing_behaviours_formatter.rb
@@ -0,0 +1,29 @@
+module Spec
+ module Runner
+ module Formatter
+ class FailingBehavioursFormatter < BaseTextFormatter
+ def add_behaviour(behaviour_name)
+ if behaviour_name =~ /(.*) \(druby.*\)$/
+ @behaviour_name = $1
+ else
+ @behaviour_name = behaviour_name
+ end
+ end
+
+ def example_failed(example, counter, failure)
+ unless @behaviour_name.nil?
+ @output.puts @behaviour_name
+ @behaviour_name = nil
+ @output.flush
+ end
+ end
+
+ def dump_failure(counter, failure)
+ end
+
+ def dump_summary(duration, example_count, failure_count, pending_count)
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/formatter/failing_examples_formatter.rb b/test/lib/spec/runner/formatter/failing_examples_formatter.rb
new file mode 100644
index 000000000..9728deaf0
--- /dev/null
+++ b/test/lib/spec/runner/formatter/failing_examples_formatter.rb
@@ -0,0 +1,22 @@
+module Spec
+ module Runner
+ module Formatter
+ class FailingExamplesFormatter < BaseTextFormatter
+ def add_behaviour(behaviour_name)
+ @behaviour_name = behaviour_name
+ end
+
+ def example_failed(example, counter, failure)
+ @output.puts "#{@behaviour_name} #{example.description}"
+ @output.flush
+ end
+
+ def dump_failure(counter, failure)
+ end
+
+ def dump_summary(duration, example_count, failure_count, pending_count)
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/formatter/html_formatter.rb b/test/lib/spec/runner/formatter/html_formatter.rb
index 13b796581..d9c422e55 100644
--- a/test/lib/spec/runner/formatter/html_formatter.rb
+++ b/test/lib/spec/runner/formatter/html_formatter.rb
@@ -1,219 +1,323 @@
+require 'erb'
+
module Spec
module Runner
module Formatter
class HtmlFormatter < BaseTextFormatter
- attr_reader :current_spec_number, :current_context_number
+ include ERB::Util # for the #h method
- def initialize(output, dry_run=false, colour=false)
+ def initialize(output)
super
- @current_spec_number = 0
- @current_context_number = 0
+ @current_behaviour_number = 0
+ @current_example_number = 0
end
- def start(spec_count)
- @spec_count = spec_count
+ # The number of the currently running behaviour
+ def current_behaviour_number
+ @current_behaviour_number
+ end
+
+ # The number of the currently running example (a global counter)
+ def current_example_number
+ @current_example_number
+ end
+
+ def start(example_count)
+ @example_count = example_count
- @output.puts HEADER_1
- @output.puts extra_header_content unless extra_header_content.nil?
- @output.puts HEADER_2
- STDOUT.flush
+ @output.puts html_header
+ @output.puts report_header
+ @output.flush
end
- def add_context(name, first)
- @current_context_number += 1
- unless first
+ def add_behaviour(name)
+ @behaviour_red = false
+ @behaviour_red = false
+ @current_behaviour_number += 1
+ unless current_behaviour_number == 1
@output.puts " "
@output.puts ""
end
- @output.puts "#{format_backtrace(failure.exception.backtrace)}
#{@snippet_extractor.snippet(failure.exception)}
"
end
def move_progress
- percent_done = @spec_count == 0 ? 100.0 : (@current_spec_number.to_f / @spec_count.to_f * 1000).to_i / 10.0
+ percent_done = @example_count == 0 ? 100.0 : ((current_example_number + 1).to_f / @example_count.to_f * 1000).to_i / 10.0
@output.puts " "
- end
-
- def escape(string)
- string.gsub(/&/n, '&').gsub(/\"/n, '"').gsub(/>/n, '>').gsub(/ 0
end
@output.puts ""
@output.puts ""
@output.puts "