summaryrefslogtreecommitdiffstats
path: root/test/lib/spec/runner
diff options
context:
space:
mode:
authorluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-03-17 02:48:41 +0000
committerluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-03-17 02:48:41 +0000
commitba23a5ac276e59fdda8186750c6d0fd2cfecdeac (patch)
tree1e14b25ade74ea52d8da2788ede9b12b507867e8 /test/lib/spec/runner
parent8ea6adaeb1e3d0aa6348c2a2c3a385d185372d06 (diff)
downloadpuppet-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/runner')
-rw-r--r--test/lib/spec/runner/backtrace_tweaker.rb55
-rw-r--r--test/lib/spec/runner/command_line.rb34
-rw-r--r--test/lib/spec/runner/context.rb154
-rw-r--r--test/lib/spec/runner/context_eval.rb142
-rw-r--r--test/lib/spec/runner/context_runner.rb55
-rw-r--r--test/lib/spec/runner/drb_command_line.rb21
-rw-r--r--test/lib/spec/runner/execution_context.rb17
-rw-r--r--test/lib/spec/runner/extensions/kernel.rb17
-rw-r--r--test/lib/spec/runner/extensions/object.rb32
-rw-r--r--test/lib/spec/runner/formatter.rb5
-rw-r--r--test/lib/spec/runner/formatter/base_text_formatter.rb118
-rw-r--r--test/lib/spec/runner/formatter/html_formatter.rb219
-rw-r--r--test/lib/spec/runner/formatter/progress_bar_formatter.rb27
-rw-r--r--test/lib/spec/runner/formatter/rdoc_formatter.rb22
-rw-r--r--test/lib/spec/runner/formatter/specdoc_formatter.rb23
-rw-r--r--test/lib/spec/runner/heckle_runner.rb71
-rw-r--r--test/lib/spec/runner/heckle_runner_win.rb10
-rw-r--r--test/lib/spec/runner/option_parser.rb224
-rw-r--r--test/lib/spec/runner/reporter.rb105
-rwxr-xr-xtest/lib/spec/runner/spec_matcher.rb25
-rw-r--r--test/lib/spec/runner/spec_parser.rb41
-rw-r--r--test/lib/spec/runner/spec_should_raise_handler.rb74
-rw-r--r--test/lib/spec/runner/specification.rb114
23 files changed, 1605 insertions, 0 deletions
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, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
+ 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">&nbsp;</p>
+ <p id="totals">&nbsp;</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