JezK
Edit File: notifications.rb
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) class ExampleNotification # @private def self.for(example) execution_result = example.execution_result if execution_result.pending_fixed? PendingExampleFixedNotification.new(example) elsif execution_result.example_skipped? SkippedExampleNotification.new(example) elsif execution_result.status == :pending PendingExampleFailedAsExpectedNotification.new(example) elsif 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 [Array<RSpec::Core::Notifications::SkippedExampleNotification, # RSpec::Core::Notifications::PendingExampleFailedAsExpectedNotification>] # 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) formatted = "\nPending: (Failures listed here are expected and do not affect your suite's status)\n" 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 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_lines(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_lines(failure_lines, colorizer).map do |line| colorizer.wrap line, message_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) "\n #{failure_number}) #{description}\n#{formatted_message_and_backtrace(colorizer)}" 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_lines(lines, colorizer) 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) formatted = "" 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 def message_color RSpec.configuration.failure_color 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 # @private module PendingExampleNotificationMethods private def fully_formatted_header(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) colorizer.wrap("\n #{pending_number}) #{example.full_description}\n", :pending) << colorizer.wrap(" # #{example.execution_result.pending_message}\n", :detail) end end # The `PendingExampleFailedAsExpectedNotification` extends `FailedExampleNotification` with # things useful for pending specs that fail as expected. # # @attr [RSpec::Core::Example] example the current example # @see ExampleNotification class PendingExampleFailedAsExpectedNotification < FailedExampleNotification include PendingExampleNotificationMethods public_class_method :new # @return [Exception] The exception that occurred while the pending example was executed def exception example.execution_result.pending_exception end # @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) fully_formatted_header(pending_number, colorizer) << formatted_message_and_backtrace(colorizer) end private def message_color RSpec.configuration.pending_color end 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 include PendingExampleNotificationMethods 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) fully_formatted_header(pending_number, colorizer) << colorizer.wrap(" # #{formatted_caller}\n", :detail) 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<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) 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") 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.rerun_argument}", 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) class ProfileNotification # @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) 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 end end