# encoding: utf-8 # frozen_string_literal: true require "cucumber/messages" require "cucumber/messages/time_conversion" module Cucumber module Core module Test module Result TYPES = [:failed, :flaky, :skipped, :undefined, :pending, :passed, :unknown].freeze STRICT_AFFECTED_TYPES = [:flaky, :undefined, :pending].freeze def self.ok?(type, be_strict = StrictConfiguration.new) class_name = type.to_s.slice(0, 1).capitalize + type.to_s.slice(1..-1) const_get(class_name).ok?(be_strict.strict?(type)) end # Defines to_sym on a result class for the given result type # # Defines predicate methods on a result class with only the given one # returning true def self.query_methods(result_type) Module.new do define_method :to_sym do result_type end TYPES.each do |possible_result_type| define_method("#{possible_result_type}?") do possible_result_type == to_sym end end end end # Null object for results. Represents the state where we haven't run anything yet class Unknown include Result.query_methods :unknown def describe_to(visitor, *args) self end def with_filtered_backtrace(filter) self end def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::UNKNOWN, duration: UnknownDuration.new.to_message_duration ) end end class Passed include Result.query_methods :passed attr_accessor :duration def self.ok?(be_strict = false) true end def initialize(duration) raise ArgumentError unless duration @duration = duration end def describe_to(visitor, *args) visitor.passed(*args) visitor.duration(duration, *args) self end def to_s "✓" end def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::PASSED, duration: duration.to_message_duration ) end def ok?(be_strict = nil) self.class.ok? end def with_appended_backtrace(step) self end def with_filtered_backtrace(filter) self end end class Failed include Result.query_methods :failed attr_reader :duration, :exception def self.ok?(be_strict = false) false end def initialize(duration, exception) raise ArgumentError unless duration raise ArgumentError unless exception @duration = duration @exception = exception end def describe_to(visitor, *args) visitor.failed(*args) visitor.duration(duration, *args) visitor.exception(exception, *args) if exception self end def to_s "✗" end def to_message begin message = exception.backtrace.join("\n") rescue NoMethodError message = "" end Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::FAILED, duration: duration.to_message_duration, message: message ) end def ok?(be_strict = nil) self.class.ok? end def with_duration(new_duration) self.class.new(new_duration, exception) end def with_appended_backtrace(step) exception.backtrace << step.backtrace_line if step.respond_to?(:backtrace_line) self end def with_filtered_backtrace(filter) self.class.new(duration, filter.new(exception.dup).exception) end end # Flaky is not used directly as an execution result, but is used as a # reporting result type for test cases that fails and the passes on # retry, therefore only the class method self.ok? is needed. class Flaky def self.ok?(be_strict = false) !be_strict end end # Base class for exceptions that can be raised in a step definition causing # the step to have that result. class Raisable < StandardError attr_reader :message, :duration def initialize(message = "", duration = UnknownDuration.new, backtrace = nil) @message, @duration = message, duration super(message) set_backtrace(backtrace) if backtrace end def with_message(new_message) self.class.new(new_message, duration, backtrace) end def with_duration(new_duration) self.class.new(message, new_duration, backtrace) end def with_appended_backtrace(step) return self unless step.respond_to?(:backtrace_line) set_backtrace([]) unless backtrace backtrace << step.backtrace_line self end def with_filtered_backtrace(filter) return self unless backtrace filter.new(dup).exception end def ok?(be_strict = StrictConfiguration.new) self.class.ok?(be_strict.strict?(to_sym)) end end class Undefined < Raisable include Result.query_methods :undefined def self.ok?(be_strict = false) !be_strict end def describe_to(visitor, *args) visitor.undefined(*args) visitor.duration(duration, *args) self end def to_s "?" end def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::UNDEFINED, duration: duration.to_message_duration ) end end class Skipped < Raisable include Result.query_methods :skipped def self.ok?(be_strict = false) true end def describe_to(visitor, *args) visitor.skipped(*args) visitor.duration(duration, *args) self end def to_s "-" end def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::SKIPPED, duration: duration.to_message_duration ) end end class Pending < Raisable include Result.query_methods :pending def self.ok?(be_strict = false) !be_strict end def describe_to(visitor, *args) visitor.pending(self, *args) visitor.duration(duration, *args) self end def to_s "P" end def to_message Cucumber::Messages::TestStepResult.new( status: Cucumber::Messages::TestStepResultStatus::PENDING, duration: duration.to_message_duration ) end end # Handles the strict settings for the result types that are # affected by the strict options (that is the STRICT_AFFECTED_TYPES). class StrictConfiguration attr_accessor :settings private :settings def initialize(strict_types = []) @settings = STRICT_AFFECTED_TYPES.map { |t| [t, :default] }.to_h strict_types.each do |type| set_strict(true, type) end end def strict?(type = nil) if type.nil? settings.each do |_key, value| return true if value == true end false else return false unless settings.key?(type) return false unless set?(type) settings[type] end end def set_strict(setting, type = nil) if type.nil? STRICT_AFFECTED_TYPES.each do |t| set_strict(setting, t) end else settings[type] = setting end end def merge!(other) settings.each_key do |type| set_strict(other.strict?(type), type) if other.set?(type) end self end def set?(type) settings[type] != :default end end # # An object that responds to the description protocol from the results # and collects summary information. # # e.g. # summary = Result::Summary.new # Result::Passed.new(0).describe_to(summary) # puts summary.total_passed # => 1 # class Summary attr_reader :exceptions, :durations def initialize @totals = Hash.new { 0 } @exceptions = [] @durations = [] end def method_missing(name, *args) if name =~ /^total_/ get_total(name) else increment_total(name) end end def ok?(be_strict = StrictConfiguration.new) TYPES.each do |type| if get_total(type) > 0 return false unless Result.ok?(type, be_strict) end end true end def exception(exception) @exceptions << exception self end def duration(duration) @durations << duration self end def total(for_status = nil) if for_status @totals.fetch(for_status, 0) else @totals.reduce(0) { |total, status| total += status[1] } end end def decrement_failed @totals[:failed] -= 1 end private def get_total(method_name) status = method_name.to_s.gsub('total_', '').to_sym return @totals.fetch(status, 0) end def increment_total(status) @totals[status] += 1 self end end class Duration include Cucumber::Messages::TimeConversion attr_reader :nanoseconds def initialize(nanoseconds) @nanoseconds = nanoseconds end def to_message_duration duration_hash = seconds_to_duration(nanoseconds.to_f / NANOSECONDS_PER_SECOND) duration_hash.transform_keys! do |key| key.to_sym rescue Error return key end Cucumber::Messages::Duration.from_h(duration_hash) end end class UnknownDuration include Cucumber::Messages::TimeConversion def tap(&block) self end def nanoseconds raise "#nanoseconds only allowed to be used in #tap block" end def to_message_duration Cucumber::Messages::Duration.new(seconds: 0, nanos: 0) end end end end end end