RSpec::Support.require_rspec_core "formatters/console_codes" RSpec::Support.require_rspec_core "formatters/exception_presenter" RSpec::Support.require_rspec_core "formatters/helpers" RSpec::Support.require_rspec_core "shell_escape" module RSpec::Core # Notifications are value objects passed to formatters to provide them # with information about a particular event of interest. module Notifications # @private module NullColorizer module_function def wrap(line, _code_or_symbol) line end end # The `StartNotification` represents a notification sent by the reporter # when the suite is started. It contains the expected amount of examples # to be executed, and the load time of RSpec. # # @attr count [Fixnum] the number counted # @attr load_time [Float] the number of seconds taken to boot RSpec # and load the spec files StartNotification = Struct.new(:count, :load_time) # The `ExampleNotification` represents notifications sent by the reporter # which contain information about the current (or soon to be) example. # It is used by formatters to access information about that example. # # @example # def example_started(notification) # puts "Hey I started #{notification.example.description}" # end # # @attr example [RSpec::Core::Example] the current example ExampleNotification = Struct.new(:example) class ExampleNotification # @private def self.for(example) execution_result = example.execution_result return SkippedExampleNotification.new(example) if execution_result.example_skipped? return new(example) unless execution_result.status == :pending || execution_result.status == :failed klass = if execution_result.pending_fixed? PendingExampleFixedNotification elsif execution_result.status == :pending PendingExampleFailedAsExpectedNotification else FailedExampleNotification end klass.new(example) end private_class_method :new end # The `ExamplesNotification` represents notifications sent by the reporter # which contain information about the suites examples. # # @example # def stop(notification) # puts "Hey I ran #{notification.examples.size}" # end # class ExamplesNotification def initialize(reporter) @reporter = reporter end # @return [Array] list of examples def examples @reporter.examples end # @return [Array] list of failed examples def failed_examples @reporter.failed_examples end # @return [Array] list of pending examples def pending_examples @reporter.pending_examples end # @return [Array] # returns examples as notifications def notifications @notifications ||= format_examples(examples) end # @return [Array] # returns failed examples as notifications def failure_notifications @failed_notifications ||= format_examples(failed_examples) end # @return [Array] # returns pending examples as notifications def pending_notifications @pending_notifications ||= format_examples(pending_examples) end # @return [String] The list of failed examples, fully formatted in the way # that RSpec's built-in formatters emit. def fully_formatted_failed_examples(colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted = "\nFailures:\n" failure_notifications.each_with_index do |failure, index| formatted += failure.fully_formatted(index.next, colorizer) end formatted end # @return [String] The list of pending examples, fully formatted in the # way that RSpec's built-in formatters emit. def fully_formatted_pending_examples(colorizer=::RSpec::Core::Formatters::ConsoleCodes) return if RSpec.configuration.pending_failure_output == :skip formatted = "\nPending: (Failures listed here are expected and do not affect your suite's status)\n".dup pending_notifications.each_with_index do |notification, index| formatted << notification.fully_formatted(index.next, colorizer) end formatted end private def format_examples(examples) examples.map do |example| ExampleNotification.for(example) end end end # The `FailedExampleNotification` extends `ExampleNotification` with # things useful for examples that have failure info -- typically a # failed or pending spec. # # @example # def example_failed(notification) # puts "Hey I failed :(" # puts "Here's my stack trace" # puts notification.exception.backtrace.join("\n") # end # # @attr [RSpec::Core::Example] example the current example # @see ExampleNotification class FailedExampleNotification < ExampleNotification public_class_method :new # @return [Exception] The example failure def exception @exception_presenter.exception end # @return [String] The example description def description @exception_presenter.description end # Returns the message generated for this failure line by line. # # @return [Array] The example failure message def message_lines @exception_presenter.message_lines end # Returns the message generated for this failure colorized line by line. # # @param colorizer [#wrap] An object to colorize the message_lines by # @return [Array] The example failure message colorized def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) @exception_presenter.colorized_message_lines(colorizer) end # Returns the failures formatted backtrace. # # @return [Array] the examples backtrace lines def formatted_backtrace @exception_presenter.formatted_backtrace end # Returns the failures colorized formatted backtrace. # # @param colorizer [#wrap] An object to colorize the message_lines by # @return [Array] the examples colorized backtrace lines def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes) @exception_presenter.colorized_formatted_backtrace(colorizer) end # @return [String] The failure information fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) @exception_presenter.fully_formatted(failure_number, colorizer) end # @return [Array] The failure information fully formatted in the way that # RSpec's built-in formatters emit, split by line. def fully_formatted_lines(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) @exception_presenter.fully_formatted_lines(failure_number, colorizer) end private def initialize(example, exception_presenter=Formatters::ExceptionPresenter::Factory.new(example).build) @exception_presenter = exception_presenter super(example) end end # @deprecated Use {FailedExampleNotification} instead. class PendingExampleFixedNotification < FailedExampleNotification; end # @deprecated Use {FailedExampleNotification} instead. class PendingExampleFailedAsExpectedNotification < FailedExampleNotification; end # The `SkippedExampleNotification` extends `ExampleNotification` with # things useful for specs that are skipped. # # @attr [RSpec::Core::Example] example the current example # @see ExampleNotification class SkippedExampleNotification < ExampleNotification public_class_method :new # @return [String] The pending detail fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) [ colorizer.wrap("\n #{pending_number}) #{example.full_description}", :pending), "\n ", Formatters::ExceptionPresenter::PENDING_DETAIL_FORMATTER.call(example, colorizer), "\n", colorizer.wrap(" # #{formatted_caller}\n", :detail) ].join("") end end # The `GroupNotification` represents notifications sent by the reporter # which contain information about the currently running (or soon to be) # example group. It is used by formatters to access information about that # group. # # @example # def example_group_started(notification) # puts "Hey I started #{notification.group.description}" # end # @attr group [RSpec::Core::ExampleGroup] the current group GroupNotification = Struct.new(:group) # The `MessageNotification` encapsulates generic messages that the reporter # sends to formatters. # # @attr message [String] the message MessageNotification = Struct.new(:message) # The `SeedNotification` holds the seed used to randomize examples and # whether that seed has been used or not. # # @attr seed [Fixnum] the seed used to randomize ordering # @attr used [Boolean] whether the seed has been used or not SeedNotification = Struct.new(:seed, :used) class SeedNotification # @api # @return [Boolean] has the seed been used? def seed_used? !!used end private :used # @return [String] The seed information fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted "\nRandomized with seed #{seed}\n" end end # The `SummaryNotification` holds information about the results of running # a test suite. It is used by formatters to provide information at the end # of the test run. # # @attr duration [Float] the time taken (in seconds) to run the suite # @attr examples [Array] the examples run # @attr failed_examples [Array] the failed examples # @attr pending_examples [Array] the pending examples # @attr load_time [Float] the number of seconds taken to boot RSpec # and load the spec files # @attr errors_outside_of_examples_count [Integer] the number of errors that # have occurred processing # the spec suite SummaryNotification = Struct.new(:duration, :examples, :failed_examples, :pending_examples, :load_time, :errors_outside_of_examples_count) class SummaryNotification # @api # @return [Fixnum] the number of examples run def example_count @example_count ||= examples.size end # @api # @return [Fixnum] the number of failed examples def failure_count @failure_count ||= failed_examples.size end # @api # @return [Fixnum] the number of pending examples def pending_count @pending_count ||= pending_examples.size end # @api # @return [String] A line summarising the result totals of the spec run. def totals_line summary = Formatters::Helpers.pluralize(example_count, "example") + ", " + Formatters::Helpers.pluralize(failure_count, "failure") summary += ", #{pending_count} pending" if pending_count > 0 if errors_outside_of_examples_count > 0 summary += ( ", " + Formatters::Helpers.pluralize(errors_outside_of_examples_count, "error") + " occurred outside of examples" ) end summary end # @api public # # Wraps the results line with colors based on the configured # colors for failure, pending, and success. Defaults to red, # yellow, green accordingly. # # @param colorizer [#wrap] An object which supports wrapping text with # specific colors. # @return [String] A colorized results line. def colorized_totals_line(colorizer=::RSpec::Core::Formatters::ConsoleCodes) if failure_count > 0 || errors_outside_of_examples_count > 0 colorizer.wrap(totals_line, RSpec.configuration.failure_color) elsif pending_count > 0 colorizer.wrap(totals_line, RSpec.configuration.pending_color) else colorizer.wrap(totals_line, RSpec.configuration.success_color) end end # @api public # # Formats failures into a rerunable command format. # # @param colorizer [#wrap] An object which supports wrapping text with # specific colors. # @return [String] A colorized summary line. def colorized_rerun_commands(colorizer=::RSpec::Core::Formatters::ConsoleCodes) "\nFailed examples:\n\n" + failed_examples.map do |example| colorizer.wrap("rspec #{rerun_argument_for(example)}", RSpec.configuration.failure_color) + " " + colorizer.wrap("# #{example.full_description}", RSpec.configuration.detail_color) end.join("\n") end # @return [String] a formatted version of the time it took to run the # suite def formatted_duration Formatters::Helpers.format_duration(duration) end # @return [String] a formatted version of the time it took to boot RSpec # and load the spec files def formatted_load_time Formatters::Helpers.format_duration(load_time) end # @return [String] The summary information fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted(colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted = "\nFinished in #{formatted_duration} " \ "(files took #{formatted_load_time} to load)\n" \ "#{colorized_totals_line(colorizer)}\n" unless failed_examples.empty? formatted += (colorized_rerun_commands(colorizer) + "\n") end formatted end private include RSpec::Core::ShellEscape def rerun_argument_for(example) location = example.location_rerun_argument return location unless duplicate_rerun_locations.include?(location) conditionally_quote(example.id) end def duplicate_rerun_locations @duplicate_rerun_locations ||= begin locations = RSpec.world.all_examples.map(&:location_rerun_argument) Set.new.tap do |s| locations.group_by { |l| l }.each do |l, ls| s << l if ls.count > 1 end end end end end # The `ProfileNotification` holds information about the results of running a # test suite when profiling is enabled. It is used by formatters to provide # information at the end of the test run for profiling information. # # @attr duration [Float] the time taken (in seconds) to run the suite # @attr examples [Array] the examples run # @attr number_of_examples [Fixnum] the number of examples to profile # @attr example_groups [Array] example groups run class ProfileNotification def initialize(duration, examples, number_of_examples, example_groups) @duration = duration @examples = examples @number_of_examples = number_of_examples @example_groups = example_groups end attr_reader :duration, :examples, :number_of_examples # @return [Array] the slowest examples def slowest_examples @slowest_examples ||= examples.sort_by do |example| -example.execution_result.run_time end.first(number_of_examples) end # @return [Float] the time taken (in seconds) to run the slowest examples def slow_duration @slow_duration ||= slowest_examples.inject(0.0) do |i, e| i + e.execution_result.run_time end end # @return [String] the percentage of total time taken def percentage @percentage ||= begin time_taken = slow_duration / duration '%.1f' % ((time_taken.nan? ? 0.0 : time_taken) * 100) end end # @return [Array] the slowest example groups def slowest_groups @slowest_groups ||= calculate_slowest_groups end private def calculate_slowest_groups # stop if we've only one example group return {} if @example_groups.keys.length <= 1 @example_groups.each_value do |hash| hash[:average] = hash[:total_time].to_f / hash[:count] end groups = @example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples) groups.map { |group, data| [group.location, data] } end end # The `DeprecationNotification` is issued by the reporter when a deprecated # part of RSpec is encountered. It represents information about the # deprecated call site. # # @attr message [String] A custom message about the deprecation # @attr deprecated [String] A custom message about the deprecation (alias of # message) # @attr replacement [String] An optional replacement for the deprecation # @attr call_site [String] An optional call site from which the deprecation # was issued DeprecationNotification = Struct.new(:deprecated, :message, :replacement, :call_site) class DeprecationNotification private_class_method :new # @api # Convenience way to initialize the notification def self.from_hash(data) new data[:deprecated], data[:message], data[:replacement], data[:call_site] end end # `NullNotification` represents a placeholder value for notifications that # currently require no information, but we may wish to extend in future. class NullNotification end # `CustomNotification` is used when sending custom events to formatters / # other registered listeners, it creates attributes based on supplied hash # of options. class CustomNotification < Struct # @param options [Hash] A hash of method / value pairs to create on this notification # @return [CustomNotification] # # Build a custom notification based on the supplied option key / values. def self.for(options={}) return NullNotification if options.keys.empty? new(*options.keys).new(*options.values) end end end end