diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d8d66..17b7220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,22 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.12.1] - 2024-08-13 -### Fixed -- Fixed some global namespace issues with Crystal 1.13. [#57](https://github.com/icy-arctic-fox/spectator/pull/57) Thanks @GrantBirki ! -- Remove usage of deprecated double splat in macros. - -## [0.12.0] - 2024-02-03 -### Added -- Added ability to use matchers for case equality. [#55](https://github.com/icy-arctic-fox/spectator/issues/55) -- Added support for nested case equality when checking arguments with Array, Tuple, Hash, and NamedTuple. - -### Fixed -- Fixed some issues with the `be_within` matcher when used with expected and union types. - -## [0.11.7] - 2023-10-16 -### Fixed -- Fix memoized value (`let`) with a union type causing segfault. [#81](https://gitlab.com/arctic-fox/spectator/-/issues/81) +## [Unreleased] ## [0.11.6] - 2023-01-26 ### Added @@ -463,10 +448,7 @@ This has been changed so that it compiles and raises an error at runtime with a First version ready for public use. -[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.1...master -[0.12.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.0...v0.12.1 -[0.12.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.7...v0.12.0 -[0.11.7]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...v0.11.7 +[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...master [0.11.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.5...v0.11.6 [0.11.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...v0.11.5 [0.11.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4 diff --git a/README.md b/README.md index 00b5eb9..973b9a9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Add this to your application's `shard.yml`: development_dependencies: spectator: gitlab: arctic-fox/spectator - version: ~> 0.12.0 + version: ~> 0.11.0 ``` Usage @@ -287,7 +287,7 @@ Spectator.describe Driver do # Call the mock method. subject.do_something(interface, dbl) # Verify everything went okay. - expect(interface).to have_received(:invoke).with(dbl) + expect(interface).to have_received(:invoke).with(thing) end end ``` diff --git a/shard.yml b/shard.yml index ac8b54f..06ba936 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.12.1 +version: 0.11.6 description: | Feature-rich testing framework for Crystal inspired by RSpec. diff --git a/spec/issues/github_issue_55_spec.cr b/spec/issues/github_issue_55_spec.cr deleted file mode 100644 index 92c2b42..0000000 --- a/spec/issues/github_issue_55_spec.cr +++ /dev/null @@ -1,48 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #55" do - GROUP_NAME = "CallCenter" - - let(name) { "TimeTravel" } - let(source) { "my.time.travel.experiment" } - - class Analytics(T) - property start_time = Time.local - property end_time = Time.local - - def initialize(@brain_talker : T) - end - - def instrument(*, name, source, &) - @brain_talker.send(payload: { - :group => GROUP_NAME, - :name => name, - :source => source, - :start => start_time, - :end => end_time, - }, action: "analytics") - end - end - - double(:brain_talker, send: nil) - - let(brain_talker) { double(:brain_talker) } - let(analytics) { Analytics.new(brain_talker) } - - it "tracks the time it takes to run the block" do - analytics.start_time = expected_start_time = Time.local - expected_end_time = expected_start_time + 10.seconds - analytics.end_time = expected_end_time + 0.5.seconds # Offset to ensure non-exact match. - - analytics.instrument(name: name, source: source) do - end - - expect(brain_talker).to have_received(:send).with(payload: { - :group => GROUP_NAME, - :name => name, - :source => source, - :start => expected_start_time, - :end => be_within(1.second).of(expected_end_time), - }, action: "analytics") - end -end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 5eff1a9..ab05636 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -31,7 +31,7 @@ module Spectator::DSL ::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %} # Define the plain double type. - ::Spectator::Double.define({{double_type_name}}, {{name}}, {{value_methods.double_splat}}) do + ::Spectator::Double.define({{double_type_name}}, {{name}}, {{**value_methods}}) do # Returns a new double that responds to undefined methods with itself. # See: `NullDouble` def as_null_object @@ -43,7 +43,7 @@ module Spectator::DSL {% begin %} # Define a matching null double type. - ::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{value_methods.double_splat}}) {{block}} + ::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{**value_methods}}) {{block}} {% end %} end @@ -94,9 +94,9 @@ module Spectator::DSL begin %double = {% if found_tuple %} - {{found_tuple[2].id}}.new({{value_methods.double_splat}}) + {{found_tuple[2].id}}.new({{**value_methods}}) {% else %} - ::Spectator::LazyDouble.new({{name}}, {{value_methods.double_splat}}) + ::Spectator::LazyDouble.new({{name}}, {{**value_methods}}) {% end %} ::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset }) %double @@ -176,7 +176,7 @@ module Spectator::DSL # See `#def_double`. macro double(name, **value_methods, &block) {% begin %} - {% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{value_methods.double_splat}}) {{block}} + {% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{**value_methods}}) {{block}} {% end %} end @@ -189,7 +189,7 @@ module Spectator::DSL # expect(dbl.foo).to eq(42) # ``` macro double(**value_methods) - ::Spectator::LazyDouble.new({{value_methods.double_splat}}) + ::Spectator::LazyDouble.new({{**value_methods}}) end # Defines a new mock type. @@ -226,7 +226,7 @@ module Spectator::DSL # Store information about how the mock is defined and its context. # This is important for constructing an instance of the mock later. - ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "#{"::".id unless resolved.name.starts_with?("::")}#{resolved.name}::#{mock_type_name}".id.symbolize} + ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "::#{resolved.name}::#{mock_type_name}".id.symbolize} base = if resolved.class? :class @@ -237,8 +237,8 @@ module Spectator::DSL end %} {% begin %} - {{base.id}} {{"::".id unless resolved.name.starts_with?("::")}}{{resolved.name}} - ::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{value_methods.double_splat}}) {{block}} + {{base.id}} ::{{resolved.name}} + ::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}} end {% end %} end @@ -321,7 +321,7 @@ module Spectator::DSL macro mock(type, **value_methods, &block) {% raise "First argument of `mock` must be a type name, not #{type}" unless type.is_a?(Path) || type.is_a?(Generic) || type.is_a?(Union) || type.is_a?(Metaclass) || type.is_a?(TypeNode) %} {% begin %} - {% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{value_methods.double_splat}}) {{block}} + {% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{**value_methods}}) {{block}} {% end %} end @@ -431,7 +431,7 @@ module Spectator::DSL # This isn't required, but new_mock() should still find this type. ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, resolved.name.symbolize} %} - ::Spectator::Mock.inject({{base}}, {{resolved.name}}, {{value_methods.double_splat}}) {{block}} + ::Spectator::Mock.inject({{base}}, ::{{resolved.name}}, {{**value_methods}}) {{block}} end # Targets a stubbable object (such as a mock or double) for operations. diff --git a/src/spectator/matchers/matcher.cr b/src/spectator/matchers/matcher.cr index 05adb81..e54e55e 100644 --- a/src/spectator/matchers/matcher.cr +++ b/src/spectator/matchers/matcher.cr @@ -1,4 +1,3 @@ -require "../value" require "./match_data" module Spectator::Matchers @@ -23,19 +22,6 @@ module Spectator::Matchers # A successful match with `#match` should normally fail for this method, and vice-versa. abstract def negated_match(actual : Expression(T)) : MatchData forall T - # Compares a matcher against a value. - # Enables composable matchers. - def ===(actual : Expression(T)) : Bool - match(actual).matched? - end - - # Compares a matcher against a value. - # Enables composable matchers. - def ===(other) : Bool - expression = Value.new(other) - match(expression).matched? - end - private def match_data_description(actual : Expression(T)) : String forall T match_data_description(actual.label) end diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index 8a9a307..8c31810 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -29,26 +29,7 @@ module Spectator::Matchers # Checks whether the matcher is satisfied with the expression given to it. private def match?(actual : Expression(T)) : Bool forall T - actual_value = actual.value - expected_value = expected.value - if expected_value.is_a?(Range) && actual_value.is_a?(Comparable) - return match_impl?(expected_value, actual_value) - end - return false unless actual_value.is_a?(Comparable(typeof(expected_value.begin))) - expected_value.includes?(actual_value) - end - - private def match_impl?(expected_value : Range(B, E), actual_value : Comparable(B)) : Bool forall B, E - expected_value.includes?(actual_value) - end - - private def match_impl?(expected_value : Range(B, E), actual_value : T) : Bool forall B, E, T - return false unless actual_value.is_a?(B) || actual_value.is_a?(Comparable(B)) - expected_value.includes?(actual_value) - end - - private def match_impl?(expected_value : Range(Number, Number), actual_value : Number) : Bool - expected_value.includes?(actual_value) + expected.value.includes?(actual.value) end # Message displayed when the matcher isn't satisfied. diff --git a/src/spectator/mocks/abstract_arguments.cr b/src/spectator/mocks/abstract_arguments.cr index 4a6f75f..442c1d2 100644 --- a/src/spectator/mocks/abstract_arguments.cr +++ b/src/spectator/mocks/abstract_arguments.cr @@ -7,7 +7,7 @@ module Spectator end # Utility method for comparing two tuples considering special types. - private def compare_tuples(a : Tuple | Array, b : Tuple | Array) + private def compare_tuples(a : Tuple, b : Tuple) return false if a.size != b.size a.zip(b) do |a_value, b_value| @@ -18,14 +18,14 @@ module Spectator # Utility method for comparing two tuples considering special types. # Supports nilable tuples (ideal for splats). - private def compare_tuples(a : Tuple? | Array?, b : Tuple? | Array?) + private def compare_tuples(a : Tuple?, b : Tuple?) return false if a.nil? ^ b.nil? compare_tuples(a.not_nil!, b.not_nil!) end # Utility method for comparing two named tuples ignoring order. - private def compare_named_tuples(a : NamedTuple | Hash, b : NamedTuple | Hash) + private def compare_named_tuples(a : NamedTuple, b : NamedTuple) a.each do |k, v1| v2 = b.fetch(k) { return false } return false unless compare_values(v1, v2) @@ -45,14 +45,11 @@ module Spectator when Range # Ranges can only be matched against if their right side is comparable. # Ensure the right side is comparable, otherwise compare directly. - return a === b if b.is_a?(Comparable(typeof(b))) - a == b - when Tuple, Array - return compare_tuples(a, b) if b.is_a?(Tuple) || b.is_a?(Array) - a === b - when NamedTuple, Hash - return compare_named_tuples(a, b) if b.is_a?(NamedTuple) || b.is_a?(Hash) - a === b + if b.is_a?(Comparable(typeof(b))) + a === b + else + a == b + end else a === b end diff --git a/src/spectator/mocks/mock.cr b/src/spectator/mocks/mock.cr index d2a1fde..87e8e23 100644 --- a/src/spectator/mocks/mock.cr +++ b/src/spectator/mocks/mock.cr @@ -149,7 +149,7 @@ module Spectator macro inject(base, type_name, name = nil, **value_methods, &block) {% begin %} {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - {{base.id}} {{"::".id unless type_name.id.starts_with?("::")}}{{type_name.id}} + {{base.id}} ::{{type_name.id}} include ::Spectator::Mocked extend ::Spectator::StubbedType diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr index 76f6f44..9874dec 100644 --- a/src/spectator/wrapper.cr +++ b/src/spectator/wrapper.cr @@ -13,13 +13,18 @@ module Spectator # Creates a wrapper for the specified value. def initialize(value) - @pointer = Value.new(value).as(Void*) + @pointer = Box.box(value) end # Retrieves the previously wrapped value. # The *type* of the wrapped value must match otherwise an error will be raised. def get(type : T.class) : T forall T - @pointer.unsafe_as(Value(T)).get + {% begin %} + {% if T.nilable? %} + @pointer.null? ? nil : + {% end %} + Box(T).unbox(@pointer) + {% end %} end # Retrieves the previously wrapped value. @@ -34,20 +39,12 @@ module Spectator # type = wrapper.get { Int32 } # Returns Int32 # ``` def get(& : -> T) : T forall T - @pointer.unsafe_as(Value(T)).get - end - - # Wrapper for a value. - # Similar to `Box`, but doesn't segfault on nil and unions. - private class Value(T) - # Creates the wrapper. - def initialize(@value : T) - end - - # Retrieves the value. - def get : T - @value - end + {% begin %} + {% if T.nilable? %} + @pointer.null? ? nil : + {% end %} + Box(T).unbox(@pointer) + {% end %} end end end