mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Initial work on response constraints
This commit is contained in:
parent
de7cd90d11
commit
2adc867843
7 changed files with 85 additions and 6 deletions
|
@ -1,7 +1,7 @@
|
||||||
require "../../spec_helper"
|
require "../../spec_helper"
|
||||||
|
|
||||||
Spectator.describe Spectator::Double do
|
Spectator.describe Spectator::Double do
|
||||||
subject(dbl) { Spectator::Double.new("foobar", foo: 42, bar: "baz") }
|
subject(dbl) { Spectator::Double({foo: Int32, bar: String}).new("foobar", foo: 42, bar: "baz") }
|
||||||
|
|
||||||
it "responds to defined messages" do
|
it "responds to defined messages" do
|
||||||
aggregate_failures do
|
aggregate_failures do
|
||||||
|
|
5
src/spectator/mocks/abstract_arguments.cr
Normal file
5
src/spectator/mocks/abstract_arguments.cr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module Spectator
|
||||||
|
# Untyped arguments to a method call (message).
|
||||||
|
abstract class AbstractArguments
|
||||||
|
end
|
||||||
|
end
|
11
src/spectator/mocks/abstract_response.cr
Normal file
11
src/spectator/mocks/abstract_response.cr
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Spectator
|
||||||
|
# Untyped response to a method call (message).
|
||||||
|
abstract class AbstractResponse
|
||||||
|
# Name of the method this response is for.
|
||||||
|
getter method : Symbol
|
||||||
|
|
||||||
|
# Creates the base of the response.
|
||||||
|
def initialize(@method : Symbol)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,10 +1,12 @@
|
||||||
|
require "./abstract_arguments"
|
||||||
|
|
||||||
module Spectator
|
module Spectator
|
||||||
# Arguments used in a method call.
|
# Arguments used in a method call.
|
||||||
#
|
#
|
||||||
# Can also be used to match arguments.
|
# Can also be used to match arguments.
|
||||||
# *T* must be a `Tuple` type representing the positional arguments.
|
# *T* must be a `Tuple` type representing the positional arguments.
|
||||||
# *NT* must be a `NamedTuple` type representing the keyword arguments.
|
# *NT* must be a `NamedTuple` type representing the keyword arguments.
|
||||||
class Arguments(T, NT)
|
class Arguments(T, NT) < AbstractArguments
|
||||||
# Positional arguments.
|
# Positional arguments.
|
||||||
getter args : T
|
getter args : T
|
||||||
|
|
||||||
|
@ -20,6 +22,11 @@ module Spectator
|
||||||
new(args, kwargs)
|
new(args, kwargs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Constructs an instance of empty arguments.
|
||||||
|
macro empty
|
||||||
|
{{@type.name(generic_args: false)}}.capture
|
||||||
|
end
|
||||||
|
|
||||||
# Constructs a string representation of the arguments.
|
# Constructs a string representation of the arguments.
|
||||||
def to_s(io : IO) : Nil
|
def to_s(io : IO) : Nil
|
||||||
io << '('
|
io << '('
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
require "./abstract_response"
|
||||||
require "./unexpected_message"
|
require "./unexpected_message"
|
||||||
|
|
||||||
module Spectator
|
module Spectator
|
||||||
|
@ -5,10 +6,20 @@ module Spectator
|
||||||
#
|
#
|
||||||
# Handles all messages (method calls), but only responds to those configured.
|
# Handles all messages (method calls), but only responds to those configured.
|
||||||
# Methods called that were not configured will raise `UnexpectedMessage`.
|
# Methods called that were not configured will raise `UnexpectedMessage`.
|
||||||
class Double(Messages)
|
# `NT` must be a type of `NamedTuple` that maps method names to their return types.
|
||||||
|
class Double(NT)
|
||||||
|
# Stores responses to messages (method calls).
|
||||||
|
@responses : Array(AbstractResponse)
|
||||||
|
|
||||||
# Creates a double with pre-configures responses.
|
# Creates a double with pre-configures responses.
|
||||||
# A *name* can be provided, otherwise it is considered an anonymous double.
|
# A *name* can be provided, otherwise it is considered an anonymous double.
|
||||||
def initialize(@name : String? = nil, **@messages : **Messages)
|
def initialize(@responses : Array(AbstractResponse), @name : String? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(@name : String? = nil, **methods : **NT)
|
||||||
|
@responses = methods.map do |method, value|
|
||||||
|
Response.new(method, value).as(AbstractResponse)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Utility returning the double's name as a string.
|
# Utility returning the double's name as a string.
|
||||||
|
@ -37,7 +48,7 @@ module Spectator
|
||||||
{% if meth.double_splat %}**{{meth.double_splat}}, {% end %}
|
{% if meth.double_splat %}**{{meth.double_splat}}, {% end %}
|
||||||
{% if meth.block_arg %}&{{meth.block_arg}}{% elsif meth.accepts_block? %}&{% end %}
|
{% if meth.block_arg %}&{{meth.block_arg}}{% elsif meth.accepts_block? %}&{% end %}
|
||||||
){% if meth.return_type %} : {{meth.return_type}}{% end %}{% if !meth.free_vars.empty? %} forall {{meth.free_vars.splat}}{% end %}
|
){% if meth.return_type %} : {{meth.return_type}}{% end %}{% if !meth.free_vars.empty? %} forall {{meth.free_vars.splat}}{% end %}
|
||||||
\{% if type = Messages[{{meth.name.symbolize}}] %}
|
\{% if type = NT[{{meth.name.symbolize}}] %}
|
||||||
{% if meth.return_type %}
|
{% if meth.return_type %}
|
||||||
\{% if type <= {{meth.return_type}} %}
|
\{% if type <= {{meth.return_type}} %}
|
||||||
# Return type appears to match configured type.
|
# Return type appears to match configured type.
|
||||||
|
@ -70,7 +81,10 @@ module Spectator
|
||||||
# Handle all methods but only respond to configured messages.
|
# Handle all methods but only respond to configured messages.
|
||||||
# Raises an `UnexpectedMessage` error for non-configures messages.
|
# Raises an `UnexpectedMessage` error for non-configures messages.
|
||||||
macro method_missing(call)
|
macro method_missing(call)
|
||||||
\{% if Messages.keys.includes?({{call.name.symbolize}}.id) %}
|
\{% if NT.keys.includes?({{call.name.symbolize}}.id) %}
|
||||||
|
# Find a suitable response.
|
||||||
|
call = MethodCall.capture({{call.name.symbolize}}, {{call.args.splat}})
|
||||||
|
response = @responses.find &.===(call)
|
||||||
# Return configured response.
|
# Return configured response.
|
||||||
@messages[{{call.name.symbolize}}]
|
@messages[{{call.name.symbolize}}]
|
||||||
\{% else %}
|
\{% else %}
|
||||||
|
|
18
src/spectator/mocks/method_call.cr
Normal file
18
src/spectator/mocks/method_call.cr
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
require "./abstract_arguments"
|
||||||
|
require "./arguments"
|
||||||
|
|
||||||
|
module Spectator
|
||||||
|
class MethodCall
|
||||||
|
getter method : Symbol
|
||||||
|
|
||||||
|
getter arguments : AbstractArguments
|
||||||
|
|
||||||
|
def initialize(@method : Symbol, @arguments : Arguments = Arguments.empty)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.capture(method : Symbol, *args, **kwargs)
|
||||||
|
arguments = Arguments.new(args, kwargs)
|
||||||
|
new(method, arguments)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
24
src/spectator/mocks/response.cr
Normal file
24
src/spectator/mocks/response.cr
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
require "./abstract_arguments"
|
||||||
|
require "./abstract_response"
|
||||||
|
|
||||||
|
module Spectator
|
||||||
|
class Response(T) < AbstractResponse
|
||||||
|
# Return value.
|
||||||
|
getter value : T
|
||||||
|
|
||||||
|
# Arguments the method must have been called with to provide this response.
|
||||||
|
# Is nil when there's no constraint - only the method name must match.
|
||||||
|
getter constraint : AbstractArguments?
|
||||||
|
|
||||||
|
# Creates the response.
|
||||||
|
def initialize(@method : Symbol, @value : T, @constraint : Arguments? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ===(call : MethodCall)
|
||||||
|
return false if method != call.method
|
||||||
|
return true unless constraint = @constraint
|
||||||
|
|
||||||
|
constraint === call.arguments
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue