module RSpec module Core module Formatters # @private class ExceptionPresenter attr_reader :exception, :example, :description, :message_color, :detail_formatter, :extra_detail_formatter, :backtrace_formatter private :message_color, :detail_formatter, :extra_detail_formatter, :backtrace_formatter def initialize(exception, example, options={}) @exception = exception @example = example @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color } @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self) @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} } @extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} } @backtrace_formatter = options.fetch(:backtrace_formatter) { RSpec.configuration.backtrace_formatter } @indentation = options.fetch(:indentation, 2) @skip_shared_group_trace = options.fetch(:skip_shared_group_trace, false) @failure_lines = options[:failure_lines] end def message_lines add_shared_group_lines(failure_lines, Notifications::NullColorizer) end def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) add_shared_group_lines(failure_lines, colorizer).map do |line| colorizer.wrap line, message_color end end def formatted_backtrace backtrace_formatter.format_backtrace(exception_backtrace, example.metadata) end def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted_backtrace.map do |backtrace_info| colorizer.wrap "# #{backtrace_info}", RSpec.configuration.detail_color end end def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) alignment_basis = "#{' ' * @indentation}#{failure_number}) " indentation = ' ' * alignment_basis.length "\n#{alignment_basis}#{description_and_detail(colorizer, indentation)}" \ "\n#{formatted_message_and_backtrace(colorizer, indentation)}" \ "#{extra_detail_formatter.call(failure_number, colorizer, indentation)}" end def failure_slash_error_line @failure_slash_error_line ||= "Failure/Error: #{read_failed_line.strip}" end private def description_and_detail(colorizer, indentation) detail = detail_formatter.call(example, colorizer, indentation) return (description || detail) unless description && detail "#{description}\n#{indentation}#{detail}" end if String.method_defined?(:encoding) def encoding_of(string) string.encoding end def encoded_string(string) RSpec::Support::EncodedString.new(string, Encoding.default_external) end else # for 1.8.7 # :nocov: def encoding_of(_string) end def encoded_string(string) RSpec::Support::EncodedString.new(string) end # :nocov: end def exception_class_name name = exception.class.name.to_s name = "(anonymous error class)" if name == '' name end def failure_lines @failure_lines ||= begin lines = [] lines << failure_slash_error_line unless (description == failure_slash_error_line) lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ encoded_string(exception.message.to_s).split("\n").each do |line| lines << " #{line}" end lines end end def add_shared_group_lines(lines, colorizer) return lines if @skip_shared_group_trace example.metadata[:shared_group_inclusion_backtrace].each do |frame| lines << colorizer.wrap(frame.description, RSpec.configuration.default_color) end lines end def read_failed_line matching_line = find_failed_line unless matching_line return "Unable to find matching line from backtrace" end file_path, line_number = matching_line.match(/(.+?):(\d+)(|:\d+)/)[1..2] if File.exist?(file_path) File.readlines(file_path)[line_number.to_i - 1] || "Unable to find matching line in #{file_path}" else "Unable to find #{file_path} to read failed line" end rescue SecurityError "Unable to read failed line" end def find_failed_line example_path = example.metadata[:absolute_file_path].downcase exception_backtrace.find do |line| next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1]) File.expand_path(line_path).downcase == example_path end end def formatted_message_and_backtrace(colorizer, indentation) lines = colorized_message_lines(colorizer) + colorized_formatted_backtrace(colorizer) formatted = "" lines.each do |line| formatted << RSpec::Support::EncodedString.new("#{indentation}#{line}\n", encoding_of(formatted)) end formatted end def exception_backtrace exception.backtrace || [] end # @private # Configuring the `ExceptionPresenter` with the right set of options to handle # pending vs failed vs skipped and aggregated (or not) failures is not simple. # This class takes care of building an appropriate `ExceptionPresenter` for the # provided example. class Factory def build ExceptionPresenter.new(@exception, @example, options) end private def initialize(example) @example = example @execution_result = example.execution_result @exception = if @execution_result.status == :pending @execution_result.pending_exception else @execution_result.exception end end def options with_multiple_error_options_as_needed(@exception, pending_options || {}) end def pending_options if @execution_result.pending_fixed? { :description_formatter => Proc.new { "#{@example.full_description} FIXED" }, :message_color => RSpec.configuration.fixed_color, :failure_lines => [ "Expected pending '#{@execution_result.pending_message}' to fail. No Error was raised." ] } elsif @execution_result.status == :pending { :message_color => RSpec.configuration.pending_color, :detail_formatter => PENDING_DETAIL_FORMATTER } end end def with_multiple_error_options_as_needed(exception, options) return options unless multiple_exceptions_error?(exception) options = options.merge( :failure_lines => [], :extra_detail_formatter => sub_failure_list_formatter(exception, options[:message_color]), :detail_formatter => multiple_exception_summarizer(exception, options[:detail_formatter], options[:message_color]) ) options[:description_formatter] &&= Proc.new {} return options unless exception.aggregation_metadata[:hide_backtrace] options[:backtrace_formatter] = EmptyBacktraceFormatter options end def multiple_exceptions_error?(exception) MultipleExceptionError::InterfaceTag === exception end def multiple_exception_summarizer(exception, prior_detail_formatter, color) lambda do |example, colorizer, indentation| summary = if exception.aggregation_metadata[:hide_backtrace] # Since the backtrace is hidden, the subfailures will come # immediately after this, and using `:` will read well. "Got #{exception.exception_count_description}:" else # The backtrace comes after this, so using a `:` doesn't make sense # since the failures may be many lines below. "#{exception.summary}." end summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color) return summary unless prior_detail_formatter "#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}" end end def sub_failure_list_formatter(exception, message_color) common_backtrace_truncater = CommonBacktraceTruncater.new(exception) lambda do |failure_number, colorizer, indentation| exception.all_exceptions.each_with_index.map do |failure, index| options = with_multiple_error_options_as_needed( failure, :description_formatter => :failure_slash_error_line.to_proc, :indentation => indentation.length, :message_color => message_color || RSpec.configuration.failure_color, :skip_shared_group_trace => true ) failure = common_backtrace_truncater.with_truncated_backtrace(failure) presenter = ExceptionPresenter.new(failure, @example, options) presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer) end.join end end # @private # Used to prevent a confusing backtrace from showing up from the `aggregate_failures` # block declared for `:aggregate_failures` metadata. module EmptyBacktraceFormatter def self.format_backtrace(*) [] end end # @private class CommonBacktraceTruncater def initialize(parent) @parent = parent end def with_truncated_backtrace(child) child_bt = child.backtrace parent_bt = @parent.backtrace return child if child_bt.nil? || child_bt.empty? || parent_bt.nil? index_before_first_common_frame = -1.downto(-child_bt.size).find do |index| parent_bt[index] != child_bt[index] end return child if index_before_first_common_frame == -1 child = child.dup child.set_backtrace(child_bt[0..index_before_first_common_frame]) child end end end # @private PENDING_DETAIL_FORMATTER = Proc.new do |example, colorizer| colorizer.wrap("# #{example.execution_result.pending_message}", :detail) end end end # Provides a single exception instance that provides access to # multiple sub-exceptions. This is used in situations where a single # individual spec has multiple exceptions, such as one in the `it` block # and one in an `after` block. class MultipleExceptionError < StandardError # @private # Used so there is a common module in the ancestor chain of this class # and `RSpec::Expectations::MultipleExpectationsNotMetError`, which allows # code to detect exceptions that are instances of either, without first # checking to see if rspec-expectations is loaded. module InterfaceTag # Appends the provided exception to the list. # @param exception [Exception] Exception to append to the list. # @private def add(exception) # `PendingExampleFixedError` can be assigned to an example that initially has no # failures, but when the `aggregate_failures` around hook completes, it notifies of # a failure. If we do not ignore `PendingExampleFixedError` it would be surfaced to # the user as part of a multiple exception error, which is undesirable. While it's # pretty weird we handle this here, it's the best solution I've been able to come # up with, and `PendingExampleFixedError` always represents the _lack_ of any exception # so clearly when we are transitioning to a `MultipleExceptionError`, it makes sense to # ignore it. return if Pending::PendingExampleFixedError === exception all_exceptions << exception if exception.class.name =~ /RSpec/ failures << exception else other_errors << exception end end # Provides a way to force `ex` to be something that satisfies the multiple # exception error interface. If it already satisfies it, it will be returned; # otherwise it will wrap it in a `MultipleExceptionError`. # @private def self.for(ex) return ex if self === ex MultipleExceptionError.new(ex) end end include InterfaceTag # @return [Array] The list of failures. attr_reader :failures # @return [Array] The list of other errors. attr_reader :other_errors # @return [Array] The list of failures and other exceptions, combined. attr_reader :all_exceptions # @return [Hash] Metadata used by RSpec for formatting purposes. attr_reader :aggregation_metadata # @return [nil] Provided only for interface compatibility with # `RSpec::Expectations::MultipleExpectationsNotMetError`. attr_reader :aggregation_block_label # @param exceptions [Array] The initial list of exceptions. def initialize(*exceptions) super() @failures = [] @other_errors = [] @all_exceptions = [] @aggregation_metadata = { :hide_backtrace => true } @aggregation_block_label = nil exceptions.each { |e| add e } end # @return [String] Combines all the exception messages into a single string. # @note RSpec does not actually use this -- instead it formats each exception # individually. def message all_exceptions.map(&:message).join("\n\n") end # @return [String] A summary of the failure, including the block label and a count of failures. def summary "Got #{exception_count_description}" end # return [String] A description of the failure/error counts. def exception_count_description failure_count = Formatters::Helpers.pluralize(failures.size, "failure") return failure_count if other_errors.empty? error_count = Formatters::Helpers.pluralize(other_errors.size, "other error") "#{failure_count} and #{error_count}" end end end end