rspec/rspec-core

View on GitHub
lib/rspec/core/formatters.rb

Summary

Maintainability
A
3 hrs
Test Coverage
RSpec::Support.require_rspec_support "directory_maker"

# ## Built-in Formatters
#
# * progress (default) - Prints dots for passing examples, `F` for failures, `*`
#                        for pending.
# * documentation - Prints the docstrings passed to `describe` and `it` methods
#                   (and their aliases).
# * html
# * json - Useful for archiving data for subsequent analysis.
#
# The progress formatter is the default, but you can choose any one or more of
# the other formatters by passing with the `--format` (or `-f` for short)
# command-line option, e.g.
#
#     rspec --format documentation
#
# You can also send the output of multiple formatters to different streams, e.g.
#
#     rspec --format documentation --format html --out results.html
#
# This example sends the output of the documentation formatter to `$stdout`, and
# the output of the html formatter to results.html.
#
# ## Custom Formatters
#
# You can tell RSpec to use a custom formatter by passing its path and name to
# the `rspec` command. For example, if you define MyCustomFormatter in
# path/to/my_custom_formatter.rb, you would type this command:
#
#     rspec --require path/to/my_custom_formatter.rb --format MyCustomFormatter
#
# The reporter calls every formatter with this protocol:
#
# * To start
#   * `start(StartNotification)`
# * Once per example group
#   * `example_group_started(GroupNotification)`
# * Once per example
#   * `example_started(ExampleNotification)`
# * One of these per example, depending on outcome
#   * `example_passed(ExampleNotification)`
#   * `example_failed(FailedExampleNotification)`
#   * `example_pending(ExampleNotification)`
# * Optionally at any time
#   * `message(MessageNotification)`
# * At the end of the suite
#   * `stop(ExamplesNotification)`
#   * `start_dump(NullNotification)`
#   * `dump_pending(ExamplesNotification)`
#   * `dump_failures(ExamplesNotification)`
#   * `dump_summary(SummaryNotification)`
#   * `seed(SeedNotification)`
#   * `close(NullNotification)`
#
# Only the notifications to which you subscribe your formatter will be called
# on your formatter. To subscribe your formatter use:
# `RSpec::Core::Formatters#register` e.g.
#
# `RSpec::Core::Formatters.register FormatterClassName, :example_passed, :example_failed`
#
# We recommend you implement the methods yourself; for simplicity we provide the
# default formatter output via our notification objects but if you prefer you
# can subclass `RSpec::Core::Formatters::BaseTextFormatter` and override the
# methods you wish to enhance.
#
# @see RSpec::Core::Formatters::BaseTextFormatter
# @see RSpec::Core::Reporter
module RSpec::Core::Formatters
  autoload :DocumentationFormatter,   'rspec/core/formatters/documentation_formatter'
  autoload :HtmlFormatter,            'rspec/core/formatters/html_formatter'
  autoload :FallbackMessageFormatter, 'rspec/core/formatters/fallback_message_formatter'
  autoload :ProgressFormatter,        'rspec/core/formatters/progress_formatter'
  autoload :ProfileFormatter,         'rspec/core/formatters/profile_formatter'
  autoload :JsonFormatter,            'rspec/core/formatters/json_formatter'
  autoload :BisectDRbFormatter,       'rspec/core/formatters/bisect_drb_formatter'
  autoload :ExceptionPresenter,       'rspec/core/formatters/exception_presenter'
  autoload :FailureListFormatter,     'rspec/core/formatters/failure_list_formatter'

  # Register the formatter class
  # @param formatter_class [Class] formatter class to register
  # @param notifications [Symbol, ...] one or more notifications to be
  #   registered to the specified formatter
  #
  # @see RSpec::Core::Formatters::BaseFormatter
  def self.register(formatter_class, *notifications)
    Loader.formatters[formatter_class] = notifications
  end

  # @api private
  #
  # `RSpec::Core::Formatters::Loader` is an internal class for
  # managing formatters used by a particular configuration. It is
  # not expected to be used directly, but only through the configuration
  # interface.
  class Loader
    # @api private
    #
    # Internal formatters are stored here when loaded.
    def self.formatters
      @formatters ||= {}
    end

    # @api private
    def initialize(reporter)
      @formatters = []
      @reporter = reporter
      self.default_formatter = 'progress'
    end

    # @return [Array] the loaded formatters
    attr_reader :formatters

    # @return [Reporter] the reporter
    attr_reader :reporter

    # @return [String] the default formatter to setup, defaults to `progress`
    attr_accessor :default_formatter

    # @private
    def prepare_default(output_stream, deprecation_stream)
      reporter.prepare_default(self, output_stream, deprecation_stream)
    end

    # @private
    def setup_default(output_stream, deprecation_stream)
      add default_formatter, output_stream if @formatters.empty?

      unless @formatters.any? { |formatter| DeprecationFormatter === formatter }
        add DeprecationFormatter, deprecation_stream, output_stream
      end

      unless existing_formatter_implements?(:message)
        add FallbackMessageFormatter, output_stream
      end

      return unless RSpec.configuration.profile_examples?
      return if existing_formatter_implements?(:dump_profile)

      add RSpec::Core::Formatters::ProfileFormatter, output_stream
    end

    # @private
    def add(formatter_to_use, *paths)
      # If a formatter instance was passed, we can register it directly,
      # with no need for any of the further processing that happens below.
      if Loader.formatters.key?(formatter_to_use.class)
        register formatter_to_use, notifications_for(formatter_to_use.class)
        return
      end

      formatter_class = find_formatter(formatter_to_use)

      args = paths.map { |p| p.respond_to?(:puts) ? p : open_stream(p) }

      if !Loader.formatters[formatter_class].nil?
        formatter = formatter_class.new(*args)
        register formatter, notifications_for(formatter_class)
      elsif defined?(RSpec::LegacyFormatters)
        formatter = RSpec::LegacyFormatters.load_formatter formatter_class, *args
        register formatter, formatter.notifications
      else
        call_site = "Formatter added at: #{::RSpec::CallerFilter.first_non_rspec_line}"

        RSpec.warn_deprecation <<-WARNING.gsub(/\s*\|/, ' ')
          |The #{formatter_class} formatter uses the deprecated formatter
          |interface not supported directly by RSpec 3.
          |
          |To continue to use this formatter you must install the
          |`rspec-legacy_formatters` gem, which provides support
          |for legacy formatters or upgrade the formatter to a
          |compatible version.
          |
          |#{call_site}
        WARNING
      end
    end

  private

    def find_formatter(formatter_to_use)
      built_in_formatter(formatter_to_use) ||
      custom_formatter(formatter_to_use)   ||
      (raise ArgumentError, "Formatter '#{formatter_to_use}' unknown - " \
                            "maybe you meant 'documentation' or 'progress'?.")
    end

    def register(formatter, notifications)
      return if duplicate_formatter_exists?(formatter)
      @reporter.register_listener formatter, *notifications
      @formatters << formatter
      formatter
    end

    def duplicate_formatter_exists?(new_formatter)
      @formatters.any? do |formatter|
        formatter.class == new_formatter.class && formatter.output == new_formatter.output
      end
    end

    def existing_formatter_implements?(notification)
      @reporter.registered_listeners(notification).any?
    end

    def built_in_formatter(key)
      case key.to_s
      when 'd', 'doc', 'documentation'
        DocumentationFormatter
      when 'h', 'html'
        HtmlFormatter
      when 'p', 'progress'
        ProgressFormatter
      when 'j', 'json'
        JsonFormatter
      when 'bisect-drb'
        BisectDRbFormatter
      when 'f', 'failures'
        FailureListFormatter
      end
    end

    def notifications_for(formatter_class)
      formatter_class.ancestors.inject(::RSpec::Core::Set.new) do |notifications, klass|
        notifications.merge Loader.formatters.fetch(klass) { ::RSpec::Core::Set.new }
      end
    end

    def custom_formatter(formatter_ref)
      if Class === formatter_ref
        formatter_ref
      elsif string_const?(formatter_ref)
        begin
          formatter_ref.gsub(/^::/, '').split('::').inject(Object) { |a, e| a.const_get e }
        rescue NameError
          require(path_for(formatter_ref)) ? retry : raise
        end
      end
    end

    def string_const?(str)
      str.is_a?(String) && /\A[A-Z][a-zA-Z0-9_:]*\z/ =~ str
    end

    def path_for(const_ref)
      underscore_with_fix_for_non_standard_rspec_naming(const_ref)
    end

    def underscore_with_fix_for_non_standard_rspec_naming(string)
      underscore(string).sub(%r{(^|/)r_spec($|/)}, '\\1rspec\\2')
    end

    # activesupport/lib/active_support/inflector/methods.rb, line 48
    def underscore(camel_cased_word)
      word = camel_cased_word.to_s.dup
      word.gsub!(/::/, '/')
      word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
      word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
      word.tr!("-", "_")
      word.downcase!
      word
    end

    def open_stream(path_or_wrapper)
      if RSpec::Core::OutputWrapper === path_or_wrapper
        path_or_wrapper.output = open_stream(path_or_wrapper.output)
        path_or_wrapper
      else
        RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(path_or_wrapper))
        File.new(path_or_wrapper, 'w')
      end
    end
  end
end