mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Compare commits
No commits in common. "master" and "v0.11.6" have entirely different histories.
10 changed files with 39 additions and 144 deletions
22
CHANGELOG.md
22
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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: spectator
|
||||
version: 0.12.1
|
||||
version: 0.11.6
|
||||
description: |
|
||||
Feature-rich testing framework for Crystal inspired by RSpec.
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue