RSpec::Support.require_rspec_core "formatters/helpers" RSpec::Support.require_rspec_support "encoded_string" 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) do # @private def self.for(example) if example.execution_result.pending_fixed? PendingExampleFixedNotification.new(example) elsif example.execution_result.status == :failed FailedExampleNotification.new(example) else new(example) end 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(RSpec::Core::Example)] list of examples def examples @reporter.examples end # @return [Array(RSpec::Core::Example)] list of failed examples def failed_examples @reporter.failed_examples end # @return [Array(RSpec::Core::Example)] list of pending examples def pending_examples @reporter.pending_examples end # @return [Array(Rspec::Core::Notifications::ExampleNotification] # returns examples as notifications def notifications @notifications ||= format_examples(examples) end # @return [Array(Rspec::Core::Notifications::FailedExampleNotification] # returns failed examples as notifications def failure_notifications @failed_notifications ||= format_examples(failed_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) formatted = "\nPending:\n" pending_examples.each do |example| formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) formatted << " #{colorizer.wrap(example.full_description, :pending)}\n" \ " # #{colorizer.wrap(example.execution_result.pending_message, :detail)}\n" \ " # #{colorizer.wrap(formatted_caller, :detail)}\n" 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 failed specs. # # @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 example.execution_result.exception end # @return [String] The example description def description example.full_description end # Returns the message generated for this failure line by line. # # @return [Array(String)] The example failure message def message_lines add_shared_group_line(failure_lines, NullColorizer) 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(String)] The example failure message colorized def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) add_shared_group_line(failure_lines, colorizer).map do |line| colorizer.wrap line, RSpec.configuration.failure_color end end # Returns the failures formatted backtrace. # # @return [Array(String)] the examples backtrace lines def formatted_backtrace backtrace_formatter.format_backtrace(exception.backtrace, example.metadata) end # Returns the failures colorized formatted backtrace. # # @param colorizer [#wrap] An object to colorize the message_lines by # @return [Array(String)] the examples colorized backtrace lines 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 # @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) formatted = "\n #{failure_number}) #{description}\n" colorized_message_lines(colorizer).each do |line| formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) end colorized_formatted_backtrace(colorizer).each do |line| formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) end formatted end private if String.method_defined?(:encoding) def encoding_of(string) string.encoding end else def encoding_of(_string) end end def backtrace_formatter RSpec.configuration.backtrace_formatter 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 = ["Failure/Error: #{read_failed_line.strip}"] lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/ exception.message.to_s.split("\n").each do |line| lines << " #{line}" if exception.message end lines end end def add_shared_group_line(lines, colorizer) unless shared_group_line == "" lines << colorizer.wrap(shared_group_line, RSpec.configuration.default_color) end lines end def shared_group @shared_group ||= group_and_parent_groups.find { |group| group.metadata[:shared_group_name] } end def shared_group_line @shared_group_line ||= if shared_group "Shared Example Group: \"#{shared_group.metadata[:shared_group_name]}\"" \ " called from #{backtrace_formatter.backtrace_line(shared_group.location)}" else "" end end def group_and_parent_groups example.example_group.parent_groups + [example.example_group] 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 path = File.expand_path(example.file_path) exception.backtrace.find do |line| match = line.match(/(.+?):(\d+)(|:\d+)/) match && match[1].downcase == path.downcase end end end # The `PendingExampleFixedNotification` extends `ExampleNotification` with # things useful for specs that pass when they are expected to fail. # # @attr [RSpec::Core::Example] example the current example # @see ExampleNotification class PendingExampleFixedNotification < FailedExampleNotification public_class_method :new # Returns the examples description # # @return [String] The example description def description "#{example.full_description} FIXED" end # Returns the message generated for this failure line by line. # # @return [Array(String)] The example failure message def message_lines ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] 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(String)] The example failure message colorized def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) message_lines.map { |line| colorizer.wrap(line, RSpec.configuration.fixed_color) } 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 # wether that seed has been used or not. # # @attr seed [Fixnum] the seed used to randomize ordering # @attr used [Boolean] wether the seed has been used or not SeedNotification = Struct.new(:seed, :used) do # @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\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(RSpec::Core::Example)] the examples run # @attr failed_examples [Array(RSpec::Core::Example)] the failed examples # @attr pending_examples [Array(RSpec::Core::Example)] the pending examples # @attr load_time [Float] the number of seconds taken to boot RSpec # and load the spec files SummaryNotification = Struct.new(:duration, :examples, :failed_examples, :pending_examples, :load_time) do # @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") summary << ", " << Formatters::Helpers.pluralize(failure_count, "failure") summary << ", #{pending_count} pending" if pending_count > 0 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 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 #{example.location}", 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 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(RSpec::Core::Example)] the examples run # @attr number_of_examples [Fixnum] the number of examples to profile ProfileNotification = Struct.new(:duration, :examples, :number_of_examples) do # @return [Array(RSpec::Core::Example)] 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(RSpec::Core::Example)] the slowest example groups def slowest_groups @slowest_groups ||= calculate_slowest_groups end private def calculate_slowest_groups example_groups = {} examples.each do |example| location = example.example_group.parent_groups.last.metadata[:location] location_hash = example_groups[location] ||= Hash.new(0) location_hash[:total_time] += example.execution_result.run_time location_hash[:count] += 1 next if location_hash.key?(:description) location_hash[:description] = example.example_group.top_level_description end # 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 example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples) 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) do 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 end end