diff options
| author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-17 02:48:41 +0000 |
|---|---|---|
| committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-17 02:48:41 +0000 |
| commit | ba23a5ac276e59fdda8186750c6d0fd2cfecdeac (patch) | |
| tree | 1e14b25ade74ea52d8da2788ede9b12b507867e8 /test/lib/spec/runner | |
| parent | 8ea6adaeb1e3d0aa6348c2a2c3a385d185372d06 (diff) | |
| download | puppet-ba23a5ac276e59fdda8186750c6d0fd2cfecdeac.tar.gz puppet-ba23a5ac276e59fdda8186750c6d0fd2cfecdeac.tar.xz puppet-ba23a5ac276e59fdda8186750c6d0fd2cfecdeac.zip | |
Adding spec libs, so we can use them some day
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2283 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'test/lib/spec/runner')
23 files changed, 1605 insertions, 0 deletions
diff --git a/test/lib/spec/runner/backtrace_tweaker.rb b/test/lib/spec/runner/backtrace_tweaker.rb new file mode 100644 index 000000000..7300b36b8 --- /dev/null +++ b/test/lib/spec/runner/backtrace_tweaker.rb @@ -0,0 +1,55 @@ +module Spec + module Runner + class BacktraceTweaker + def clean_up_double_slashes(line) + line.gsub!('//','/') + end + end + + class NoisyBacktraceTweaker < BacktraceTweaker + def tweak_backtrace(error, spec_name) + return if error.backtrace.nil? + error.backtrace.each do |line| + clean_up_double_slashes(line) + end + end + end + + # Tweaks raised Exceptions to mask noisy (unneeded) parts of the backtrace + class QuietBacktraceTweaker < BacktraceTweaker + unless defined?(IGNORE_PATTERNS) + root_dir = File.expand_path(File.join(__FILE__, '..', '..', '..', '..')) + spec_files = Dir["#{root_dir}/lib/spec/*"].map do |path| + subpath = path[root_dir.length..-1] + /#{subpath}/ + end + IGNORE_PATTERNS = spec_files + [ + /\/lib\/ruby\//, + /bin\/spec:/, + /bin\/rcov:/, + /lib\/rspec_on_rails/, + /vendor\/rails/, + # TextMate's Ruby and RSpec plugins + /Ruby\.tmbundle\/Support\/tmruby.rb:/, + /RSpec\.tmbundle\/Support\/lib/, + /temp_textmate\./ + ] + end + + def tweak_backtrace(error, spec_name) + return if error.backtrace.nil? + error.backtrace.collect! do |line| + clean_up_double_slashes(line) + IGNORE_PATTERNS.each do |ignore| + if line =~ ignore + line = nil + break + end + end + line + end + error.backtrace.compact! + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/command_line.rb b/test/lib/spec/runner/command_line.rb new file mode 100644 index 000000000..db928ad9b --- /dev/null +++ b/test/lib/spec/runner/command_line.rb @@ -0,0 +1,34 @@ +require 'spec/runner/option_parser' + +module Spec + module Runner + # Facade to run specs without having to fork a new ruby process (using `spec ...`) + class CommandLine + # Runs specs. +argv+ is the commandline args as per the spec commandline API, +err+ + # and +out+ are the streams output will be written to. +exit+ tells whether or + # not a system exit should be called after the specs are run and + # +warn_if_no_files+ tells whether or not a warning (the help message) + # should be printed to +err+ in case no files are specified. + def self.run(argv, err, out, exit=true, warn_if_no_files=true) + old_context_runner = defined?($context_runner) ? $context_runner : nil + $context_runner = OptionParser.new.create_context_runner(argv, err, out, warn_if_no_files) + return if $context_runner.nil? # This is the case if we use --drb + + # If ARGV is a glob, it will actually each over each one of the matching files. + argv.each do |file_or_dir| + if File.directory?(file_or_dir) + Dir["#{file_or_dir}/**/*.rb"].each do |file| + load file + end + elsif File.file?(file_or_dir) + load file_or_dir + else + raise "File or directory not found: #{file_or_dir}" + end + end + $context_runner.run(exit) + $context_runner = old_context_runner + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/context.rb b/test/lib/spec/runner/context.rb new file mode 100644 index 000000000..3155e169a --- /dev/null +++ b/test/lib/spec/runner/context.rb @@ -0,0 +1,154 @@ +module Spec + module Runner + class ContextEvalModule < Module + end + class Context + module InstanceMethods + def initialize(description, &context_block) + @description = description + + @context_eval_module = ContextEvalModule.new + @context_eval_module.extend ContextEval::ModuleMethods + @context_eval_module.include ContextEval::InstanceMethods + before_context_eval + @context_eval_module.class_eval(&context_block) + end + + def before_context_eval + end + + def inherit_context_eval_module_from(klass) + @context_eval_module.inherit klass + end + alias :inherit :inherit_context_eval_module_from + + def include(mod) + @context_eval_module.include(mod) + end + + def run(reporter, dry_run=false) + reporter.add_context(@description) + prepare_execution_context_class + errors = run_context_setup(reporter, dry_run) + + specifications.each do |specification| + specification_execution_context = execution_context(specification) + specification_execution_context.copy_instance_variables_from(@once_only_execution_context_instance, []) unless context_setup_block.nil? + specification.run(reporter, setup_block, teardown_block, dry_run, specification_execution_context) + end unless errors.length > 0 + + run_context_teardown(reporter, dry_run) + end + + def number_of_specs + specifications.length + end + + def matches?(full_description) + matcher ||= SpecMatcher.new(@description) + specifications.each do |spec| + return true if spec.matches?(matcher, full_description) + end + return false + end + + def run_single_spec(full_description) + return if @description == full_description + matcher = SpecMatcher.new(@description) + specifications.reject! do |spec| + !spec.matches?(matcher, full_description) + end + end + + def methods + my_methods = super + my_methods |= @context_eval_module.methods + my_methods + end + + protected + + def method_missing(*args) + @context_eval_module.method_missing(*args) + end + + def context_setup_block + @context_eval_module.send :context_setup_block + end + + def context_teardown_block + @context_eval_module.send :context_teardown_block + end + + def specifications + @context_eval_module.send :specifications + end + + def setup_block + @context_eval_module.send :setup_block + end + + def teardown_block + @context_eval_module.send :teardown_block + end + + def prepare_execution_context_class + weave_in_context_modules + execution_context_class + end + + def weave_in_context_modules + mods = context_modules + context_eval_module = @context_eval_module + execution_context_class.class_eval do + include context_eval_module + mods.each do |mod| + include mod + end + end + end + + def context_modules + @context_eval_module.send :context_modules + end + + def execution_context_class + @context_eval_module.send :execution_context_class + end + + def execution_context specification + execution_context_class.new(specification) + end + + def run_context_setup(reporter, dry_run) + errors = [] + unless dry_run + begin + @once_only_execution_context_instance = execution_context(nil) + @once_only_execution_context_instance.instance_eval(&context_setup_block) + rescue => e + errors << e + location = "context_setup" + reporter.spec_finished(location, e, location) if reporter + end + end + errors + end + + def run_context_teardown(reporter, dry_run) + unless dry_run + begin + @once_only_execution_context_instance ||= execution_context(nil) + @once_only_execution_context_instance.instance_eval(&context_teardown_block) + rescue => e + location = "context_teardown" + reporter.spec_finished(location, e, location) if reporter + end + end + end + + end + include InstanceMethods + end + end +end diff --git a/test/lib/spec/runner/context_eval.rb b/test/lib/spec/runner/context_eval.rb new file mode 100644 index 000000000..2cee8f1cd --- /dev/null +++ b/test/lib/spec/runner/context_eval.rb @@ -0,0 +1,142 @@ +module Spec + module Runner + module ContextEval + module ModuleMethods + def inherit(klass) + @context_superclass = klass + derive_execution_context_class_from_context_superclass + end + + def include(mod) + context_modules << mod + mod.send :included, self + end + + def context_setup(&block) + context_setup_parts << block + end + + def context_teardown(&block) + context_teardown_parts << block + end + + def setup(&block) + setup_parts << block + end + + def teardown(&block) + teardown_parts << block + end + + def specify(spec_name=:__generate_description, opts={}, &block) + specifications << Specification.new(spec_name, opts, &block) + end + + def methods + my_methods = super + my_methods |= context_superclass.methods + my_methods + end + + protected + + def method_missing(method_name, *args) + if context_superclass.respond_to?(method_name) + return execution_context_class.send(method_name, *args) + end + super + end + + private + + def context_setup_block + parts = context_setup_parts.dup + add_context_superclass_method(:context_setup, parts) + create_block_from_parts(parts) + end + + def context_teardown_block + parts = context_teardown_parts.dup + add_context_superclass_method(:context_teardown, parts) + create_block_from_parts(parts) + end + + def setup_block + parts = setup_parts.dup + add_context_superclass_method(:setup, parts) + create_block_from_parts(parts) + end + + def teardown_block + parts = teardown_parts.dup + add_context_superclass_method(:teardown, parts) + create_block_from_parts(parts) + end + + def execution_context_class + @execution_context_class ||= derive_execution_context_class_from_context_superclass + end + + def derive_execution_context_class_from_context_superclass + @execution_context_class = Class.new(context_superclass) + @execution_context_class.class_eval do + include ::Spec::Runner::ExecutionContext::InstanceMethods + end + end + + def context_superclass + @context_superclass ||= Object + end + + def context_modules + @context_modules ||= [Spec::Matchers, Spec::Mocks] + end + + def specifications + @specifications ||= [] + end + + def context_setup_parts + @context_setup_parts ||= [] + end + + def context_teardown_parts + @context_teardown_parts ||= [] + end + + def setup_parts + @setup_parts ||= [] + end + + def teardown_parts + @teardown_parts ||= [] + end + + def add_context_superclass_method sym, parts + superclass_method = begin + context_superclass.instance_method(sym) + rescue + nil + end + parts.unshift superclass_method if superclass_method + end + + def create_block_from_parts(parts) + proc do + parts.each do |part| + if part.is_a?(UnboundMethod) + part.bind(self).call + else + instance_eval(&part) + end + end + end + end + end + + module InstanceMethods + end + + end + end +end diff --git a/test/lib/spec/runner/context_runner.rb b/test/lib/spec/runner/context_runner.rb new file mode 100644 index 000000000..0a4d7e6e9 --- /dev/null +++ b/test/lib/spec/runner/context_runner.rb @@ -0,0 +1,55 @@ +module Spec + module Runner + class ContextRunner + + def initialize(options) + @contexts = [] + @options = options + end + + def add_context(context) + return unless spec_description.nil? || context.matches?(spec_description) + context.run_single_spec(spec_description) if context.matches?(spec_description) + @contexts << context + end + + # Runs all contexts and returns the number of failures. + def run(exit_when_done) + @options.reporter.start(number_of_specs) + begin + @contexts.each do |context| + context.run(@options.reporter, @options.dry_run) + end + rescue Interrupt + ensure + @options.reporter.end + end + failure_count = @options.reporter.dump + + if(failure_count == 0 && !@options.heckle_runner.nil?) + heckle_runner = @options.heckle_runner + @options.heckle_runner = nil + context_runner = self.class.new(@options) + context_runner.instance_variable_set(:@contexts, @contexts) + heckle_runner.heckle_with(context_runner) + end + + if(exit_when_done) + exit_code = (failure_count == 0) ? 0 : 1 + exit(exit_code) + end + failure_count + end + + def number_of_specs + @contexts.inject(0) {|sum, context| sum + context.number_of_specs} + end + + private + def spec_description + @options.spec_name + end + + end + end +end diff --git a/test/lib/spec/runner/drb_command_line.rb b/test/lib/spec/runner/drb_command_line.rb new file mode 100644 index 000000000..d4c7d937d --- /dev/null +++ b/test/lib/spec/runner/drb_command_line.rb @@ -0,0 +1,21 @@ +require "drb/drb" + +module Spec + module Runner + # Facade to run specs by connecting to a DRB server + class DrbCommandLine + # Runs specs on a DRB server. Note that this API is similar to that of + # CommandLine - making it possible for clients to use both interchangeably. + def self.run(argv, stderr, stdout, exit=true, warn_if_no_files=true) + begin + DRb.start_service + rails_spec_server = DRbObject.new_with_uri("druby://localhost:8989") + rails_spec_server.run(argv, stderr, stdout) + rescue DRb::DRbConnError + stderr.puts "No server is running" + exit 1 if exit + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/execution_context.rb b/test/lib/spec/runner/execution_context.rb new file mode 100644 index 000000000..484c55830 --- /dev/null +++ b/test/lib/spec/runner/execution_context.rb @@ -0,0 +1,17 @@ +module Spec + module Runner + class ExecutionContext + module InstanceMethods + def initialize(*args) #:nodoc: + #necessary for RSpec's own specs + end + + def violated(message="") + raise Spec::Expectations::ExpectationNotMetError.new(message) + end + + end + include InstanceMethods + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/extensions/kernel.rb b/test/lib/spec/runner/extensions/kernel.rb new file mode 100644 index 000000000..f060ec859 --- /dev/null +++ b/test/lib/spec/runner/extensions/kernel.rb @@ -0,0 +1,17 @@ +module Kernel + def context(name, &block) + context = Spec::Runner::Context.new(name, &block) + context_runner.add_context(context) + end + +private + + def context_runner + # TODO: Figure out a better way to get this considered "covered" and keep this statement on multiple lines + unless $context_runner; \ + $context_runner = ::Spec::Runner::OptionParser.new.create_context_runner(ARGV.dup, STDERR, STDOUT, false); \ + at_exit { $context_runner.run(false) }; \ + end + $context_runner + end +end diff --git a/test/lib/spec/runner/extensions/object.rb b/test/lib/spec/runner/extensions/object.rb new file mode 100644 index 000000000..49745352f --- /dev/null +++ b/test/lib/spec/runner/extensions/object.rb @@ -0,0 +1,32 @@ +# The following copyright applies to Object#copy_instance_variables_from, +# which we borrowed from active_support. +# +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +class Object + # From active_support + def copy_instance_variables_from(object, exclude = []) # :nodoc: + exclude += object.protected_instance_variables if object.respond_to? :protected_instance_variables + + instance_variables = object.instance_variables - exclude.map { |name| name.to_s } + instance_variables.each { |name| instance_variable_set(name, object.instance_variable_get(name)) } + end +end diff --git a/test/lib/spec/runner/formatter.rb b/test/lib/spec/runner/formatter.rb new file mode 100644 index 000000000..f62e81733 --- /dev/null +++ b/test/lib/spec/runner/formatter.rb @@ -0,0 +1,5 @@ +require 'spec/runner/formatter/base_text_formatter' +require 'spec/runner/formatter/progress_bar_formatter' +require 'spec/runner/formatter/rdoc_formatter' +require 'spec/runner/formatter/specdoc_formatter' +require 'spec/runner/formatter/html_formatter' diff --git a/test/lib/spec/runner/formatter/base_text_formatter.rb b/test/lib/spec/runner/formatter/base_text_formatter.rb new file mode 100644 index 000000000..31d1c3132 --- /dev/null +++ b/test/lib/spec/runner/formatter/base_text_formatter.rb @@ -0,0 +1,118 @@ +module Spec + module Runner + module Formatter + # Baseclass for text-based formatters. Can in fact be used for + # non-text based ones too - just ignore the +output+ constructor + # argument. + class BaseTextFormatter + def initialize(output, dry_run=false, colour=false) + @output = output + @dry_run = dry_run + @colour = colour + begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end + end + + # This method is invoked before any specs are run, right after + # they have all been collected. This can be useful for special + # formatters that need to provide progress on feedback (graphical ones) + # + # This method will only be invoked once, and the next one to be invoked + # is #add_context + def start(spec_count) + end + + # This method is invoked at the beginning of the execution of each context. + # +name+ is the name of the context and +first+ is true if it is the + # first context - otherwise it's false. + # + # The next method to be invoked after this is #spec_started + def add_context(name, first) + end + + # This method is invoked right before a spec is executed. + # The next method to be invoked after this one is one of #spec_failed + # or #spec_passed. + def spec_started(name) + end + + # This method is invoked when a spec fails, i.e. an exception occurred + # inside it (such as a failed should or other exception). +name+ is the name + # of the specification. +counter+ is the sequence number of the failure + # (starting at 1) and +failure+ is the associated Failure object. + def spec_failed(name, counter, failure) + end + + # This method is invoked when a spec passes. +name+ is the name of the + # specification. + def spec_passed(name) + end + + # This method is invoked after all of the specs have executed. The next method + # to be invoked after this one is #dump_failure (once for each failed spec), + def start_dump + end + + # Dumps detailed information about a spec failure. + # This method is invoked for each failed spec after all specs have run. +counter+ is the sequence number + # of the associated spec. +failure+ is a Failure object, which contains detailed + # information about the failure. + def dump_failure(counter, failure) + @output.puts + @output.puts "#{counter.to_s})" + if(failure.expectation_not_met?) + @output.puts red(failure.header) + @output.puts red(failure.exception.message) + else + @output.puts magenta(failure.header) + @output.puts magenta(failure.exception.message) + end + @output.puts format_backtrace(failure.exception.backtrace) + STDOUT.flush + end + + # This method is invoked at the very end. + def dump_summary(duration, spec_count, failure_count) + return if @dry_run + @output.puts + @output.puts "Finished in #{duration} seconds" + @output.puts + summary = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}" + if failure_count == 0 + @output.puts green(summary) + else + @output.puts red(summary) + end + end + + def format_backtrace(backtrace) + return "" if backtrace.nil? + backtrace.map { |line| backtrace_line(line) }.join("\n") + end + + protected + + def backtrace_line(line) + line.sub(/\A([^:]+:\d+)$/, '\\1:') + end + + def colour(text, colour_code) + return text unless @colour && output_to_tty? + "#{colour_code}#{text}\e[0m" + end + + def output_to_tty? + begin + @output == Kernel || @output.tty? + rescue NoMethodError + false + end + end + + def red(text); colour(text, "\e[31m"); end + def green(text); colour(text, "\e[32m"); end + def magenta(text); colour(text, "\e[35m"); end + + end + end + end +end diff --git a/test/lib/spec/runner/formatter/html_formatter.rb b/test/lib/spec/runner/formatter/html_formatter.rb new file mode 100644 index 000000000..13b796581 --- /dev/null +++ b/test/lib/spec/runner/formatter/html_formatter.rb @@ -0,0 +1,219 @@ +module Spec + module Runner + module Formatter + class HtmlFormatter < BaseTextFormatter + attr_reader :current_spec_number, :current_context_number + + def initialize(output, dry_run=false, colour=false) + super + @current_spec_number = 0 + @current_context_number = 0 + end + + def start(spec_count) + @spec_count = spec_count + + @output.puts HEADER_1 + @output.puts extra_header_content unless extra_header_content.nil? + @output.puts HEADER_2 + STDOUT.flush + end + + def add_context(name, first) + @current_context_number += 1 + unless first + @output.puts " </dl>" + @output.puts "</div>" + end + @output.puts "<div class=\"context\">" + @output.puts " <dl>" + @output.puts " <dt id=\"context_#{@current_context_number}\">#{name}</dt>" + STDOUT.flush + end + + def start_dump + @output.puts " </dl>" + @output.puts "</div>" + STDOUT.flush + end + + def spec_started(name) + @current_spec_number += 1 + STDOUT.flush + end + + def spec_passed(name) + move_progress + @output.puts " <dd class=\"spec passed\"><span class=\"passed_spec_name\">#{escape(name)}</span></dd>" + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.puts " <script type=\"text/javascript\">makeRed('header');</script>" + @output.puts " <script type=\"text/javascript\">makeRed('context_#{@current_context_number}');</script>" + move_progress + @output.puts " <dd class=\"spec failed\">" + @output.puts " <span class=\"failed_spec_name\">#{escape(name)}</span>" + @output.puts " <div class=\"failure\" id=\"failure_#{counter}\">" + @output.puts " <div class=\"message\"><pre>#{escape(failure.exception.message)}</pre></div>" unless failure.exception.nil? + @output.puts " <div class=\"backtrace\"><pre>#{format_backtrace(failure.exception.backtrace)}</pre></div>" unless failure.exception.nil? + @output.puts extra_failure_content unless extra_failure_content.nil? + @output.puts " </div>" + @output.puts " </dd>" + STDOUT.flush + end + + # Override this method if you wish to output extra HTML in the header + # + def extra_header_content + end + + # Override this method if you wish to output extra HTML for a failed spec. For example, you + # could output links to images or other files produced during the specs. + # + def extra_failure_content + end + + def move_progress + percent_done = @spec_count == 0 ? 100.0 : (@current_spec_number.to_f / @spec_count.to_f * 1000).to_i / 10.0 + @output.puts " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>" + end + + def escape(string) + string.gsub(/&/n, '&').gsub(/\"/n, '"').gsub(/>/n, '>').gsub(/</n, '<') + end + + def dump_failure(counter, failure) + end + + def dump_summary(duration, spec_count, failure_count) + if @dry_run + totals = "This was a dry-run" + else + totals = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}" + end + @output.puts "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{duration} seconds</strong>\";</script>" + @output.puts "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{totals}\";</script>" + @output.puts "</div>" + @output.puts "</body>" + @output.puts "</html>" + STDOUT.flush + end + + HEADER_1 = <<-EOF +<?xml version="1.0" encoding="iso-8859-1"?> +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <title>RSpec results</title> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> + <meta http-equiv="Expires" content="-1" /> + <meta http-equiv="Pragma" content="no-cache" /> +EOF + + HEADER_2 = <<-EOF + <script type="text/javascript"> + function moveProgressBar(percentDone) { + document.getElementById("header").style.width = percentDone +"%"; + } + function makeRed(element_id) { + document.getElementById(element_id).style.background = '#C40D0D'; + } + </script> + <style type="text/css"> + body { + margin: 0; padding: 0; + background: #fff; + } + + #header { + background: #65C400; color: #fff; + } + + h1 { + margin: 0 0 10px; + padding: 10px; + font: bold 18px "Lucida Grande", Helvetica, sans-serif; + } + + #summary { + margin: 0; padding: 5px 10px; + font: bold 10px "Lucida Grande", Helvetica, sans-serif; + text-align: right; + position: absolute; + top: 0px; + right: 0px; + } + + #summary p { + margin: 0 0 2px; + } + + #summary #totals { + font-size: 14px; + } + + .context { + margin: 0 10px 5px; + background: #fff; + } + + dl { + margin: 0; padding: 0 0 5px; + font: normal 11px "Lucida Grande", Helvetica, sans-serif; + } + + dt { + padding: 3px; + background: #65C400; + color: #fff; + font-weight: bold; + } + + dd { + margin: 5px 0 5px 5px; + padding: 3px 3px 3px 18px; + } + + dd.spec.passed { + border-left: 5px solid #65C400; + border-bottom: 1px solid #65C400; + background: #DBFFB4; color: #3D7700; + } + + dd.spec.failed { + border-left: 5px solid #C20000; + border-bottom: 1px solid #C20000; + color: #C20000; background: #FFFBD3; + } + + div.backtrace { + color: #000; + font-size: 12px; + } + + a { + color: #BE5C00; + } + </style> +</head> +<body> + +<div id="header"> + <h1>RSpec Results</h1> + + <div id="summary"> + <p id="duration"> </p> + <p id="totals"> </p> + </div> +</div> + +<div id="results"> +EOF + end + end + end +end diff --git a/test/lib/spec/runner/formatter/progress_bar_formatter.rb b/test/lib/spec/runner/formatter/progress_bar_formatter.rb new file mode 100644 index 000000000..fe519d4d8 --- /dev/null +++ b/test/lib/spec/runner/formatter/progress_bar_formatter.rb @@ -0,0 +1,27 @@ +module Spec + module Runner + module Formatter + class ProgressBarFormatter < BaseTextFormatter + def add_context(name, first) + @output.puts if first + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.print failure.expectation_not_met? ? red('F') : magenta('F') + STDOUT.flush + end + + def spec_passed(name) + @output.print green('.') + STDOUT.flush + end + + def start_dump + @output.puts + STDOUT.flush + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/formatter/rdoc_formatter.rb b/test/lib/spec/runner/formatter/rdoc_formatter.rb new file mode 100644 index 000000000..eae55c3ea --- /dev/null +++ b/test/lib/spec/runner/formatter/rdoc_formatter.rb @@ -0,0 +1,22 @@ +module Spec + module Runner + module Formatter + class RdocFormatter < BaseTextFormatter + def add_context(name, first) + @output.print "# #{name}\n" + STDOUT.flush + end + + def spec_passed(name) + @output.print "# * #{name}\n" + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.print "# * #{name} [#{counter} - FAILED]\n" + STDOUT.flush + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/formatter/specdoc_formatter.rb b/test/lib/spec/runner/formatter/specdoc_formatter.rb new file mode 100644 index 000000000..67b4312bf --- /dev/null +++ b/test/lib/spec/runner/formatter/specdoc_formatter.rb @@ -0,0 +1,23 @@ +module Spec + module Runner + module Formatter + class SpecdocFormatter < BaseTextFormatter + def add_context(name, first) + @output.puts + @output.puts name + STDOUT.flush + end + + def spec_failed(name, counter, failure) + @output.puts failure.expectation_not_met? ? red("- #{name} (FAILED - #{counter})") : magenta("- #{name} (ERROR - #{counter})") + STDOUT.flush + end + + def spec_passed(name) + @output.print green("- #{name}\n") + STDOUT.flush + end + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/heckle_runner.rb b/test/lib/spec/runner/heckle_runner.rb new file mode 100644 index 000000000..fd36389de --- /dev/null +++ b/test/lib/spec/runner/heckle_runner.rb @@ -0,0 +1,71 @@ +begin + require 'rubygems' + require 'heckle' +rescue LoadError ; raise "You must gem install heckle to use --heckle" ; end + +module Spec + module Runner + # Creates a new Heckler configured to heckle all methods in the classes + # whose name matches +filter+ + class HeckleRunner + def initialize(filter, heckle_class=Heckler) + @filter = filter + @heckle_class = heckle_class + end + + # Runs all the contexts held by +context_runner+ once for each of the + # methods in the matched classes. + def heckle_with(context_runner) + if @filter =~ /(.*)[#\.](.*)/ + heckle_method($1, $2) + else + heckle_class_or_module(@filter) + end + end + + def heckle_method(class_name, method_name) + verify_constant(class_name) + heckle = @heckle_class.new(class_name, method_name, context_runner) + heckle.validate + end + + def heckle_class_or_module(class_or_module_name) + verify_constant(class_or_module_name) + pattern = /^#{class_or_module_name}/ + classes = [] + ObjectSpace.each_object(Class) do |klass| + classes << klass if klass.name =~ pattern + end + + classes.each do |klass| + klass.instance_methods(false).each do |method_name| + heckle = @heckle_class.new(klass.name, method_name, context_runner) + heckle.validate + end + end + end + + def verify_constant(name) + begin + # This is defined in Heckle + name.to_class + rescue + raise "Heckling failed - \"#{name}\" is not a known class or module" + end + end + end + + #Supports Heckle 1.2 and prior (earlier versions used Heckle::Base) + class Heckler < (Heckle.const_defined?(:Base) ? Heckle::Base : Heckle) + def initialize(klass_name, method_name, context_runner) + super(klass_name, method_name) + @context_runner = context_runner + end + + def tests_pass? + failure_count = @context_runner.run(false) + failure_count == 0 + end + end + end +end diff --git a/test/lib/spec/runner/heckle_runner_win.rb b/test/lib/spec/runner/heckle_runner_win.rb new file mode 100644 index 000000000..031386599 --- /dev/null +++ b/test/lib/spec/runner/heckle_runner_win.rb @@ -0,0 +1,10 @@ +module Spec
+ module Runner
+ # Dummy implementation for Windows that just fails (Heckle is not supported on Windows)
+ class HeckleRunner
+ def initialize(filter)
+ raise "Heckle not supported on Windows"
+ end
+ end
+ end
+end
diff --git a/test/lib/spec/runner/option_parser.rb b/test/lib/spec/runner/option_parser.rb new file mode 100644 index 000000000..38725d848 --- /dev/null +++ b/test/lib/spec/runner/option_parser.rb @@ -0,0 +1,224 @@ +require 'ostruct' +require 'optparse' +require 'spec/runner/spec_parser' +require 'spec/runner/formatter' +require 'spec/runner/backtrace_tweaker' +require 'spec/runner/reporter' +require 'spec/runner/context_runner' + +module Spec + module Runner + class OptionParser + def initialize + @spec_parser = SpecParser.new + @file_factory = File + end + + def create_context_runner(args, err, out, warn_if_no_files) + options = parse(args, err, out, warn_if_no_files) + # Some exit points in parse (--generate-options, --drb) don't return the options, + # but hand over control. In that case we don't want to continue. + return nil unless options.is_a?(OpenStruct) + + formatter = options.formatter_type.new(options.out, options.dry_run, options.colour) + options.reporter = Reporter.new(formatter, options.backtrace_tweaker) + + # this doesn't really belong here. + # it should, but the way things are coupled, it doesn't + if options.differ_class + Spec::Expectations.differ = options.differ_class.new(options.diff_format, options.context_lines, options.colour) + end + + unless options.generate + ContextRunner.new(options) + end + end + + def parse(args, err, out, warn_if_no_files) + options_file = nil + args_copy = args.dup + options = OpenStruct.new + options.out = (out == STDOUT ? Kernel : out) + options.formatter_type = Formatter::ProgressBarFormatter + options.backtrace_tweaker = QuietBacktraceTweaker.new + options.spec_name = nil + + opts = ::OptionParser.new do |opts| + opts.banner = "Usage: spec [options] (FILE|DIRECTORY|GLOB)+" + opts.separator "" + + opts.on("-D", "--diff [FORMAT]", "Show diff of objects that are expected to be equal when they are not", + "Builtin formats: unified|u|context|c", + "You can also specify a custom differ class", + "(in which case you should also specify --require)") do |format| + + # TODO make context_lines settable + options.context_lines = 3 + + case format + when 'context', 'c' + options.diff_format = :context + when 'unified', 'u', '', nil + options.diff_format = :unified + end + + if [:context,:unified].include? options.diff_format + require 'spec/expectations/differs/default' + options.differ_class = Spec::Expectations::Differs::Default + else + begin + options.diff_format = :custom + options.differ_class = eval(format) + rescue NameError + err.puts "Couldn't find differ class #{format}" + err.puts "Make sure the --require option is specified *before* --diff" + exit if out == $stdout + end + end + + end + + opts.on("-c", "--colour", "--color", "Show coloured (red/green) output") do + options.colour = true + end + + opts.on("-s", "--spec SPECIFICATION_NAME", "Execute context or specification with matching name") do |spec_name| + options.spec_name = spec_name + end + + opts.on("-l", "--line LINE_NUMBER", Integer, "Execute context or specification at given line") do |line_number| + options.line_number = line_number.to_i + end + + opts.on("-f", "--format FORMAT", "Builtin formats: specdoc|s|rdoc|r|html|h", + "You can also specify a custom formatter class", + "(in which case you should also specify --require)") do |format| + case format + when 'specdoc', 's' + options.formatter_type = Formatter::SpecdocFormatter + when 'html', 'h' + options.formatter_type = Formatter::HtmlFormatter + when 'rdoc', 'r' + options.formatter_type = Formatter::RdocFormatter + options.dry_run = true + else + begin + options.formatter_type = eval(format) + rescue NameError + err.puts "Couldn't find formatter class #{format}" + err.puts "Make sure the --require option is specified *before* --format" + exit if out == $stdout + end + end + end + + opts.on("-r", "--require FILE", "Require FILE before running specs", + "Useful for loading custom formatters or other extensions", + "If this option is used it must come before the others") do |req| + req.split(",").each{|file| require file} + end + + opts.on("-b", "--backtrace", "Output full backtrace") do + options.backtrace_tweaker = NoisyBacktraceTweaker.new + end + + opts.on("-H", "--heckle CODE", "If all specs pass, this will run your specs many times, mutating", + "the specced code a little each time. The intent is that specs", + "*should* fail, and RSpec will tell you if they don't.", + "CODE should be either Some::Module, Some::Class or Some::Fabulous#method}") do |heckle| + heckle_runner = PLATFORM == 'i386-mswin32' ? 'spec/runner/heckle_runner_win' : 'spec/runner/heckle_runner' + require heckle_runner + options.heckle_runner = HeckleRunner.new(heckle) + end + + opts.on("-d", "--dry-run", "Don't execute specs") do + options.dry_run = true + end + + opts.on("-o", "--out OUTPUT_FILE", "Path to output file (defaults to STDOUT)") do |out_file| + options.out = File.new(out_file, 'w') + end + + opts.on("-O", "--options PATH", "Read options from a file") do |options_file| + # Remove the --options option and the argument before writing to file + index = args_copy.index("-O") || args_copy.index("--options") + args_copy.delete_at(index) + args_copy.delete_at(index) + + new_args = args_copy + IO.readlines(options_file).each {|s| s.chomp!} + return CommandLine.run(new_args, err, out, true, warn_if_no_files) + end + + opts.on("-G", "--generate-options PATH", "Generate an options file for --options") do |options_file| + # Remove the --generate-options option and the argument before writing to file + index = args_copy.index("-G") || args_copy.index("--generate-options") + args_copy.delete_at(index) + args_copy.delete_at(index) + + File.open(options_file, 'w') do |io| + io.puts args_copy.join("\n") + end + out.puts "\nOptions written to #{options_file}. You can now use these options with:" + out.puts "spec --options #{options_file}" + options.generate = true + end + + opts.on("-X", "--drb", "Run specs via DRb. (For example against script/rails_spec_server)") do |options_file| + # Remove the --options option and the argument before writing to file + index = args_copy.index("-X") || args_copy.index("--drb") + args_copy.delete_at(index) + + return DrbCommandLine.run(args_copy, err, out, true, warn_if_no_files) + end + + opts.on("-v", "--version", "Show version") do + out.puts ::Spec::VERSION::DESCRIPTION + exit if out == $stdout + end + + opts.on_tail("-h", "--help", "You're looking at it") do + out.puts opts + exit if out == $stdout + end + + end + opts.parse!(args) + + if args.empty? && warn_if_no_files + err.puts "No files specified." + err.puts opts + exit(6) if err == $stderr + end + + if options.line_number + set_spec_from_line_number(options, args, err) + end + + options + end + + def set_spec_from_line_number(options, args, err) + unless options.spec_name + if args.length == 1 + if @file_factory.file?(args[0]) + source = @file_factory.open(args[0]) + options.spec_name = @spec_parser.spec_name_for(source, options.line_number) + elsif @file_factory.directory?(args[0]) + err.puts "You must specify one file, not a directory when using the --line option" + exit(1) if err == $stderr + else + err.puts "#{args[0]} does not exist" + exit(2) if err == $stderr + end + else + err.puts "Only one file can be specified when using the --line option: #{args.inspect}" + exit(3) if err == $stderr + end + else + err.puts "You cannot use both --line and --spec" + exit(4) if err == $stderr + end + end + end + end +end diff --git a/test/lib/spec/runner/reporter.rb b/test/lib/spec/runner/reporter.rb new file mode 100644 index 000000000..e4fb1cb0e --- /dev/null +++ b/test/lib/spec/runner/reporter.rb @@ -0,0 +1,105 @@ +module Spec + module Runner + class Reporter + + def initialize(formatter, backtrace_tweaker) + @formatter = formatter + @backtrace_tweaker = backtrace_tweaker + clear! + end + + def add_context(name) + #TODO - @context_names.empty? tells the formatter whether this is the first context or not - that's a little slippery + @formatter.add_context(name, @context_names.empty?) + @context_names << name + end + + def spec_started(name) + @spec_names << name + @formatter.spec_started(name) + end + + def spec_finished(name, error=nil, failure_location=nil) + if error.nil? + spec_passed(name) + else + @backtrace_tweaker.tweak_backtrace(error, failure_location) + spec_failed(name, Failure.new(@context_names.last, name, error)) + end + end + + def start(number_of_specs) + clear! + @start_time = Time.new + @formatter.start(number_of_specs) + end + + def end + @end_time = Time.new + end + + # Dumps the summary and returns the total number of failures + def dump + @formatter.start_dump + dump_failures + @formatter.dump_summary(duration, @spec_names.length, @failures.length) + @failures.length + end + + private + + def clear! + @context_names = [] + @failures = [] + @spec_names = [] + @start_time = nil + @end_time = nil + end + + def dump_failures + return if @failures.empty? + @failures.inject(1) do |index, failure| + @formatter.dump_failure(index, failure) + index + 1 + end + end + + def duration + return @end_time - @start_time unless (@end_time.nil? or @start_time.nil?) + return "0.0" + end + + def spec_passed(name) + @formatter.spec_passed(name) + end + + def spec_failed(name, failure) + @failures << failure + @formatter.spec_failed(name, @failures.length, failure) + end + + class Failure + attr_reader :exception + + def initialize(context_name, spec_name, exception) + @context_name = context_name + @spec_name = spec_name + @exception = exception + end + + def header + if expectation_not_met? + "'#{@context_name} #{@spec_name}' FAILED" + else + "#{@exception.class.name} in '#{@context_name} #{@spec_name}'" + end + end + + def expectation_not_met? + @exception.is_a?(Spec::Expectations::ExpectationNotMetError) + end + + end + end + end +end diff --git a/test/lib/spec/runner/spec_matcher.rb b/test/lib/spec/runner/spec_matcher.rb new file mode 100755 index 000000000..687fdaa00 --- /dev/null +++ b/test/lib/spec/runner/spec_matcher.rb @@ -0,0 +1,25 @@ +module Spec + module Runner + class SpecMatcher + + attr_writer :spec_desc + def initialize(context_desc, spec_desc=nil) + @context_desc = context_desc + @spec_desc = spec_desc + end + + def matches?(desc) + desc =~ /(^#{context_regexp} #{spec_regexp}$|^#{context_regexp}$|^#{spec_regexp}$)/ + end + + private + def context_regexp + Regexp.escape(@context_desc) + end + + def spec_regexp + Regexp.escape(@spec_desc) + end + end + end +end diff --git a/test/lib/spec/runner/spec_parser.rb b/test/lib/spec/runner/spec_parser.rb new file mode 100644 index 000000000..2cb8518fc --- /dev/null +++ b/test/lib/spec/runner/spec_parser.rb @@ -0,0 +1,41 @@ +module Spec + module Runner + # Parses a spec file and finds the nearest spec for a given line number. + class SpecParser + def spec_name_for(io, line_number) + source = io.read + context = context_at_line(source, line_number) + spec = spec_at_line(source, line_number) + if context && spec + "#{context} #{spec}" + elsif context + context + else + nil + end + end + + protected + + def context_at_line(source, line_number) + find_above(source, line_number, /^\s*context\s+['|"](.*)['|"]/) + end + + def spec_at_line(source, line_number) + find_above(source, line_number, /^\s*specify\s+['|"](.*)['|"]/) + end + + def find_above(source, line_number, pattern) + lines_above_reversed(source, line_number).each do |line| + return $1 if line =~ pattern + end + nil + end + + def lines_above_reversed(source, line_number) + lines = source.split("\n") + lines[0...line_number].reverse + end + end + end +end
\ No newline at end of file diff --git a/test/lib/spec/runner/spec_should_raise_handler.rb b/test/lib/spec/runner/spec_should_raise_handler.rb new file mode 100644 index 000000000..c7fa41c4e --- /dev/null +++ b/test/lib/spec/runner/spec_should_raise_handler.rb @@ -0,0 +1,74 @@ +module Spec + module Runner + class SpecShouldRaiseHandler + def initialize(file_and_line_number, opts) + @file_and_line_number = file_and_line_number + @options = opts + @expected_error_class = determine_error_class(opts) + @expected_error_message = determine_error_message(opts) + end + + def determine_error_class(opts) + if candidate = opts[:should_raise] + if candidate.is_a?(Class) + return candidate + elsif candidate.is_a?(Array) + return candidate[0] + else + return Exception + end + end + end + + def determine_error_message(opts) + if candidate = opts[:should_raise] + if candidate.is_a?(Array) + return candidate[1] + end + end + return nil + end + + def build_message(exception=nil) + if @expected_error_message.nil? + message = "specify block expected #{@expected_error_class.to_s}" + else + message = "specify block expected #{@expected_error_class.new(@expected_error_message.to_s).inspect}" + end + message << " but raised #{exception.inspect}" if exception + message << " but nothing was raised" unless exception + message << "\n" + message << @file_and_line_number + end + + def error_matches?(error) + return false unless error.kind_of?(@expected_error_class) + unless @expected_error_message.nil? + if @expected_error_message.is_a?(Regexp) + return false unless error.message =~ @expected_error_message + else + return false unless error.message == @expected_error_message + end + end + return true + end + + def handle(errors) + if @expected_error_class + if errors.empty? + errors << Spec::Expectations::ExpectationNotMetError.new(build_message) + else + error_to_remove = errors.detect do |error| + error_matches?(error) + end + if error_to_remove.nil? + errors.insert(0,Spec::Expectations::ExpectationNotMetError.new(build_message(errors[0]))) + else + errors.delete(error_to_remove) + end + end + end + end + end + end +end diff --git a/test/lib/spec/runner/specification.rb b/test/lib/spec/runner/specification.rb new file mode 100644 index 000000000..de8d750fd --- /dev/null +++ b/test/lib/spec/runner/specification.rb @@ -0,0 +1,114 @@ +module Spec + module Runner + class Specification + + class << self + attr_accessor :current, :generated_description + protected :current= + + callback_events :before_setup, :after_teardown + end + + attr_reader :spec_block + callback_events :before_setup, :after_teardown + + def initialize(name, opts={}, &spec_block) + @from = caller(0)[3] + @description = name + @options = opts + @spec_block = spec_block + @description_generated_callback = lambda { |desc| @generated_description = desc } + end + + def run(reporter, setup_block, teardown_block, dry_run, execution_context) + reporter.spec_started(name) if reporter + return reporter.spec_finished(name) if dry_run + + errors = [] + begin + set_current + setup_ok = setup_spec(execution_context, errors, &setup_block) + spec_ok = execute_spec(execution_context, errors) if setup_ok + teardown_ok = teardown_spec(execution_context, errors, &teardown_block) + ensure + clear_current + end + + SpecShouldRaiseHandler.new(@from, @options).handle(errors) + reporter.spec_finished(name, errors.first, failure_location(setup_ok, spec_ok, teardown_ok)) if reporter + end + + def matches?(matcher, description) + matcher.spec_desc = name + matcher.matches?(description) + end + + private + def name + @description == :__generate_description ? generated_description : @description + end + + def generated_description + @generated_description || "NAME NOT GENERATED" + end + + def setup_spec(execution_context, errors, &setup_block) + notify_before_setup(errors) + execution_context.instance_eval(&setup_block) if setup_block + return errors.empty? + rescue => e + errors << e + return false + end + + def execute_spec(execution_context, errors) + begin + execution_context.instance_eval(&spec_block) + return true + rescue Exception => e + errors << e + return false + end + end + + def teardown_spec(execution_context, errors, &teardown_block) + execution_context.instance_eval(&teardown_block) if teardown_block + notify_after_teardown(errors) + return errors.empty? + rescue => e + errors << e + return false + end + + def notify_before_setup(errors) + notify_class_callbacks(:before_setup, self, &append_errors(errors)) + notify_callbacks(:before_setup, self, &append_errors(errors)) + end + + def notify_after_teardown(errors) + notify_callbacks(:after_teardown, self, &append_errors(errors)) + notify_class_callbacks(:after_teardown, self, &append_errors(errors)) + end + + def append_errors(errors) + proc {|error| errors << error} + end + + def set_current + Spec::Matchers.description_generated(&@description_generated_callback) + self.class.send(:current=, self) + end + + def clear_current + Spec::Matchers.unregister_callback(:description_generated, @description_generated_callback) + self.class.send(:current=, nil) + end + + def failure_location(setup_ok, spec_ok, teardown_ok) + return 'setup' unless setup_ok + return name unless spec_ok + return 'teardown' unless teardown_ok + end + end + end +end |
