module Spec # :nodoc: struct EqualExpectation(T) def initialize(@expected_value : T) end def match(actual_value) expected_value = @expected_value # For the case of comparing strings we want to make sure that two strings # are equal if their content is equal, but also their bytesize and size # should be equal. Otherwise, an incorrect bytesize or size was used # when creating them. if actual_value.is_a?(String) && expected_value.is_a?(String) actual_value == expected_value && actual_value.bytesize == expected_value.bytesize && actual_value.size == expected_value.size else actual_value == @expected_value end end def failure_message(actual_value) expected_value = @expected_value # Check for the case of string equality when the content match # but not the bytesize or size. if actual_value.is_a?(String) && expected_value.is_a?(String) && actual_value == expected_value if actual_value.bytesize != expected_value.bytesize return <<-MSG Expected bytesize: #{expected_value.bytesize} got bytesize: #{actual_value.bytesize} MSG end <<-MSG Expected size: #{expected_value.size} got size: #{actual_value.size} MSG else expected = expected_value.pretty_inspect got = actual_value.pretty_inspect if expected == got expected += " : #{@expected_value.class}" got += " : #{actual_value.class}" end "Expected: #{expected}\n got: #{got}" end end def negative_failure_message(actual_value) "Expected: actual_value != #{@expected_value.pretty_inspect}\n got: #{actual_value.pretty_inspect}" end end # :nodoc: struct BeExpectation(T) def initialize(@expected_value : T) end def match(actual_value) actual_value.same? @expected_value end def failure_message(actual_value) "Expected: #{@expected_value.pretty_inspect} (#{identify(@expected_value)})\n got: #{actual_value.pretty_inspect} (#{identify(actual_value)})" end def negative_failure_message(actual_value) "Expected: #{@expected_value.pretty_inspect} (#{identify(@expected_value)})\n got: #{actual_value.pretty_inspect} (#{identify(actual_value)})" end private def identify(value) if value.responds_to?(:to_unsafe) if !value.responds_to?(:object_id) return value.to_unsafe end end "object_id: #{value.object_id}" end end # :nodoc: struct BeTruthyExpectation def match(actual_value) !!actual_value end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} to be truthy" end def negative_failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} not to be truthy" end end # :nodoc: struct BeFalseyExpectation def match(actual_value) !actual_value end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} to be falsey" end def negative_failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} not to be falsey" end end # :nodoc: struct BeNilExpectation def match(actual_value) actual_value.nil? end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} to be nil" end def negative_failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} not to be nil" end end # :nodoc: struct CloseExpectation(T, D) def initialize(@expected_value : T, @delta : D) end def match(actual_value) (actual_value - @expected_value).abs <= @delta end def failure_message(actual_value) "Expected #{actual_value.pretty_inspect} to be within #{@delta} of #{@expected_value.pretty_inspect}" end def negative_failure_message(actual_value) "Expected #{actual_value.pretty_inspect} not to be within #{@delta} of #{@expected_value.pretty_inspect}" end end # :nodoc: struct BeAExpectation(T) def match(actual_value) actual_value.is_a?(T) end def failure_message(actual_value) "Expected #{actual_value.pretty_inspect} (#{actual_value.class}) to be a #{T}" end def negative_failure_message(actual_value) "Expected #{actual_value.pretty_inspect} (#{actual_value.class}) not to be a #{T}" end end # :nodoc: struct Be(T) enum Relation LessThan LessOrEqual GreaterThan GreaterOrEqual end def self.<(other) Be.new(other, :less_than) end def self.<=(other) Be.new(other, :less_or_equal) end def self.>(other) Be.new(other, :greater_than) end def self.>=(other) Be.new(other, :greater_or_equal) end def initialize(@expected_value : T, @op : Relation) end def match(actual_value) case @op in .less_than? actual_value < @expected_value in .less_or_equal? actual_value <= @expected_value in .greater_than? actual_value > @expected_value in .greater_or_equal? actual_value >= @expected_value end end def failure_message(actual_value) "Expected #{actual_value.pretty_inspect} to be #{@op} #{@expected_value.pretty_inspect}" end def negative_failure_message(actual_value) "Expected #{actual_value.pretty_inspect} not to be #{@op} #{@expected_value.pretty_inspect}" end end # :nodoc: struct MatchExpectation(T) def initialize(@expected_value : T) end def match(actual_value) actual_value =~ @expected_value end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect}\nto match: #{@expected_value.pretty_inspect}" end def negative_failure_message(actual_value) "Expected: value #{actual_value.pretty_inspect}\n to not match: #{@expected_value.pretty_inspect}" end end # :nodoc: struct ContainExpectation(T) def initialize(@expected_value : T) end def match(actual_value) actual_value.includes?(@expected_value) end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect}\nto include: #{@expected_value.pretty_inspect}" end def negative_failure_message(actual_value) "Expected: value #{actual_value.pretty_inspect}\nto not include: #{@expected_value.pretty_inspect}" end end # :nodoc: struct StartWithExpectation(T) def initialize(@expected_value : T) end def match(actual_value) actual_value.starts_with?(@expected_value) end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect}\nto start with: #{@expected_value.pretty_inspect}" end def negative_failure_message(actual_value) "Expected: value #{actual_value.pretty_inspect}\nnot to start with: #{@expected_value.pretty_inspect}" end end # :nodoc: struct EndWithExpectation(T) def initialize(@expected_value : T) end def match(actual_value) actual_value.ends_with?(@expected_value) end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect}\nto end with: #{@expected_value.pretty_inspect}" end def negative_failure_message(actual_value) "Expected: value #{actual_value.pretty_inspect}\nnot to end with: #{@expected_value.pretty_inspect}" end end # :nodoc: struct BeEmptyExpectation def match(actual_value) actual_value.empty? end def failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} to be empty" end def negative_failure_message(actual_value) "Expected: #{actual_value.pretty_inspect} not to be empty" end end # This module defines a number of methods to create expectations, which are # automatically included into the top level namespace. # # Expectations are used by `Spec::ObjectExtensions#should` and `Spec::ObjectExtensions#should_not`. module Expectations # Creates an `Expectation` that passes if actual equals *value* (`==`). def eq(value) Spec::EqualExpectation.new value end # Creates an `Expectation` that passes if actual and *value* are identical (`.same?`). def be(value) Spec::BeExpectation.new value end # Creates an `Expectation` that passes if actual is true (`== true`). def be_true eq true end # Creates an `Expectation` that passes if actual is false (`== false`). def be_false eq false end # Creates an `Expectation` that passes if actual is truthy (neither `nil` nor `false`). def be_truthy Spec::BeTruthyExpectation.new end # Creates an `Expectation` that passes if actual is falsy (`nil` or `false`). def be_falsey Spec::BeFalseyExpectation.new end # Creates an `Expectation` that passes if actual is nil (`Object#nil?`). def be_nil Spec::BeNilExpectation.new end # Creates an `Expectation` that passes if actual is within *delta* of *expected*. def be_close(expected, delta) Spec::CloseExpectation.new(expected, delta) end # Returns a factory to create a comparison `Expectation` that: # # * passes if actual is lesser than *value*: `be < value` # * passes if actual is lesser than or equal *value*: `be <= value` # * passes if actual is greater than *value*: `be > value` # * passes if actual is greater than or equal *value*: `be >= value` def be Spec::Be end # Creates an `Expectation` that passes if actual matches *value* (`=~`). def match(value) Spec::MatchExpectation.new(value) end # Creates an `Expectation` that passes if actual includes *expected* (`.includes?`). # Works on collections and `String`. def contain(expected) Spec::ContainExpectation.new(expected) end # Creates an `Expectation` that passes if actual starts with *expected* (`.starts_with?`). # Works on `String`. def start_with(expected) Spec::StartWithExpectation.new(expected) end # Creates an `Expectation` that passes if actual ends with *expected* (`.ends_with?`). # Works on `String`. def end_with(expected) Spec::EndWithExpectation.new(expected) end # Creates an `Expectation` that passes if actual is empty (`.empty?`). def be_empty Spec::BeEmptyExpectation.new end # Creates an `Expectation` that passes if actual is of type *type* (`is_a?`). macro be_a(type) ::Spec::BeAExpectation({{type}}).new end # Runs the block and passes if it raises an exception of type *klass* and the error message matches. # # If *message* is a string, it matches if the exception's error message contains that string. # If *message* is a regular expression, it is used to match the error message. # # It returns the rescued exception. {% if flag?(:wasm32) %} def expect_raises(klass : T.class, message : String | Regex | Nil = nil, file = __FILE__, line = __LINE__, &) forall T # TODO: Enable "expect_raises" for wasm32 after exceptions are working. end {% else %} def expect_raises(klass : T.class, message : String | Regex | Nil = nil, file = __FILE__, line = __LINE__, &) forall T yield rescue ex : T # We usually bubble Spec::AssertionFailed, unless this is the expected exception if ex.is_a?(Spec::AssertionFailed) && klass != Spec::AssertionFailed raise ex end # `NestingSpecError` is treated as the same above. if ex.is_a?(Spec::NestingSpecError) && klass != Spec::NestingSpecError raise ex end exception_as_string = ex.to_s case message when Regex unless exception_as_string =~ message expectation_failed_message = build_expectation_failed_message(klass, message, ex, exception_as_string) fail expectation_failed_message, file, line end when String unless exception_as_string.includes?(message) expectation_failed_message = build_expectation_failed_message(klass, message, ex, exception_as_string) fail expectation_failed_message, file, line end when Nil # No need to check the message end ex rescue ex expectation_failed_message = build_expectation_failed_message(klass, ex) fail expectation_failed_message, file, line else fail "Expected #{klass} but nothing was raised", file, line end private def build_expectation_failed_message(klass : Class, message : String, exception : Exception, exception_as_string : String) backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} with message containing: #{message.inspect} got #{exception.class} with message: #{exception_as_string.inspect} Backtrace: #{backtrace} MESSAGE end private def build_expectation_failed_message(klass : Class, message : Regex, exception : Exception, exception_as_string : String) backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} with message matching: #{message.inspect} got #{exception.class} with message: #{exception_as_string.inspect} Backtrace: #{backtrace} MESSAGE end private def build_expectation_failed_message(klass : Class, exception : Exception) exception_as_string = exception.to_s backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} got #{exception.class} with message: #{exception_as_string.inspect} Backtrace: #{backtrace} MESSAGE end {% end %} end module ObjectExtensions # Validates an expectation and fails the example if it does not match. # # This overload returns a value whose type is restricted to the expected type. For example: # # ``` # x = 1 || 'a' # typeof(x) # => Int32 | Char # x = x.should be_a(Int32) # typeof(x) # => Int32 # ``` # # See `Spec::Expectations` for available expectations. def should(expectation : BeAExpectation(T), failure_message : String? = nil, *, file = __FILE__, line = __LINE__) : T forall T if expectation.match self self.is_a?(T) ? self : (raise "Bug: expected #{self} to be a #{T}") else failure_message ||= expectation.failure_message(self) fail(failure_message, file, line) end end # Validates an expectation and fails the example if it does not match. # # See `Spec::Expectations` for available expectations. def should(expectation, failure_message : String? = nil, *, file = __FILE__, line = __LINE__) unless expectation.match self failure_message ||= expectation.failure_message(self) fail(failure_message, file, line) end end # Validates an expectation and fails the example if it matches. # # This overload returns a value whose type is restricted to exclude the given # type in `should_not be_a`. For example: # # ``` # x = 1 || 'a' # typeof(x) # => Int32 | Char # x = x.should_not be_a(Char) # typeof(x) # => Int32 # ``` # # See `Spec::Expectations` for available expectations. def should_not(expectation : BeAExpectation(T), failure_message : String? = nil, *, file = __FILE__, line = __LINE__) forall T if expectation.match self failure_message ||= expectation.negative_failure_message(self) fail(failure_message, file, line) else self.is_a?(T) ? (raise "Bug: expected #{self} not to be a #{T}") : self end end # Validates an expectation and fails the example if it matches. # # This overload returns a value whose type is restricted to be not `Nil`. For example: # # ``` # x = 1 || nil # typeof(x) # => Int32 | Nil # x = x.should_not be_nil # typeof(x) # => Int32 # ``` # # See `Spec::Expectations` for available expectations. def should_not(expectation : BeNilExpectation, failure_message : String? = nil, *, file = __FILE__, line = __LINE__) if expectation.match self failure_message ||= expectation.negative_failure_message(self) fail(failure_message, file, line) else self.not_nil! end end # Validates an expectation and fails the example if it matches. # # See `Spec::Expectations` for available expectations. def should_not(expectation, failure_message : String? = nil, *, file = __FILE__, line = __LINE__) if expectation.match self failure_message ||= expectation.negative_failure_message(self) fail(failure_message, file, line) end end end end include Spec::Expectations class Object include Spec::ObjectExtensions end