diff options
author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-17 02:48:41 +0000 |
---|---|---|
committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-17 02:48:41 +0000 |
commit | ba23a5ac276e59fdda8186750c6d0fd2cfecdeac (patch) | |
tree | 1e14b25ade74ea52d8da2788ede9b12b507867e8 /test/lib/spec | |
parent | 8ea6adaeb1e3d0aa6348c2a2c3a385d185372d06 (diff) | |
download | puppet-ba23a5ac276e59fdda8186750c6d0fd2cfecdeac.tar.gz puppet-ba23a5ac276e59fdda8186750c6d0fd2cfecdeac.tar.xz puppet-ba23a5ac276e59fdda8186750c6d0fd2cfecdeac.zip |
Adding spec libs, so we can use them some day
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2283 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'test/lib/spec')
72 files changed, 5087 insertions, 0 deletions
diff --git a/test/lib/spec/callback.rb b/test/lib/spec/callback.rb new file mode 100644 index 000000000..aa7ecec0f --- /dev/null +++ b/test/lib/spec/callback.rb @@ -0,0 +1,11 @@ +require 'spec/callback/callback_container' +require 'spec/callback/extensions/module' +require 'spec/callback/extensions/object' + +# Callback is a fork of Brian Takita's "callback library" (see http://callback.rubyforge.org), +# which Brian graciously contributed to RSpec in order to avoid the dependency. +# +# RSpec uses Callback internally to create hooks to Spec::Runner events. If you're interested +# in a simple, powerful API for generating callback events, check out http://callback.rubyforge.org. +module Callback +end diff --git a/test/lib/spec/callback/callback_container.rb b/test/lib/spec/callback/callback_container.rb new file mode 100644 index 000000000..24d4c0ced --- /dev/null +++ b/test/lib/spec/callback/callback_container.rb @@ -0,0 +1,60 @@ +module Callback + class CallbackContainer + def initialize + @callback_registry = Hash.new do |hash, key| + hash[key] = Array.new + end + end + + # Defines the callback with the key in this container. + def define(key, callback_proc=nil, &callback_block) + callback = extract_callback(callback_block, callback_proc) do + raise "You must define the callback that accepts the call method." + end + @callback_registry[key] << callback + callback + end + + # Undefines the callback with the key in this container. + def undefine(key, callback_proc) + callback = extract_callback(callback_proc) do + raise "You may only undefine callbacks that use the call method." + end + @callback_registry[key].delete callback + callback + end + + # Notifies the callbacks for the key. Arguments may be passed. + # An error handler may be passed in as a block. If there is an error, the block is called with + # error object as an argument. + # An array of the return values of the callbacks is returned. + def notify(key, *args, &error_handler) + @callback_registry[key].collect do |callback| + begin + callback.call(*args) + rescue Exception => e + yield(e) if error_handler + end + end + end + + # Clears all of the callbacks in this container. + def clear + @callback_registry.clear + end + + protected + def extract_callback(first_choice_callback, second_choice_callback = nil) + callback = nil + if first_choice_callback + callback = first_choice_callback + elsif second_choice_callback + callback = second_choice_callback + end + unless callback.respond_to? :call + yield + end + return callback + end + end +end diff --git a/test/lib/spec/callback/extensions/module.rb b/test/lib/spec/callback/extensions/module.rb new file mode 100644 index 000000000..429268ed1 --- /dev/null +++ b/test/lib/spec/callback/extensions/module.rb @@ -0,0 +1,24 @@ +module Callback + module ModuleMethods + # For each event_name submitted, defines a callback event with this name. + # Client code can then register as a callback listener using object.event_name. + def callback_events(*event_names) + event_names.each do |event_name| + define_callback_event(event_name) + end + end + + private + def define_callback_event(event_name) + module_eval <<-EOS + def #{event_name}(&block) + register_callback(:#{event_name}, &block) + end + EOS + end + end +end + +class Module + include Callback::ModuleMethods +end diff --git a/test/lib/spec/callback/extensions/object.rb b/test/lib/spec/callback/extensions/object.rb new file mode 100644 index 000000000..c6ac6fd14 --- /dev/null +++ b/test/lib/spec/callback/extensions/object.rb @@ -0,0 +1,37 @@ +module Callback + module InstanceMethods + # Registers a callback for the event on the object. The callback can either be a block or a proc. + # When the callbacks are notified, the return value of the proc is passed to the caller. + def register_callback(event, callback_proc=nil, &callback_block) + callbacks.define(event, callback_proc, &callback_block) + end + + # Removes the callback from the event. The callback proc must be the same + # object as the one that was passed to register_callback. + def unregister_callback(event, callback_proc) + callbacks.undefine(event, callback_proc) + end + + protected + # Notifies the callbacks registered with the event on the object. Arguments can be passed to the callbacks. + # An error handler may be passed in as a block. If there is an error, the block is called with + # error object as an argument. + # An array of the return values of the callbacks is returned. + def notify_callbacks(event, *args, &error_handler) + callbacks.notify(event, *args, &error_handler) + end + + def notify_class_callbacks(event, *args, &error_handler) + self.class.send(:notify_callbacks, event, *args, &error_handler) + end + + # The CallbackContainer for this object. + def callbacks + @callbacks ||= CallbackContainer.new + end + end +end + +class Object + include Callback::InstanceMethods +end
\ No newline at end of file diff --git a/test/lib/spec/deprecated.rb b/test/lib/spec/deprecated.rb new file mode 100644 index 000000000..e9c1cd829 --- /dev/null +++ b/test/lib/spec/deprecated.rb @@ -0,0 +1,3 @@ +def deprecated(&block) + block.call unless ENV['RSPEC_DISABLE_DEPRECATED_FEATURES'] == 'true' +end diff --git a/test/lib/spec/expectations.rb b/test/lib/spec/expectations.rb new file mode 100644 index 000000000..cc58bba15 --- /dev/null +++ b/test/lib/spec/expectations.rb @@ -0,0 +1,59 @@ +require 'spec/deprecated' +require 'spec/matchers' +require 'spec/expectations/sugar' +require 'spec/expectations/errors' +require 'spec/expectations/extensions' +require 'spec/expectations/should' +require 'spec/expectations/handler' + +module Spec + + # Spec::Expectations lets you set expectations on your objects. + # + # result.should == 37 + # team.should have(11).players_on_the_field + # + # == How Expectations work. + # + # Spec::Expectations adds two methods to Object: + # + # should(matcher=nil) + # should_not(matcher=nil) + # + # Both methods take an optional Expression Matcher (See Spec::Matchers). + # + # When +should+ receives an Expression Matcher, it calls <tt>matches?(self)</tt>. If + # it returns +true+, the spec passes and execution continues. If it returns + # +false+, then the spec fails with the message returned by <tt>matcher.failure_message</tt>. + # + # Similarly, when +should_not+ receives a matcher, it calls <tt>matches?(self)</tt>. If + # it returns +false+, the spec passes and execution continues. If it returns + # +true+, then the spec fails with the message returned by <tt>matcher.negative_failure_message</tt>. + # + # RSpec ships with a standard set of useful matchers, and writing your own + # matchers is quite simple. See Spec::Matchers for details. + module Expectations + class << self + attr_accessor :differ + + # raises a Spec::Expectations::ExpectationNotMetError with message + # + # When a differ has been assigned and fail_with is passed + # <code>expected</code> and <code>target</code>, 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 diff --git a/test/lib/spec/expectations/differs/default.rb b/test/lib/spec/expectations/differs/default.rb new file mode 100644 index 000000000..e08325728 --- /dev/null +++ b/test/lib/spec/expectations/differs/default.rb @@ -0,0 +1,62 @@ +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/errors.rb b/test/lib/spec/expectations/errors.rb new file mode 100644 index 000000000..03e81a064 --- /dev/null +++ b/test/lib/spec/expectations/errors.rb @@ -0,0 +1,6 @@ +module Spec + module Expectations + class ExpectationNotMetError < StandardError + end + end +end diff --git a/test/lib/spec/expectations/extensions.rb b/test/lib/spec/expectations/extensions.rb new file mode 100644 index 000000000..0381dc7f3 --- /dev/null +++ b/test/lib/spec/expectations/extensions.rb @@ -0,0 +1,3 @@ +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 new file mode 100644 index 000000000..dd5498fdd --- /dev/null +++ b/test/lib/spec/expectations/extensions/object.rb @@ -0,0 +1,109 @@ +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 + # + # receiver.should(matcher) + # => Passes if matcher.matches?(receiver) + # + # 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) + end + + # :call-seq: + # should_not(matcher) + # 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 =~ 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 + 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/proc.rb b/test/lib/spec/expectations/extensions/proc.rb new file mode 100644 index 000000000..8286708ed --- /dev/null +++ b/test/lib/spec/expectations/extensions/proc.rb @@ -0,0 +1,57 @@ +module Spec + module Expectations + module ProcExpectations + # Given a receiver and a message (Symbol), specifies that the result + # of sending that message that receiver should change after + # executing the proc. + # + # lambda { @team.add player }.should_change(@team.players, :size) + # lambda { @team.add player }.should_change(@team.players, :size).by(1) + # lambda { @team.add player }.should_change(@team.players, :size).to(23) + # lambda { @team.add player }.should_change(@team.players, :size).from(22).to(23) + # + # You can use a block instead of a message and receiver. + # + # lambda { @team.add player }.should_change{@team.players.size} + # lambda { @team.add player }.should_change{@team.players.size}.by(1) + # lambda { @team.add player }.should_change{@team.players.size}.to(23) + # lambda { @team.add player }.should_change{@team.players.size}.from(22).to(23) + def should_change(receiver=nil, message=nil, &block) + should.change(receiver, message, &block) + end + + # Given a receiver and a message (Symbol), specifies that the result + # of sending that message that receiver should NOT change after + # executing the proc. + # + # lambda { @team.add player }.should_not_change(@team.players, :size) + # + # You can use a block instead of a message and receiver. + # + # lambda { @team.add player }.should_not_change{@team.players.size} + def should_not_change(receiver, message) + should.not.change(receiver, message) + end + + def should_raise(exception=Exception, message=nil) + should.raise(exception, message) + end + + def should_not_raise(exception=Exception, message=nil) + should.not.raise(exception, message) + end + + def should_throw(symbol) + should.throw(symbol) + end + + def should_not_throw(symbol=:___this_is_a_symbol_that_will_likely_never_occur___) + should.not.throw(symbol) + end + end + end +end + +class Proc + include Spec::Expectations::ProcExpectations +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 new file mode 100644 index 000000000..30f60d4d0 --- /dev/null +++ b/test/lib/spec/expectations/extensions/string_and_symbol.rb @@ -0,0 +1,17 @@ +module Spec + module Expectations + module StringHelpers + def starts_with?(prefix) + to_s[0..(prefix.length - 1)] == prefix + end + end + end +end + +class String + include Spec::Expectations::StringHelpers +end + +class Symbol + include Spec::Expectations::StringHelpers +end
\ No newline at end of file diff --git a/test/lib/spec/expectations/handler.rb b/test/lib/spec/expectations/handler.rb new file mode 100644 index 000000000..9d3fd1f88 --- /dev/null +++ b/test/lib/spec/expectations/handler.rb @@ -0,0 +1,47 @@ +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 + 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 + end + end + end + end + + end +end + diff --git a/test/lib/spec/expectations/should.rb b/test/lib/spec/expectations/should.rb new file mode 100644 index 000000000..f64e6ff78 --- /dev/null +++ b/test/lib/spec/expectations/should.rb @@ -0,0 +1,5 @@ +require 'spec/expectations/should/base' +require 'spec/expectations/should/have' +require 'spec/expectations/should/not' +require 'spec/expectations/should/should' +require 'spec/expectations/should/change' diff --git a/test/lib/spec/expectations/should/base.rb b/test/lib/spec/expectations/should/base.rb new file mode 100755 index 000000000..1be4677e8 --- /dev/null +++ b/test/lib/spec/expectations/should/base.rb @@ -0,0 +1,64 @@ +module Spec + module Expectations + module Should + class Base + + #== and =~ will stay after the new syntax + def ==(expected) + __delegate_method_missing_to_target "==", "==", expected + end + + def =~(expected) + __delegate_method_missing_to_target "=~", "=~", expected + end + + #<, <=, >=, > are all implemented in Spec::Matchers::Be + # and will be removed with 0.9 + deprecated do + def <(expected) + __delegate_method_missing_to_target "<", "<", expected + end + + def <=(expected) + __delegate_method_missing_to_target "<=", "<=", expected + end + + def >=(expected) + __delegate_method_missing_to_target ">=", ">=", expected + end + + def >(expected) + __delegate_method_missing_to_target ">", ">", expected + end + end + + def default_message(expectation, expected=nil) + return "expected #{expected.inspect}, got #{@target.inspect} (using #{expectation})" if expectation == '==' + "expected #{expectation} #{expected.inspect}, got #{@target.inspect}" unless expectation == '==' + end + + def fail_with_message(message, expected=nil, target=nil) + Spec::Expectations.fail_with(message, expected, target) + end + + def find_supported_sym(original_sym) + ["#{original_sym}?", "#{original_sym}s?"].each do |alternate_sym| + return alternate_sym.to_s if @target.respond_to?(alternate_sym.to_s) + end + end + + deprecated do + def method_missing(original_sym, *args, &block) + if original_sym.to_s =~ /^not_/ + return Not.new(@target).__send__(sym, *args, &block) + end + if original_sym.to_s =~ /^have_/ + return have.__send__(original_sym.to_s[5..-1].to_sym, *args, &block) + end + __delegate_method_missing_to_target original_sym, find_supported_sym(original_sym), *args + end + end + end + end + end +end diff --git a/test/lib/spec/expectations/should/change.rb b/test/lib/spec/expectations/should/change.rb new file mode 100644 index 000000000..98304f1b3 --- /dev/null +++ b/test/lib/spec/expectations/should/change.rb @@ -0,0 +1,69 @@ +module Spec + module Expectations + module Should + class Change < Base + + def initialize(target, receiver=nil, message=nil, &block) + @block = block + @target = target + @receiver = receiver + @message = message + execute_change + evaluate_change + end + + def execute_change + @before_change = @block.nil? ? @receiver.send(@message) : @block.call + @target.call + @after_change = @block.nil? ? @receiver.send(@message) : @block.call + end + + def message + @message.nil? ? 'result' : @message + end + + def evaluate_change + if @before_change == @after_change + fail_with_message "#{message} should have changed, but is still #{@after_change.inspect}" + end + end + + def from(value) + if @before_change != value + fail_with_message "#{message} should have initially been #{value.inspect}, but was #{@before_change.inspect}" + end + self + end + + def to(value) + if @after_change != value + fail_with_message "#{message} should have been changed to #{value.inspect}, but is now #{@after_change.inspect}" + end + self + end + + def by(expected_delta) + if actual_delta != expected_delta + fail_with_message "#{message} should have been changed by #{expected_delta}, but was changed by #{actual_delta}" + end + self + end + + private + def actual_delta + @after_change - @before_change + end + end + + class NotChange < Change + def evaluate_change + if @before_change != @after_change + fail_with_message "#{@message} should not have changed, but did change from #{@before_change.inspect} to #{@after_change.inspect}" + end + end + end + + end + end +end + diff --git a/test/lib/spec/expectations/should/have.rb b/test/lib/spec/expectations/should/have.rb new file mode 100644 index 000000000..47ebe81db --- /dev/null +++ b/test/lib/spec/expectations/should/have.rb @@ -0,0 +1,128 @@ +module Spec + module Expectations + module Should + class Have + def initialize(target, relativity=:exactly, expected=nil) + @target = target + init_collection_handler(target, relativity, expected) + init_item_handler(target) + end + + def init_collection_handler(target, relativity, expected) + @collection_handler = CollectionHandler.new(target, relativity, expected) + end + + def init_item_handler(target) + @item_handler = PositiveItemHandler.new(target) + end + + def method_missing(sym, *args) + if @collection_handler.wants_to_handle(sym) + @collection_handler.handle_message(sym, *args) + elsif @item_handler.wants_to_handle(sym) + @item_handler.handle_message(sym, *args) + else + Spec::Expectations.fail_with("target does not respond to #has_#{sym}?") + end + end + end + + class NotHave < Have + def init_item_handler(target) + @item_handler = NegativeItemHandler.new(target) + end + end + + class CollectionHandler + def initialize(target, relativity=:exactly, expected=nil) + @target = target + @expected = expected == :no ? 0 : expected + @at_least = (relativity == :at_least) + @at_most = (relativity == :at_most) + end + + def at_least(expected_number=nil) + @at_least = true + @at_most = false + @expected = expected_number == :no ? 0 : expected_number + self + end + + def at_most(expected_number=nil) + @at_least = false + @at_most = true + @expected = expected_number == :no ? 0 : expected_number + self + end + + def method_missing(sym, *args) + if @target.respond_to?(sym) + handle_message(sym, *args) + end + end + + def wants_to_handle(sym) + respond_to?(sym) || @target.respond_to?(sym) + end + + def handle_message(sym, *args) + return at_least(args[0]) if sym == :at_least + return at_most(args[0]) if sym == :at_most + Spec::Expectations.fail_with(build_message(sym, args)) unless as_specified?(sym, args) + end + + def build_message(sym, args) + message = "expected" + message += " at least" if @at_least + message += " at most" if @at_most + message += " #{@expected} #{sym}, got #{actual_size_of(collection(sym, args))}" + end + + def as_specified?(sym, args) + return actual_size_of(collection(sym, args)) >= @expected if @at_least + return actual_size_of(collection(sym, args)) <= @expected if @at_most + return actual_size_of(collection(sym, args)) == @expected + end + + def collection(sym, args) + @target.send(sym, *args) + end + + def actual_size_of(collection) + return collection.length if collection.respond_to? :length + return collection.size if collection.respond_to? :size + end + end + + class ItemHandler + def wants_to_handle(sym) + @target.respond_to?("has_#{sym}?") + end + + def initialize(target) + @target = target + end + + def fail_with(message) + Spec::Expectations.fail_with(message) + end + end + + class PositiveItemHandler < ItemHandler + def handle_message(sym, *args) + fail_with( + "expected #has_#{sym}?(#{args.collect{|arg| arg.inspect}.join(', ')}) to return true, got false" + ) unless @target.send("has_#{sym}?", *args) + end + end + + class NegativeItemHandler < ItemHandler + def handle_message(sym, *args) + fail_with( + "expected #has_#{sym}?(#{args.collect{|arg| arg.inspect}.join(', ')}) to return false, got true" + ) if @target.send("has_#{sym}?", *args) + end + end + end + end +end diff --git a/test/lib/spec/expectations/should/not.rb b/test/lib/spec/expectations/should/not.rb new file mode 100755 index 000000000..5ad530be6 --- /dev/null +++ b/test/lib/spec/expectations/should/not.rb @@ -0,0 +1,74 @@ +module Spec + module Expectations + module Should + + class Not < Base #:nodoc: + def initialize(target) + @target = target + @be_seen = false + end + + deprecated do + #Gone for 0.9 + def be(expected = :___no_arg) + @be_seen = true + return self if (expected == :___no_arg) + fail_with_message(default_message("should not be", expected)) if (@target.equal?(expected)) + end + + #Gone for 0.9 + def have(expected_number=nil) + NotHave.new(@target, :exactly, expected_number) + end + + #Gone for 0.9 + def change(receiver, message) + NotChange.new(@target, receiver, message) + end + + #Gone for 0.9 + def raise(exception=Exception, message=nil) + begin + @target.call + rescue exception => e + return unless message.nil? || e.message == message || (message.is_a?(Regexp) && e.message =~ message) + if e.kind_of?(exception) + failure_message = "expected no " + failure_message << exception.to_s + unless message.nil? + failure_message << " with " + failure_message << "message matching " if message.is_a?(Regexp) + failure_message << message.inspect + end + failure_message << ", got " << e.inspect + fail_with_message(failure_message) + end + rescue + true + end + end + + #Gone for 0.9 + def throw(symbol=:___this_is_a_symbol_that_will_likely_never_occur___) + begin + catch symbol do + @target.call + return true + end + fail_with_message("expected #{symbol.inspect} not to be thrown, but it was") + rescue NameError + true + end + end + + def __delegate_method_missing_to_target original_sym, actual_sym, *args + ::Spec::Matchers.generated_description = "should not #{original_sym} #{args[0].inspect}" + return unless @target.__send__(actual_sym, *args) + fail_with_message(default_message("not #{original_sym}", args[0])) + end + end + end + + end + end +end diff --git a/test/lib/spec/expectations/should/should.rb b/test/lib/spec/expectations/should/should.rb new file mode 100755 index 000000000..cb9f3c4ce --- /dev/null +++ b/test/lib/spec/expectations/should/should.rb @@ -0,0 +1,81 @@ +module Spec + module Expectations + module Should # :nodoc: + + class Should < Base + + def initialize(target, expectation=nil) + @target = target + @be_seen = false + end + + deprecated do + #Gone for 0.9 + def not + Not.new(@target) + end + + #Gone for 0.9 + def be(expected = :___no_arg) + @be_seen = true + return self if (expected == :___no_arg) + if Symbol === expected + fail_with_message(default_message("should be", expected)) unless (@target.equal?(expected)) + else + fail_with_message("expected #{expected}, got #{@target} (using .equal?)") unless (@target.equal?(expected)) + end + end + + #Gone for 0.9 + def have(expected_number=nil) + Have.new(@target, :exactly, expected_number) + end + + #Gone for 0.9 + def change(receiver=nil, message=nil, &block) + Change.new(@target, receiver, message, &block) + end + + #Gone for 0.9 + def raise(exception=Exception, message=nil) + begin + @target.call + rescue exception => e + unless message.nil? + if message.is_a?(Regexp) + e.message.should =~ message + else + e.message.should == message + end + end + return + rescue => e + fail_with_message("expected #{exception}#{message.nil? ? "" : " with #{message.inspect}"}, got #{e.inspect}") + end + fail_with_message("expected #{exception}#{message.nil? ? "" : " with #{message.inspect}"} but nothing was raised") + end + + #Gone for 0.9 + def throw(symbol) + begin + catch symbol do + @target.call + fail_with_message("expected #{symbol.inspect} to be thrown, but nothing was thrown") + end + rescue NameError => e + fail_with_message("expected #{symbol.inspect} to be thrown, got #{e.inspect}") + end + end + end + + private + def __delegate_method_missing_to_target(original_sym, actual_sym, *args) + ::Spec::Matchers.generated_description = "should #{original_sym} #{args[0].inspect}" + return if @target.send(actual_sym, *args) + fail_with_message(default_message(original_sym, args[0]), args[0], @target) + end + end + + end + end +end diff --git a/test/lib/spec/expectations/sugar.rb b/test/lib/spec/expectations/sugar.rb new file mode 100644 index 000000000..906111f0e --- /dev/null +++ b/test/lib/spec/expectations/sugar.rb @@ -0,0 +1,47 @@ +deprecated do +module Spec + module Expectations + # This module adds syntactic sugar that allows usage of should_* instead of should.* + module UnderscoreSugar + def handle_underscores_for_rspec! # :nodoc: + original_method_missing = instance_method(:method_missing) + class_eval do + def method_missing(sym, *args, &block) + _method_missing(sym, args, block) + end + + define_method :_method_missing do |sym, args, block| + return original_method_missing.bind(self).call(sym, *args, &block) unless sym.to_s =~ /^should_/ + if sym.to_s =~ /^should_not_/ + if __matcher.respond_to?(__strip_should_not(sym)) + return should_not(__matcher.__send__(__strip_should_not(sym), *args, &block)) + else + return Spec::Expectations::Should::Not.new(self).__send__(__strip_should_not(sym), *args, &block) if sym.to_s =~ /^should_not_/ + end + else + if __matcher.respond_to?(__strip_should(sym)) + return should(__matcher.__send__(__strip_should(sym), *args, &block)) + else + return Spec::Expectations::Should::Should.new(self).__send__(__strip_should(sym), *args, &block) + end + end + end + + def __strip_should(sym) # :nodoc + sym.to_s[7..-1] + end + + def __strip_should_not(sym) # :nodoc + sym.to_s[11..-1] + end + + def __matcher + @matcher ||= Spec::Matchers::Matcher.new + end + end + end + end + end +end + +end
\ No newline at end of file diff --git a/test/lib/spec/matchers.rb b/test/lib/spec/matchers.rb new file mode 100644 index 000000000..9db24d486 --- /dev/null +++ b/test/lib/spec/matchers.rb @@ -0,0 +1,160 @@ +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' + +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 + # + # When you find that none of the stock Expression 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)) + # end + # def failure_message + # "expected #{@actual.inspect} to be in Zone #{@expected}" + # end + # def negative_failure_message + # "expected #{@actual.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 + # include CustomGameMatchers + # ... + # end + module Matchers + + class << self + callback_events :description_generated + def generated_description=(name) + notify_callbacks(:description_generated, name) + end + end + + 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 diff --git a/test/lib/spec/matchers/be.rb b/test/lib/spec/matchers/be.rb new file mode 100644 index 000000000..957f23de8 --- /dev/null +++ b/test/lib/spec/matchers/be.rb @@ -0,0 +1,161 @@ +module Spec + module Matchers + + class Be #:nodoc: + def initialize(expected=nil, *args) + @expected = parse_expected(expected) + @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.equal?(@expected) + 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}" + 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) + end + end + return expected + 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(', ')})" + end + + def handling_predicate? + return false if [:true, :false, :nil].include?(@expected) + return @handling_predicate + 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 new file mode 100644 index 000000000..b09e3fd2f --- /dev/null +++ b/test/lib/spec/matchers/be_close.rb @@ -0,0 +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}" + end + + def description + "be close to #{@expected} (+- #{@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 diff --git a/test/lib/spec/matchers/change.rb b/test/lib/spec/matchers/change.rb new file mode 100644 index 000000000..41a718aca --- /dev/null +++ b/test/lib/spec/matchers/change.rb @@ -0,0 +1,120 @@ +module Spec + module Matchers + + #Based on patch from Wilson Bilkovich + class Change #:nodoc: + def initialize(receiver=nil, message=nil, &block) + @receiver = receiver + @message = message + @block = block + end + + def matches?(target, &block) + if block + raise MatcherError.new(<<-EOF +block passed to should or should_not change must use {} instead of do/end +EOF +) + end + @target = target + execute_change + return false if @from && (@from != @before) + return false if @to && (@to != @after) + return (@before + @amount == @after) if @amount + return @before != @after + end + + def execute_change + @before = @block.nil? ? @receiver.send(@message) : @block.call + @target.call + @after = @block.nil? ? @receiver.send(@message) : @block.call + end + + def failure_message + if @to + "#{result} should have been changed to #{@to.inspect}, but is now #{@after.inspect}" + elsif @from + "#{result} should have initially been #{@from.inspect}, but was #{@before.inspect}" + elsif @amount + "#{result} should have been changed by #{@amount.inspect}, but was changed by #{actual_delta.inspect}" + else + "#{result} should have changed, but is still #{@before.inspect}" + end + end + + def result + @message || "result" + end + + def actual_delta + @after - @before + end + + def negative_failure_message + "#{result} should not have changed, but did change from #{@before.inspect} to #{@after.inspect}" + end + + def by(amount) + @amount = amount + self + end + + def to(to) + @to = to + self + end + + def from (from) + @from = from + self + end + end + + # :call-seq: + # should change(receiver, message, &block) + # should change(receiver, message, &block).by(value) + # should change(receiver, message, &block).from(old).to(new) + # should_not change(receiver, message, &block) + # + # Allows you to specify that a Proc will cause some value to change. + # + # == Examples + # + # lambda { + # team.add_player(player) + # }.should change(roster, :count) + # + # lambda { + # team.add_player(player) + # }.should change(roster, :count).by(1) + # + # string = "string" + # lambda { + # string.reverse + # }.should change { string }.from("string").to("gnirts") + # + # lambda { + # person.happy_birthday + # }.should change(person, :birthday).from(32).to(33) + # + # lambda { + # employee.develop_great_new_social_networking_app + # }.should change(employee, :title).from("Mail Clerk").to("CEO") + # + # Evaluates +receiver.message+ or +block+ before and + # after it evaluates the c object (generated by the lambdas in the examples above). + # + # Then compares the values before and after the +receiver.message+ and + # evaluates the difference compared to the expected difference. + # + # == Warning + # +should_not+ +change+ only supports the form with no subsequent calls to + # +be+, +to+ or +from+. + # + # blocks passed to +should+ +change+ and +should_not+ +change+ + # must use the <tt>{}</tt> form (<tt>do/end</tt> is not supported) + def change(target=nil, message=nil, &block) + Matchers::Change.new(target, message, &block) + end + end +end diff --git a/test/lib/spec/matchers/eql.rb b/test/lib/spec/matchers/eql.rb new file mode 100644 index 000000000..caca1f7c6 --- /dev/null +++ b/test/lib/spec/matchers/eql.rb @@ -0,0 +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 diff --git a/test/lib/spec/matchers/equal.rb b/test/lib/spec/matchers/equal.rb new file mode 100644 index 000000000..e987e73cb --- /dev/null +++ b/test/lib/spec/matchers/equal.rb @@ -0,0 +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 diff --git a/test/lib/spec/matchers/has.rb b/test/lib/spec/matchers/has.rb new file mode 100644 index 000000000..cc5a250b8 --- /dev/null +++ b/test/lib/spec/matchers/has.rb @@ -0,0 +1,44 @@ +module Spec + module Matchers + + class Has #:nodoc: + def initialize(sym, *args) + @sym = sym + @args = args + end + + def matches?(target) + @target = target + begin + return target.send(predicate, *@args) + rescue => @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 + return false + end + + def failure_message + raise @error if @error + "expected ##{predicate}(#{@args[0].inspect}) to return true, got false" + end + + def negative_failure_message + raise @error if @error + "expected ##{predicate}(#{@args[0].inspect}) to return false, got true" + end + + def description + "have key #{@args[0].inspect}" + end + + private + def predicate + "#{@sym.to_s.sub("have_","has_")}?".to_sym + end + + end + + end +end diff --git a/test/lib/spec/matchers/have.rb b/test/lib/spec/matchers/have.rb new file mode 100644 index 000000000..81f9af3e3 --- /dev/null +++ b/test/lib/spec/matchers/have.rb @@ -0,0 +1,140 @@ +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) + elsif (collection_owner.respond_to?(:length) || collection_owner.respond_to?(:size)) + collection = collection_owner + else + collection_owner.send(collection_name, *@args, &@block) + end + @actual = collection.length if collection.respond_to?(:length) + @actual = collection.size if collection.respond_to?(:size) + return @actual >= @expected if @relativity == :at_least + return @actual <= @expected if @relativity == :at_most + return @actual == @expected + end + + def failure_message + "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}" + 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} +We recommend that you use this instead: + 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} +We recommend that you use this instead: + should have_at_most(#{@expected - 1}).#{collection_name} +EOF + end + end + + def description + "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 <tt>Team</tt> instance has a + # collection named <tt>#players</tt>, you must use that name + # to set the expectation. + # + # If the receiver IS the collection, you can use any name + # you like for <tt>named_collection</tt>. 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 diff --git a/test/lib/spec/matchers/include.rb b/test/lib/spec/matchers/include.rb new file mode 100644 index 000000000..0d387f323 --- /dev/null +++ b/test/lib/spec/matchers/include.rb @@ -0,0 +1,50 @@ +module Spec + module Matchers + + class Include #:nodoc: + + def initialize(expected) + @expected = expected + end + + def matches?(actual) + @actual = actual + actual.include?(@expected) + end + + def failure_message + _message + end + + def negative_failure_message + _message("not ") + end + + def description + "include #{@expected.inspect}" + end + + private + def _message(maybe_not="") + "expected #{@actual.inspect} #{maybe_not}to include #{@expected.inspect}" + end + end + + # :call-seq: + # should include(expected) + # should_not include(expected) + # + # Passes if actual includes expected. This works for + # collections and Strings + # + # == Examples + # + # [1,2,3].should include(3) + # [1,2,3].should_not include(4) + # "spread".should include("read") + # "spread".should_not include("red") + def include(expected) + Matchers::Include.new(expected) + end + end +end diff --git a/test/lib/spec/matchers/match.rb b/test/lib/spec/matchers/match.rb new file mode 100644 index 000000000..61ab52429 --- /dev/null +++ b/test/lib/spec/matchers/match.rb @@ -0,0 +1,41 @@ +module Spec + module Matchers + + class Match #:nodoc: + def initialize(expected) + @expected = expected + end + + def matches?(actual) + @actual = actual + return true if actual =~ @expected + return false + end + + def failure_message + return "expected #{@actual.inspect} to match #{@expected.inspect}", @expected, @actual + end + + def negative_failure_message + return "expected #{@actual.inspect} not to match #{@expected.inspect}", @expected, @actual + end + + def description + "match #{@expected.inspect}" + end + end + + # :call-seq: + # should match(regexp) + # should_not match(regexp) + # + # Given a Regexp, passes if actual =~ regexp + # + # == Examples + # + # email.should match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) + def match(regexp) + Matchers::Match.new(regexp) + end + end +end diff --git a/test/lib/spec/matchers/raise_error.rb b/test/lib/spec/matchers/raise_error.rb new file mode 100644 index 000000000..95e82ad5e --- /dev/null +++ b/test/lib/spec/matchers/raise_error.rb @@ -0,0 +1,100 @@ +module Spec + module Matchers + + class RaiseError #:nodoc: + def initialize(exception=Exception, message=nil) + @expected_error = exception + @expected_message = message + 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 + @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 new file mode 100644 index 000000000..013a36f1d --- /dev/null +++ b/test/lib/spec/matchers/respond_to.rb @@ -0,0 +1,35 @@ +module Spec + module Matchers + + class RespondTo #:nodoc: + def initialize(sym) + @sym = sym + end + + def matches?(target) + return target.respond_to?(@sym) + end + + def failure_message + "expected target to respond to #{@sym.inspect}" + end + + def negative_failure_message + "expected target not to respond to #{@sym.inspect}" + end + + def description + "respond to ##{@sym.to_s}" + end + end + + # :call-seq: + # should respond_to(:sym) + # should_not respond_to(:sym) + # + # Matches if the target object responds to :sym + def respond_to(sym) + Matchers::RespondTo.new(sym) + end + end +end diff --git a/test/lib/spec/matchers/satisfy.rb b/test/lib/spec/matchers/satisfy.rb new file mode 100644 index 000000000..6c0ca95bc --- /dev/null +++ b/test/lib/spec/matchers/satisfy.rb @@ -0,0 +1,47 @@ +module Spec + module Matchers + + class Satisfy #:nodoc: + def initialize(&block) + @block = block + end + + def matches?(actual, &block) + @block = block if block + @actual = actual + @block.call(actual) + end + + def failure_message + "expected #{@actual} to satisfy block" + end + + def negative_failure_message + "expected #{@actual} not to satisfy block" + end + end + + # :call-seq: + # should satisfy {} + # should_not satisfy {} + # + # Passes if the submitted block returns true. Yields target to the + # block. + # + # Generally speaking, this should be thought of as a last resort when + # you can't find any other way to specify the behaviour you wish to + # specify. + # + # If you do find yourself in such a situation, you could always write + # a custom matcher, which would likely make your specs more expressive. + # + # == Examples + # + # 5.should satisfy { |n| + # n > 3 + # } + def satisfy(&block) + Matchers::Satisfy.new(&block) + end + end +end diff --git a/test/lib/spec/matchers/throw_symbol.rb b/test/lib/spec/matchers/throw_symbol.rb new file mode 100644 index 000000000..6732f6fed --- /dev/null +++ b/test/lib/spec/matchers/throw_symbol.rb @@ -0,0 +1,75 @@ +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) + 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 new file mode 100644 index 000000000..d0a5d0299 --- /dev/null +++ b/test/lib/spec/mocks.rb @@ -0,0 +1,232 @@ +require 'spec/mocks/methods' +require 'spec/mocks/mock_handler' +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' + +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(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, 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 diff --git a/test/lib/spec/mocks/argument_expectation.rb b/test/lib/spec/mocks/argument_expectation.rb new file mode 100644 index 000000000..a4870e767 --- /dev/null +++ b/test/lib/spec/mocks/argument_expectation.rb @@ -0,0 +1,132 @@ +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 matches?(value) + true + end + end + + class NumericArgConstraint + def initialize(ignore) + end + + def matches?(value) + value.is_a?(Numeric) + end + end + + class BooleanArgConstraint + def initialize(ignore) + 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 + end + + def matches?(value) + @methods_to_respond_do.all? { |sym| value.respond_to?(sym) } + 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 = [] + else @expected_params = process_arg_constraints(args) + end + end + + def process_arg_constraints(constraints) + constraints.collect do |constraint| + convert_constraint(constraint) + end + end + + def convert_constraint(constraint) + return @@constraint_classes[constraint].new(constraint) if constraint.is_a?(Symbol) + return constraint if constraint.is_a?(DuckTypeArgConstraint) + 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 new file mode 100644 index 000000000..950864a7a --- /dev/null +++ b/test/lib/spec/mocks/error_generator.rb @@ -0,0 +1,85 @@ +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)) + 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 + 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 diff --git a/test/lib/spec/mocks/errors.rb b/test/lib/spec/mocks/errors.rb new file mode 100644 index 000000000..68fdfe006 --- /dev/null +++ b/test/lib/spec/mocks/errors.rb @@ -0,0 +1,10 @@ +module Spec + module Mocks + class MockExpectationError < StandardError + end + + class AmbiguousReturnError < StandardError + end + end +end + diff --git a/test/lib/spec/mocks/extensions/object.rb b/test/lib/spec/mocks/extensions/object.rb new file mode 100644 index 000000000..4b7531066 --- /dev/null +++ b/test/lib/spec/mocks/extensions/object.rb @@ -0,0 +1,3 @@ +class Object + include Spec::Mocks::Methods +end diff --git a/test/lib/spec/mocks/message_expectation.rb b/test/lib/spec/mocks/message_expectation.rb new file mode 100644 index 000000000..152e65a47 --- /dev/null +++ b/test/lib/spec/mocks/message_expectation.rb @@ -0,0 +1,231 @@ +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]) + @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 + + 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 new file mode 100644 index 000000000..a5f102fcf --- /dev/null +++ b/test/lib/spec/mocks/methods.rb @@ -0,0 +1,40 @@ +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) + end + + def should_not_receive(sym, &block) + __mock_handler.add_negative_message_expectation(caller(1)[0], sym, &block) + end + + def stub!(sym) + __mock_handler.add_stub(caller(1)[0], sym) + end + + def received_message?(sym, *args, &block) #:nodoc: + __mock_handler.received_message?(sym, *args, &block) + end + + def __verify #:nodoc: + __mock_handler.verify + end + + def __reset_mock #:nodoc: + __mock_handler.reset + end + + def method_missing(sym, *args, &block) #:nodoc: + __mock_handler.instance_eval {@messages_received << [sym, args, block]} + super(sym, *args, &block) + end + + private + + def __mock_handler + @mock_handler ||= MockHandler.new(self, @name, @options) + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/mocks/mock.rb b/test/lib/spec/mocks/mock.rb new file mode 100644 index 000000000..68de11ff4 --- /dev/null +++ b/test/lib/spec/mocks/mock.rb @@ -0,0 +1,26 @@ +module Spec + module Mocks + class Mock + include Methods + + # Creates a new mock with a +name+ (that will be used in error messages only) + # == Options: + # * <tt>:null_object</tt> - 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]} + begin + return self if __mock_handler.null_object? + super(sym, *args, &block) + rescue NoMethodError + __mock_handler.raise_unexpected_message_error sym, *args + end + end + + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/mocks/mock_handler.rb b/test/lib/spec/mocks/mock_handler.rb new file mode 100644 index 000000000..ef6f97a1c --- /dev/null +++ b/test/lib/spec/mocks/mock_handler.rb @@ -0,0 +1,166 @@ +module Spec + module Mocks + class MockHandler + DEFAULT_OPTIONS = { + :null_object => false, + :auto_verify => true + } + + 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 expected_from, 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 expected_from, 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 expected_from, 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(expected_from, sym, block) + # TODO - this is the only reference in the 'spec/mocks' to the Runner + current_spec = Runner::Specification.current + current_spec.after_teardown {verify} if current_spec && @options[:auto_verify] + define_expected_method(sym) + end + + def define_expected_method(sym) + if target_responds_to?(sym) && !@proxied_methods.include?(sym) + @proxied_methods << sym + metaclass.__send__(:alias_method, munge(sym), sym) + end + + metaclass_eval(<<-EOF, __FILE__, __LINE__) + def #{sym}(*args, &block) + __mock_handler.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| + metaclass.__send__(:alias_method, sym, munge(sym)) + metaclass.__send__(:undef_method, munge(sym)) + 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
\ No newline at end of file diff --git a/test/lib/spec/mocks/order_group.rb b/test/lib/spec/mocks/order_group.rb new file mode 100644 index 000000000..9983207eb --- /dev/null +++ b/test/lib/spec/mocks/order_group.rb @@ -0,0 +1,29 @@ +module Spec + module Mocks + class OrderGroup + def initialize error_generator + @error_generator = error_generator + @ordering = Array.new + end + + def register(expectation) + @ordering << expectation + end + + def ready_for?(expectation) + return @ordering.first == expectation + end + + def consume + @ordering.shift + end + + def handle_order_constraint expectation + return unless @ordering.include? expectation + return consume if ready_for?(expectation) + @error_generator.raise_out_of_order_error expectation.sym + end + + end + end +end diff --git a/test/lib/spec/rake/spectask.rb b/test/lib/spec/rake/spectask.rb new file mode 100644 index 000000000..5c9b365c1 --- /dev/null +++ b/test/lib/spec/rake/spectask.rb @@ -0,0 +1,173 @@ +#!/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 + # + class SpecTask < ::Rake::TaskLib + + # 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 ['<the absolute path to RSpec's lib directory>'] + attr_accessor :libs + + # If true, requests that the specs be run with the warning flag set. + # E.g. warning=true implies "ruby -w" used to run the specs. Defaults to false. + attr_accessor :warning + + # Glob pattern to match spec files. (default is 'spec/**/*_spec.rb') + attr_accessor :pattern + + # Array of commandline options to pass to RSpec. Defaults to []. + attr_accessor :spec_opts + + # Where RSpec's output is written. Defaults to STDOUT. + attr_accessor :out + + # Whether or not to use RCov (default is false) + # See http://eigenclass.org/hiki.rb?rcov + attr_accessor :rcov + + # Array of commandline options to pass to RCov. Defaults to ['--exclude', 'lib\/spec,bin\/spec']. + # Ignored if rcov=false + attr_accessor :rcov_opts + + # Directory where the RCov report is written. Defaults to "coverage" + # Ignored if rcov=false + attr_accessor :rcov_dir + + # Array of commandline options to pass to ruby. Defaults to []. + attr_accessor :ruby_opts + + # Whether or not to fail Rake when an error occurs (typically when specs fail). + # Defaults to true. + attr_accessor :fail_on_error + + # A message to print to stdout when there are failures. + attr_accessor :failure_message + + # Explicitly define the list of spec files to be included in a + # spec. +list+ is expected to be an array of file names (a + # FileList is acceptable). If both +pattern+ and +spec_files+ are + # used, then the list of spec files is the union of the two. + def spec_files=(list) + @spec_files = list + end + + # Create a specing task. + def initialize(name=:spec) + @name = name + @libs = [File.expand_path(File.dirname(__FILE__) + '/../../../lib')] + @pattern = nil + @spec_files = nil + @spec_opts = [] + @warning = false + @ruby_opts = [] + @out = nil + @fail_on_error = true + @rcov = false + @rcov_opts = ['--exclude', 'lib\/spec,bin\/spec,config\/boot.rb'] + @rcov_dir = "coverage" + + yield self if block_given? + @pattern = 'spec/**/*_spec.rb' if @pattern.nil? && @spec_files.nil? + define + end + + def define + spec_script = File.expand_path(File.dirname(__FILE__) + '/../../../bin/spec') + + lib_path = @libs.join(File::PATH_SEPARATOR) + actual_name = Hash === name ? name.keys.first : name + unless ::Rake.application.last_comment + desc "Run RSpec for #{actual_name}" + (@rcov ? " using RCov" : "") + end + task @name do + RakeFileUtils.verbose(@verbose) do + ruby_opts = @ruby_opts.clone + ruby_opts.push( "-I\"#{lib_path}\"" ) + ruby_opts.push( "-S rcov" ) if @rcov + ruby_opts.push( "-w" ) if @warning + + redirect = @out.nil? ? "" : " > \"#{@out}\"" + + unless spec_file_list.empty? + # ruby [ruby_opts] -Ilib -S rcov [rcov_opts] bin/spec -- [spec_opts] examples + # or + # ruby [ruby_opts] -Ilib bin/spec [spec_opts] examples + begin + ruby( + ruby_opts.join(" ") + " " + + rcov_option_list + + (@rcov ? %[ -o "#{@rcov_dir}" ] : "") + + '"' + spec_script + '"' + " " + + (@rcov ? "-- " : "") + + spec_file_list.collect { |fn| %["#{fn}"] }.join(' ') + " " + + spec_option_list + " " + + redirect + ) + rescue => e + puts @failure_message if @failure_message + raise e if @fail_on_error + end + end + end + end + + if @rcov + desc "Remove rcov products for #{actual_name}" + task paste("clobber_", actual_name) do + rm_r @rcov_dir rescue nil + end + + clobber_task = paste("clobber_", actual_name) + task :clobber => [clobber_task] + + task actual_name => clobber_task + end + self + end + + def rcov_option_list # :nodoc: + return "" unless @rcov + ENV['RCOVOPTS'] || @rcov_opts.join(" ") || "" + end + + def spec_option_list # :nodoc: + ENV['RSPECOPTS'] || @spec_opts.join(" ") || "" + end + + def spec_file_list # :nodoc: + if ENV['SPEC'] + FileList[ ENV['SPEC'] ] + else + result = [] + result += @spec_files.to_a if @spec_files + result += FileList[ @pattern ].to_a if @pattern + FileList[result] + end + end + + end + end +end + diff --git a/test/lib/spec/rake/verify_rcov.rb b/test/lib/spec/rake/verify_rcov.rb new file mode 100644 index 000000000..a05153e99 --- /dev/null +++ b/test/lib/spec/rake/verify_rcov.rb @@ -0,0 +1,47 @@ +module RCov + # A task that can verify that the RCov coverage doesn't + # drop below a certain threshold. It should be run after + # running Spec::Rake::SpecTask. + class VerifyTask < Rake::TaskLib + # Name of the task. Defaults to :verify_rcov + attr_accessor :name + + # Path to the index.html file generated by RCov, which + # is the file containing the total coverage. + # Defaults to 'coverage/index.html' + attr_accessor :index_html + + # Whether or not to output details. Defaults to true. + attr_accessor :verbose + + # The threshold value (in percent) for coverage. If the + # actual coverage is not equal to this value, the task will raise an + # exception. + attr_accessor :threshold + + def initialize(name=:verify_rcov) + @name = name + @index_html = 'coverage/index.html' + @verbose = true + yield self if block_given? + raise "Threshold must be set" if @threshold.nil? + define + end + + def define + desc "Verify that rcov coverage is at least #{threshold}%" + task @name do + total_coverage = nil + File.open(index_html).each_line do |line| + if line =~ /<tt.*>(\d+\.\d+)%<\/tt> <\/td>/ + total_coverage = eval($1) + break + end + end + puts "Coverage: #{total_coverage}% (threshold: #{threshold}%)" if verbose + raise "Coverage must be at least #{threshold}% but was #{total_coverage}%" if total_coverage < threshold + raise "Coverage has increased above the threshold of #{threshold}% to #{total_coverage}%. You should update your threshold value." if total_coverage > threshold + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner.rb b/test/lib/spec/runner.rb new file mode 100644 index 000000000..976802bd1 --- /dev/null +++ b/test/lib/spec/runner.rb @@ -0,0 +1,132 @@ +require 'spec/runner/formatter' +require 'spec/runner/context' +require 'spec/runner/context_eval' +require 'spec/runner/specification' +require 'spec/runner/execution_context' +require 'spec/runner/context_runner' +require 'spec/runner/option_parser' +require 'spec/runner/command_line' +require 'spec/runner/drb_command_line' +require 'spec/runner/backtrace_tweaker' +require 'spec/runner/reporter' +require 'spec/runner/spec_matcher' +require 'spec/runner/extensions/object' +require 'spec/runner/extensions/kernel' +require 'spec/runner/spec_should_raise_handler' +require 'spec/runner/spec_parser' + +module Spec + # == Contexts and Specifications + # + # Rather than expressing examples in classes, RSpec uses a custom domain specific language to express + # examples using contexts and specifications. + # + # A context is the equivalent of a fixture in xUnit-speak. It is a metaphor for the context + # in which you will run your executable example - a set of known objects in a known starting state. + # + # context "A new account" do + # + # setup do + # @account = Account.new + # end + # + # specify "should have a balance of $0" do + # @account.balance.should_eql Money.new(0, :dollars) + # end + # + # end + # + # We use the setup block to set up the context (given), and then the specify method to + # hold the example code that expresses the event (when) and the expected outcome (then). + # + # == Helper Methods + # + # A primary goal of RSpec is to keep the examples clear. We therefore prefer + # less indirection than you might see in xUnit examples and in well factored, DRY production code. We feel + # that duplication is OK if removing it makes it harder to understand an example without + # having to look elsewhere to understand its context. + # + # That said, RSpec does support some level of encapsulating common code in helper + # methods that can exist within a context or within an included module. + # + # == Setup and Teardown + # + # You can use setup, teardown, context_setup and context_teardown within a context: + # + # context "..." do + # context_setup do + # ... + # end + # + # setup do + # ... + # end + # + # specify "number one" do + # ... + # end + # + # specify "number two" do + # ... + # end + # + # teardown do + # ... + # end + # + # context_teardown do + # ... + # end + # + # end + # + # The <tt>setup</tt> block will run before each of the specs, once for each spec. Likewise, + # the <tt>teardown</tt> block will run after each of the specs. + # + # It is also possible to specify a <tt>context_setup</tt> and <tt>context_teardown</tt> + # block that will run only once for each context, respectively before the first <code>setup</code> + # and after the last <code>teardown</code>. The use of these is generally discouraged, because it + # introduces dependencies between the specs. 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 + # + # specify "..." 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 + # include AccountExampleHelperMethods + # setup do + # @account = Account.new + # end + # + # specify "should have a balance of $0" do + # helper_method + # @account.balance.should eql(Money.new(0, :dollars)) + # end + # end + module Runner + end +end diff --git a/test/lib/spec/runner/backtrace_tweaker.rb b/test/lib/spec/runner/backtrace_tweaker.rb new file mode 100644 index 000000000..7300b36b8 --- /dev/null +++ b/test/lib/spec/runner/backtrace_tweaker.rb @@ -0,0 +1,55 @@ +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\./ + ] + 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 diff --git a/test/lib/spec/runner/command_line.rb b/test/lib/spec/runner/command_line.rb new file mode 100644 index 000000000..db928ad9b --- /dev/null +++ b/test/lib/spec/runner/command_line.rb @@ -0,0 +1,34 @@ +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 + + # 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 + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/context.rb b/test/lib/spec/runner/context.rb new file mode 100644 index 000000000..3155e169a --- /dev/null +++ b/test/lib/spec/runner/context.rb @@ -0,0 +1,154 @@ +module Spec + module Runner + class ContextEvalModule < Module + end + class Context + module InstanceMethods + def initialize(description, &context_block) + @description = description + + @context_eval_module = ContextEvalModule.new + @context_eval_module.extend ContextEval::ModuleMethods + @context_eval_module.include ContextEval::InstanceMethods + before_context_eval + @context_eval_module.class_eval(&context_block) + end + + def before_context_eval + end + + def inherit_context_eval_module_from(klass) + @context_eval_module.inherit klass + end + alias :inherit :inherit_context_eval_module_from + + def include(mod) + @context_eval_module.include(mod) + end + + def run(reporter, dry_run=false) + reporter.add_context(@description) + prepare_execution_context_class + errors = run_context_setup(reporter, dry_run) + + specifications.each do |specification| + specification_execution_context = execution_context(specification) + specification_execution_context.copy_instance_variables_from(@once_only_execution_context_instance, []) unless context_setup_block.nil? + specification.run(reporter, setup_block, teardown_block, dry_run, specification_execution_context) + end unless errors.length > 0 + + run_context_teardown(reporter, dry_run) + end + + def number_of_specs + specifications.length + end + + def matches?(full_description) + matcher ||= SpecMatcher.new(@description) + specifications.each do |spec| + return true if spec.matches?(matcher, full_description) + end + return false + end + + def run_single_spec(full_description) + return if @description == full_description + matcher = SpecMatcher.new(@description) + specifications.reject! do |spec| + !spec.matches?(matcher, full_description) + end + end + + def methods + my_methods = super + my_methods |= @context_eval_module.methods + my_methods + end + + protected + + def method_missing(*args) + @context_eval_module.method_missing(*args) + end + + def context_setup_block + @context_eval_module.send :context_setup_block + end + + def context_teardown_block + @context_eval_module.send :context_teardown_block + end + + def specifications + @context_eval_module.send :specifications + end + + def setup_block + @context_eval_module.send :setup_block + end + + def teardown_block + @context_eval_module.send :teardown_block + end + + def prepare_execution_context_class + weave_in_context_modules + execution_context_class + end + + def weave_in_context_modules + mods = context_modules + context_eval_module = @context_eval_module + execution_context_class.class_eval do + include context_eval_module + mods.each do |mod| + include mod + end + end + end + + def context_modules + @context_eval_module.send :context_modules + end + + def execution_context_class + @context_eval_module.send :execution_context_class + end + + def execution_context specification + execution_context_class.new(specification) + end + + def run_context_setup(reporter, dry_run) + errors = [] + unless dry_run + begin + @once_only_execution_context_instance = execution_context(nil) + @once_only_execution_context_instance.instance_eval(&context_setup_block) + rescue => e + errors << e + location = "context_setup" + reporter.spec_finished(location, e, location) if reporter + end + end + errors + end + + def run_context_teardown(reporter, dry_run) + unless dry_run + begin + @once_only_execution_context_instance ||= execution_context(nil) + @once_only_execution_context_instance.instance_eval(&context_teardown_block) + rescue => e + location = "context_teardown" + reporter.spec_finished(location, e, location) if reporter + end + end + end + + end + include InstanceMethods + end + end +end diff --git a/test/lib/spec/runner/context_eval.rb b/test/lib/spec/runner/context_eval.rb new file mode 100644 index 000000000..2cee8f1cd --- /dev/null +++ b/test/lib/spec/runner/context_eval.rb @@ -0,0 +1,142 @@ +module Spec + module Runner + module ContextEval + module ModuleMethods + def inherit(klass) + @context_superclass = klass + derive_execution_context_class_from_context_superclass + end + + def include(mod) + context_modules << mod + mod.send :included, self + end + + def context_setup(&block) + context_setup_parts << block + end + + def context_teardown(&block) + context_teardown_parts << block + end + + def setup(&block) + setup_parts << block + end + + def teardown(&block) + teardown_parts << block + end + + def specify(spec_name=:__generate_description, opts={}, &block) + specifications << Specification.new(spec_name, opts, &block) + end + + def methods + my_methods = super + my_methods |= context_superclass.methods + my_methods + end + + protected + + def method_missing(method_name, *args) + if context_superclass.respond_to?(method_name) + return execution_context_class.send(method_name, *args) + end + super + end + + private + + def context_setup_block + parts = context_setup_parts.dup + add_context_superclass_method(:context_setup, parts) + create_block_from_parts(parts) + end + + def context_teardown_block + parts = context_teardown_parts.dup + add_context_superclass_method(:context_teardown, parts) + create_block_from_parts(parts) + end + + def setup_block + parts = setup_parts.dup + add_context_superclass_method(:setup, parts) + create_block_from_parts(parts) + end + + def teardown_block + parts = teardown_parts.dup + add_context_superclass_method(:teardown, parts) + create_block_from_parts(parts) + end + + def execution_context_class + @execution_context_class ||= derive_execution_context_class_from_context_superclass + end + + def derive_execution_context_class_from_context_superclass + @execution_context_class = Class.new(context_superclass) + @execution_context_class.class_eval do + include ::Spec::Runner::ExecutionContext::InstanceMethods + end + end + + def context_superclass + @context_superclass ||= Object + end + + def context_modules + @context_modules ||= [Spec::Matchers, Spec::Mocks] + end + + def specifications + @specifications ||= [] + end + + def context_setup_parts + @context_setup_parts ||= [] + end + + def context_teardown_parts + @context_teardown_parts ||= [] + end + + def setup_parts + @setup_parts ||= [] + end + + def teardown_parts + @teardown_parts ||= [] + end + + def add_context_superclass_method sym, parts + superclass_method = begin + context_superclass.instance_method(sym) + rescue + nil + end + parts.unshift superclass_method if superclass_method + end + + def create_block_from_parts(parts) + proc do + parts.each do |part| + if part.is_a?(UnboundMethod) + part.bind(self).call + else + instance_eval(&part) + end + end + end + end + end + + module InstanceMethods + end + + end + end +end diff --git a/test/lib/spec/runner/context_runner.rb b/test/lib/spec/runner/context_runner.rb new file mode 100644 index 000000000..0a4d7e6e9 --- /dev/null +++ b/test/lib/spec/runner/context_runner.rb @@ -0,0 +1,55 @@ +module Spec + module Runner + class ContextRunner + + def initialize(options) + @contexts = [] + @options = options + end + + def add_context(context) + return unless spec_description.nil? || context.matches?(spec_description) + context.run_single_spec(spec_description) if context.matches?(spec_description) + @contexts << context + end + + # Runs all contexts and returns the number of failures. + def run(exit_when_done) + @options.reporter.start(number_of_specs) + begin + @contexts.each do |context| + context.run(@options.reporter, @options.dry_run) + end + rescue Interrupt + ensure + @options.reporter.end + end + failure_count = @options.reporter.dump + + if(failure_count == 0 && !@options.heckle_runner.nil?) + heckle_runner = @options.heckle_runner + @options.heckle_runner = nil + context_runner = self.class.new(@options) + context_runner.instance_variable_set(:@contexts, @contexts) + heckle_runner.heckle_with(context_runner) + end + + if(exit_when_done) + exit_code = (failure_count == 0) ? 0 : 1 + exit(exit_code) + end + failure_count + end + + def number_of_specs + @contexts.inject(0) {|sum, context| sum + context.number_of_specs} + end + + private + def spec_description + @options.spec_name + end + + end + end +end diff --git a/test/lib/spec/runner/drb_command_line.rb b/test/lib/spec/runner/drb_command_line.rb new file mode 100644 index 000000000..d4c7d937d --- /dev/null +++ b/test/lib/spec/runner/drb_command_line.rb @@ -0,0 +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) + rescue DRb::DRbConnError + stderr.puts "No server is running" + exit 1 if exit + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/execution_context.rb b/test/lib/spec/runner/execution_context.rb new file mode 100644 index 000000000..484c55830 --- /dev/null +++ b/test/lib/spec/runner/execution_context.rb @@ -0,0 +1,17 @@ +module Spec + module Runner + class ExecutionContext + module InstanceMethods + def initialize(*args) #:nodoc: + #necessary for RSpec's own specs + end + + def violated(message="") + raise Spec::Expectations::ExpectationNotMetError.new(message) + end + + end + include InstanceMethods + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/extensions/kernel.rb b/test/lib/spec/runner/extensions/kernel.rb new file mode 100644 index 000000000..f060ec859 --- /dev/null +++ b/test/lib/spec/runner/extensions/kernel.rb @@ -0,0 +1,17 @@ +module Kernel + def context(name, &block) + context = Spec::Runner::Context.new(name, &block) + context_runner.add_context(context) + end + +private + + def context_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) }; \ + end + $context_runner + end +end diff --git a/test/lib/spec/runner/extensions/object.rb b/test/lib/spec/runner/extensions/object.rb new file mode 100644 index 000000000..49745352f --- /dev/null +++ b/test/lib/spec/runner/extensions/object.rb @@ -0,0 +1,32 @@ +# The following copyright applies to Object#copy_instance_variables_from, +# which we borrowed from active_support. +# +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +class Object + # From active_support + def copy_instance_variables_from(object, exclude = []) # :nodoc: + exclude += object.protected_instance_variables if object.respond_to? :protected_instance_variables + + instance_variables = object.instance_variables - exclude.map { |name| name.to_s } + instance_variables.each { |name| instance_variable_set(name, object.instance_variable_get(name)) } + end +end diff --git a/test/lib/spec/runner/formatter.rb b/test/lib/spec/runner/formatter.rb new file mode 100644 index 000000000..f62e81733 --- /dev/null +++ b/test/lib/spec/runner/formatter.rb @@ -0,0 +1,5 @@ +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' diff --git a/test/lib/spec/runner/formatter/base_text_formatter.rb b/test/lib/spec/runner/formatter/base_text_formatter.rb new file mode 100644 index 000000000..31d1c3132 --- /dev/null +++ b/test/lib/spec/runner/formatter/base_text_formatter.rb @@ -0,0 +1,118 @@ +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) + end + + # This method is invoked when a spec passes. +name+ is the name of the + # specification. + def spec_passed(name) + 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 + 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})" + if(failure.expectation_not_met?) + @output.puts red(failure.header) + @output.puts red(failure.exception.message) + else + @output.puts magenta(failure.header) + @output.puts magenta(failure.exception.message) + 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) + 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}" + if failure_count == 0 + @output.puts green(summary) + else + @output.puts red(summary) + 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 magenta(text); colour(text, "\e[35m"); 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 new file mode 100644 index 000000000..13b796581 --- /dev/null +++ b/test/lib/spec/runner/formatter/html_formatter.rb @@ -0,0 +1,219 @@ +module Spec + module Runner + module Formatter + class HtmlFormatter < BaseTextFormatter + attr_reader :current_spec_number, :current_context_number + + def initialize(output, dry_run=false, colour=false) + super + @current_spec_number = 0 + @current_context_number = 0 + end + + def start(spec_count) + @spec_count = spec_count + + @output.puts HEADER_1 + @output.puts extra_header_content unless extra_header_content.nil? + @output.puts HEADER_2 + STDOUT.flush + end + + def add_context(name, first) + @current_context_number += 1 + unless first + @output.puts " </dl>" + @output.puts "</div>" + end + @output.puts "<div class=\"context\">" + @output.puts " <dl>" + @output.puts " <dt id=\"context_#{@current_context_number}\">#{name}</dt>" + STDOUT.flush + end + + def start_dump + @output.puts " </dl>" + @output.puts "</div>" + STDOUT.flush + end + + def spec_started(name) + @current_spec_number += 1 + STDOUT.flush + end + + def spec_passed(name) + move_progress + @output.puts " <dd class=\"spec passed\"><span class=\"passed_spec_name\">#{escape(name)}</span></dd>" + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.puts " <script type=\"text/javascript\">makeRed('header');</script>" + @output.puts " <script type=\"text/javascript\">makeRed('context_#{@current_context_number}');</script>" + move_progress + @output.puts " <dd class=\"spec failed\">" + @output.puts " <span class=\"failed_spec_name\">#{escape(name)}</span>" + @output.puts " <div class=\"failure\" id=\"failure_#{counter}\">" + @output.puts " <div class=\"message\"><pre>#{escape(failure.exception.message)}</pre></div>" unless failure.exception.nil? + @output.puts " <div class=\"backtrace\"><pre>#{format_backtrace(failure.exception.backtrace)}</pre></div>" unless failure.exception.nil? + @output.puts extra_failure_content unless extra_failure_content.nil? + @output.puts " </div>" + @output.puts " </dd>" + STDOUT.flush + end + + # Override this method if you wish to output extra HTML in the header + # + def extra_header_content + end + + # Override this method if you wish to output extra HTML for a failed spec. For example, you + # could output links to images or other files produced during the specs. + # + def extra_failure_content + 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 + @output.puts " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>" + end + + def escape(string) + string.gsub(/&/n, '&').gsub(/\"/n, '"').gsub(/>/n, '>').gsub(/</n, '<') + end + + def dump_failure(counter, failure) + end + + def dump_summary(duration, spec_count, failure_count) + if @dry_run + totals = "This was a dry-run" + else + totals = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}" + end + @output.puts "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{duration} seconds</strong>\";</script>" + @output.puts "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{totals}\";</script>" + @output.puts "</div>" + @output.puts "</body>" + @output.puts "</html>" + STDOUT.flush + end + + HEADER_1 = <<-EOF +<?xml version="1.0" encoding="iso-8859-1"?> +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <title>RSpec results</title> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> + <meta http-equiv="Expires" content="-1" /> + <meta http-equiv="Pragma" content="no-cache" /> +EOF + + HEADER_2 = <<-EOF + <script type="text/javascript"> + function moveProgressBar(percentDone) { + document.getElementById("header").style.width = percentDone +"%"; + } + function makeRed(element_id) { + document.getElementById(element_id).style.background = '#C40D0D'; + } + </script> + <style type="text/css"> + body { + margin: 0; padding: 0; + background: #fff; + } + + #header { + background: #65C400; color: #fff; + } + + h1 { + margin: 0 0 10px; + padding: 10px; + font: bold 18px "Lucida Grande", Helvetica, sans-serif; + } + + #summary { + margin: 0; padding: 5px 10px; + font: bold 10px "Lucida Grande", Helvetica, sans-serif; + text-align: right; + position: absolute; + top: 0px; + right: 0px; + } + + #summary p { + margin: 0 0 2px; + } + + #summary #totals { + font-size: 14px; + } + + .context { + margin: 0 10px 5px; + background: #fff; + } + + dl { + margin: 0; padding: 0 0 5px; + font: normal 11px "Lucida Grande", Helvetica, sans-serif; + } + + dt { + padding: 3px; + background: #65C400; + color: #fff; + font-weight: bold; + } + + dd { + margin: 5px 0 5px 5px; + padding: 3px 3px 3px 18px; + } + + dd.spec.passed { + border-left: 5px solid #65C400; + border-bottom: 1px solid #65C400; + background: #DBFFB4; color: #3D7700; + } + + dd.spec.failed { + border-left: 5px solid #C20000; + border-bottom: 1px solid #C20000; + color: #C20000; background: #FFFBD3; + } + + div.backtrace { + color: #000; + font-size: 12px; + } + + a { + color: #BE5C00; + } + </style> +</head> +<body> + +<div id="header"> + <h1>RSpec Results</h1> + + <div id="summary"> + <p id="duration"> </p> + <p id="totals"> </p> + </div> +</div> + +<div id="results"> +EOF + end + end + end +end diff --git a/test/lib/spec/runner/formatter/progress_bar_formatter.rb b/test/lib/spec/runner/formatter/progress_bar_formatter.rb new file mode 100644 index 000000000..fe519d4d8 --- /dev/null +++ b/test/lib/spec/runner/formatter/progress_bar_formatter.rb @@ -0,0 +1,27 @@ +module Spec + module Runner + module Formatter + class ProgressBarFormatter < BaseTextFormatter + def add_context(name, first) + @output.puts if first + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.print failure.expectation_not_met? ? red('F') : magenta('F') + STDOUT.flush + end + + def spec_passed(name) + @output.print green('.') + STDOUT.flush + end + + def start_dump + @output.puts + STDOUT.flush + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/formatter/rdoc_formatter.rb b/test/lib/spec/runner/formatter/rdoc_formatter.rb new file mode 100644 index 000000000..eae55c3ea --- /dev/null +++ b/test/lib/spec/runner/formatter/rdoc_formatter.rb @@ -0,0 +1,22 @@ +module Spec + module Runner + module Formatter + class RdocFormatter < BaseTextFormatter + def add_context(name, first) + @output.print "# #{name}\n" + STDOUT.flush + end + + def spec_passed(name) + @output.print "# * #{name}\n" + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.print "# * #{name} [#{counter} - FAILED]\n" + STDOUT.flush + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/formatter/specdoc_formatter.rb b/test/lib/spec/runner/formatter/specdoc_formatter.rb new file mode 100644 index 000000000..67b4312bf --- /dev/null +++ b/test/lib/spec/runner/formatter/specdoc_formatter.rb @@ -0,0 +1,23 @@ +module Spec + module Runner + module Formatter + class SpecdocFormatter < BaseTextFormatter + def add_context(name, first) + @output.puts + @output.puts name + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.puts failure.expectation_not_met? ? red("- #{name} (FAILED - #{counter})") : magenta("- #{name} (ERROR - #{counter})") + STDOUT.flush + end + + def spec_passed(name) + @output.print green("- #{name}\n") + STDOUT.flush + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/heckle_runner.rb b/test/lib/spec/runner/heckle_runner.rb new file mode 100644 index 000000000..fd36389de --- /dev/null +++ b/test/lib/spec/runner/heckle_runner.rb @@ -0,0 +1,71 @@ +begin + require 'rubygems' + require 'heckle' +rescue LoadError ; raise "You must gem install heckle to use --heckle" ; end + +module Spec + module Runner + # Creates a new Heckler configured to heckle all methods in the classes + # whose name matches +filter+ + class HeckleRunner + def initialize(filter, heckle_class=Heckler) + @filter = filter + @heckle_class = heckle_class + end + + # Runs all the contexts held by +context_runner+ once for each of the + # methods in the matched classes. + def heckle_with(context_runner) + if @filter =~ /(.*)[#\.](.*)/ + heckle_method($1, $2) + else + heckle_class_or_module(@filter) + end + end + + def heckle_method(class_name, method_name) + verify_constant(class_name) + heckle = @heckle_class.new(class_name, method_name, context_runner) + heckle.validate + end + + def heckle_class_or_module(class_or_module_name) + verify_constant(class_or_module_name) + pattern = /^#{class_or_module_name}/ + classes = [] + ObjectSpace.each_object(Class) do |klass| + classes << klass if klass.name =~ pattern + end + + classes.each do |klass| + klass.instance_methods(false).each do |method_name| + heckle = @heckle_class.new(klass.name, method_name, context_runner) + heckle.validate + end + end + end + + def verify_constant(name) + begin + # This is defined in Heckle + name.to_class + rescue + raise "Heckling failed - \"#{name}\" is not a known class or module" + end + end + end + + #Supports Heckle 1.2 and prior (earlier versions used Heckle::Base) + class Heckler < (Heckle.const_defined?(:Base) ? Heckle::Base : Heckle) + def initialize(klass_name, method_name, context_runner) + super(klass_name, method_name) + @context_runner = context_runner + end + + def tests_pass? + failure_count = @context_runner.run(false) + failure_count == 0 + end + end + end +end diff --git a/test/lib/spec/runner/heckle_runner_win.rb b/test/lib/spec/runner/heckle_runner_win.rb new file mode 100644 index 000000000..031386599 --- /dev/null +++ b/test/lib/spec/runner/heckle_runner_win.rb @@ -0,0 +1,10 @@ +module Spec
+ module Runner
+ # Dummy implementation for Windows that just fails (Heckle is not supported on Windows)
+ class HeckleRunner
+ def initialize(filter)
+ raise "Heckle not supported on Windows"
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/option_parser.rb b/test/lib/spec/runner/option_parser.rb new file mode 100644 index 000000000..38725d848 --- /dev/null +++ b/test/lib/spec/runner/option_parser.rb @@ -0,0 +1,224 @@ +require 'ostruct' +require 'optparse' +require 'spec/runner/spec_parser' +require 'spec/runner/formatter' +require 'spec/runner/backtrace_tweaker' +require 'spec/runner/reporter' +require 'spec/runner/context_runner' + +module Spec + module Runner + class OptionParser + def initialize + @spec_parser = SpecParser.new + @file_factory = File + end + + def create_context_runner(args, err, out, warn_if_no_files) + options = parse(args, err, out, warn_if_no_files) + # Some exit points in parse (--generate-options, --drb) don't return the options, + # but hand over control. In that case we don't want to continue. + return nil unless options.is_a?(OpenStruct) + + formatter = options.formatter_type.new(options.out, options.dry_run, options.colour) + options.reporter = Reporter.new(formatter, options.backtrace_tweaker) + + # this doesn't really belong here. + # it should, but the way things are coupled, it doesn't + if options.differ_class + Spec::Expectations.differ = options.differ_class.new(options.diff_format, options.context_lines, options.colour) + end + + unless options.generate + ContextRunner.new(options) + end + end + + def parse(args, err, out, warn_if_no_files) + options_file = nil + args_copy = args.dup + options = OpenStruct.new + options.out = (out == STDOUT ? Kernel : out) + options.formatter_type = Formatter::ProgressBarFormatter + options.backtrace_tweaker = QuietBacktraceTweaker.new + options.spec_name = nil + + opts = ::OptionParser.new do |opts| + opts.banner = "Usage: spec [options] (FILE|DIRECTORY|GLOB)+" + opts.separator "" + + opts.on("-D", "--diff [FORMAT]", "Show diff of objects that are expected to be equal when they are not", + "Builtin formats: unified|u|context|c", + "You can also specify a custom differ class", + "(in which case you should also specify --require)") do |format| + + # TODO make context_lines settable + options.context_lines = 3 + + case format + when 'context', 'c' + options.diff_format = :context + when 'unified', 'u', '', nil + options.diff_format = :unified + end + + if [:context,:unified].include? options.diff_format + require 'spec/expectations/differs/default' + options.differ_class = Spec::Expectations::Differs::Default + else + begin + options.diff_format = :custom + options.differ_class = eval(format) + rescue NameError + err.puts "Couldn't find differ class #{format}" + err.puts "Make sure the --require option is specified *before* --diff" + exit if out == $stdout + end + end + + end + + opts.on("-c", "--colour", "--color", "Show coloured (red/green) output") do + options.colour = true + end + + opts.on("-s", "--spec SPECIFICATION_NAME", "Execute context or specification with matching name") do |spec_name| + options.spec_name = spec_name + end + + opts.on("-l", "--line LINE_NUMBER", Integer, "Execute context or specification at given line") do |line_number| + options.line_number = line_number.to_i + end + + opts.on("-f", "--format FORMAT", "Builtin formats: specdoc|s|rdoc|r|html|h", + "You can also specify a custom formatter class", + "(in which case you should also specify --require)") do |format| + case format + when 'specdoc', 's' + options.formatter_type = Formatter::SpecdocFormatter + when 'html', 'h' + options.formatter_type = Formatter::HtmlFormatter + when 'rdoc', 'r' + options.formatter_type = Formatter::RdocFormatter + options.dry_run = true + else + begin + options.formatter_type = eval(format) + rescue NameError + err.puts "Couldn't find formatter class #{format}" + err.puts "Make sure the --require option is specified *before* --format" + exit if out == $stdout + end + end + end + + opts.on("-r", "--require FILE", "Require FILE before running specs", + "Useful for loading custom formatters or other extensions", + "If this option is used it must come before the others") do |req| + req.split(",").each{|file| require file} + end + + opts.on("-b", "--backtrace", "Output full backtrace") do + options.backtrace_tweaker = NoisyBacktraceTweaker.new + end + + opts.on("-H", "--heckle CODE", "If all specs pass, this will run your specs many times, mutating", + "the specced code a little each time. The intent is that specs", + "*should* fail, and RSpec will tell you if they don't.", + "CODE should be either Some::Module, Some::Class or Some::Fabulous#method}") do |heckle| + heckle_runner = PLATFORM == 'i386-mswin32' ? 'spec/runner/heckle_runner_win' : 'spec/runner/heckle_runner' + require heckle_runner + options.heckle_runner = HeckleRunner.new(heckle) + end + + opts.on("-d", "--dry-run", "Don't execute specs") do + options.dry_run = true + end + + opts.on("-o", "--out OUTPUT_FILE", "Path to output file (defaults to STDOUT)") do |out_file| + options.out = File.new(out_file, 'w') + end + + opts.on("-O", "--options PATH", "Read options from a file") do |options_file| + # Remove the --options option and the argument before writing to file + index = args_copy.index("-O") || args_copy.index("--options") + args_copy.delete_at(index) + args_copy.delete_at(index) + + new_args = args_copy + IO.readlines(options_file).each {|s| s.chomp!} + return CommandLine.run(new_args, err, out, true, warn_if_no_files) + end + + opts.on("-G", "--generate-options PATH", "Generate an options file for --options") do |options_file| + # Remove the --generate-options option and the argument before writing to file + index = args_copy.index("-G") || args_copy.index("--generate-options") + args_copy.delete_at(index) + args_copy.delete_at(index) + + File.open(options_file, 'w') do |io| + io.puts args_copy.join("\n") + end + out.puts "\nOptions written to #{options_file}. You can now use these options with:" + out.puts "spec --options #{options_file}" + options.generate = true + end + + opts.on("-X", "--drb", "Run specs via DRb. (For example against script/rails_spec_server)") do |options_file| + # Remove the --options option and the argument before writing to file + index = args_copy.index("-X") || args_copy.index("--drb") + args_copy.delete_at(index) + + return DrbCommandLine.run(args_copy, err, out, true, warn_if_no_files) + end + + opts.on("-v", "--version", "Show version") do + out.puts ::Spec::VERSION::DESCRIPTION + exit if out == $stdout + end + + opts.on_tail("-h", "--help", "You're looking at it") do + out.puts opts + exit if out == $stdout + end + + end + opts.parse!(args) + + if args.empty? && warn_if_no_files + err.puts "No files specified." + err.puts opts + exit(6) if err == $stderr + end + + if options.line_number + set_spec_from_line_number(options, args, err) + end + + options + end + + def set_spec_from_line_number(options, args, err) + unless options.spec_name + if args.length == 1 + if @file_factory.file?(args[0]) + source = @file_factory.open(args[0]) + options.spec_name = @spec_parser.spec_name_for(source, options.line_number) + elsif @file_factory.directory?(args[0]) + err.puts "You must specify one file, not a directory when using the --line option" + exit(1) if err == $stderr + else + err.puts "#{args[0]} does not exist" + exit(2) if err == $stderr + end + else + err.puts "Only one file can be specified when using the --line option: #{args.inspect}" + exit(3) if err == $stderr + end + else + err.puts "You cannot use both --line and --spec" + exit(4) if err == $stderr + end + end + end + end +end diff --git a/test/lib/spec/runner/reporter.rb b/test/lib/spec/runner/reporter.rb new file mode 100644 index 000000000..e4fb1cb0e --- /dev/null +++ b/test/lib/spec/runner/reporter.rb @@ -0,0 +1,105 @@ +module Spec + module Runner + class Reporter + + def initialize(formatter, backtrace_tweaker) + @formatter = formatter + @backtrace_tweaker = backtrace_tweaker + clear! + end + + def add_context(name) + #TODO - @context_names.empty? tells the formatter whether this is the first context or not - that's a little slippery + @formatter.add_context(name, @context_names.empty?) + @context_names << name + end + + def spec_started(name) + @spec_names << name + @formatter.spec_started(name) + end + + def spec_finished(name, error=nil, failure_location=nil) + if error.nil? + spec_passed(name) + else + @backtrace_tweaker.tweak_backtrace(error, failure_location) + spec_failed(name, Failure.new(@context_names.last, name, error)) + end + end + + def start(number_of_specs) + clear! + @start_time = Time.new + @formatter.start(number_of_specs) + end + + def end + @end_time = Time.new + end + + # Dumps the summary and returns the total number of failures + def dump + @formatter.start_dump + dump_failures + @formatter.dump_summary(duration, @spec_names.length, @failures.length) + @failures.length + end + + private + + def clear! + @context_names = [] + @failures = [] + @spec_names = [] + @start_time = nil + @end_time = nil + end + + def dump_failures + return if @failures.empty? + @failures.inject(1) do |index, failure| + @formatter.dump_failure(index, failure) + index + 1 + end + end + + def duration + return @end_time - @start_time unless (@end_time.nil? or @start_time.nil?) + return "0.0" + end + + def spec_passed(name) + @formatter.spec_passed(name) + end + + def spec_failed(name, failure) + @failures << failure + @formatter.spec_failed(name, @failures.length, failure) + end + + class Failure + attr_reader :exception + + def initialize(context_name, spec_name, exception) + @context_name = context_name + @spec_name = spec_name + @exception = exception + end + + def header + if expectation_not_met? + "'#{@context_name} #{@spec_name}' FAILED" + else + "#{@exception.class.name} in '#{@context_name} #{@spec_name}'" + end + end + + def expectation_not_met? + @exception.is_a?(Spec::Expectations::ExpectationNotMetError) + end + + end + end + end +end diff --git a/test/lib/spec/runner/spec_matcher.rb b/test/lib/spec/runner/spec_matcher.rb new file mode 100755 index 000000000..687fdaa00 --- /dev/null +++ b/test/lib/spec/runner/spec_matcher.rb @@ -0,0 +1,25 @@ +module Spec + module Runner + class SpecMatcher + + attr_writer :spec_desc + def initialize(context_desc, spec_desc=nil) + @context_desc = context_desc + @spec_desc = spec_desc + end + + def matches?(desc) + desc =~ /(^#{context_regexp} #{spec_regexp}$|^#{context_regexp}$|^#{spec_regexp}$)/ + end + + private + def context_regexp + Regexp.escape(@context_desc) + end + + def spec_regexp + Regexp.escape(@spec_desc) + end + end + end +end diff --git a/test/lib/spec/runner/spec_parser.rb b/test/lib/spec/runner/spec_parser.rb new file mode 100644 index 000000000..2cb8518fc --- /dev/null +++ b/test/lib/spec/runner/spec_parser.rb @@ -0,0 +1,41 @@ +module Spec + module Runner + # Parses a spec file and finds the nearest spec for a given line number. + class SpecParser + def spec_name_for(io, line_number) + source = io.read + context = context_at_line(source, line_number) + spec = spec_at_line(source, line_number) + if context && spec + "#{context} #{spec}" + elsif context + context + else + nil + end + end + + protected + + def context_at_line(source, line_number) + find_above(source, line_number, /^\s*context\s+['|"](.*)['|"]/) + end + + def spec_at_line(source, line_number) + find_above(source, line_number, /^\s*specify\s+['|"](.*)['|"]/) + end + + def find_above(source, line_number, pattern) + lines_above_reversed(source, line_number).each do |line| + return $1 if line =~ pattern + end + nil + end + + def lines_above_reversed(source, line_number) + lines = source.split("\n") + lines[0...line_number].reverse + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/spec_should_raise_handler.rb b/test/lib/spec/runner/spec_should_raise_handler.rb new file mode 100644 index 000000000..c7fa41c4e --- /dev/null +++ b/test/lib/spec/runner/spec_should_raise_handler.rb @@ -0,0 +1,74 @@ +module Spec + module Runner + class SpecShouldRaiseHandler + def initialize(file_and_line_number, opts) + @file_and_line_number = file_and_line_number + @options = opts + @expected_error_class = determine_error_class(opts) + @expected_error_message = determine_error_message(opts) + end + + def determine_error_class(opts) + if candidate = opts[:should_raise] + if candidate.is_a?(Class) + return candidate + elsif candidate.is_a?(Array) + return candidate[0] + else + return Exception + end + end + end + + def determine_error_message(opts) + if candidate = opts[:should_raise] + if candidate.is_a?(Array) + return candidate[1] + end + end + return nil + end + + def build_message(exception=nil) + if @expected_error_message.nil? + message = "specify block expected #{@expected_error_class.to_s}" + else + message = "specify block expected #{@expected_error_class.new(@expected_error_message.to_s).inspect}" + end + message << " but raised #{exception.inspect}" if exception + message << " but nothing was raised" unless exception + message << "\n" + message << @file_and_line_number + end + + def error_matches?(error) + return false unless error.kind_of?(@expected_error_class) + unless @expected_error_message.nil? + if @expected_error_message.is_a?(Regexp) + return false unless error.message =~ @expected_error_message + else + return false unless error.message == @expected_error_message + end + end + return true + end + + def handle(errors) + if @expected_error_class + if errors.empty? + errors << Spec::Expectations::ExpectationNotMetError.new(build_message) + else + error_to_remove = errors.detect do |error| + error_matches?(error) + end + if error_to_remove.nil? + errors.insert(0,Spec::Expectations::ExpectationNotMetError.new(build_message(errors[0]))) + else + errors.delete(error_to_remove) + end + end + end + end + end + end +end diff --git a/test/lib/spec/runner/specification.rb b/test/lib/spec/runner/specification.rb new file mode 100644 index 000000000..de8d750fd --- /dev/null +++ b/test/lib/spec/runner/specification.rb @@ -0,0 +1,114 @@ +module Spec + module Runner + class Specification + + class << self + attr_accessor :current, :generated_description + protected :current= + + callback_events :before_setup, :after_teardown + end + + attr_reader :spec_block + callback_events :before_setup, :after_teardown + + def initialize(name, opts={}, &spec_block) + @from = caller(0)[3] + @description = name + @options = opts + @spec_block = spec_block + @description_generated_callback = lambda { |desc| @generated_description = desc } + end + + def run(reporter, setup_block, teardown_block, dry_run, execution_context) + reporter.spec_started(name) if reporter + return reporter.spec_finished(name) if dry_run + + errors = [] + begin + set_current + setup_ok = setup_spec(execution_context, errors, &setup_block) + spec_ok = execute_spec(execution_context, errors) if setup_ok + teardown_ok = teardown_spec(execution_context, errors, &teardown_block) + ensure + clear_current + end + + SpecShouldRaiseHandler.new(@from, @options).handle(errors) + reporter.spec_finished(name, errors.first, failure_location(setup_ok, spec_ok, teardown_ok)) if reporter + end + + def matches?(matcher, description) + matcher.spec_desc = name + matcher.matches?(description) + end + + private + def name + @description == :__generate_description ? generated_description : @description + end + + def generated_description + @generated_description || "NAME NOT GENERATED" + end + + def setup_spec(execution_context, errors, &setup_block) + notify_before_setup(errors) + execution_context.instance_eval(&setup_block) if setup_block + return errors.empty? + rescue => e + errors << e + return false + end + + def execute_spec(execution_context, errors) + begin + execution_context.instance_eval(&spec_block) + return true + rescue Exception => e + errors << e + return false + end + end + + def teardown_spec(execution_context, errors, &teardown_block) + execution_context.instance_eval(&teardown_block) if teardown_block + notify_after_teardown(errors) + return errors.empty? + rescue => e + errors << e + return false + end + + def notify_before_setup(errors) + notify_class_callbacks(:before_setup, self, &append_errors(errors)) + notify_callbacks(:before_setup, self, &append_errors(errors)) + end + + def notify_after_teardown(errors) + notify_callbacks(:after_teardown, self, &append_errors(errors)) + notify_class_callbacks(:after_teardown, self, &append_errors(errors)) + end + + def append_errors(errors) + proc {|error| errors << error} + end + + def set_current + Spec::Matchers.description_generated(&@description_generated_callback) + self.class.send(:current=, self) + end + + def clear_current + Spec::Matchers.unregister_callback(:description_generated, @description_generated_callback) + self.class.send(:current=, nil) + end + + def failure_location(setup_ok, spec_ok, teardown_ok) + return 'setup' unless setup_ok + return name unless spec_ok + return 'teardown' unless teardown_ok + end + end + end +end diff --git a/test/lib/spec/translator.rb b/test/lib/spec/translator.rb new file mode 100644 index 000000000..970c8ca00 --- /dev/null +++ b/test/lib/spec/translator.rb @@ -0,0 +1,87 @@ +require 'fileutils' + +module Spec + class Translator + def translate_dir(from, to) + from = File.expand_path(from) + to = File.expand_path(to) + if File.directory?(from) + FileUtils.mkdir_p(to) unless File.directory?(to) + Dir["#{from}/*"].each do |sub_from| + path = sub_from[from.length+1..-1] + sub_to = File.join(to, path) + translate_dir(sub_from, sub_to) + end + else + translate_file(from, to) + end + end + + def translate_file(from, to) + translation = "" + File.open(from) do |io| + io.each_line do |line| + translation << translate(line) + end + end + File.open(to, "w") do |io| + io.write(translation) + end + end + + def translate(line) + return line if line =~ /(should_not|should)_receive/ + + if line =~ /(.*\.)(should_not|should)(?:_be)(?!_)(.*)/m + pre = $1 + should = $2 + post = $3 + be_or_equal = post =~ /(<|>)/ ? "be" : "equal" + + return "#{pre}#{should} #{be_or_equal}#{post}" + end + + if line =~ /(.*\.)(should_not|should)_(?!not)(.*)/m + pre = $1 + should = $2 + post = $3 + + post.gsub!(/^raise/, 'raise_error') + post.gsub!(/^throw/, 'throw_symbol') + + unless standard_matcher?(post) + post = "be_#{post}" + end + + line = "#{pre}#{should} #{post}" + end + + line + end + + def standard_matcher?(matcher) + patterns = [ + /^be/, + /^be_close/, + /^eql/, + /^equal/, + /^has/, + /^have/, + /^change/, + /^include/, + /^match/, + /^raise_error/, + /^respond_to/, + /^satisfy/, + /^throw_symbol/, + # Extra ones that we use in spec_helper + /^pass/, + /^fail/, + /^fail_with/, + ] + matched = patterns.detect{ |p| matcher =~ p } + !matched.nil? + end + + end +end
\ No newline at end of file diff --git a/test/lib/spec/version.rb b/test/lib/spec/version.rb new file mode 100644 index 000000000..924d8458a --- /dev/null +++ b/test/lib/spec/version.rb @@ -0,0 +1,30 @@ +module Spec
+ module VERSION
+ def self.build_tag
+ tag = "REL_" + [MAJOR, MINOR, TINY].join('_')
+ if defined?(RELEASE_CANDIDATE)
+ tag << "_" << RELEASE_CANDIDATE
+ end
+ tag
+ end
+
+ unless defined? MAJOR
+ MAJOR = 0
+ MINOR = 8
+ TINY = 2
+ # RELEASE_CANDIDATE = "RC1"
+
+ # RANDOM_TOKEN: 0.375509844656552 + REV = "$LastChangedRevision$".match(/LastChangedRevision: (\d+)/)[1]
+
+ STRING = [MAJOR, MINOR, TINY].join('.')
+ FULL_VERSION = "#{STRING} (r#{REV})"
+ TAG = build_tag
+
+ NAME = "RSpec"
+ URL = "http://rspec.rubyforge.org/"
+
+ DESCRIPTION = "#{NAME}-#{FULL_VERSION} - BDD for Ruby\n#{URL}"
+ end
+ end
+end
\ No newline at end of file |