Added Rspec and Webrat plugins and started porting Selenium on Rails tests to Rspec Plain Text Stories driving Webrat driving Selenium.

This commit is contained in:
Luke Melia 2008-06-18 02:57:57 -04:00
parent 0600756bbf
commit 0f7d6f7a1d
602 changed files with 47788 additions and 29 deletions

View file

@ -0,0 +1,57 @@
module Spec
module Runner
class BacktraceTweaker
def clean_up_double_slashes(line)
line.gsub!('//','/')
end
end
class NoisyBacktraceTweaker < BacktraceTweaker
def tweak_backtrace(error)
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/*"].map do |path|
subpath = path[root_dir.length..-1]
/#{subpath}/
end
IGNORE_PATTERNS = spec_files + [
/\/lib\/ruby\//,
/bin\/spec:/,
/bin\/rcov:/,
/lib\/rspec-rails/,
/vendor\/rails/,
# TextMate's Ruby and RSpec plugins
/Ruby\.tmbundle\/Support\/tmruby.rb:/,
/RSpec\.tmbundle\/Support\/lib/,
/temp_textmate\./,
/mock_frameworks\/rspec/,
/spec_server/
]
end
def tweak_backtrace(error)
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

View file

@ -0,0 +1,16 @@
module Spec
module Runner
class ClassAndArgumentsParser
class << self
def parse(s)
if s =~ /([a-zA-Z_]+(?:::[a-zA-Z_]+)*):?(.*)/
arg = $2 == "" ? nil : $2
[$1, arg]
else
raise "Couldn't parse #{s.inspect}"
end
end
end
end
end
end

View file

@ -0,0 +1,28 @@
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
class << self
# Runs specs. +argv+ is the commandline args as per the spec commandline API, +err+
# and +out+ are the streams output will be written to.
def run(instance_rspec_options)
# NOTE - this call to init_rspec_options is not spec'd, but neither is any of this
# swapping of $rspec_options. That is all here to enable rspec to run against itself
# and maintain coverage in a single process. Therefore, DO NOT mess with this stuff
# unless you know what you are doing!
init_rspec_options(instance_rspec_options)
orig_rspec_options = rspec_options
begin
$rspec_options = instance_rspec_options
return $rspec_options.run_examples
ensure
::Spec.run = true
$rspec_options = orig_rspec_options
end
end
end
end
end
end

View file

@ -0,0 +1,20 @@
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(options)
begin
DRb.start_service
spec_server = DRbObject.new_with_uri("druby://127.0.0.1:8989")
spec_server.run(options.argv, options.error_stream, options.output_stream)
rescue DRb::DRbConnError => e
options.error_stream.puts "No server is running"
end
end
end
end
end

View file

@ -0,0 +1,59 @@
module Spec
module Runner
class ExampleGroupRunner
def initialize(options)
@options = options
end
def load_files(files)
# It's important that loading files (or choosing not to) stays the
# responsibility of the ExampleGroupRunner. Some implementations (like)
# the one using DRb may choose *not* to load files, but instead tell
# someone else to do it over the wire.
files.each do |file|
load file
end
end
def run
prepare
success = true
example_groups.each do |example_group|
success = success & example_group.run
end
return success
ensure
finish
end
protected
def prepare
reporter.start(number_of_examples)
example_groups.reverse! if reverse
end
def finish
reporter.end
reporter.dump
end
def reporter
@options.reporter
end
def reverse
@options.reverse
end
def example_groups
@options.example_groups
end
def number_of_examples
@options.number_of_examples
end
end
# TODO: BT - Deprecate BehaviourRunner?
BehaviourRunner = ExampleGroupRunner
end
end

View file

@ -0,0 +1,77 @@
module Spec
module Runner
module Formatter
# Baseclass for formatters that implements all required methods as no-ops.
class BaseFormatter
attr_accessor :example_group, :options, :where
def initialize(options, where)
@options = options
@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_example_group
def start(example_count)
end
# This method is invoked at the beginning of the execution of each example_group.
# +example_group+ is the example_group.
#
# The next method to be invoked after this is #example_failed or #example_finished
def add_example_group(example_group)
@example_group = example_group
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.
# +message+ is the message from the ExamplePendingError, if it exists, or the
# default value of "Not Yet Implemented"
def example_pending(example, 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

View file

@ -0,0 +1,130 @@
require 'spec/runner/formatter/base_formatter'
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 < BaseFormatter
attr_reader :output, :pending_examples
# 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(options, where)
super
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
@pending_examples = []
end
def example_pending(example, message)
@pending_examples << [example.__full_description, message]
end
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?)
red(s)
elsif(failure.pending_fixed?)
blue(s)
else
magenta(s)
end
end
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 = "#{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
if pending_count > 0
@output.puts yellow(summary)
else
@output.puts green(summary)
end
else
@output.puts red(summary)
end
@output.flush
end
def dump_pending
unless @pending_examples.empty?
@output.puts
@output.puts "Pending:"
@pending_examples.each do |pending_example|
@output.puts "#{pending_example[0]} (#{pending_example[1]})"
end
end
@output.flush
end
def close
if IO === @output
@output.close
end
end
def format_backtrace(backtrace)
return "" if backtrace.nil?
backtrace.map { |line| backtrace_line(line) }.join("\n")
end
protected
def colour?
@options.colour ? true : false
end
def dry_run?
@options.dry_run ? true : false
end
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 green(text); colour(text, "\e[32m"); end
def red(text); colour(text, "\e[31m"); end
def magenta(text); colour(text, "\e[35m"); end
def yellow(text); colour(text, "\e[33m"); end
def blue(text); colour(text, "\e[34m"); end
end
end
end
end

View file

@ -0,0 +1,27 @@
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
class FailingExampleGroupsFormatter < BaseTextFormatter
def example_failed(example, counter, failure)
if @example_group
description_parts = @example_group.description_parts.collect do |description|
description =~ /(.*) \(druby.*\)$/ ? $1 : description
end
@output.puts ::Spec::Example::ExampleGroupMethods.description_text(*description_parts)
@output.flush
@example_group = nil
end
end
def dump_failure(counter, failure)
end
def dump_summary(duration, example_count, failure_count, pending_count)
end
end
end
end
end

View file

@ -0,0 +1,20 @@
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
class FailingExamplesFormatter < BaseTextFormatter
def example_failed(example, counter, failure)
@output.puts "#{example_group.description} #{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

View file

@ -0,0 +1,337 @@
require 'erb'
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
class HtmlFormatter < BaseTextFormatter
include ERB::Util # for the #h method
def initialize(options, output)
super
@example_group_number = 0
@example_number = 0
end
def method_missing(sym, *args)
# no-op
end
# The number of the currently running example_group
def example_group_number
@example_group_number
end
# The number of the currently running example (a global counter)
def example_number
@example_number
end
def start(example_count)
@example_count = example_count
@output.puts html_header
@output.puts report_header
@output.flush
end
def add_example_group(example_group)
super
@example_group_red = false
@example_group_red = false
@example_group_number += 1
unless example_group_number == 1
@output.puts " </dl>"
@output.puts "</div>"
end
@output.puts "<div class=\"example_group\">"
@output.puts " <dl>"
@output.puts " <dt id=\"example_group_#{example_group_number}\">#{h(example_group.description)}</dt>"
@output.flush
end
def start_dump
@output.puts " </dl>"
@output.puts "</div>"
@output.flush
end
def example_started(example)
@example_number += 1
end
def example_passed(example)
move_progress
@output.puts " <dd class=\"spec passed\"><span class=\"passed_spec_name\">#{h(example.description)}</span></dd>"
@output.flush
end
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('example_group_#{example_group_number}');</script>" unless @example_group_red
@example_group_red = true
move_progress
@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>#{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 unless extra == ""
@output.puts " </div>"
@output.puts " </dd>"
@output.flush
end
def example_pending(example, message)
@output.puts " <script type=\"text/javascript\">makeYellow('rspec-header');</script>" unless @header_red
@output.puts " <script type=\"text/javascript\">makeYellow('example_group_#{example_group_number}');</script>" unless @example_group_red
move_progress
@output.puts " <dd class=\"spec not_implemented\"><span class=\"not_implemented_spec_name\">#{h(example.description)} (PENDING: #{h(message)})</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(failure)
require 'spec/runner/formatter/snippet_extractor'
@snippet_extractor ||= SnippetExtractor.new
" <pre class=\"ruby\"><code>#{@snippet_extractor.snippet(failure.exception)}</code></pre>"
end
def move_progress
@output.puts " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
@output.flush
end
def percent_done
result = 100.0
if @example_count != 0
result = ((example_number).to_f / @example_count.to_f * 1000).to_i / 10.0
end
result
end
def dump_failure(counter, failure)
end
def dump_summary(duration, example_count, failure_count, pending_count)
if dry_run?
totals = "This was a dry-run"
else
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>"
@output.flush
end
def html_header
<<-EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.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=utf-8" />
<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
def report_header
<<-EOF
<div class="rspec-report">
<script type="text/javascript">
// <![CDATA[
#{global_scripts}
// ]]>
</script>
<style type="text/css">
#{global_styles}
</style>
<div id="rspec-header">
<h1>RSpec Results</h1>
<div id="summary">
<p id="totals">&nbsp;</p>
<p id="duration">&nbsp;</p>
</div>
</div>
<div class="results">
EOF
end
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';
}
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';
}
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;
}
.rspec-report h1 {
margin: 0px 10px 0px 10px;
padding: 10px;
font-family: "Lucida Grande", Helvetica, sans-serif;
font-size: 1.8em;
}
#summary {
margin: 0; padding: 5px 10px;
font-family: "Lucida Grande", Helvetica, sans-serif;
text-align: right;
position: absolute;
top: 0px;
right: 0px;
}
#summary p {
margin: 0 0 0 2px;
}
#summary #totals {
font-size: 1.2em;
}
.example_group {
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;
}
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
end

View file

@ -0,0 +1,65 @@
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
class NestedTextFormatter < BaseTextFormatter
attr_reader :previous_nested_example_groups
def initialize(options, where)
super
@previous_nested_example_groups = []
end
def add_example_group(example_group)
super
current_nested_example_groups = described_example_group_chain
current_nested_example_groups.each_with_index do |nested_example_group, i|
unless nested_example_group == previous_nested_example_groups[i]
output.puts "#{' ' * i}#{nested_example_group.description_args}"
end
end
@previous_nested_example_groups = described_example_group_chain
end
def example_failed(example, counter, failure)
message = if failure.expectation_not_met?
"#{current_indentation}#{example.description} (FAILED - #{counter})"
else
"#{current_indentation}#{example.description} (ERROR - #{counter})"
end
output.puts(failure.expectation_not_met? ? red(message) : magenta(message))
output.flush
end
def example_passed(example)
message = "#{current_indentation}#{example.description}"
output.puts green(message)
output.flush
end
def example_pending(example, message)
super
output.puts yellow("#{current_indentation}#{example.description} (PENDING: #{message})")
output.flush
end
def current_indentation
' ' * previous_nested_example_groups.length
end
def described_example_group_chain
example_group_chain = []
example_group.send(:execute_in_class_hierarchy) do |parent_example_group|
if parent_example_group.description_args && !parent_example_group.description_args.empty?
example_group_chain << parent_example_group
end
end
example_group_chain
end
end
end
end
end

View file

@ -0,0 +1,51 @@
require 'spec/runner/formatter/progress_bar_formatter'
module Spec
module Runner
module Formatter
class ProfileFormatter < ProgressBarFormatter
def initialize(options, where)
super
@example_times = []
end
def start(count)
@output.puts "Profiling enabled."
end
def example_started(example)
@time = Time.now
end
def example_passed(example)
super
@example_times << [
example_group.description,
example.description,
Time.now - @time
]
end
def start_dump
super
@output.puts "\n\nTop 10 slowest examples:\n"
@example_times = @example_times.sort_by do |description, example, time|
time
end.reverse
@example_times[0..9].each do |description, example, time|
@output.print red(sprintf("%.7f", time))
@output.puts " #{description} #{example}"
end
@output.flush
end
def method_missing(sym, *args)
# ignore
end
end
end
end
end

View file

@ -0,0 +1,34 @@
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
class ProgressBarFormatter < BaseTextFormatter
def example_failed(example, counter, failure)
@output.print colourise('F', failure)
@output.flush
end
def example_passed(example)
@output.print green('.')
@output.flush
end
def example_pending(example, message)
super
@output.print yellow('P')
@output.flush
end
def start_dump
@output.puts
@output.flush
end
def method_missing(sym, *args)
# ignore
end
end
end
end
end

View file

@ -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

View file

@ -0,0 +1,39 @@
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
class SpecdocFormatter < BaseTextFormatter
def add_example_group(example_group)
super
output.puts
output.puts example_group.description
output.flush
end
def example_failed(example, counter, failure)
message = if failure.expectation_not_met?
"- #{example.description} (FAILED - #{counter})"
else
"- #{example.description} (ERROR - #{counter})"
end
output.puts(failure.expectation_not_met? ? red(message) : magenta(message))
output.flush
end
def example_passed(example)
message = "- #{example.description}"
output.puts green(message)
output.flush
end
def example_pending(example, message)
super
output.puts yellow("- #{example.description} (PENDING: #{message})")
output.flush
end
end
end
end
end

View file

@ -0,0 +1,161 @@
require 'erb'
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
module Story
class HtmlFormatter < BaseTextFormatter
include ERB::Util
def initialize(options, where)
super
@previous_type = nil
@scenario_text = ""
@story_text = ""
@scenario_failed = false
@story_failed = false
end
def run_started(count)
@output.puts <<-EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Stories</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Expires" content="-1" />
<meta http-equiv="Pragma" content="no-cache" />
<script src="javascripts/prototype.js" type="text/javascript"></script>
<script src="javascripts/scriptaculous.js" type="text/javascript"></script>
<script src="javascripts/rspec.js" type="text/javascript"></script>
<link href="stylesheets/rspec.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="container">
EOF
end
def collected_steps(steps)
unless steps.empty?
@output.puts " <ul id=\"stock_steps\" style=\"display: none;\">"
steps.each do |step|
@output.puts " <li>#{step}</li>"
end
@output.puts " </ul>"
end
end
def run_ended
@output.puts <<-EOF
</div>
</body>
</head>
EOF
end
def story_started(title, narrative)
@story_failed = false
@story_text = <<-EOF
<dt>Story: #{h title}</dt>
<dd>
<p>
#{h(narrative).split("\n").join("<br />")}
</p>
EOF
end
def story_ended(title, narrative)
if @story_failed
@output.puts <<-EOF
<dl class="story failed">
EOF
else
@output.puts <<-EOF
<dl class="story passed">
EOF
end
@output.puts <<-EOF
#{@story_text}
</dd>
</dl>
EOF
end
def scenario_started(story_title, scenario_name)
@scenario_failed = false
@scenario_text = <<-EOF
<dt>Scenario: #{h scenario_name}</dt>
<dd>
<ul class="steps">
EOF
end
def scenario_ended
if @scenario_failed
@story_text += <<-EOF
<dl class="failed">
EOF
else
@story_text += <<-EOF
<dl class="passed">
EOF
end
@story_text += <<-EOF
#{@scenario_text}
</ul>
</dd>
</dl>
EOF
end
def found_scenario(type, description)
end
def scenario_succeeded(story_title, scenario_name)
scenario_ended
end
def scenario_pending(story_title, scenario_name, reason)
scenario_ended
end
def scenario_failed(story_title, scenario_name, err)
@scenario_failed = true
@story_failed = true
scenario_ended
end
def step_upcoming(type, description, *args)
end
def step_succeeded(type, description, *args)
print_step('passed', type, description, *args) # TODO: uses succeeded CSS class
end
def step_pending(type, description, *args)
print_step('pending', type, description, *args)
end
def step_failed(type, description, *args)
print_step('failed', type, description, *args)
end
def print_step(klass, type, description, *args)
spans = args.map { |arg| "<span class=\"param\">#{arg}</span>" }
desc_string = description.step_name
arg_regexp = description.arg_regexp
i = -1
inner = type.to_s.capitalize + ' ' + desc_string.gsub(arg_regexp) { |param| spans[i+=1] }
@scenario_text += " <li class=\"#{klass}\">#{inner}</li>\n"
end
end
end
end
end
end

View file

@ -0,0 +1,188 @@
require 'spec/runner/formatter/base_text_formatter'
module Spec
module Runner
module Formatter
module Story
class PlainTextFormatter < BaseTextFormatter
def initialize(options, where)
super
@successful_scenario_count = 0
@pending_scenario_count = 0
@pre_story_pending_count = 0
@pre_story_successful_count = 0
@failed_scenarios = []
@pending_steps = []
@previous_type = nil
@scenario_body_text = ""
@story_body_text = ""
@scenario_head_text = ""
@story_head_text = ""
@scenario_failed = false
@story_failed = false
end
def run_started(count)
@count = count
@output.puts "Running #@count scenarios\n\n"
end
def story_started(title, narrative)
@pre_story_pending_count = @pending_scenario_count
@pre_story_successful_count = @successful_scenario_count
@current_story_title = title
@story_failed = false
@story_body_text = ""
@story_head_text = "Story: #{title}\n\n"
narrative.each_line do |line|
@story_head_text += " "
@story_head_text += line
end
end
def story_ended(title, narrative)
if @story_failed
@output.print red(@story_head_text)
elsif @pre_story_successful_count == @successful_scenario_count &&
@pending_scenario_count >= @pre_story_pending_count
@output.print yellow(@story_head_text)
else
@output.print green(@story_head_text)
end
@output.print @story_body_text
@output.puts
@output.puts
end
def scenario_started(story_title, scenario_name)
@current_scenario_name = scenario_name
@scenario_already_failed = false
@scenario_head_text = "\n\n Scenario: #{scenario_name}"
@scenario_body_text = ""
@scenario_ok = true
@scenario_pending = false
@scenario_failed = false
end
def scenario_succeeded(story_title, scenario_name)
@successful_scenario_count += 1
scenario_ended
end
def scenario_failed(story_title, scenario_name, err)
@options.backtrace_tweaker.tweak_backtrace(err)
@failed_scenarios << [story_title, scenario_name, err] unless @scenario_already_failed
@scenario_already_failed = true
@story_failed = true
@scenario_failed = true
scenario_ended
end
def scenario_pending(story_title, scenario_name, msg)
@pending_scenario_count += 1 unless @scenario_already_failed
@scenario_pending = true
@scenario_already_failed = true
scenario_ended
end
def scenario_ended
if @scenario_failed
@story_body_text += red(@scenario_head_text)
elsif @scenario_pending
@story_body_text += yellow(@scenario_head_text)
else
@story_body_text += green(@scenario_head_text)
end
@story_body_text += @scenario_body_text
end
def run_ended
@output.puts "#@count scenarios: #@successful_scenario_count succeeded, #{@failed_scenarios.size} failed, #@pending_scenario_count pending"
unless @pending_steps.empty?
@output.puts "\nPending Steps:"
@pending_steps.each_with_index do |pending, i|
story_name, scenario_name, msg = pending
@output.puts "#{i+1}) #{story_name} (#{scenario_name}): #{msg}"
end
end
unless @failed_scenarios.empty?
@output.print "\nFAILURES:"
@failed_scenarios.each_with_index do |failure, i|
title, scenario_name, err = failure
@output.print %[
#{i+1}) #{title} (#{scenario_name}) FAILED
#{err.class}: #{err.message}
#{err.backtrace.join("\n")}
]
end
end
end
def step_upcoming(type, description, *args)
end
def step_succeeded(type, description, *args)
found_step(type, description, false, false, *args)
end
def step_pending(type, description, *args)
found_step(type, description, false, true, *args)
@pending_steps << [@current_story_title, @current_scenario_name, description]
@scenario_body_text += yellow(" (PENDING)")
@scenario_pending = true
@scenario_ok = false
end
def step_failed(type, description, *args)
found_step(type, description, true, @scenario_pending, *args)
if @scenario_pending
@scenario_body_text += yellow(" (SKIPPED)")
else
@scenario_body_text += red(@scenario_ok ? " (FAILED)" : " (SKIPPED)")
end
@scenario_ok = false
end
def collected_steps(steps)
end
def method_missing(sym, *args, &block) #:nodoc:
# noop - ignore unknown messages
end
private
def found_step(type, description, failed, pending, *args)
desc_string = description.step_name
arg_regexp = description.arg_regexp
text = if(type == @previous_type)
"\n And "
else
"\n\n #{type.to_s.capitalize} "
end
i = -1
text << desc_string.gsub(arg_regexp) { |param| args[i+=1] }
if pending
@scenario_body_text += yellow(text)
else
@scenario_body_text += (failed ? red(text) : green(text))
end
if type == :'given scenario'
@previous_type = :given
else
@previous_type = type
end
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
require 'spec/runner/formatter/html_formatter'
module Spec
module Runner
module Formatter
# Formats backtraces so they're clickable by TextMate
class TextMateFormatter < HtmlFormatter
def backtrace_line(line)
line.gsub(/([^:]*\.rb):(\d*)/) do
"<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
end
end
end
end
end
end

View file

@ -0,0 +1,72 @@
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 example groups held by +rspec_options+ once for each of the
# methods in the matched classes.
def heckle_with
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, rspec_options)
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, rspec_options)
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, rspec_options)
super(klass_name, method_name)
@rspec_options = rspec_options
end
def tests_pass?
success = @rspec_options.run_examples
success
end
end
end
end

View file

@ -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

View file

@ -0,0 +1,203 @@
require 'optparse'
require 'stringio'
module Spec
module Runner
class OptionParser < ::OptionParser
class << self
def parse(args, err, out)
parser = new(err, out)
parser.parse(args)
parser.options
end
end
attr_reader :options
OPTIONS = {
:pattern => ["-p", "--pattern [PATTERN]","Limit files loaded to those matching this pattern. Defaults to '**/*_spec.rb'",
"Separate multiple patterns with commas.",
"Applies only to directories named on the command line (files",
"named explicitly on the command line will be loaded regardless)."],
: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 for examples: ",
"progress|p : Text progress",
"profile|o : Text progress with profiling of 10 slowest examples",
"specdoc|s : Example doc as text",
"indented|i : Example doc as indented text",
"html|h : A nice HTML report",
"failing_examples|e : Write all failing examples - input for --example",
"failing_example_groups|g : Write all failing example groups - input for --example",
" ",
"Builtin formats for stories: ",
"plain|p : Plain Text",
"html|h : A nice HTML report",
" ",
"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 Runner."],
: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(err, out)
super()
@error_stream = err
@out_stream = out
@options = Options.new(@error_stream, @out_stream)
@file_factory = File
self.banner = "Usage: spec (FILE|DIRECTORY|GLOB)+ [options]"
self.separator ""
on(*OPTIONS[:pattern]) {|pattern| @options.filename_pattern = pattern}
on(*OPTIONS[:diff]) {|diff| @options.parse_diff(diff)}
on(*OPTIONS[:colour]) {@options.colour = true}
on(*OPTIONS[:example]) {|example| @options.parse_example(example)}
on(*OPTIONS[:specification]) {|example| @options.parse_example(example)}
on(*OPTIONS[:line]) {|line_number| @options.line_number = line_number.to_i}
on(*OPTIONS[:format]) {|format| @options.parse_format(format)}
on(*OPTIONS[:require]) {|requires| invoke_requires(requires)}
on(*OPTIONS[:backtrace]) {@options.backtrace_tweaker = NoisyBacktraceTweaker.new}
on(*OPTIONS[:loadby]) {|loadby| @options.loadby = loadby}
on(*OPTIONS[:reverse]) {@options.reverse = true}
on(*OPTIONS[:timeout]) {|timeout| @options.timeout = timeout.to_f}
on(*OPTIONS[:heckle]) {|heckle| @options.load_heckle_runner(heckle)}
on(*OPTIONS[:dry_run]) {@options.dry_run = true}
on(*OPTIONS[:options_file]) {|options_file| parse_options_file(options_file)}
on(*OPTIONS[:generate_options]) {|options_file|}
on(*OPTIONS[:runner]) {|runner| @options.user_input_for_runner = runner}
on(*OPTIONS[:drb]) {}
on(*OPTIONS[:version]) {parse_version}
on_tail(*OPTIONS[:help]) {parse_help}
end
def order!(argv, &blk)
@argv = argv
@options.argv = @argv.dup
return if parse_generate_options
return if parse_drb
super(@argv) do |file|
@options.files << file
blk.call(file) if blk
end
@options
end
protected
def invoke_requires(requires)
requires.split(",").each do |file|
require file
end
end
def parse_options_file(options_file)
option_file_args = IO.readlines(options_file).map {|l| l.chomp.split " "}.flatten
@argv.push(*option_file_args)
# TODO - this is a brute force solution to http://rspec.lighthouseapp.com/projects/5645/tickets/293.
# Let's look for a cleaner way. Might not be one. But let's look. If not, perhaps
# this can be moved to a different method to indicate the special handling for drb?
parse_drb(@argv)
end
def parse_generate_options
# Remove the --generate-options option and the argument before writing to file
options_file = nil
['-G', '--generate-options'].each do |option|
if index = @argv.index(option)
@argv.delete_at(index)
options_file = @argv.delete_at(index)
end
end
if options_file
write_generated_options(options_file)
return true
else
return false
end
end
def write_generated_options(options_file)
File.open(options_file, 'w') do |io|
io.puts @argv.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}"
@options.examples_should_not_be_run
end
def parse_drb(argv = nil)
argv ||= @options.argv # TODO - see note about about http://rspec.lighthouseapp.com/projects/5645/tickets/293
is_drb = false
is_drb ||= argv.delete(OPTIONS[:drb][0])
is_drb ||= argv.delete(OPTIONS[:drb][1])
return false unless is_drb
@options.examples_should_not_be_run
DrbCommandLine.run(
self.class.parse(argv, @error_stream, @out_stream)
)
true
end
def parse_version
@out_stream.puts ::Spec::VERSION::DESCRIPTION
exit if stdout?
end
def parse_help
@out_stream.puts self
exit if stdout?
end
def stdout?
@out_stream == $stdout
end
end
end
end

View file

@ -0,0 +1,309 @@
module Spec
module Runner
class Options
FILE_SORTERS = {
'mtime' => lambda {|file_a, file_b| File.mtime(file_b) <=> File.mtime(file_a)}
}
EXAMPLE_FORMATTERS = { # Load these lazily for better speed
'specdoc' => ['spec/runner/formatter/specdoc_formatter', 'Formatter::SpecdocFormatter'],
's' => ['spec/runner/formatter/specdoc_formatter', 'Formatter::SpecdocFormatter'],
'nested' => ['spec/runner/formatter/nested_text_formatter', 'Formatter::NestedTextFormatter'],
'n' => ['spec/runner/formatter/nested_text_formatter', 'Formatter::NestedTextFormatter'],
'html' => ['spec/runner/formatter/html_formatter', 'Formatter::HtmlFormatter'],
'h' => ['spec/runner/formatter/html_formatter', 'Formatter::HtmlFormatter'],
'progress' => ['spec/runner/formatter/progress_bar_formatter', 'Formatter::ProgressBarFormatter'],
'p' => ['spec/runner/formatter/progress_bar_formatter', 'Formatter::ProgressBarFormatter'],
'failing_examples' => ['spec/runner/formatter/failing_examples_formatter', 'Formatter::FailingExamplesFormatter'],
'e' => ['spec/runner/formatter/failing_examples_formatter', 'Formatter::FailingExamplesFormatter'],
'failing_example_groups' => ['spec/runner/formatter/failing_example_groups_formatter', 'Formatter::FailingExampleGroupsFormatter'],
'g' => ['spec/runner/formatter/failing_example_groups_formatter', 'Formatter::FailingExampleGroupsFormatter'],
'profile' => ['spec/runner/formatter/profile_formatter', 'Formatter::ProfileFormatter'],
'o' => ['spec/runner/formatter/profile_formatter', 'Formatter::ProfileFormatter'],
'textmate' => ['spec/runner/formatter/text_mate_formatter', 'Formatter::TextMateFormatter']
}
STORY_FORMATTERS = {
'plain' => ['spec/runner/formatter/story/plain_text_formatter', 'Formatter::Story::PlainTextFormatter'],
'p' => ['spec/runner/formatter/story/plain_text_formatter', 'Formatter::Story::PlainTextFormatter'],
'html' => ['spec/runner/formatter/story/html_formatter', 'Formatter::Story::HtmlFormatter'],
'h' => ['spec/runner/formatter/story/html_formatter', 'Formatter::Story::HtmlFormatter']
}
attr_accessor(
:filename_pattern,
:backtrace_tweaker,
:context_lines,
:diff_format,
:dry_run,
:profile,
:examples,
:heckle_runner,
:line_number,
:loadby,
:reporter,
:reverse,
:timeout,
:verbose,
:user_input_for_runner,
:error_stream,
:output_stream,
:before_suite_parts,
:after_suite_parts,
# TODO: BT - Figure out a better name
:argv
)
attr_reader :colour, :differ_class, :files, :example_groups
def initialize(error_stream, output_stream)
@error_stream = error_stream
@output_stream = output_stream
@filename_pattern = "**/*_spec.rb"
@backtrace_tweaker = QuietBacktraceTweaker.new
@examples = []
@colour = false
@profile = false
@dry_run = false
@reporter = Reporter.new(self)
@context_lines = 3
@diff_format = :unified
@files = []
@example_groups = []
@result = nil
@examples_run = false
@examples_should_be_run = nil
@user_input_for_runner = nil
@before_suite_parts = []
@after_suite_parts = []
end
def add_example_group(example_group)
@example_groups << example_group
end
def remove_example_group(example_group)
@example_groups.delete(example_group)
end
def run_examples
return true unless examples_should_be_run?
success = true
begin
before_suite_parts.each do |part|
part.call
end
runner = custom_runner || ExampleGroupRunner.new(self)
unless @files_loaded
runner.load_files(files_to_load)
@files_loaded = true
end
if example_groups.empty?
true
else
set_spec_from_line_number if line_number
success = runner.run
@examples_run = true
heckle if heckle_runner
success
end
ensure
after_suite_parts.each do |part|
part.call(success)
end
end
end
def examples_run?
@examples_run
end
def examples_should_not_be_run
@examples_should_be_run = false
end
def colour=(colour)
@colour = colour
if @colour && RUBY_PLATFORM =~ /win32/ ;\
begin ;\
require 'rubygems' ;\
require 'Win32/Console/ANSI' ;\
rescue LoadError ;\
warn "You must 'gem install win32console' to use colour on Windows" ;\
@colour = false ;\
end
end
end
def parse_diff(format)
case format
when :context, 'context', 'c'
@diff_format = :context
default_differ
when :unified, 'unified', 'u', '', nil
@diff_format = :unified
default_differ
else
@diff_format = :custom
self.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 = ClassAndArgumentsParser.parse(format_arg)
unless where
raise "When using several --format options only one of them can be without a file" if @out_used
where = @output_stream
@out_used = true
end
@format_options ||= []
@format_options << [format, where]
end
def formatters
@format_options ||= [['progress', @output_stream]]
@formatters ||= load_formatters(@format_options, EXAMPLE_FORMATTERS)
end
def story_formatters
@format_options ||= [['plain', @output_stream]]
@formatters ||= load_formatters(@format_options, STORY_FORMATTERS)
end
def load_formatters(format_options, formatters)
format_options.map do |format, where|
formatter_type = if formatters[format]
require formatters[format][0]
eval(formatters[format][1], binding, __FILE__, __LINE__)
else
load_class(format, 'formatter', '--format')
end
formatter_type.new(self, where)
end
end
def load_heckle_runner(heckle)
suffix = [/mswin/, /java/].detect{|p| p =~ RUBY_PLATFORM} ? '_unsupported' : ''
require "spec/runner/heckle_runner#{suffix}"
@heckle_runner = HeckleRunner.new(heckle)
end
def number_of_examples
total = 0
@example_groups.each do |example_group|
total += example_group.number_of_examples
end
total
end
def files_to_load
result = []
sorted_files.each do |file|
if File.directory?(file)
filename_pattern.split(",").each do |pattern|
result += Dir[File.expand_path("#{file}/#{pattern.strip}")]
end
elsif File.file?(file)
result << file
else
raise "File or directory not found: #{file}"
end
end
result
end
protected
def examples_should_be_run?
return @examples_should_be_run unless @examples_should_be_run.nil?
@examples_should_be_run = true
end
def differ_class=(klass)
return unless klass
@differ_class = klass
Spec::Expectations.differ = self.differ_class.new(self)
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"
@error_stream.puts m
raise m
end
begin
eval(name, binding, __FILE__, __LINE__)
rescue NameError => e
@error_stream.puts "Couldn't find #{kind} class #{name}"
@error_stream.puts "Make sure the --require option is specified *before* #{option}"
if $_spec_spec ; raise e ; else exit(1) ; end
end
end
def custom_runner
return nil unless custom_runner?
klass_name, arg = ClassAndArgumentsParser.parse(user_input_for_runner)
runner_type = load_class(klass_name, 'behaviour runner', '--runner')
return runner_type.new(self, arg)
end
def custom_runner?
return user_input_for_runner ? true : false
end
def heckle
heckle_runner = self.heckle_runner
self.heckle_runner = nil
heckle_runner.heckle_with
end
def sorted_files
return sorter ? files.sort(&sorter) : files
end
def sorter
FILE_SORTERS[loadby]
end
def default_differ
require 'spec/expectations/differs/default'
self.differ_class = Spec::Expectations::Differs::Default
end
def set_spec_from_line_number
if examples.empty?
if files.length == 1
if File.directory?(files[0])
error_stream.puts "You must specify one file, not a directory when using the --line option"
exit(1) if stderr?
else
example = SpecParser.new.spec_name_for(files[0], line_number)
@examples = [example]
end
else
error_stream.puts "Only one file can be specified when using the --line option: #{files.inspect}"
exit(3) if stderr?
end
else
error_stream.puts "You cannot use both --line and --example"
exit(4) if stderr?
end
end
def stderr?
@error_stream == $stderr
end
end
end
end

View file

@ -0,0 +1,147 @@
module Spec
module Runner
class Reporter
attr_reader :options, :example_groups
def initialize(options)
@options = options
@options.reporter = self
clear
end
def add_example_group(example_group)
formatters.each do |f|
f.add_example_group(example_group)
end
example_groups << example_group
end
def example_started(example)
formatters.each{|f| f.example_started(example)}
end
def example_finished(example, error=nil)
@examples << example
if error.nil?
example_passed(example)
elsif Spec::Example::ExamplePendingError === error
example_pending(example, error.message)
else
example_failed(example, error)
end
end
def failure(example, error)
backtrace_tweaker.tweak_backtrace(error)
failure = Failure.new(example, error)
@failures << failure
formatters.each do |f|
f.example_failed(example, @failures.length, failure)
end
end
alias_method :example_failed, :failure
def start(number_of_examples)
clear
@start_time = Time.new
formatters.each{|f| f.start(number_of_examples)}
end
def end
@end_time = Time.new
end
# Dumps the summary and returns the total number of failures
def dump
formatters.each{|f| f.start_dump}
dump_pending
dump_failures
formatters.each do |f|
f.dump_summary(duration, @examples.length, @failures.length, @pending_count)
f.close
end
@failures.length
end
private
def formatters
@options.formatters
end
def backtrace_tweaker
@options.backtrace_tweaker
end
def clear
@example_groups = []
@failures = []
@pending_count = 0
@examples = []
@start_time = nil
@end_time = nil
end
def dump_failures
return if @failures.empty?
@failures.inject(1) do |index, failure|
formatters.each{|f| f.dump_failure(index, failure)}
index + 1
end
end
def dump_pending
formatters.each{|f| f.dump_pending}
end
def duration
return @end_time - @start_time unless (@end_time.nil? or @start_time.nil?)
return "0.0"
end
def example_passed(example)
formatters.each{|f| f.example_passed(example)}
end
def example_pending(example, message="Not Yet Implemented")
@pending_count += 1
formatters.each do |f|
f.example_pending(example, message)
end
end
class Failure
attr_reader :example, :exception
def initialize(example, exception)
@example = example
@exception = exception
end
def header
if expectation_not_met?
"'#{example_name}' FAILED"
elsif pending_fixed?
"'#{example_name}' FIXED"
else
"#{@exception.class.name} in '#{example_name}'"
end
end
def pending_fixed?
@exception.is_a?(Spec::Example::PendingExampleFixedError)
end
def expectation_not_met?
@exception.is_a?(Spec::Expectations::ExpectationNotMetError)
end
protected
def example_name
@example.__full_description
end
end
end
end
end

View file

@ -0,0 +1,71 @@
module Spec
module Runner
# Parses a spec file and finds the nearest example for a given line number.
class SpecParser
attr_reader :best_match
def initialize
@best_match = {}
end
def spec_name_for(file, line_number)
best_match.clear
file = File.expand_path(file)
rspec_options.example_groups.each do |example_group|
consider_example_groups_for_best_match example_group, file, line_number
example_group.examples.each do |example|
consider_example_for_best_match example, example_group, file, line_number
end
end
if best_match[:example_group]
if best_match[:example]
"#{best_match[:example_group].description} #{best_match[:example].description}"
else
best_match[:example_group].description
end
else
nil
end
end
protected
def consider_example_groups_for_best_match(example_group, file, line_number)
parsed_backtrace = parse_backtrace(example_group.registration_backtrace)
parsed_backtrace.each do |example_file, example_line|
if is_best_match?(file, line_number, example_file, example_line)
best_match.clear
best_match[:example_group] = example_group
best_match[:line] = example_line
end
end
end
def consider_example_for_best_match(example, example_group, file, line_number)
parsed_backtrace = parse_backtrace(example.implementation_backtrace)
parsed_backtrace.each do |example_file, example_line|
if is_best_match?(file, line_number, example_file, example_line)
best_match.clear
best_match[:example_group] = example_group
best_match[:example] = example
best_match[:line] = example_line
end
end
end
def is_best_match?(file, line_number, example_file, example_line)
file == File.expand_path(example_file) &&
example_line <= line_number &&
example_line > best_match[:line].to_i
end
def parse_backtrace(backtrace)
backtrace.collect do |trace_line|
split_line = trace_line.split(':')
[split_line[0], Integer(split_line[1])]
end
end
end
end
end