summaryrefslogtreecommitdiffstats
path: root/test/lib/spec/runner
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2007-08-23 00:56:42 -0500
committerLuke Kanies <luke@madstop.com>2007-08-23 00:56:42 -0500
commit5601ecf75d3854a66d087a108e1b06885fa2be12 (patch)
tree28d5892bab14c9296bcd4232075f3658ee1224a0 /test/lib/spec/runner
parent7c4d39ec09c10871d7eb234fe4392381245ff443 (diff)
Upgrading rspec to version 1.0.8. This only includes the contents of the lib directory, and even then only the spec-related stuff, not the autotest stuff.
Diffstat (limited to 'test/lib/spec/runner')
-rw-r--r--test/lib/spec/runner/backtrace_tweaker.rb6
-rw-r--r--test/lib/spec/runner/behaviour_runner.rb123
-rw-r--r--test/lib/spec/runner/command_line.rb24
-rw-r--r--test/lib/spec/runner/drb_command_line.rb6
-rw-r--r--test/lib/spec/runner/extensions/kernel.rb51
-rw-r--r--test/lib/spec/runner/formatter.rb4
-rw-r--r--test/lib/spec/runner/formatter/base_formatter.rb76
-rw-r--r--test/lib/spec/runner/formatter/base_text_formatter.rb130
-rw-r--r--test/lib/spec/runner/formatter/failing_behaviours_formatter.rb29
-rw-r--r--test/lib/spec/runner/formatter/failing_examples_formatter.rb22
-rw-r--r--test/lib/spec/runner/formatter/html_formatter.rb350
-rw-r--r--test/lib/spec/runner/formatter/progress_bar_formatter.rb24
-rw-r--r--test/lib/spec/runner/formatter/rdoc_formatter.rb22
-rw-r--r--test/lib/spec/runner/formatter/snippet_extractor.rb52
-rw-r--r--test/lib/spec/runner/formatter/specdoc_formatter.rb24
-rw-r--r--test/lib/spec/runner/heckle_runner.rb15
-rw-r--r--test/lib/spec/runner/heckle_runner_unsupported.rb10
-rw-r--r--test/lib/spec/runner/option_parser.rb301
-rw-r--r--test/lib/spec/runner/options.rb175
-rw-r--r--test/lib/spec/runner/reporter.rb86
-rw-r--r--test/lib/spec/runner/spec_parser.rb39
21 files changed, 1122 insertions, 447 deletions
diff --git a/test/lib/spec/runner/backtrace_tweaker.rb b/test/lib/spec/runner/backtrace_tweaker.rb
index 7300b36b8..aacc2c8b8 100644
--- a/test/lib/spec/runner/backtrace_tweaker.rb
+++ b/test/lib/spec/runner/backtrace_tweaker.rb
@@ -32,7 +32,9 @@ module Spec
# TextMate's Ruby and RSpec plugins
/Ruby\.tmbundle\/Support\/tmruby.rb:/,
/RSpec\.tmbundle\/Support\/lib/,
- /temp_textmate\./
+ /temp_textmate\./,
+ /mock_frameworks\/rspec/,
+ /spec_server/
]
end
@@ -52,4 +54,4 @@ module Spec
end
end
end
-end \ No newline at end of file
+end
diff --git a/test/lib/spec/runner/behaviour_runner.rb b/test/lib/spec/runner/behaviour_runner.rb
new file mode 100644
index 000000000..1ac891f3c
--- /dev/null
+++ b/test/lib/spec/runner/behaviour_runner.rb
@@ -0,0 +1,123 @@
+module Spec
+ module Runner
+ class BehaviourRunner
+
+ def initialize(options, arg=nil)
+ @behaviours = []
+ @options = options
+ end
+
+ def add_behaviour(behaviour)
+ if !specified_examples.nil? && !specified_examples.empty?
+ behaviour.retain_examples_matching!(specified_examples)
+ end
+ @behaviours << behaviour if behaviour.number_of_examples != 0 && !behaviour.shared?
+ end
+
+ # Runs all behaviours and returns the number of failures.
+ def run(paths, exit_when_done)
+ prepare!(paths)
+ begin
+ run_behaviours
+ rescue Interrupt
+ ensure
+ report_end
+ end
+ failure_count = report_dump
+
+ heckle if(failure_count == 0 && !@options.heckle_runner.nil?)
+
+ if(exit_when_done)
+ exit_code = (failure_count == 0) ? 0 : 1
+ exit(exit_code)
+ end
+ failure_count
+ end
+
+ def report_end
+ @options.reporter.end
+ end
+
+ def report_dump
+ @options.reporter.dump
+ end
+
+ def prepare!(paths)
+ unless paths.nil? # It's nil when running single specs with ruby
+ paths = find_paths(paths)
+ sorted_paths = sort_paths(paths)
+ load_specs(sorted_paths) # This will populate @behaviours via callbacks to add_behaviour
+ end
+ @options.reporter.start(number_of_examples)
+ @behaviours.reverse! if @options.reverse
+ set_sequence_numbers
+ end
+
+ def run_behaviours
+ @behaviours.each do |behaviour|
+ behaviour.run(@options.reporter, @options.dry_run, @options.reverse, @options.timeout)
+ end
+ end
+
+ def number_of_examples
+ @behaviours.inject(0) {|sum, behaviour| sum + behaviour.number_of_examples}
+ end
+
+ FILE_SORTERS = {
+ 'mtime' => lambda {|file_a, file_b| File.mtime(file_b) <=> File.mtime(file_a)}
+ }
+
+ def sorter(paths)
+ FILE_SORTERS[@options.loadby]
+ end
+
+ def sort_paths(paths)
+ sorter = sorter(paths)
+ paths = paths.sort(&sorter) unless sorter.nil?
+ paths
+ end
+
+ private
+
+ # Sets the #number on each Example
+ def set_sequence_numbers
+ number = 0
+ @behaviours.each do |behaviour|
+ number = behaviour.set_sequence_numbers(number, @options.reverse)
+ end
+ end
+
+ def find_paths(paths)
+ result = []
+ paths.each do |path|
+ if File.directory?(path)
+ result += Dir["#{path}/**/*.rb"]
+ elsif File.file?(path)
+ result << path
+ else
+ raise "File or directory not found: #{path}"
+ end
+ end
+ result
+ end
+
+ def load_specs(paths)
+ paths.each do |path|
+ load path
+ end
+ end
+
+ def specified_examples
+ @options.examples
+ end
+
+ def heckle
+ heckle_runner = @options.heckle_runner
+ @options.heckle_runner = nil
+ behaviour_runner = self.class.new(@options)
+ behaviour_runner.instance_variable_set(:@behaviours, @behaviours)
+ heckle_runner.heckle_with(behaviour_runner)
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/command_line.rb b/test/lib/spec/runner/command_line.rb
index db928ad9b..0d70337e1 100644
--- a/test/lib/spec/runner/command_line.rb
+++ b/test/lib/spec/runner/command_line.rb
@@ -10,25 +10,13 @@ module Spec
# +warn_if_no_files+ tells whether or not a warning (the help message)
# should be printed to +err+ in case no files are specified.
def self.run(argv, err, out, exit=true, warn_if_no_files=true)
- old_context_runner = defined?($context_runner) ? $context_runner : nil
- $context_runner = OptionParser.new.create_context_runner(argv, err, out, warn_if_no_files)
- return if $context_runner.nil? # This is the case if we use --drb
+ old_behaviour_runner = defined?($behaviour_runner) ? $behaviour_runner : nil
+ $behaviour_runner = OptionParser.new.create_behaviour_runner(argv, err, out, warn_if_no_files)
+ return if $behaviour_runner.nil? # This is the case if we use --drb
- # If ARGV is a glob, it will actually each over each one of the matching files.
- argv.each do |file_or_dir|
- if File.directory?(file_or_dir)
- Dir["#{file_or_dir}/**/*.rb"].each do |file|
- load file
- end
- elsif File.file?(file_or_dir)
- load file_or_dir
- else
- raise "File or directory not found: #{file_or_dir}"
- end
- end
- $context_runner.run(exit)
- $context_runner = old_context_runner
+ $behaviour_runner.run(argv, exit)
+ $behaviour_runner = old_behaviour_runner
end
end
end
-end \ No newline at end of file
+end
diff --git a/test/lib/spec/runner/drb_command_line.rb b/test/lib/spec/runner/drb_command_line.rb
index d4c7d937d..7e745fb71 100644
--- a/test/lib/spec/runner/drb_command_line.rb
+++ b/test/lib/spec/runner/drb_command_line.rb
@@ -9,8 +9,8 @@ module Spec
def self.run(argv, stderr, stdout, exit=true, warn_if_no_files=true)
begin
DRb.start_service
- rails_spec_server = DRbObject.new_with_uri("druby://localhost:8989")
- rails_spec_server.run(argv, stderr, stdout)
+ spec_server = DRbObject.new_with_uri("druby://localhost:8989")
+ spec_server.run(argv, stderr, stdout)
rescue DRb::DRbConnError
stderr.puts "No server is running"
exit 1 if exit
@@ -18,4 +18,4 @@ module Spec
end
end
end
-end \ No newline at end of file
+end
diff --git a/test/lib/spec/runner/extensions/kernel.rb b/test/lib/spec/runner/extensions/kernel.rb
index f060ec859..75f2c335e 100644
--- a/test/lib/spec/runner/extensions/kernel.rb
+++ b/test/lib/spec/runner/extensions/kernel.rb
@@ -1,17 +1,50 @@
module Kernel
- def context(name, &block)
- context = Spec::Runner::Context.new(name, &block)
- context_runner.add_context(context)
+ # Creates and registers an instance of a Spec::DSL::Behaviour (or a subclass).
+ # The instantiated behaviour class depends on the directory of the file
+ # calling this method. For example, Spec::Rails will use different
+ # classes for specs living in <tt>spec/models</tt>, <tt>spec/helpers</tt>,
+ # <tt>spec/views</tt> and <tt>spec/controllers</tt>.
+ #
+ # It is also possible to override autodiscovery of the behaviour class
+ # with an options Hash as the last argument:
+ #
+ # describe "name", :behaviour_type => :something_special do ...
+ #
+ # The reason for using different behaviour classes is to have
+ # different matcher methods available from within the <tt>describe</tt>
+ # block.
+ #
+ # See Spec::DSL::BehaviourFactory#add_behaviour_class for details about
+ # how to register special Spec::DSL::Behaviour implementations.
+ #
+ def describe(*args, &block)
+ raise ArgumentError if args.empty?
+ args << {} unless Hash === args.last
+ args.last[:spec_path] = caller(0)[1]
+ register_behaviour(Spec::DSL::BehaviourFactory.create(*args, &block))
end
-
+ alias :context :describe
+
+ def respond_to(*names)
+ Spec::Matchers::RespondTo.new(*names)
+ end
+
private
- def context_runner
+ def register_behaviour(behaviour)
+ if behaviour.shared?
+ Spec::DSL::Behaviour.add_shared_behaviour(behaviour)
+ else
+ behaviour_runner.add_behaviour(behaviour)
+ end
+ end
+
+ def behaviour_runner
# TODO: Figure out a better way to get this considered "covered" and keep this statement on multiple lines
- unless $context_runner; \
- $context_runner = ::Spec::Runner::OptionParser.new.create_context_runner(ARGV.dup, STDERR, STDOUT, false); \
- at_exit { $context_runner.run(false) }; \
+ unless $behaviour_runner; \
+ $behaviour_runner = ::Spec::Runner::OptionParser.new.create_behaviour_runner(ARGV.dup, STDERR, STDOUT, false); \
+ at_exit { $behaviour_runner.run(nil, false) }; \
end
- $context_runner
+ $behaviour_runner
end
end
diff --git a/test/lib/spec/runner/formatter.rb b/test/lib/spec/runner/formatter.rb
index f62e81733..17512d958 100644
--- a/test/lib/spec/runner/formatter.rb
+++ b/test/lib/spec/runner/formatter.rb
@@ -1,5 +1,9 @@
+require 'spec/runner/formatter/base_formatter'
require 'spec/runner/formatter/base_text_formatter'
require 'spec/runner/formatter/progress_bar_formatter'
require 'spec/runner/formatter/rdoc_formatter'
require 'spec/runner/formatter/specdoc_formatter'
require 'spec/runner/formatter/html_formatter'
+require 'spec/runner/formatter/failing_examples_formatter'
+require 'spec/runner/formatter/failing_behaviours_formatter'
+require 'spec/runner/formatter/snippet_extractor'
diff --git a/test/lib/spec/runner/formatter/base_formatter.rb b/test/lib/spec/runner/formatter/base_formatter.rb
new file mode 100644
index 000000000..7cc43ef0e
--- /dev/null
+++ b/test/lib/spec/runner/formatter/base_formatter.rb
@@ -0,0 +1,76 @@
+module Spec
+ module Runner
+ module Formatter
+ # Baseclass for formatters that implements all required methods as no-ops.
+ class BaseFormatter
+ def initialize(where)
+ @where = where
+ end
+
+ # This method is invoked before any examples are run, right after
+ # they have all been collected. This can be useful for special
+ # formatters that need to provide progress on feedback (graphical ones)
+ #
+ # This method will only be invoked once, and the next one to be invoked
+ # is #add_behaviour
+ def start(example_count)
+ end
+
+ # This method is invoked at the beginning of the execution of each behaviour.
+ # +name+ is the name of the behaviour and +first+ is true if it is the
+ # first behaviour - otherwise it's false.
+ #
+ # The next method to be invoked after this is #example_failed or #example_finished
+ def add_behaviour(name)
+ end
+
+ # This method is invoked when an +example+ starts.
+ def example_started(example)
+ end
+
+ # This method is invoked when an +example+ passes.
+ def example_passed(example)
+ end
+
+ # This method is invoked when an +example+ fails, i.e. an exception occurred
+ # inside it (such as a failed should or other exception). +counter+ is the
+ # sequence number of the failure (starting at 1) and +failure+ is the associated
+ # Failure object.
+ def example_failed(example, counter, failure)
+ end
+
+ # This method is invoked when an example is not yet implemented (i.e. has not
+ # been provided a block), or when an ExamplePendingError is raised.
+ # +name+ is the name of the example.
+ # +message+ is the message from the ExamplePendingError, if it exists, or the
+ # default value of "Not Yet Implemented"
+ def example_pending(behaviour_name, example_name, message)
+ end
+
+ # This method is invoked after all of the examples have executed. The next method
+ # to be invoked after this one is #dump_failure (once for each failed example),
+ def start_dump
+ end
+
+ # Dumps detailed information about an example failure.
+ # This method is invoked for each failed example after all examples have run. +counter+ is the sequence number
+ # of the associated example. +failure+ is a Failure object, which contains detailed
+ # information about the failure.
+ def dump_failure(counter, failure)
+ end
+
+ # This method is invoked after the dumping of examples and failures.
+ def dump_summary(duration, example_count, failure_count, pending_count)
+ end
+
+ # This gets invoked after the summary if option is set to do so.
+ def dump_pending
+ end
+
+ # This method is invoked at the very end. Allows the formatter to clean up, like closing open streams.
+ def close
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/formatter/base_text_formatter.rb b/test/lib/spec/runner/formatter/base_text_formatter.rb
index 31d1c3132..c3cf01b76 100644
--- a/test/lib/spec/runner/formatter/base_text_formatter.rb
+++ b/test/lib/spec/runner/formatter/base_text_formatter.rb
@@ -4,86 +4,96 @@ module Spec
# Baseclass for text-based formatters. Can in fact be used for
# non-text based ones too - just ignore the +output+ constructor
# argument.
- class BaseTextFormatter
- def initialize(output, dry_run=false, colour=false)
- @output = output
- @dry_run = dry_run
- @colour = colour
- begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end
- end
-
- # This method is invoked before any specs are run, right after
- # they have all been collected. This can be useful for special
- # formatters that need to provide progress on feedback (graphical ones)
- #
- # This method will only be invoked once, and the next one to be invoked
- # is #add_context
- def start(spec_count)
- end
-
- # This method is invoked at the beginning of the execution of each context.
- # +name+ is the name of the context and +first+ is true if it is the
- # first context - otherwise it's false.
- #
- # The next method to be invoked after this is #spec_started
- def add_context(name, first)
- end
-
- # This method is invoked right before a spec is executed.
- # The next method to be invoked after this one is one of #spec_failed
- # or #spec_passed.
- def spec_started(name)
- end
-
- # This method is invoked when a spec fails, i.e. an exception occurred
- # inside it (such as a failed should or other exception). +name+ is the name
- # of the specification. +counter+ is the sequence number of the failure
- # (starting at 1) and +failure+ is the associated Failure object.
- def spec_failed(name, counter, failure)
+ class BaseTextFormatter < BaseFormatter
+ attr_writer :dry_run
+
+ # Creates a new instance that will write to +where+. If +where+ is a
+ # String, output will be written to the File with that name, otherwise
+ # +where+ is exected to be an IO (or an object that responds to #puts and #write).
+ def initialize(where)
+ super(where)
+ if where.is_a?(String)
+ @output = File.open(where, 'w')
+ elsif where == STDOUT
+ @output = Kernel
+ def @output.flush
+ STDOUT.flush
+ end
+ else
+ @output = where
+ end
+ @colour = false
+ @dry_run = false
+ @snippet_extractor = SnippetExtractor.new
+ @pending_examples = []
end
-
- # This method is invoked when a spec passes. +name+ is the name of the
- # specification.
- def spec_passed(name)
+
+ def example_pending(behaviour_name, example_name, message)
+ @pending_examples << ["#{behaviour_name} #{example_name}", message]
end
-
- # This method is invoked after all of the specs have executed. The next method
- # to be invoked after this one is #dump_failure (once for each failed spec),
- def start_dump
+
+ def colour=(colour)
+ @colour = colour
+ begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end
end
- # Dumps detailed information about a spec failure.
- # This method is invoked for each failed spec after all specs have run. +counter+ is the sequence number
- # of the associated spec. +failure+ is a Failure object, which contains detailed
- # information about the failure.
def dump_failure(counter, failure)
@output.puts
@output.puts "#{counter.to_s})"
+ @output.puts colourise("#{failure.header}\n#{failure.exception.message}", failure)
+ @output.puts format_backtrace(failure.exception.backtrace)
+ @output.flush
+ end
+
+ def colourise(s, failure)
if(failure.expectation_not_met?)
- @output.puts red(failure.header)
- @output.puts red(failure.exception.message)
+ red(s)
+ elsif(failure.pending_fixed?)
+ blue(s)
else
- @output.puts magenta(failure.header)
- @output.puts magenta(failure.exception.message)
+ magenta(s)
end
- @output.puts format_backtrace(failure.exception.backtrace)
- STDOUT.flush
end
- # This method is invoked at the very end.
- def dump_summary(duration, spec_count, failure_count)
+ def dump_summary(duration, example_count, failure_count, pending_count)
return if @dry_run
@output.puts
@output.puts "Finished in #{duration} seconds"
@output.puts
- summary = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
+
+ summary = "#{example_count} example#{'s' unless example_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
+ summary << ", #{pending_count} pending" if pending_count > 0
+
if failure_count == 0
- @output.puts green(summary)
+ if pending_count > 0
+ @output.puts yellow(summary)
+ else
+ @output.puts green(summary)
+ end
else
@output.puts red(summary)
end
+ @output.flush
+ dump_pending
end
+ def dump_pending
+ unless @pending_examples.empty?
+ @output.puts
+ @output.puts "Pending:"
+ @pending_examples.each do |pending_example|
+ @output.puts "#{pending_example[0]} (#{pending_example[1]})"
+ end
+ end
+ @output.flush
+ end
+
+ def close
+ if IO === @output
+ @output.close
+ end
+ end
+
def format_backtrace(backtrace)
return "" if backtrace.nil?
backtrace.map { |line| backtrace_line(line) }.join("\n")
@@ -108,9 +118,11 @@ module Spec
end
end
- def red(text); colour(text, "\e[31m"); end
def green(text); colour(text, "\e[32m"); end
+ def red(text); colour(text, "\e[31m"); end
def magenta(text); colour(text, "\e[35m"); end
+ def yellow(text); colour(text, "\e[33m"); end
+ def blue(text); colour(text, "\e[34m"); end
end
end
diff --git a/test/lib/spec/runner/formatter/failing_behaviours_formatter.rb b/test/lib/spec/runner/formatter/failing_behaviours_formatter.rb
new file mode 100644
index 000000000..2b3940fd3
--- /dev/null
+++ b/test/lib/spec/runner/formatter/failing_behaviours_formatter.rb
@@ -0,0 +1,29 @@
+module Spec
+ module Runner
+ module Formatter
+ class FailingBehavioursFormatter < BaseTextFormatter
+ def add_behaviour(behaviour_name)
+ if behaviour_name =~ /(.*) \(druby.*\)$/
+ @behaviour_name = $1
+ else
+ @behaviour_name = behaviour_name
+ end
+ end
+
+ def example_failed(example, counter, failure)
+ unless @behaviour_name.nil?
+ @output.puts @behaviour_name
+ @behaviour_name = nil
+ @output.flush
+ end
+ end
+
+ def dump_failure(counter, failure)
+ end
+
+ def dump_summary(duration, example_count, failure_count, pending_count)
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/formatter/failing_examples_formatter.rb b/test/lib/spec/runner/formatter/failing_examples_formatter.rb
new file mode 100644
index 000000000..9728deaf0
--- /dev/null
+++ b/test/lib/spec/runner/formatter/failing_examples_formatter.rb
@@ -0,0 +1,22 @@
+module Spec
+ module Runner
+ module Formatter
+ class FailingExamplesFormatter < BaseTextFormatter
+ def add_behaviour(behaviour_name)
+ @behaviour_name = behaviour_name
+ end
+
+ def example_failed(example, counter, failure)
+ @output.puts "#{@behaviour_name} #{example.description}"
+ @output.flush
+ end
+
+ def dump_failure(counter, failure)
+ end
+
+ def dump_summary(duration, example_count, failure_count, pending_count)
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/formatter/html_formatter.rb b/test/lib/spec/runner/formatter/html_formatter.rb
index 13b796581..d9c422e55 100644
--- a/test/lib/spec/runner/formatter/html_formatter.rb
+++ b/test/lib/spec/runner/formatter/html_formatter.rb
@@ -1,106 +1,126 @@
+require 'erb'
+
module Spec
module Runner
module Formatter
class HtmlFormatter < BaseTextFormatter
- attr_reader :current_spec_number, :current_context_number
+ include ERB::Util # for the #h method
- def initialize(output, dry_run=false, colour=false)
+ def initialize(output)
super
- @current_spec_number = 0
- @current_context_number = 0
+ @current_behaviour_number = 0
+ @current_example_number = 0
end
- def start(spec_count)
- @spec_count = spec_count
+ # The number of the currently running behaviour
+ def current_behaviour_number
+ @current_behaviour_number
+ end
+
+ # The number of the currently running example (a global counter)
+ def current_example_number
+ @current_example_number
+ end
+
+ def start(example_count)
+ @example_count = example_count
- @output.puts HEADER_1
- @output.puts extra_header_content unless extra_header_content.nil?
- @output.puts HEADER_2
- STDOUT.flush
+ @output.puts html_header
+ @output.puts report_header
+ @output.flush
end
- def add_context(name, first)
- @current_context_number += 1
- unless first
+ def add_behaviour(name)
+ @behaviour_red = false
+ @behaviour_red = false
+ @current_behaviour_number += 1
+ unless current_behaviour_number == 1
@output.puts " </dl>"
@output.puts "</div>"
end
- @output.puts "<div class=\"context\">"
+ @output.puts "<div class=\"behaviour\">"
@output.puts " <dl>"
- @output.puts " <dt id=\"context_#{@current_context_number}\">#{name}</dt>"
- STDOUT.flush
+ @output.puts " <dt id=\"behaviour_#{current_behaviour_number}\">#{h(name)}</dt>"
+ @output.flush
end
def start_dump
@output.puts " </dl>"
@output.puts "</div>"
- STDOUT.flush
+ @output.flush
end
- def spec_started(name)
- @current_spec_number += 1
- STDOUT.flush
+ def example_started(example)
+ @current_example_number = example.number
end
- def spec_passed(name)
+ def example_passed(example)
move_progress
- @output.puts " <dd class=\"spec passed\"><span class=\"passed_spec_name\">#{escape(name)}</span></dd>"
- STDOUT.flush
+ @output.puts " <dd class=\"spec passed\"><span class=\"passed_spec_name\">#{h(example.description)}</span></dd>"
+ @output.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>"
+ def example_failed(example, counter, failure)
+ extra = extra_failure_content(failure)
+ failure_style = failure.pending_fixed? ? 'pending_fixed' : 'failed'
+ @output.puts " <script type=\"text/javascript\">makeRed('rspec-header');</script>" unless @header_red
+ @header_red = true
+ @output.puts " <script type=\"text/javascript\">makeRed('behaviour_#{current_behaviour_number}');</script>" unless @behaviour_red
+ @behaviour_red = true
move_progress
- @output.puts " <dd class=\"spec failed\">"
- @output.puts " <span class=\"failed_spec_name\">#{escape(name)}</span>"
+ @output.puts " <dd class=\"spec #{failure_style}\">"
+ @output.puts " <span class=\"failed_spec_name\">#{h(example.description)}</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=\"message\"><pre>#{h(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 extra unless extra == ""
@output.puts " </div>"
@output.puts " </dd>"
- STDOUT.flush
+ @output.flush
end
-
- # Override this method if you wish to output extra HTML in the header
- #
- def extra_header_content
+
+ def example_pending(behaviour_name, example_name, message)
+ @output.puts " <script type=\"text/javascript\">makeYellow('rspec-header');</script>" unless @header_red
+ @output.puts " <script type=\"text/javascript\">makeYellow('behaviour_#{current_behaviour_number}');</script>" unless @behaviour_red
+ move_progress
+ @output.puts " <dd class=\"spec not_implemented\"><span class=\"not_implemented_spec_name\">#{h(example_name)}</span></dd>"
+ @output.flush
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
+ def extra_failure_content(failure)
+ " <pre class=\"ruby\"><code>#{@snippet_extractor.snippet(failure.exception)}</code></pre>"
end
def move_progress
- percent_done = @spec_count == 0 ? 100.0 : (@current_spec_number.to_f / @spec_count.to_f * 1000).to_i / 10.0
+ percent_done = @example_count == 0 ? 100.0 : ((current_example_number + 1).to_f / @example_count.to_f * 1000).to_i / 10.0
@output.puts " <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;')
+ @output.flush
end
def dump_failure(counter, failure)
end
- def dump_summary(duration, spec_count, failure_count)
+ def dump_summary(duration, example_count, failure_count, pending_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}"
+ totals = "#{example_count} example#{'s' unless example_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
+ totals << ", #{pending_count} pending" if pending_count > 0
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 "</div>"
@output.puts "</body>"
@output.puts "</html>"
- STDOUT.flush
+ @output.flush
end
- HEADER_1 = <<-EOF
+ def html_header
+ <<-EOF
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
@@ -112,107 +132,191 @@ module Spec
<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" />
+ <style type="text/css">
+ body {
+ margin: 0;
+ padding: 0;
+ background: #fff;
+ font-size: 80%;
+ }
+ </style>
+</head>
+<body>
EOF
+ end
- HEADER_2 = <<-EOF
+ def report_header
+ <<-EOF
+<div class="rspec-report">
<script type="text/javascript">
- function moveProgressBar(percentDone) {
- document.getElementById("header").style.width = percentDone +"%";
- }
- function makeRed(element_id) {
- document.getElementById(element_id).style.background = '#C40D0D';
- }
+ // <![CDATA[
+#{global_scripts}
+ // ]]>
</script>
<style type="text/css">
- body {
- margin: 0; padding: 0;
- background: #fff;
- }
+#{global_styles}
+ </style>
- #header {
- background: #65C400; color: #fff;
- }
+<div id="rspec-header">
+ <h1>RSpec Results</h1>
- h1 {
- margin: 0 0 10px;
- padding: 10px;
- font: bold 18px "Lucida Grande", Helvetica, sans-serif;
- }
+ <div id="summary">
+ <p id="totals">&nbsp;</p>
+ <p id="duration">&nbsp;</p>
+ </div>
+</div>
- #summary {
- margin: 0; padding: 5px 10px;
- font: bold 10px "Lucida Grande", Helvetica, sans-serif;
- text-align: right;
- position: absolute;
- top: 0px;
- right: 0px;
- }
+<div class="results">
+EOF
+ end
- #summary p {
- margin: 0 0 2px;
- }
+ def global_scripts
+ <<-EOF
+function moveProgressBar(percentDone) {
+ document.getElementById("rspec-header").style.width = percentDone +"%";
+}
+function makeRed(element_id) {
+ document.getElementById(element_id).style.background = '#C40D0D';
+ document.getElementById(element_id).style.color = '#FFFFFF';
+}
- #summary #totals {
- font-size: 14px;
+function makeYellow(element_id) {
+ if (element_id == "rspec-header" && document.getElementById(element_id).style.background != '#C40D0D')
+ {
+ document.getElementById(element_id).style.background = '#FAF834';
+ document.getElementById(element_id).style.color = '#000000';
}
-
- .context {
- margin: 0 10px 5px;
- background: #fff;
+ else
+ {
+ document.getElementById(element_id).style.background = '#FAF834';
+ document.getElementById(element_id).style.color = '#000000';
}
+}
+EOF
+ end
+
+ def global_styles
+ <<-EOF
+#rspec-header {
+ background: #65C400; color: #fff;
+}
- dl {
- margin: 0; padding: 0 0 5px;
- font: normal 11px "Lucida Grande", Helvetica, sans-serif;
- }
+.rspec-report h1 {
+ margin: 0px 10px 0px 10px;
+ padding: 10px;
+ font-family: "Lucida Grande", Helvetica, sans-serif;
+ font-size: 1.8em;
+}
- dt {
- padding: 3px;
- background: #65C400;
- color: #fff;
- font-weight: bold;
- }
+#summary {
+ margin: 0; padding: 5px 10px;
+ font-family: "Lucida Grande", Helvetica, sans-serif;
+ text-align: right;
+ position: absolute;
+ top: 0px;
+ right: 0px;
+}
- dd {
- margin: 5px 0 5px 5px;
- padding: 3px 3px 3px 18px;
- }
+#summary p {
+ margin: 0 0 0 2px;
+}
- dd.spec.passed {
- border-left: 5px solid #65C400;
- border-bottom: 1px solid #65C400;
- background: #DBFFB4; color: #3D7700;
- }
+#summary #totals {
+ font-size: 1.2em;
+}
- dd.spec.failed {
- border-left: 5px solid #C20000;
- border-bottom: 1px solid #C20000;
- color: #C20000; background: #FFFBD3;
- }
+.behaviour {
+ margin: 0 10px 5px;
+ background: #fff;
+}
- div.backtrace {
- color: #000;
- font-size: 12px;
- }
+dl {
+ margin: 0; padding: 0 0 5px;
+ font: normal 11px "Lucida Grande", Helvetica, sans-serif;
+}
- a {
- color: #BE5C00;
- }
- </style>
-</head>
-<body>
+dt {
+ padding: 3px;
+ background: #65C400;
+ color: #fff;
+ font-weight: bold;
+}
-<div id="header">
- <h1>RSpec Results</h1>
+dd {
+ margin: 5px 0 5px 5px;
+ padding: 3px 3px 3px 18px;
+}
- <div id="summary">
- <p id="duration">&nbsp;</p>
- <p id="totals">&nbsp;</p>
- </div>
-</div>
+dd.spec.passed {
+ border-left: 5px solid #65C400;
+ border-bottom: 1px solid #65C400;
+ background: #DBFFB4; color: #3D7700;
+}
-<div id="results">
+dd.spec.failed {
+ border-left: 5px solid #C20000;
+ border-bottom: 1px solid #C20000;
+ color: #C20000; background: #FFFBD3;
+}
+
+dd.spec.not_implemented {
+ border-left: 5px solid #FAF834;
+ border-bottom: 1px solid #FAF834;
+ background: #FCFB98; color: #131313;
+}
+
+dd.spec.pending_fixed {
+ border-left: 5px solid #0000C2;
+ border-bottom: 1px solid #0000C2;
+ color: #0000C2; background: #D3FBFF;
+}
+
+.backtrace {
+ color: #000;
+ font-size: 12px;
+}
+
+a {
+ color: #BE5C00;
+}
+
+/* Ruby code, style similar to vibrant ink */
+.ruby {
+ font-size: 12px;
+ font-family: monospace;
+ color: white;
+ background-color: black;
+ padding: 0.1em 0 0.2em 0;
+}
+
+.ruby .keyword { color: #FF6600; }
+.ruby .constant { color: #339999; }
+.ruby .attribute { color: white; }
+.ruby .global { color: white; }
+.ruby .module { color: white; }
+.ruby .class { color: white; }
+.ruby .string { color: #66FF00; }
+.ruby .ident { color: white; }
+.ruby .method { color: #FFCC00; }
+.ruby .number { color: white; }
+.ruby .char { color: white; }
+.ruby .comment { color: #9933CC; }
+.ruby .symbol { color: white; }
+.ruby .regex { color: #44B4CC; }
+.ruby .punct { color: white; }
+.ruby .escape { color: white; }
+.ruby .interp { color: white; }
+.ruby .expr { color: white; }
+
+.ruby .offending { background-color: gray; }
+.ruby .linenum {
+ width: 75px;
+ padding: 0.1em 1em 0.2em 0;
+ color: #000000;
+ background-color: #FFFBD3;
+}
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
index fe519d4d8..624f06e7c 100644
--- a/test/lib/spec/runner/formatter/progress_bar_formatter.rb
+++ b/test/lib/spec/runner/formatter/progress_bar_formatter.rb
@@ -2,26 +2,30 @@ module Spec
module Runner
module Formatter
class ProgressBarFormatter < BaseTextFormatter
- def add_context(name, first)
- @output.puts if first
- STDOUT.flush
+ def add_behaviour(name)
end
- def spec_failed(name, counter, failure)
- @output.print failure.expectation_not_met? ? red('F') : magenta('F')
- STDOUT.flush
+ def example_failed(example, counter, failure)
+ @output.print colourise('F', failure)
+ @output.flush
end
- def spec_passed(name)
+ def example_passed(example)
@output.print green('.')
- STDOUT.flush
+ @output.flush
end
+ def example_pending(behaviour_name, example_name, message)
+ super
+ @output.print yellow('P')
+ @output.flush
+ end
+
def start_dump
@output.puts
- STDOUT.flush
+ @output.flush
end
end
end
end
-end \ No newline at end of file
+end
diff --git a/test/lib/spec/runner/formatter/rdoc_formatter.rb b/test/lib/spec/runner/formatter/rdoc_formatter.rb
index eae55c3ea..0fd22ba6c 100644
--- a/test/lib/spec/runner/formatter/rdoc_formatter.rb
+++ b/test/lib/spec/runner/formatter/rdoc_formatter.rb
@@ -2,21 +2,23 @@ module Spec
module Runner
module Formatter
class RdocFormatter < BaseTextFormatter
- def add_context(name, first)
- @output.print "# #{name}\n"
- STDOUT.flush
+ def add_behaviour(name)
+ @output.puts "# #{name}"
end
- def spec_passed(name)
- @output.print "# * #{name}\n"
- STDOUT.flush
+ def example_passed(example)
+ @output.puts "# * #{example.description}"
+ @output.flush
end
- def spec_failed(name, counter, failure)
- @output.print "# * #{name} [#{counter} - FAILED]\n"
- STDOUT.flush
+ def example_failed(example, counter, failure)
+ @output.puts "# * #{example.description} [#{counter} - FAILED]"
+ end
+
+ def example_pending(behaviour_name, example_name, message)
+ @output.puts "# * #{behaviour_name} #{example_name} [PENDING: #{message}]"
end
end
end
end
-end \ No newline at end of file
+end
diff --git a/test/lib/spec/runner/formatter/snippet_extractor.rb b/test/lib/spec/runner/formatter/snippet_extractor.rb
new file mode 100644
index 000000000..41119fe46
--- /dev/null
+++ b/test/lib/spec/runner/formatter/snippet_extractor.rb
@@ -0,0 +1,52 @@
+module Spec
+ module Runner
+ module Formatter
+ # This class extracts code snippets by looking at the backtrace of the passed error
+ class SnippetExtractor #:nodoc:
+ class NullConverter; def convert(code, pre); code; end; end #:nodoc:
+ begin; require 'rubygems'; require 'syntax/convertors/html'; @@converter = Syntax::Convertors::HTML.for_syntax "ruby"; rescue LoadError => e; @@converter = NullConverter.new; end
+
+ def snippet(error)
+ raw_code, line = snippet_for(error.backtrace[0])
+ highlighted = @@converter.convert(raw_code, false)
+ highlighted << "\n<span class=\"comment\"># gem install syntax to get syntax highlighting</span>" if @@converter.is_a?(NullConverter)
+ post_process(highlighted, line)
+ end
+
+ def snippet_for(error_line)
+ if error_line =~ /(.*):(\d+)/
+ file = $1
+ line = $2.to_i
+ [lines_around(file, line), line]
+ else
+ ["# Couldn't get snippet for #{error_line}", 1]
+ end
+ end
+
+ def lines_around(file, line)
+ if File.file?(file)
+ lines = File.open(file).read.split("\n")
+ min = [0, line-3].max
+ max = [line+1, lines.length-1].min
+ selected_lines = []
+ selected_lines.join("\n")
+ lines[min..max].join("\n")
+ else
+ "# Couldn't get snippet for #{file}"
+ end
+ end
+
+ def post_process(highlighted, offending_line)
+ new_lines = []
+ highlighted.split("\n").each_with_index do |line, i|
+ new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
+ new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
+ new_lines << new_line
+ end
+ new_lines.join("\n")
+ end
+
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/formatter/specdoc_formatter.rb b/test/lib/spec/runner/formatter/specdoc_formatter.rb
index 67b4312bf..ad794b238 100644
--- a/test/lib/spec/runner/formatter/specdoc_formatter.rb
+++ b/test/lib/spec/runner/formatter/specdoc_formatter.rb
@@ -2,22 +2,28 @@ module Spec
module Runner
module Formatter
class SpecdocFormatter < BaseTextFormatter
- def add_context(name, first)
+ def add_behaviour(name)
@output.puts
@output.puts name
- STDOUT.flush
+ @output.flush
end
- def spec_failed(name, counter, failure)
- @output.puts failure.expectation_not_met? ? red("- #{name} (FAILED - #{counter})") : magenta("- #{name} (ERROR - #{counter})")
- STDOUT.flush
+ def example_failed(example, counter, failure)
+ @output.puts failure.expectation_not_met? ? red("- #{example.description} (FAILED - #{counter})") : magenta("- #{example.description} (ERROR - #{counter})")
+ @output.flush
end
- def spec_passed(name)
- @output.print green("- #{name}\n")
- STDOUT.flush
+ def example_passed(example)
+ @output.puts green("- #{example.description}")
+ @output.flush
+ end
+
+ def example_pending(behaviour_name, example_name, message)
+ super
+ @output.puts yellow("- #{example_name} (PENDING: #{message})")
+ @output.flush
end
end
end
end
-end \ No newline at end of file
+end
diff --git a/test/lib/spec/runner/heckle_runner.rb b/test/lib/spec/runner/heckle_runner.rb
index fd36389de..b6de4ef73 100644
--- a/test/lib/spec/runner/heckle_runner.rb
+++ b/test/lib/spec/runner/heckle_runner.rb
@@ -13,9 +13,9 @@ module Spec
@heckle_class = heckle_class
end
- # Runs all the contexts held by +context_runner+ once for each of the
+ # Runs all the contexts held by +behaviour_runner+ once for each of the
# methods in the matched classes.
- def heckle_with(context_runner)
+ def heckle_with(behaviour_runner)
if @filter =~ /(.*)[#\.](.*)/
heckle_method($1, $2)
else
@@ -25,7 +25,7 @@ module Spec
def heckle_method(class_name, method_name)
verify_constant(class_name)
- heckle = @heckle_class.new(class_name, method_name, context_runner)
+ heckle = @heckle_class.new(class_name, method_name, behaviour_runner)
heckle.validate
end
@@ -39,7 +39,7 @@ module Spec
classes.each do |klass|
klass.instance_methods(false).each do |method_name|
- heckle = @heckle_class.new(klass.name, method_name, context_runner)
+ heckle = @heckle_class.new(klass.name, method_name, behaviour_runner)
heckle.validate
end
end
@@ -57,13 +57,14 @@ module Spec
#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)
+ def initialize(klass_name, method_name, behaviour_runner)
super(klass_name, method_name)
- @context_runner = context_runner
+ @behaviour_runner = behaviour_runner
end
def tests_pass?
- failure_count = @context_runner.run(false)
+ paths = [] # We can pass an empty array of paths - our specs are already loaded.
+ failure_count = @behaviour_runner.run(paths, false)
failure_count == 0
end
end
diff --git a/test/lib/spec/runner/heckle_runner_unsupported.rb b/test/lib/spec/runner/heckle_runner_unsupported.rb
new file mode 100644
index 000000000..02aa37953
--- /dev/null
+++ b/test/lib/spec/runner/heckle_runner_unsupported.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
index 38725d848..1facb85a8 100644
--- a/test/lib/spec/runner/option_parser.rb
+++ b/test/lib/spec/runner/option_parser.rb
@@ -1,186 +1,156 @@
-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'
+require 'stringio'
module Spec
module Runner
class OptionParser
+ BUILT_IN_FORMATTERS = {
+ 'specdoc' => Formatter::SpecdocFormatter,
+ 's' => Formatter::SpecdocFormatter,
+ 'html' => Formatter::HtmlFormatter,
+ 'h' => Formatter::HtmlFormatter,
+ 'rdoc' => Formatter::RdocFormatter,
+ 'r' => Formatter::RdocFormatter,
+ 'progress' => Formatter::ProgressBarFormatter,
+ 'p' => Formatter::ProgressBarFormatter,
+ 'failing_examples' => Formatter::FailingExamplesFormatter,
+ 'e' => Formatter::FailingExamplesFormatter,
+ 'failing_behaviours' => Formatter::FailingBehavioursFormatter,
+ 'b' => Formatter::FailingBehavioursFormatter
+ }
+
+ COMMAND_LINE = {
+ :diff => ["-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)"],
+ :colour => ["-c", "--colour", "--color", "Show coloured (red/green) output"],
+ :example => ["-e", "--example [NAME|FILE_NAME]", "Execute example(s) with matching name(s). If the argument is",
+ "the path to an existing file (typically generated by a previous",
+ "run using --format failing_examples:file.txt), then the examples",
+ "on each line of thatfile will be executed. If the file is empty,",
+ "all examples will be run (as if --example was not specified).",
+ " ",
+ "If the argument is not an existing file, then it is treated as",
+ "an example name directly, causing RSpec to run just the example",
+ "matching that name"],
+ :specification => ["-s", "--specification [NAME]", "DEPRECATED - use -e instead", "(This will be removed when autotest works with -e)"],
+ :line => ["-l", "--line LINE_NUMBER", Integer, "Execute behaviout or specification at given line.",
+ "(does not work for dynamically generated specs)"],
+ :format => ["-f", "--format FORMAT[:WHERE]", "Specifies what format to use for output. Specify WHERE to tell",
+ "the formatter where to write the output. All built-in formats",
+ "expect WHERE to be a file name, and will write to STDOUT if it's",
+ "not specified. The --format option may be specified several times",
+ "if you want several outputs",
+ " ",
+ "Builtin formats: ",
+ "progress|p : Text progress",
+ "specdoc|s : Behaviour doc as text",
+ "rdoc|r : Behaviour doc as RDoc",
+ "html|h : A nice HTML report",
+ "failing_examples|e : Write all failing examples - input for --example",
+ "failing_behaviours|b : Write all failing behaviours - input for --example",
+ " ",
+ "FORMAT can also be the name of a custom formatter class",
+ "(in which case you should also specify --require to load it)"],
+ :require => ["-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"],
+ :backtrace => ["-b", "--backtrace", "Output full backtrace"],
+ :loadby => ["-L", "--loadby STRATEGY", "Specify the strategy by which spec files should be loaded.",
+ "STRATEGY can currently only be 'mtime' (File modification time)",
+ "By default, spec files are loaded in alphabetical order if --loadby",
+ "is not specified."],
+ :reverse => ["-R", "--reverse", "Run examples in reverse order"],
+ :timeout => ["-t", "--timeout FLOAT", "Interrupt and fail each example that doesn't complete in the",
+ "specified time"],
+ :heckle => ["-H", "--heckle CODE", "If all examples pass, this will mutate the classes and methods",
+ "identified by CODE little by little and run all the examples again",
+ "for each mutation. The intent is that for each mutation, at least",
+ "one example *should* fail, and RSpec will tell you if this is not the",
+ "case. CODE should be either Some::Module, Some::Class or",
+ "Some::Fabulous#method}"],
+ :dry_run => ["-d", "--dry-run", "Invokes formatters without executing the examples."],
+ :options_file => ["-O", "--options PATH", "Read options from a file"],
+ :generate_options => ["-G", "--generate-options PATH", "Generate an options file for --options"],
+ :runner => ["-U", "--runner RUNNER", "Use a custom BehaviourRunner."],
+ :drb => ["-X", "--drb", "Run examples via DRb. (For example against script/spec_server)"],
+ :version => ["-v", "--version", "Show version"],
+ :help => ["-h", "--help", "You're looking at it"]
+ }
+
def initialize
@spec_parser = SpecParser.new
@file_factory = File
end
- def create_context_runner(args, err, out, warn_if_no_files)
+ def create_behaviour_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
+ return nil unless options.is_a?(Options)
+ options.configure
+ options.behaviour_runner
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
+ options = Options.new(err, out)
opts = ::OptionParser.new do |opts|
- opts.banner = "Usage: spec [options] (FILE|DIRECTORY|GLOB)+"
+ opts.banner = "Usage: spec (FILE|DIRECTORY|GLOB)+ [options]"
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|
+ def opts.rspec_on(name, &block)
+ on(*COMMAND_LINE[name], &block)
+ end
- # TODO make context_lines settable
- options.context_lines = 3
+ opts.rspec_on(:diff) {|diff| options.parse_diff(diff)}
- case format
- when 'context', 'c'
- options.diff_format = :context
- when 'unified', 'u', '', nil
- options.diff_format = :unified
- end
+ opts.rspec_on(:colour) {options.colour = true}
- 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
+ opts.rspec_on(:example) {|example| options.parse_example(example)}
- 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.rspec_on(:specification) {|example| options.parse_example(example)}
- 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.rspec_on(:line) {|line_number| options.line_number = line_number.to_i}
- 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.rspec_on(:format) {|format| options.parse_format(format)}
- 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.rspec_on(:require) {|req| options.parse_require(req)}
- 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)
+ opts.rspec_on(:backtrace) {options.backtrace_tweaker = NoisyBacktraceTweaker.new}
- 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.rspec_on(:loadby) {|loadby| options.loadby = loadby}
+
+ opts.rspec_on(:reverse) {options.reverse = true}
+
+ opts.rspec_on(:timeout) {|timeout| options.timeout = timeout.to_f}
+
+ opts.rspec_on(:heckle) {|heckle| options.parse_heckle(heckle)}
- 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)
+ opts.rspec_on(:dry_run) {options.dry_run = true}
- return DrbCommandLine.run(args_copy, err, out, true, warn_if_no_files)
+ opts.rspec_on(:options_file) do |options_file|
+ return parse_options_file(options_file, out, err, args_copy, warn_if_no_files)
end
- opts.on("-v", "--version", "Show version") do
- out.puts ::Spec::VERSION::DESCRIPTION
- exit if out == $stdout
+ opts.rspec_on(:generate_options) do |options_file|
+ options.parse_generate_options(options_file, args_copy, out)
end
- opts.on_tail("-h", "--help", "You're looking at it") do
- out.puts opts
- exit if out == $stdout
+ opts.rspec_on(:runner) do |runner|
+ options.runner_arg = runner
end
-
+
+ opts.rspec_on(:drb) do
+ return parse_drb(args_copy, out, err, warn_if_no_files)
+ end
+
+ opts.rspec_on(:version) {parse_version(out)}
+
+ opts.on_tail(*COMMAND_LINE[:help]) {parse_help(opts, out)}
end
opts.parse!(args)
@@ -194,15 +164,48 @@ module Spec
set_spec_from_line_number(options, args, err)
end
+ if options.formatters.empty?
+ options.formatters << Formatter::ProgressBarFormatter.new(out)
+ end
+
options
end
-
+
+ def parse_options_file(options_file, out_stream, error_stream, args_copy, warn_if_no_files)
+ # 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).map {|l| l.chomp.split " "}.flatten
+ return CommandLine.run(new_args, error_stream, out_stream, true, warn_if_no_files)
+ end
+
+ def parse_drb(args_copy, out_stream, error_stream, warn_if_no_files)
+ # Remove the --drb option
+ index = args_copy.index("-X") || args_copy.index("--drb")
+ args_copy.delete_at(index)
+
+ return DrbCommandLine.run(args_copy, error_stream, out_stream, true, warn_if_no_files)
+ end
+
+ def parse_version(out_stream)
+ out_stream.puts ::Spec::VERSION::DESCRIPTION
+ exit if out_stream == $stdout
+ end
+
+ def parse_help(opts, out_stream)
+ out_stream.puts opts
+ exit if out_stream == $stdout
+ end
+
def set_spec_from_line_number(options, args, err)
- unless options.spec_name
+ if options.examples.empty?
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)
+ example = @spec_parser.spec_name_for(source, options.line_number)
+ options.parse_example(example)
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
@@ -215,7 +218,7 @@ module Spec
exit(3) if err == $stderr
end
else
- err.puts "You cannot use both --line and --spec"
+ err.puts "You cannot use both --line and --example"
exit(4) if err == $stderr
end
end
diff --git a/test/lib/spec/runner/options.rb b/test/lib/spec/runner/options.rb
new file mode 100644
index 000000000..a940133eb
--- /dev/null
+++ b/test/lib/spec/runner/options.rb
@@ -0,0 +1,175 @@
+module Spec
+ module Runner
+ class Options
+ BUILT_IN_FORMATTERS = {
+ 'specdoc' => Formatter::SpecdocFormatter,
+ 's' => Formatter::SpecdocFormatter,
+ 'html' => Formatter::HtmlFormatter,
+ 'h' => Formatter::HtmlFormatter,
+ 'rdoc' => Formatter::RdocFormatter,
+ 'r' => Formatter::RdocFormatter,
+ 'progress' => Formatter::ProgressBarFormatter,
+ 'p' => Formatter::ProgressBarFormatter,
+ 'failing_examples' => Formatter::FailingExamplesFormatter,
+ 'e' => Formatter::FailingExamplesFormatter,
+ 'failing_behaviours' => Formatter::FailingBehavioursFormatter,
+ 'b' => Formatter::FailingBehavioursFormatter
+ }
+
+ attr_accessor(
+ :backtrace_tweaker,
+ :colour,
+ :context_lines,
+ :diff_format,
+ :differ_class,
+ :dry_run,
+ :examples,
+ :failure_file,
+ :formatters,
+ :generate,
+ :heckle_runner,
+ :line_number,
+ :loadby,
+ :reporter,
+ :reverse,
+ :timeout,
+ :verbose,
+ :runner_arg,
+ :behaviour_runner
+ )
+
+ def initialize(err, out)
+ @err, @out = err, out
+ @backtrace_tweaker = QuietBacktraceTweaker.new
+ @examples = []
+ @formatters = []
+ @colour = false
+ @dry_run = false
+ end
+
+ def configure
+ configure_formatters
+ create_reporter
+ configure_differ
+ create_behaviour_runner
+ end
+
+ def create_behaviour_runner
+ return nil if @generate
+ @behaviour_runner = if @runner_arg
+ klass_name, arg = split_at_colon(@runner_arg)
+ runner_type = load_class(klass_name, 'behaviour runner', '--runner')
+ runner_type.new(self, arg)
+ else
+ BehaviourRunner.new(self)
+ end
+ end
+
+ def configure_formatters
+ @formatters.each do |formatter|
+ formatter.colour = @colour if formatter.respond_to?(:colour=)
+ formatter.dry_run = @dry_run if formatter.respond_to?(:dry_run=)
+ end
+ end
+
+ def create_reporter
+ @reporter = Reporter.new(@formatters, @backtrace_tweaker)
+ end
+
+ def configure_differ
+ if @differ_class
+ Spec::Expectations.differ = @differ_class.new(@diff_format, @context_lines, @colour)
+ end
+ end
+
+ def parse_diff(format)
+ @context_lines = 3
+ case format
+ when :context, 'context', 'c'
+ @diff_format = :context
+ when :unified, 'unified', 'u', '', nil
+ @diff_format = :unified
+ end
+
+ if [:context,:unified].include? @diff_format
+ require 'spec/expectations/differs/default'
+ @differ_class = Spec::Expectations::Differs::Default
+ else
+ @diff_format = :custom
+ @differ_class = load_class(format, 'differ', '--diff')
+ end
+ end
+
+ def parse_example(example)
+ if(File.file?(example))
+ @examples = File.open(example).read.split("\n")
+ else
+ @examples = [example]
+ end
+ end
+
+ def parse_format(format_arg)
+ format, where = split_at_colon(format_arg)
+ # This funky regexp checks whether we have a FILE_NAME or not
+ if where.nil?
+ raise "When using several --format options only one of them can be without a file" if @out_used
+ where = @out
+ @out_used = true
+ end
+
+ formatter_type = BUILT_IN_FORMATTERS[format] || load_class(format, 'formatter', '--format')
+ @formatters << formatter_type.new(where)
+ end
+
+ def parse_require(req)
+ req.split(",").each{|file| require file}
+ end
+
+ def parse_heckle(heckle)
+ heckle_require = [/mswin/, /java/].detect{|p| p =~ RUBY_PLATFORM} ? 'spec/runner/heckle_runner_unsupported' : 'spec/runner/heckle_runner'
+ require heckle_require
+ @heckle_runner = HeckleRunner.new(heckle)
+ end
+
+ def parse_generate_options(options_file, args_copy, out_stream)
+ # 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_stream.puts "\nOptions written to #{options_file}. You can now use these options with:"
+ out_stream.puts "spec --options #{options_file}"
+ @generate = true
+ end
+
+ def split_at_colon(s)
+ if s =~ /([a-zA-Z_]+(?:::[a-zA-Z_]+)*):?(.*)/
+ arg = $2 == "" ? nil : $2
+ [$1, arg]
+ else
+ raise "Couldn't parse #{s.inspect}"
+ end
+ end
+
+ def load_class(name, kind, option)
+ if name =~ /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/
+ arg = $2 == "" ? nil : $2
+ [$1, arg]
+ else
+ m = "#{name.inspect} is not a valid class name"
+ @err.puts m
+ raise m
+ end
+ begin
+ eval(name, binding, __FILE__, __LINE__)
+ rescue NameError => e
+ @err.puts "Couldn't find #{kind} class #{name}"
+ @err.puts "Make sure the --require option is specified *before* #{option}"
+ if $_spec_spec ; raise e ; else exit(1) ; end
+ end
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/reporter.rb b/test/lib/spec/runner/reporter.rb
index e4fb1cb0e..b1dc2a27a 100644
--- a/test/lib/spec/runner/reporter.rb
+++ b/test/lib/spec/runner/reporter.rb
@@ -2,36 +2,39 @@ module Spec
module Runner
class Reporter
- def initialize(formatter, backtrace_tweaker)
- @formatter = formatter
+ def initialize(formatters, backtrace_tweaker)
+ @formatters = formatters
@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
+ def add_behaviour(name)
+ @formatters.each{|f| f.add_behaviour(name)}
+ @behaviour_names << name
end
- def spec_started(name)
- @spec_names << name
- @formatter.spec_started(name)
+ def example_started(name)
+ @formatters.each{|f| f.example_started(name)}
end
- def spec_finished(name, error=nil, failure_location=nil)
- if error.nil?
- spec_passed(name)
+ def example_finished(name, error=nil, failure_location=nil, not_implemented = false)
+ @example_names << name
+
+ if not_implemented
+ example_pending(@behaviour_names.last, name)
+ elsif error.nil?
+ example_passed(name)
+ elsif Spec::DSL::ExamplePendingError === error
+ example_pending(@behaviour_names.last, name, error.message)
else
- @backtrace_tweaker.tweak_backtrace(error, failure_location)
- spec_failed(name, Failure.new(@context_names.last, name, error))
+ example_failed(name, error, failure_location)
end
end
- def start(number_of_specs)
+ def start(number_of_examples)
clear!
@start_time = Time.new
- @formatter.start(number_of_specs)
+ @formatters.each{|f| f.start(number_of_examples)}
end
def end
@@ -40,18 +43,22 @@ module Spec
# Dumps the summary and returns the total number of failures
def dump
- @formatter.start_dump
+ @formatters.each{|f| f.start_dump}
dump_failures
- @formatter.dump_summary(duration, @spec_names.length, @failures.length)
+ @formatters.each do |f|
+ f.dump_summary(duration, @example_names.length, @failures.length, @pending_count)
+ f.close
+ end
@failures.length
end
- private
+ private
def clear!
- @context_names = []
+ @behaviour_names = []
@failures = []
- @spec_names = []
+ @pending_count = 0
+ @example_names = []
@start_time = nil
@end_time = nil
end
@@ -59,7 +66,7 @@ module Spec
def dump_failures
return if @failures.empty?
@failures.inject(1) do |index, failure|
- @formatter.dump_failure(index, failure)
+ @formatters.each{|f| f.dump_failure(index, failure)}
index + 1
end
end
@@ -68,33 +75,46 @@ module Spec
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)
+
+ def example_passed(name)
+ @formatters.each{|f| f.example_passed(name)}
end
- def spec_failed(name, failure)
+ def example_failed(name, error, failure_location)
+ @backtrace_tweaker.tweak_backtrace(error, failure_location)
+ example_name = "#{@behaviour_names.last} #{name}"
+ failure = Failure.new(example_name, error)
@failures << failure
- @formatter.spec_failed(name, @failures.length, failure)
+ @formatters.each{|f| f.example_failed(name, @failures.length, failure)}
end
-
+
+ def example_pending(behaviour_name, example_name, message="Not Yet Implemented")
+ @pending_count += 1
+ @formatters.each{|f| f.example_pending(behaviour_name, example_name, message)}
+ end
+
class Failure
attr_reader :exception
- def initialize(context_name, spec_name, exception)
- @context_name = context_name
- @spec_name = spec_name
+ def initialize(example_name, exception)
+ @example_name = example_name
@exception = exception
end
def header
if expectation_not_met?
- "'#{@context_name} #{@spec_name}' FAILED"
+ "'#{@example_name}' FAILED"
+ elsif pending_fixed?
+ "'#{@example_name}' FIXED"
else
- "#{@exception.class.name} in '#{@context_name} #{@spec_name}'"
+ "#{@exception.class.name} in '#{@example_name}'"
end
end
+ def pending_fixed?
+ @exception.is_a?(Spec::DSL::PendingFixedError)
+ end
+
def expectation_not_met?
@exception.is_a?(Spec::Expectations::ExpectationNotMetError)
end
diff --git a/test/lib/spec/runner/spec_parser.rb b/test/lib/spec/runner/spec_parser.rb
index 2cb8518fc..bc9170065 100644
--- a/test/lib/spec/runner/spec_parser.rb
+++ b/test/lib/spec/runner/spec_parser.rb
@@ -1,15 +1,15 @@
module Spec
module Runner
- # Parses a spec file and finds the nearest spec for a given line number.
+ # Parses a spec file and finds the nearest example 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
+ behaviour, behaviour_line = behaviour_at_line(source, line_number)
+ example, example_line = example_at_line(source, line_number)
+ if behaviour && example && (behaviour_line < example_line)
+ "#{behaviour} #{example}"
+ elsif behaviour
+ behaviour
else
nil
end
@@ -17,25 +17,34 @@ module Spec
protected
- def context_at_line(source, line_number)
- find_above(source, line_number, /^\s*context\s+['|"](.*)['|"]/)
+ def behaviour_at_line(source, line_number)
+ find_above(source, line_number, /^\s*(context|describe)\s+(.*)\s+do/)
end
- def spec_at_line(source, line_number)
- find_above(source, line_number, /^\s*specify\s+['|"](.*)['|"]/)
+ def example_at_line(source, line_number)
+ find_above(source, line_number, /^\s*(specify|it)\s+(.*)\s+do/)
end
+ # Returns the context/describe or specify/it name and the line number
def find_above(source, line_number, pattern)
- lines_above_reversed(source, line_number).each do |line|
- return $1 if line =~ pattern
+ lines_above_reversed(source, line_number).each_with_index do |line, n|
+ return [parse_description($2), line_number-n] if line =~ pattern
end
nil
end
def lines_above_reversed(source, line_number)
lines = source.split("\n")
- lines[0...line_number].reverse
+ lines[0...line_number].reverse
+ end
+
+ def parse_description(str)
+ return str[1..-2] if str =~ /^['"].*['"]$/
+ if matches = /^(.*)\s*,\s*['"](.*)['"]$/.match(str)
+ return ::Spec::DSL::Description.generate_description(matches[1], matches[2])
+ end
+ return str
end
end
end
-end \ No newline at end of file
+end