Compare commits

..

No commits in common. "master" and "v0.11.6" have entirely different histories.

10 changed files with 39 additions and 144 deletions

View file

@ -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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.12.1] - 2024-08-13 ## [Unreleased]
### 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)
## [0.11.6] - 2023-01-26 ## [0.11.6] - 2023-01-26
### Added ### 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. First version ready for public use.
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.1...master [Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...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
[0.11.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.5...v0.11.6 [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.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 [0.11.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4

View file

@ -25,7 +25,7 @@ Add this to your application's `shard.yml`:
development_dependencies: development_dependencies:
spectator: spectator:
gitlab: arctic-fox/spectator gitlab: arctic-fox/spectator
version: ~> 0.12.0 version: ~> 0.11.0
``` ```
Usage Usage
@ -287,7 +287,7 @@ Spectator.describe Driver do
# Call the mock method. # Call the mock method.
subject.do_something(interface, dbl) subject.do_something(interface, dbl)
# Verify everything went okay. # Verify everything went okay.
expect(interface).to have_received(:invoke).with(dbl) expect(interface).to have_received(:invoke).with(thing)
end end
end end
``` ```

View file

@ -1,5 +1,5 @@
name: spectator name: spectator
version: 0.12.1 version: 0.11.6
description: | description: |
Feature-rich testing framework for Crystal inspired by RSpec. Feature-rich testing framework for Crystal inspired by RSpec.

View file

@ -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

View file

@ -31,7 +31,7 @@ module Spectator::DSL
::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %} ::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %}
# Define the plain double type. # 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. # Returns a new double that responds to undefined methods with itself.
# See: `NullDouble` # See: `NullDouble`
def as_null_object def as_null_object
@ -43,7 +43,7 @@ module Spectator::DSL
{% begin %} {% begin %}
# Define a matching null double type. # 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 %}
end end
@ -94,9 +94,9 @@ module Spectator::DSL
begin begin
%double = {% if found_tuple %} %double = {% if found_tuple %}
{{found_tuple[2].id}}.new({{value_methods.double_splat}}) {{found_tuple[2].id}}.new({{**value_methods}})
{% else %} {% else %}
::Spectator::LazyDouble.new({{name}}, {{value_methods.double_splat}}) ::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
{% end %} {% end %}
::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset }) ::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset })
%double %double
@ -176,7 +176,7 @@ module Spectator::DSL
# See `#def_double`. # See `#def_double`.
macro double(name, **value_methods, &block) macro double(name, **value_methods, &block)
{% begin %} {% 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 %}
end end
@ -189,7 +189,7 @@ module Spectator::DSL
# expect(dbl.foo).to eq(42) # expect(dbl.foo).to eq(42)
# ``` # ```
macro double(**value_methods) macro double(**value_methods)
::Spectator::LazyDouble.new({{value_methods.double_splat}}) ::Spectator::LazyDouble.new({{**value_methods}})
end end
# Defines a new mock type. # Defines a new mock type.
@ -226,7 +226,7 @@ module Spectator::DSL
# Store information about how the mock is defined and its context. # Store information about how the mock is defined and its context.
# This is important for constructing an instance of the mock later. # 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? base = if resolved.class?
:class :class
@ -237,8 +237,8 @@ module Spectator::DSL
end %} end %}
{% begin %} {% begin %}
{{base.id}} {{"::".id unless resolved.name.starts_with?("::")}}{{resolved.name}} {{base.id}} ::{{resolved.name}}
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{value_methods.double_splat}}) {{block}} ::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}}
end end
{% end %} {% end %}
end end
@ -321,7 +321,7 @@ module Spectator::DSL
macro mock(type, **value_methods, &block) 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) %} {% 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 %} {% 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 %}
end end
@ -431,7 +431,7 @@ module Spectator::DSL
# This isn't required, but new_mock() should still find this type. # 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::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 end
# Targets a stubbable object (such as a mock or double) for operations. # Targets a stubbable object (such as a mock or double) for operations.

View file

@ -1,4 +1,3 @@
require "../value"
require "./match_data" require "./match_data"
module Spectator::Matchers module Spectator::Matchers
@ -23,19 +22,6 @@ module Spectator::Matchers
# A successful match with `#match` should normally fail for this method, and vice-versa. # A successful match with `#match` should normally fail for this method, and vice-versa.
abstract def negated_match(actual : Expression(T)) : MatchData forall T 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 private def match_data_description(actual : Expression(T)) : String forall T
match_data_description(actual.label) match_data_description(actual.label)
end end

View file

@ -29,26 +29,7 @@ module Spectator::Matchers
# Checks whether the matcher is satisfied with the expression given to it. # Checks whether the matcher is satisfied with the expression given to it.
private def match?(actual : Expression(T)) : Bool forall T private def match?(actual : Expression(T)) : Bool forall T
actual_value = actual.value expected.value.includes?(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)
end end
# Message displayed when the matcher isn't satisfied. # Message displayed when the matcher isn't satisfied.

View file

@ -7,7 +7,7 @@ module Spectator
end end
# Utility method for comparing two tuples considering special types. # 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 return false if a.size != b.size
a.zip(b) do |a_value, b_value| a.zip(b) do |a_value, b_value|
@ -18,14 +18,14 @@ module Spectator
# Utility method for comparing two tuples considering special types. # Utility method for comparing two tuples considering special types.
# Supports nilable tuples (ideal for splats). # 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? return false if a.nil? ^ b.nil?
compare_tuples(a.not_nil!, b.not_nil!) compare_tuples(a.not_nil!, b.not_nil!)
end end
# Utility method for comparing two named tuples ignoring order. # 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| a.each do |k, v1|
v2 = b.fetch(k) { return false } v2 = b.fetch(k) { return false }
return false unless compare_values(v1, v2) return false unless compare_values(v1, v2)
@ -45,14 +45,11 @@ module Spectator
when Range when Range
# Ranges can only be matched against if their right side is comparable. # Ranges can only be matched against if their right side is comparable.
# Ensure the right side is comparable, otherwise compare directly. # Ensure the right side is comparable, otherwise compare directly.
return a === b if b.is_a?(Comparable(typeof(b))) if b.is_a?(Comparable(typeof(b)))
a == b a === b
when Tuple, Array else
return compare_tuples(a, b) if b.is_a?(Tuple) || b.is_a?(Array) a == b
a === b end
when NamedTuple, Hash
return compare_named_tuples(a, b) if b.is_a?(NamedTuple) || b.is_a?(Hash)
a === b
else else
a === b a === b
end end

View file

@ -149,7 +149,7 @@ module Spectator
macro inject(base, type_name, name = nil, **value_methods, &block) macro inject(base, type_name, name = nil, **value_methods, &block)
{% begin %} {% begin %}
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %} {% 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 include ::Spectator::Mocked
extend ::Spectator::StubbedType extend ::Spectator::StubbedType

View file

@ -13,13 +13,18 @@ module Spectator
# Creates a wrapper for the specified value. # Creates a wrapper for the specified value.
def initialize(value) def initialize(value)
@pointer = Value.new(value).as(Void*) @pointer = Box.box(value)
end end
# Retrieves the previously wrapped value. # Retrieves the previously wrapped value.
# The *type* of the wrapped value must match otherwise an error will be raised. # The *type* of the wrapped value must match otherwise an error will be raised.
def get(type : T.class) : T forall T 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 end
# Retrieves the previously wrapped value. # Retrieves the previously wrapped value.
@ -34,20 +39,12 @@ module Spectator
# type = wrapper.get { Int32 } # Returns Int32 # type = wrapper.get { Int32 } # Returns Int32
# ``` # ```
def get(& : -> T) : T forall T def get(& : -> T) : T forall T
@pointer.unsafe_as(Value(T)).get {% begin %}
end {% if T.nilable? %}
@pointer.null? ? nil :
# Wrapper for a value. {% end %}
# Similar to `Box`, but doesn't segfault on nil and unions. Box(T).unbox(@pointer)
private class Value(T) {% end %}
# Creates the wrapper.
def initialize(@value : T)
end
# Retrieves the value.
def get : T
@value
end
end end
end end
end end