mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
WIP replace mocks with Mocks shard
This commit is contained in:
parent
0e3f626932
commit
d74a772f43
40 changed files with 110 additions and 2348 deletions
|
@ -10,6 +10,10 @@ crystal: ">= 1.6.0, < 1.11"
|
|||
|
||||
license: MIT
|
||||
|
||||
dependencies:
|
||||
mocks:
|
||||
github: icy-arctic-fox/mocks
|
||||
|
||||
development_dependencies:
|
||||
ameba:
|
||||
github: crystal-ameba/ameba
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
require "../spec_helper"
|
||||
|
||||
Spectator.describe Spectator::Matchers::ReceiveMatcher do
|
||||
let(stub) { Spectator::NullStub.new(:test_method) }
|
||||
let(stub) { Mocks::NilStub.new(:test_method) }
|
||||
subject(matcher) { described_class.new(stub) }
|
||||
|
||||
let(args) { Spectator::Arguments.capture(1, "test", Symbol, foo: /bar/) }
|
||||
let(args_stub) { Spectator::NullStub.new(:test_method, args) }
|
||||
let(args) { Mocks::ArgumentsPattern.build(1, "test", Symbol, foo: /bar/) }
|
||||
let(args_stub) { Mocks::NilStub.new(:test_method, args) }
|
||||
let(args_matcher) { described_class.new(args_stub) }
|
||||
|
||||
let(no_args_stub) { Spectator::NullStub.new(:test_method, Spectator::Arguments.none) }
|
||||
let(no_args_stub) { Mocks::NilStub.new(:test_method, Mocks::ArgumentsPattern.none) }
|
||||
let(no_args_matcher) { described_class.new(no_args_stub) }
|
||||
|
||||
double(:dbl, test_method: nil, irrelevant: nil)
|
||||
|
@ -296,7 +296,7 @@ Spectator.describe Spectator::Matchers::ReceiveMatcher do
|
|||
end
|
||||
|
||||
it "has the expected call listed" do
|
||||
is_expected.to contain({:expected, "Not #{stub.message}"})
|
||||
is_expected.to contain({:expected, "Not #{stub}"})
|
||||
end
|
||||
|
||||
it "has the list of called methods" do
|
||||
|
|
|
@ -48,13 +48,13 @@ Spectator.describe "Allow stub DSL" do
|
|||
context "with a class double" do
|
||||
double(:dbl) do
|
||||
# Ensure the original is never called.
|
||||
abstract_stub def self.foo : Nil
|
||||
stub abstract def self.foo : Nil
|
||||
end
|
||||
|
||||
abstract_stub def self.foo(arg) : Nil
|
||||
stub abstract def self.foo(arg) : Nil
|
||||
end
|
||||
|
||||
abstract_stub def self.value : Int32
|
||||
stub abstract def self.value : Int32
|
||||
42
|
||||
end
|
||||
end
|
||||
|
|
|
@ -337,7 +337,7 @@ Spectator.describe "Double DSL", :smoke do
|
|||
|
||||
describe "class doubles" do
|
||||
double(:class_double) do
|
||||
abstract_stub def self.abstract_method
|
||||
stub abstract def self.abstract_method
|
||||
:abstract
|
||||
end
|
||||
|
||||
|
|
|
@ -57,13 +57,13 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
context "with a class double" do
|
||||
double(:dbl) do
|
||||
# Ensure the original is never called.
|
||||
abstract_stub def self.foo : Nil
|
||||
stub abstract def self.foo : Nil
|
||||
end
|
||||
|
||||
abstract_stub def self.foo(arg) : Nil
|
||||
stub abstract def self.foo(arg) : Nil
|
||||
end
|
||||
|
||||
abstract_stub def self.value : Int32
|
||||
stub abstract def self.value : Int32
|
||||
42
|
||||
end
|
||||
end
|
||||
|
|
|
@ -253,7 +253,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
|
@ -268,10 +268,10 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
abstract_stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
end
|
||||
|
||||
subject(fake) { mock(AbstractClass) }
|
||||
|
@ -373,10 +373,10 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
mock(AbstractClass) do
|
||||
# NOTE: Abstract methods without a type restriction on the return value
|
||||
# must be implemented with a type restriction.
|
||||
abstract_stub abstract def method1 : String
|
||||
stub abstract def method1 : String
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
|
@ -449,7 +449,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
|
@ -464,10 +464,10 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
abstract_stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
end
|
||||
|
||||
subject(fake) { mock(AbstractStruct) }
|
||||
|
@ -569,10 +569,10 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
mock(AbstractStruct) do
|
||||
# NOTE: Abstract methods without a type restriction on the return value
|
||||
# must be implemented with a type restriction.
|
||||
abstract_stub abstract def method1 : String
|
||||
stub abstract def method1 : String
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
|
@ -994,7 +994,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
mock(Dummy) do
|
||||
abstract_stub def self.abstract_method
|
||||
stub abstract def self.abstract_method
|
||||
:abstract
|
||||
end
|
||||
|
||||
|
@ -1055,7 +1055,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
mock(Dummy) do
|
||||
abstract_stub def self.abstract_method
|
||||
stub abstract def self.abstract_method
|
||||
:abstract
|
||||
end
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "with abstract stubs and return type annotations" do
|
||||
Spectator::Double.define(TestDouble) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
stub abstract def foo(value) : String
|
||||
end
|
||||
|
||||
let(arguments) { Spectator::Arguments.capture(/foo/) }
|
||||
|
@ -98,8 +98,8 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "with nillable return type annotations" do
|
||||
Spectator::Double.define(TestDouble) do
|
||||
abstract_stub abstract def foo : String?
|
||||
abstract_stub abstract def bar : Nil
|
||||
stub abstract def foo : String?
|
||||
stub abstract def bar : Nil
|
||||
end
|
||||
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, nil).as(Spectator::Stub) }
|
||||
|
@ -116,7 +116,7 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "with a method that uses NoReturn" do
|
||||
Spectator::Double.define(NoReturnDouble) do
|
||||
abstract_stub abstract def oops : NoReturn
|
||||
stub abstract def oops : NoReturn
|
||||
end
|
||||
|
||||
subject(dbl) { NoReturnDouble.new }
|
||||
|
@ -233,8 +233,8 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "without common object methods" do
|
||||
Spectator::Double.define(TestDouble) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
abstract_stub abstract def foo(value, & : -> _) : String
|
||||
stub abstract def foo(value) : String
|
||||
stub abstract def foo(value, & : -> _) : String
|
||||
end
|
||||
|
||||
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) }
|
||||
|
|
|
@ -50,7 +50,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "with abstract stubs and return type annotations" do
|
||||
Spectator::NullDouble.define(TestDouble2) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
stub abstract def foo(value) : String
|
||||
end
|
||||
|
||||
let(arguments) { Spectator::Arguments.capture(/foo/) }
|
||||
|
@ -74,8 +74,8 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "with nillable return type annotations" do
|
||||
Spectator::NullDouble.define(TestDouble) do
|
||||
abstract_stub abstract def foo : String?
|
||||
abstract_stub abstract def bar : Nil
|
||||
stub abstract def foo : String?
|
||||
stub abstract def bar : Nil
|
||||
end
|
||||
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, nil).as(Spectator::Stub) }
|
||||
|
@ -92,7 +92,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "with a method that uses NoReturn" do
|
||||
Spectator::NullDouble.define(NoReturnDouble) do
|
||||
abstract_stub abstract def oops : NoReturn
|
||||
stub abstract def oops : NoReturn
|
||||
end
|
||||
|
||||
subject(dbl) { NoReturnDouble.new }
|
||||
|
@ -202,8 +202,8 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "without common object methods" do
|
||||
Spectator::NullDouble.define(TestDouble) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
abstract_stub abstract def foo(value, & : -> _) : String
|
||||
stub abstract def foo(value) : String
|
||||
stub abstract def foo(value, & : -> _) : String
|
||||
end
|
||||
|
||||
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require "colorize"
|
||||
require "log"
|
||||
require "mocks"
|
||||
require "./spectator/includes"
|
||||
|
||||
# Module that contains all functionality related to Spectator.
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
require "../mocks"
|
||||
require "mocks/dsl/allow_syntax"
|
||||
|
||||
module Spectator::DSL
|
||||
# Methods and macros for mocks and doubles.
|
||||
module Mocks
|
||||
include ::Mocks::DSL::AllowSyntax
|
||||
|
||||
# All defined double and mock types.
|
||||
# Each tuple consists of the double name or mocked type,
|
||||
# defined context (example group), and double type name relative to its context.
|
||||
|
@ -31,20 +33,9 @@ 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}}) do
|
||||
# Returns a new double that responds to undefined methods with itself.
|
||||
# See: `NullDouble`
|
||||
def as_null_object
|
||||
{{null_double_type_name}}.new(@stubs)
|
||||
end
|
||||
|
||||
::Mocks::Double.define({{double_type_name}}, {{**value_methods}}) do
|
||||
{{block.body if block}}
|
||||
end
|
||||
|
||||
{% begin %}
|
||||
# Define a matching null double type.
|
||||
::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{**value_methods}}) {{block}}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Instantiates a double.
|
||||
|
@ -94,11 +85,11 @@ module Spectator::DSL
|
|||
|
||||
begin
|
||||
%double = {% if found_tuple %}
|
||||
{{found_tuple[2].id}}.new({{**value_methods}})
|
||||
{{found_tuple[2].id}}.new({{found_tuple[0].id.stringify}}, {{**value_methods}})
|
||||
{% else %}
|
||||
::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset })
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double.__mocks.reset })
|
||||
%double
|
||||
end
|
||||
end
|
||||
|
@ -162,7 +153,7 @@ module Spectator::DSL
|
|||
%stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%double._spectator_define_stub(%stub{key})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset })
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double.__mocks.reset })
|
||||
%double
|
||||
end
|
||||
end
|
||||
|
@ -238,7 +229,7 @@ module Spectator::DSL
|
|||
|
||||
{% begin %}
|
||||
{{base.id}} ::{{resolved.name}}
|
||||
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}}
|
||||
::Mocks::Mock.define({{mock_type_name}} < ::{{resolved.name}}, {{**value_methods}}) {{block}}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
@ -301,10 +292,10 @@ module Spectator::DSL
|
|||
{% if found_tuple %}
|
||||
{{found_tuple[2].id}}.new.tap do |%mock|
|
||||
{% for key, value in value_methods %}
|
||||
%stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock._spectator_define_stub(%stub{key})
|
||||
%stub{key} = ::Mocks::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock.__mocks.add_stub(%stub{key})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %mock._spectator_reset })
|
||||
::Spectator::Harness.current?.try(&.cleanup { %mock.__mocks.reset })
|
||||
end
|
||||
{% else %}
|
||||
{% raise "Type `#{type.id}` must be previously mocked before attempting to instantiate." %}
|
||||
|
@ -377,8 +368,8 @@ module Spectator::DSL
|
|||
begin
|
||||
%mock = {{found_tuple[2].id}}
|
||||
{% for key, value in value_methods %}
|
||||
%stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock._spectator_define_stub(%stub{key})
|
||||
%stub{key} = ::Mocks::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock.__mocks.add_stub(%stub{key})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %mock._spectator_reset })
|
||||
%mock
|
||||
|
@ -431,77 +422,46 @@ 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}}) {{block}}
|
||||
{% begin %}
|
||||
{{base.id}} {{type.id}}
|
||||
include ::Mocks::Stubbable::Automatic
|
||||
|
||||
{% for key, value in value_methods %}
|
||||
stub_any_args {{key}} = {{value}}
|
||||
{% end %}
|
||||
|
||||
{{block.body if block}}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Targets a stubbable object (such as a mock or double) for operations.
|
||||
# Constructs a stub for a method.
|
||||
#
|
||||
# The *stubbable* must be a `Stubbable` or `StubbedType`.
|
||||
# This method is expected to be followed up with `.to receive()`.
|
||||
# The *method* is the name of the method to stub.
|
||||
#
|
||||
# This is also the start of a fluent interface for defining stubs.
|
||||
#
|
||||
# Allow syntax:
|
||||
# ```
|
||||
# dbl = dbl(:foobar)
|
||||
# allow(dbl).to receive(:foo).and_return(42)
|
||||
# ```
|
||||
def allow(stubbable : Stubbable | StubbedType)
|
||||
::Spectator::Allow.new(stubbable)
|
||||
end
|
||||
|
||||
# Helper method producing a compilation error when attempting to stub a non-stubbable object.
|
||||
#
|
||||
# Triggered in cases like this:
|
||||
# ```
|
||||
# allow(42).to receive(:to_s).and_return("123")
|
||||
# ```
|
||||
def allow(stubbable)
|
||||
{% raise "Target of `allow()` must be stubbable (mock or double)." %}
|
||||
end
|
||||
|
||||
# Begins the creation of a stub.
|
||||
#
|
||||
# The *method* is the name of the method being stubbed.
|
||||
# It should not define any parameters, it should be just the method name as a literal symbol or string.
|
||||
#
|
||||
# Alone, this method returns a `NullStub`, which allows a stubbable object to return nil from a method.
|
||||
# This macro is typically followed up with a method like `and_return` to change the stub's behavior.
|
||||
#
|
||||
# ```
|
||||
# dbl = dbl(:foobar)
|
||||
# allow(dbl).to receive(:foo)
|
||||
# expect(dbl.foo).to be_nil
|
||||
#
|
||||
# allow(dbl).to receive(:foo).and_return(42)
|
||||
# expect(dbl.foo).to eq(42)
|
||||
# ```
|
||||
#
|
||||
# A block can be provided to be run every time the stub is invoked.
|
||||
# The value returned by the block is returned by the stubbed method.
|
||||
#
|
||||
# ```
|
||||
# dbl = dbl(:foobar)
|
||||
# allow(dbl).to receive(:foo) { 42 }
|
||||
# expect(dbl.foo).to eq(42)
|
||||
# allow(dbl).to receive(:some_method)
|
||||
# allow(dbl).to receive(:the_answer).and_return(42)
|
||||
# ```
|
||||
macro receive(method, *, _file = __FILE__, _line = __LINE__, &block)
|
||||
{% if block %}
|
||||
%proc = ->(%args : ::Spectator::AbstractArguments) {
|
||||
{% if !block.args.empty? %}{{*block.args}} = %args {% end %}
|
||||
{{block.body}}
|
||||
}
|
||||
::Spectator::ProcStub.new({{method.id.symbolize}}, %proc, location: ::Spectator::Location.new({{_file}}, {{_line}}))
|
||||
::Mocks::ProcStub.new({{method.id.symbolize}}) {{block}}
|
||||
{% else %}
|
||||
::Spectator::NullStub.new({{method.id.symbolize}}, location: ::Spectator::Location.new({{_file}}, {{_line}}))
|
||||
::Mocks::NilStub.new({{method.id.symbolize}})
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Returns empty arguments.
|
||||
def no_args
|
||||
::Spectator::Arguments.none
|
||||
::Mocks::Arguments.none
|
||||
end
|
||||
|
||||
# Indicates any arguments can be used (no constraint).
|
||||
def any_args
|
||||
::Spectator::Arguments.any
|
||||
::Mocks::Arguments.any
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -101,8 +101,8 @@ module Spectator
|
|||
|
||||
# Asserts that a method is called some point before the example completes.
|
||||
@[AlwaysInline]
|
||||
def to(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable %}
|
||||
|
||||
to_eventually(stub, message)
|
||||
end
|
||||
|
@ -131,15 +131,15 @@ module Spectator
|
|||
|
||||
# Asserts that a method is not called before the example completes.
|
||||
@[AlwaysInline]
|
||||
def to_not(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_not receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to_not(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_not receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable || T < ::Spectator::StubbedType %}
|
||||
|
||||
to_never(stub, message)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def not_to(stub : Stub, message = nil) : Nil
|
||||
def not_to(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
to_not(stub, message)
|
||||
end
|
||||
|
||||
|
@ -188,11 +188,11 @@ module Spectator
|
|||
end
|
||||
|
||||
# Asserts that a method is called some point before the example completes.
|
||||
def to_eventually(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_eventually receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to_eventually(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_eventually receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable || T < ::Spectator::StubbedType %}
|
||||
|
||||
stubbable = @expression.value
|
||||
unless stubbable._spectator_stub_for_method?(stub.method)
|
||||
unless stubbable.__mocks.has_stub?(stub.method_name)
|
||||
# Add stub without an argument constraint.
|
||||
# Avoids confusing logic like this:
|
||||
# ```
|
||||
|
@ -201,19 +201,19 @@ module Spectator
|
|||
# ```
|
||||
# Notice that `#foo` is called, but with different arguments.
|
||||
# Normally this would raise an error, but that should be prevented.
|
||||
unconstrained_stub = stub.with(Arguments.any)
|
||||
stubbable._spectator_define_stub(unconstrained_stub)
|
||||
unconstrained_stub = stub.with(::Mocks::Arguments.any)
|
||||
stubbable.__mocks.add_stub(unconstrained_stub)
|
||||
end
|
||||
|
||||
# Apply the stub that is expected to be called.
|
||||
stubbable._spectator_define_stub(stub)
|
||||
stubbable.__mocks.add_stub(stub)
|
||||
|
||||
# Check if the stub was invoked after the test completes.
|
||||
matcher = Matchers::ReceiveMatcher.new(stub)
|
||||
Harness.current.defer { to(matcher, message) }
|
||||
|
||||
# Prevent leaking stubs between tests.
|
||||
Harness.current.cleanup { stubbable._spectator_remove_stub(stub) }
|
||||
Harness.current.cleanup { stubbable.__mocks.remove_stub(stub) }
|
||||
end
|
||||
|
||||
# Asserts that some criteria defined by the matcher is eventually satisfied.
|
||||
|
@ -224,11 +224,11 @@ module Spectator
|
|||
end
|
||||
|
||||
# Asserts that a method is not called before the example completes.
|
||||
def to_never(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_never receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to_never(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_never receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable %}
|
||||
|
||||
stubbable = @expression.value
|
||||
unless stubbable._spectator_stub_for_method?(stub.method)
|
||||
unless stubbable.__mocks.find_stub(stub.method)
|
||||
# Add stub without an argument constraint.
|
||||
# Avoids confusing logic like this:
|
||||
# ```
|
||||
|
@ -238,23 +238,23 @@ module Spectator
|
|||
# Notice that `#foo` is called, but with different arguments.
|
||||
# Normally this would raise an error, but that should be prevented.
|
||||
unconstrained_stub = stub.with(Arguments.any)
|
||||
stubbable._spectator_define_stub(unconstrained_stub)
|
||||
stubbable.__mocks.add_stub(unconstrained_stub)
|
||||
end
|
||||
|
||||
# Apply the stub that could be called in case it is.
|
||||
stubbable._spectator_define_stub(stub)
|
||||
stubbable.__mocks.add_stub(stub)
|
||||
|
||||
# Check if the stub was invoked after the test completes.
|
||||
matcher = Matchers::ReceiveMatcher.new(stub)
|
||||
Harness.current.defer { to_not(matcher, message) }
|
||||
|
||||
# Prevent leaking stubs between tests.
|
||||
Harness.current.cleanup { stubbable._spectator_remove_stub(stub) }
|
||||
Harness.current.cleanup { stubbable.__mocks.remove_stub(stub) }
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def never_to(stub : Stub, message = nil) : Nil
|
||||
def never_to(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
to_never(stub, message)
|
||||
end
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ require "./location"
|
|||
require "./location_node_filter"
|
||||
require "./matchers"
|
||||
require "./metadata"
|
||||
require "./mocks"
|
||||
require "./name_node_filter"
|
||||
require "./null_context"
|
||||
require "./null_node_filter"
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
require "../mocks/stub"
|
||||
require "../mocks/stubbable"
|
||||
require "../mocks/stubbed_type"
|
||||
require "./matcher"
|
||||
|
||||
module Spectator::Matchers
|
||||
|
@ -9,13 +6,13 @@ module Spectator::Matchers
|
|||
alias Count = Range(Int32?, Int32?)
|
||||
|
||||
# Creates the matcher for expecting a method call matching a stub.
|
||||
def initialize(@stub : Stub, @count : Count = Count.new(1, nil))
|
||||
def initialize(@stub : Mocks::Stub, @count : Count = Count.new(1, nil))
|
||||
end
|
||||
|
||||
# Creates the matcher for expecting a method call with any arguments.
|
||||
# *expected* is an expression evaluating to the method name as a symbol.
|
||||
def initialize(expected : Expression(Symbol))
|
||||
stub = NullStub.new(expected.value).as(Stub)
|
||||
stub = Mocks::NilStub.new(expected.value).as(Mocks::Stub)
|
||||
initialize(stub)
|
||||
end
|
||||
|
||||
|
@ -81,18 +78,18 @@ module Spectator::Matchers
|
|||
|
||||
# Short text about the matcher's purpose.
|
||||
def description : String
|
||||
"received #{@stub.message} #{humanize_count}"
|
||||
"received #{@stub} #{humanize_count}"
|
||||
end
|
||||
|
||||
# Actually performs the test against the expression (value or block).
|
||||
def match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData
|
||||
def match(actual : Expression(Mocks::Stubbable)) : MatchData
|
||||
stubbed = actual.value
|
||||
calls = relevant_calls(stubbed)
|
||||
if @count.includes?(calls.size)
|
||||
SuccessfulMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}")
|
||||
SuccessfulMatchData.new("#{actual.label} received #{@stub} #{humanize_count}")
|
||||
else
|
||||
FailedMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}",
|
||||
"#{actual.label} did not receive #{@stub.message}", values(actual).to_a)
|
||||
FailedMatchData.new("#{actual.label} received #{@stub} #{humanize_count}",
|
||||
"#{actual.label} did not receive #{@stub}", values(actual).to_a)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -102,13 +99,13 @@ module Spectator::Matchers
|
|||
end
|
||||
|
||||
# Performs the test against the expression (value or block), but inverted.
|
||||
def negated_match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData
|
||||
def negated_match(actual : Expression(Mocks::Stubbable)) : MatchData
|
||||
stubbed = actual.value
|
||||
calls = relevant_calls(stubbed)
|
||||
if @count.includes?(calls.size)
|
||||
FailedMatchData.new("#{actual.label} did not receive #{@stub.message}", "#{actual.label} received #{@stub.message}", negated_values(actual).to_a)
|
||||
FailedMatchData.new("#{actual.label} did not receive #{@stub}", "#{actual.label} received #{@stub}", negated_values(actual).to_a)
|
||||
else
|
||||
SuccessfulMatchData.new("#{actual.label} did not receive #{@stub.message} #{humanize_count}")
|
||||
SuccessfulMatchData.new("#{actual.label} did not receive #{@stub} #{humanize_count}")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -120,7 +117,7 @@ module Spectator::Matchers
|
|||
# Additional information about the match failure.
|
||||
private def values(actual : Expression(T)) forall T
|
||||
{
|
||||
expected: @stub.message,
|
||||
expected: @stub.to_s,
|
||||
actual: method_call_list(actual.value),
|
||||
}
|
||||
end
|
||||
|
@ -128,14 +125,14 @@ module Spectator::Matchers
|
|||
# Additional information about the match failure when negated.
|
||||
private def negated_values(actual : Expression(T)) forall T
|
||||
{
|
||||
expected: "Not #{@stub.message}",
|
||||
expected: "Not #{@stub}",
|
||||
actual: method_call_list(actual.value),
|
||||
}
|
||||
end
|
||||
|
||||
# Filtered list of method calls relevant to the matcher.
|
||||
private def relevant_calls(stubbable)
|
||||
stubbable._spectator_calls.select { |call| @stub === call }
|
||||
stubbable.__mocks.calls.select { |call| @stub === call }
|
||||
end
|
||||
|
||||
private def humanize_count
|
||||
|
@ -148,11 +145,11 @@ module Spectator::Matchers
|
|||
|
||||
# Formatted list of method calls.
|
||||
private def method_call_list(stubbable)
|
||||
calls = stubbable._spectator_calls
|
||||
calls = stubbable.__mocks.calls
|
||||
if calls.empty?
|
||||
"None"
|
||||
else
|
||||
calls.join("\n")
|
||||
calls.join('\n')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "./mocks/*"
|
||||
|
||||
module Spectator
|
||||
# Functionality for mocking existing types.
|
||||
module Mocks
|
||||
end
|
||||
end
|
|
@ -1,58 +0,0 @@
|
|||
module Spectator
|
||||
# Untyped arguments to a method call (message).
|
||||
abstract class AbstractArguments
|
||||
# Use the string representation to avoid over complicating debug output.
|
||||
def inspect(io : IO) : Nil
|
||||
to_s(io)
|
||||
end
|
||||
|
||||
# Utility method for comparing two tuples considering special types.
|
||||
private def compare_tuples(a : Tuple, b : Tuple)
|
||||
return false if a.size != b.size
|
||||
|
||||
a.zip(b) do |a_value, b_value|
|
||||
return false unless compare_values(a_value, b_value)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Utility method for comparing two tuples considering special types.
|
||||
# Supports nilable tuples (ideal for splats).
|
||||
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, b : NamedTuple)
|
||||
a.each do |k, v1|
|
||||
v2 = b.fetch(k) { return false }
|
||||
return false unless compare_values(v1, v2)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Utility method for comparing two arguments considering special types.
|
||||
# Some types used for case-equality don't work well with unexpected right-hand types.
|
||||
# This can happen when the right side is a massive union of types.
|
||||
private def compare_values(a, b)
|
||||
case a
|
||||
when Proc
|
||||
# Using procs as argument matchers isn't supported currently.
|
||||
# Compare directly instead.
|
||||
a == b
|
||||
when Range
|
||||
# Ranges can only be matched against if their right side is comparable.
|
||||
# Ensure the right side is comparable, otherwise compare directly.
|
||||
if b.is_a?(Comparable(typeof(b)))
|
||||
a === b
|
||||
else
|
||||
a == b
|
||||
end
|
||||
else
|
||||
a === b
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
require "../harness"
|
||||
require "./stub"
|
||||
require "./stubbable"
|
||||
require "./stubbed_type"
|
||||
|
||||
module Spectator
|
||||
# Targets a stubbable object.
|
||||
#
|
||||
# This type is effectively part of the mock DSL.
|
||||
# It is primarily used in the mock DSL to provide this syntax:
|
||||
# ```
|
||||
# allow(dbl).to
|
||||
# ```
|
||||
struct Allow(T)
|
||||
# Creates the stub target.
|
||||
#
|
||||
# The *target* must be a kind of `Stubbable` or `StubbedType`.
|
||||
def initialize(@target : T)
|
||||
{% raise "Target of `allow` must be stubbable (a mock or double)." unless T < Stubbable || T < StubbedType %}
|
||||
end
|
||||
|
||||
# Applies a stub to the targeted stubbable object.
|
||||
def to(stub : Stub) : Nil
|
||||
@target._spectator_define_stub(stub)
|
||||
Harness.current?.try &.cleanup { @target._spectator_remove_stub(stub) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,110 +0,0 @@
|
|||
require "./abstract_arguments"
|
||||
|
||||
module Spectator
|
||||
# Arguments used in a method call.
|
||||
#
|
||||
# Can also be used to match arguments.
|
||||
# *Args* must be a `Tuple` representing the standard arguments.
|
||||
# *KWArgs* must be a `NamedTuple` type representing extra keyword arguments.
|
||||
class Arguments(Args, KWArgs) < AbstractArguments
|
||||
# Positional arguments.
|
||||
getter args : Args
|
||||
|
||||
# Keyword arguments.
|
||||
getter kwargs : KWArgs
|
||||
|
||||
# Creates arguments used in a method call.
|
||||
def initialize(@args : Args, @kwargs : KWArgs)
|
||||
{% raise "Positional arguments (generic type Args) must be a Tuple" unless Args <= Tuple %}
|
||||
{% raise "Keyword arguments (generic type KWArgs) must be a NamedTuple" unless KWArgs <= NamedTuple %}
|
||||
end
|
||||
|
||||
# Instance of empty arguments.
|
||||
class_getter none : AbstractArguments = capture
|
||||
|
||||
# Returns unconstrained arguments.
|
||||
def self.any : AbstractArguments?
|
||||
nil.as(AbstractArguments?)
|
||||
end
|
||||
|
||||
# Friendlier constructor for capturing arguments.
|
||||
def self.capture(*args, **kwargs)
|
||||
new(args, kwargs)
|
||||
end
|
||||
|
||||
# Returns the positional argument at the specified index.
|
||||
def [](index : Int)
|
||||
args[index]
|
||||
end
|
||||
|
||||
# Returns the specified named argument.
|
||||
def [](arg : Symbol)
|
||||
@kwargs[arg]
|
||||
end
|
||||
|
||||
# Returns all arguments and splatted arguments as a tuple.
|
||||
def positional : Tuple
|
||||
args
|
||||
end
|
||||
|
||||
# Returns all named positional and keyword arguments as a named tuple.
|
||||
def named : NamedTuple
|
||||
kwargs
|
||||
end
|
||||
|
||||
# Constructs a string representation of the arguments.
|
||||
def to_s(io : IO) : Nil
|
||||
return io << "(no args)" if args.empty? && kwargs.empty?
|
||||
|
||||
io << '('
|
||||
|
||||
# Add the positional arguments.
|
||||
args.each_with_index do |arg, i|
|
||||
io << ", " if i > 0
|
||||
arg.inspect(io)
|
||||
end
|
||||
|
||||
# Add the keyword arguments.
|
||||
kwargs.each_with_index(args.size) do |key, value, i|
|
||||
io << ", " if i > 0
|
||||
io << key << ": "
|
||||
value.inspect(io)
|
||||
end
|
||||
|
||||
io << ')'
|
||||
end
|
||||
|
||||
# Checks if this set of arguments and another are equal.
|
||||
def ==(other : AbstractArguments)
|
||||
positional == other.positional && kwargs == other.kwargs
|
||||
end
|
||||
|
||||
# Checks if another set of arguments matches this set of arguments.
|
||||
def ===(other : Arguments)
|
||||
compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def ===(other : FormalArguments)
|
||||
return false unless compare_named_tuples(kwargs, other.named)
|
||||
|
||||
i = 0
|
||||
other.args.each do |k, v2|
|
||||
break if i >= positional.size
|
||||
next if kwargs.has_key?(k) # Covered by named arguments.
|
||||
|
||||
v1 = positional[i]
|
||||
i += 1
|
||||
return false unless compare_values(v1, v2)
|
||||
end
|
||||
|
||||
other.splat.try &.each do |v2|
|
||||
v1 = positional.fetch(i) { return false }
|
||||
i += 1
|
||||
return false unless compare_values(v1, v2)
|
||||
end
|
||||
|
||||
i == positional.size
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,214 +0,0 @@
|
|||
require "./arguments"
|
||||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./stubbable"
|
||||
require "./stubbed_name"
|
||||
require "./stubbed_type"
|
||||
require "./unexpected_message"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Stands in for an object for testing that a SUT calls expected methods.
|
||||
#
|
||||
# Handles all messages (method calls), but only responds to those configured.
|
||||
# Methods called that were not configured will raise `UnexpectedMessage`.
|
||||
# Doubles should be defined with the `#define` macro.
|
||||
#
|
||||
# Use `#_spectator_define_stub` to override behavior of a method in the double.
|
||||
# Only methods defined in the double's type can have stubs.
|
||||
# New methods are not defines when a stub is added that doesn't have a matching method name.
|
||||
abstract class Double
|
||||
include Stubbable
|
||||
extend StubbedType
|
||||
|
||||
Log = Spectator::Log.for(self)
|
||||
|
||||
# Defines a test double type.
|
||||
#
|
||||
# The *type_name* is the name to give the class.
|
||||
# Instances of the double can be named by providing a *name*.
|
||||
# This can be a symbol, string, or even a type.
|
||||
# See `StubbedName` for details.
|
||||
#
|
||||
# After the names, a collection of key-value pairs can be given to quickly define methods.
|
||||
# Each key is the method name, and the corresponding value is the value returned by the method.
|
||||
# These methods accept any arguments.
|
||||
# Additionally, these methods can be overridden later with stubs.
|
||||
#
|
||||
# Lastly, a block can be provided to define additional methods and stubs.
|
||||
# The block is evaluated in the context of the double's class.
|
||||
#
|
||||
# ```
|
||||
# Double.define(SomeDouble, meth1: 42, meth2: "foobar") do
|
||||
# stub abstract def meth3 : Symbol
|
||||
#
|
||||
# # Default implementation with a dynamic value.
|
||||
# stub def meth4
|
||||
# Time.utc
|
||||
# end
|
||||
# end
|
||||
# ```
|
||||
macro define(type_name, name = nil, **value_methods, &block)
|
||||
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||
class {{type_name.id}} < {{@type.name}}
|
||||
{% for key, value in value_methods %}
|
||||
default_stub def {{key.id}}(*%args, **%kwargs)
|
||||
{{value}}
|
||||
end
|
||||
|
||||
default_stub def {{key.id}}(*%args, **%kwargs, &)
|
||||
{{key.id}}
|
||||
end
|
||||
{% end %}
|
||||
|
||||
{{block.body if block}}
|
||||
end
|
||||
end
|
||||
|
||||
@calls = [] of MethodCall
|
||||
|
||||
private class_getter _spectator_stubs : Array(Stub) = [] of Stub
|
||||
|
||||
class_getter _spectator_calls : Array(MethodCall) = [] of MethodCall
|
||||
|
||||
# Creates the double.
|
||||
#
|
||||
# An initial set of *stubs* can be provided.
|
||||
def initialize(@stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub)
|
||||
end
|
||||
|
||||
# Creates the double.
|
||||
#
|
||||
# An initial set of stubs can be provided with *value_methods*.
|
||||
def initialize(**value_methods)
|
||||
@stubs = value_methods.map do |key, value|
|
||||
ValueStub.new(key, value).as(Stub)
|
||||
end
|
||||
end
|
||||
|
||||
# Compares against another object.
|
||||
#
|
||||
# Always returns false.
|
||||
# This method exists as a workaround to provide an alternative to `Object#same?`,
|
||||
# which only accepts a `Reference` or `Nil`.
|
||||
def same?(other) : Bool
|
||||
false
|
||||
end
|
||||
|
||||
# Simplified string representation of a double.
|
||||
# Avoids displaying nested content and bloating method instantiation.
|
||||
def to_s(io : IO) : Nil
|
||||
io << "#<" + {{@type.name(generic_args: false).stringify}} + " "
|
||||
io << _spectator_stubbed_name << '>'
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def inspect(io : IO) : Nil
|
||||
io << "#<" + {{@type.name(generic_args: false).stringify}} + " "
|
||||
io << _spectator_stubbed_name
|
||||
|
||||
io << ":0x"
|
||||
object_id.to_s(io, 16)
|
||||
io << '>'
|
||||
end
|
||||
|
||||
# Defines a stub to change the behavior of a method in this double.
|
||||
#
|
||||
# NOTE: Defining a stub for a method not defined in the double's type has no effect.
|
||||
protected def _spectator_define_stub(stub : Stub) : Nil
|
||||
Log.debug { "Defined stub for #{inspect} #{stub}" }
|
||||
@stubs.unshift(stub)
|
||||
end
|
||||
|
||||
protected def _spectator_remove_stub(stub : Stub) : Nil
|
||||
Log.debug { "Removing stub #{stub} from #{inspect}" }
|
||||
@stubs.delete(stub)
|
||||
end
|
||||
|
||||
protected def _spectator_clear_stubs : Nil
|
||||
Log.debug { "Clearing stubs for #{inspect}" }
|
||||
@stubs.clear
|
||||
end
|
||||
|
||||
private def _spectator_find_stub(call : MethodCall) : Stub?
|
||||
Log.debug { "Finding stub for #{call}" }
|
||||
stub = @stubs.find &.===(call)
|
||||
Log.debug { stub ? "Found stub #{stub} for #{call}" : "Did not find stub for #{call}" }
|
||||
stub
|
||||
end
|
||||
|
||||
def _spectator_stub_for_method?(method : Symbol) : Bool
|
||||
@stubs.any? { |stub| stub.method == method }
|
||||
end
|
||||
|
||||
def _spectator_record_call(call : MethodCall) : Nil
|
||||
@calls << call
|
||||
end
|
||||
|
||||
def _spectator_calls
|
||||
@calls
|
||||
end
|
||||
|
||||
def _spectator_clear_calls : Nil
|
||||
@calls.clear
|
||||
end
|
||||
|
||||
# Returns the double's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
{% if anno = @type.annotation(StubbedName) %}
|
||||
{{(anno[0] || :Anonymous.id).stringify}}
|
||||
{% else %}
|
||||
"Anonymous"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
private def self._spectator_stubbed_name : String
|
||||
{% if anno = @type.annotation(StubbedName) %}
|
||||
{{(anno[0] || :Anonymous.id).stringify}}
|
||||
{% else %}
|
||||
"Anonymous"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
private def _spectator_stub_fallback(call : MethodCall, &)
|
||||
Log.trace { "Fallback for #{call} - call original" }
|
||||
yield
|
||||
end
|
||||
|
||||
private def _spectator_stub_fallback(call : MethodCall, type, &)
|
||||
_spectator_stub_fallback(call) { yield }
|
||||
end
|
||||
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
Log.info do
|
||||
break unless _spectator_stub_for_method?(call.method)
|
||||
|
||||
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
|
||||
end
|
||||
|
||||
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||
end
|
||||
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
_spectator_abstract_stub_fallback(call)
|
||||
end
|
||||
|
||||
# "Hide" existing methods and methods from ancestors by overriding them.
|
||||
macro finished
|
||||
stub_type {{@type.name(generic_args: false)}}
|
||||
end
|
||||
|
||||
# Handle all methods but only respond to configured messages.
|
||||
# Raises an `UnexpectedMessage` error for non-configures messages.
|
||||
macro method_missing(call)
|
||||
args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}})
|
||||
call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args)
|
||||
_spectator_record_call(call)
|
||||
|
||||
Log.trace { "#{inspect} got undefined method `#{call}{% if call.block %} { ... }{% end %}`" }
|
||||
|
||||
raise ::Spectator::UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||
nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors".
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,55 +0,0 @@
|
|||
require "../location"
|
||||
require "./arguments"
|
||||
require "./stub"
|
||||
require "./stub_modifiers"
|
||||
|
||||
module Spectator
|
||||
# Stub that raises an exception.
|
||||
class ExceptionStub < Stub
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : Nil
|
||||
raise @exception
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, @exception, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, @exception : Exception, constraint : AbstractArguments? = nil, location : Location? = nil)
|
||||
super(method, constraint, location)
|
||||
end
|
||||
|
||||
# String representation of the stub, formatted as a method call.
|
||||
def to_s(io : IO) : Nil
|
||||
super
|
||||
io << " # raises " << @exception
|
||||
end
|
||||
end
|
||||
|
||||
module StubModifiers
|
||||
# Returns a new stub that raises an exception.
|
||||
def and_raise(exception : Exception)
|
||||
ExceptionStub.new(method, exception, constraint, location)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def and_raise(exception_class : Exception.class, message)
|
||||
exception = exception_class.new(message)
|
||||
and_raise(exception)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def and_raise(message : String? = nil)
|
||||
exception = Exception.new(message)
|
||||
and_raise(exception)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def and_raise(exception_class : Exception.class)
|
||||
exception = exception_class.new
|
||||
and_raise(exception)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,133 +0,0 @@
|
|||
require "./abstract_arguments"
|
||||
|
||||
module Spectator
|
||||
# Arguments passed into a method.
|
||||
#
|
||||
# *Args* must be a `NamedTuple` type representing the standard arguments.
|
||||
# *Splat* must be a `Tuple` type representing the extra positional arguments.
|
||||
# *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments.
|
||||
class FormalArguments(Args, Splat, DoubleSplat) < AbstractArguments
|
||||
# Positional arguments.
|
||||
getter args : Args
|
||||
|
||||
# Additional positional arguments.
|
||||
getter splat : Splat
|
||||
|
||||
# Keyword arguments.
|
||||
getter kwargs : DoubleSplat
|
||||
|
||||
# Name of the splat argument, if used.
|
||||
getter splat_name : Symbol?
|
||||
|
||||
# Creates arguments used in a method call.
|
||||
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat)
|
||||
{% raise "Positional arguments (generic type Args) must be a NamedTuple" unless Args <= NamedTuple %}
|
||||
{% raise "Splat arguments (generic type Splat) must be a Tuple" unless Splat <= Tuple || Splat <= Nil %}
|
||||
{% raise "Keyword arguments (generic type DoubleSplat) must be a NamedTuple" unless DoubleSplat <= NamedTuple %}
|
||||
end
|
||||
|
||||
# Creates arguments used in a method call.
|
||||
def self.new(args : Args, kwargs : DoubleSplat)
|
||||
new(args, nil, nil, kwargs)
|
||||
end
|
||||
|
||||
# Captures arguments passed to a call.
|
||||
def self.build(args = NamedTuple.new, kwargs = NamedTuple.new)
|
||||
new(args, nil, nil, kwargs)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new)
|
||||
new(args, splat_name, splat, kwargs)
|
||||
end
|
||||
|
||||
# Instance of empty arguments.
|
||||
class_getter none : AbstractArguments = build
|
||||
|
||||
# Returns the positional argument at the specified index.
|
||||
def [](index : Int)
|
||||
positional[index]
|
||||
end
|
||||
|
||||
# Returns the specified named argument.
|
||||
def [](arg : Symbol)
|
||||
return @args[arg] if @args.has_key?(arg)
|
||||
@kwargs[arg]
|
||||
end
|
||||
|
||||
# Returns all arguments and splatted arguments as a tuple.
|
||||
def positional : Tuple
|
||||
if (splat = @splat)
|
||||
args.values + splat
|
||||
else
|
||||
args.values
|
||||
end
|
||||
end
|
||||
|
||||
# Returns all named positional and keyword arguments as a named tuple.
|
||||
def named : NamedTuple
|
||||
args.merge(kwargs)
|
||||
end
|
||||
|
||||
# Constructs a string representation of the arguments.
|
||||
def to_s(io : IO) : Nil
|
||||
return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty?
|
||||
|
||||
io << '('
|
||||
|
||||
# Add the positional arguments.
|
||||
{% if Args < NamedTuple %}
|
||||
# Include argument names.
|
||||
args.each_with_index do |name, value, i|
|
||||
io << ", " if i > 0
|
||||
io << name << ": "
|
||||
value.inspect(io)
|
||||
end
|
||||
{% else %}
|
||||
args.each_with_index do |arg, i|
|
||||
io << ", " if i > 0
|
||||
arg.inspect(io)
|
||||
end
|
||||
{% end %}
|
||||
|
||||
# Add the splat arguments.
|
||||
if (splat = @splat) && !splat.empty?
|
||||
io << ", " unless args.empty?
|
||||
if splat_name = !args.empty? && @splat_name
|
||||
io << '*' << splat_name << ": {"
|
||||
end
|
||||
splat.each_with_index do |arg, i|
|
||||
io << ", " if i > 0
|
||||
arg.inspect(io)
|
||||
end
|
||||
io << '}' if splat_name
|
||||
end
|
||||
|
||||
# Add the keyword arguments.
|
||||
offset = args.size
|
||||
offset += splat.size if (splat = @splat)
|
||||
kwargs.each_with_index(offset) do |key, value, i|
|
||||
io << ", " if i > 0
|
||||
io << key << ": "
|
||||
value.inspect(io)
|
||||
end
|
||||
|
||||
io << ')'
|
||||
end
|
||||
|
||||
# Checks if this set of arguments and another are equal.
|
||||
def ==(other : AbstractArguments)
|
||||
positional == other.positional && kwargs == other.kwargs
|
||||
end
|
||||
|
||||
# Checks if another set of arguments matches this set of arguments.
|
||||
def ===(other : Arguments)
|
||||
compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def ===(other : FormalArguments)
|
||||
compare_named_tuples(args, other.args) && compare_tuples(splat, other.splat) && compare_named_tuples(kwargs, other.kwargs)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,91 +0,0 @@
|
|||
require "../label"
|
||||
require "./arguments"
|
||||
require "./double"
|
||||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Stands in for an object for testing that a SUT calls expected methods.
|
||||
#
|
||||
# Handles all messages (method calls), but only responds to those configured.
|
||||
# Methods called that were not configured will raise `UnexpectedMessage`.
|
||||
#
|
||||
# Use `#_spectator_define_stub` to override behavior of a method in the double.
|
||||
# Only methods defined in the double's type can have stubs.
|
||||
# New methods are not defines when a stub is added that doesn't have a matching method name.
|
||||
class LazyDouble(Messages) < Double
|
||||
@name : String?
|
||||
|
||||
def initialize(_spectator_double_name = nil, _spectator_double_stubs = [] of Stub, **@messages : **Messages)
|
||||
@name = _spectator_double_name.try &.inspect
|
||||
message_stubs = messages.map do |method, value|
|
||||
ValueStub.new(method, value)
|
||||
end
|
||||
|
||||
super(_spectator_double_stubs + message_stubs)
|
||||
end
|
||||
|
||||
# Defines a stub to change the behavior of a method in this double.
|
||||
#
|
||||
# NOTE: Defining a stub for a method not defined in the double's type raises an error.
|
||||
protected def _spectator_define_stub(stub : Stub) : Nil
|
||||
return super if Messages.types.has_key?(stub.method)
|
||||
|
||||
raise "Can't define stub #{stub} on lazy double because it wasn't initially defined."
|
||||
end
|
||||
|
||||
# Returns the double's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
@name || "Anonymous"
|
||||
end
|
||||
|
||||
private def _spectator_stub_fallback(call : MethodCall, &)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
|
||||
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||
else
|
||||
Log.trace { "Fallback for #{call} - call original" }
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Handles all messages.
|
||||
macro method_missing(call)
|
||||
# Capture information about the call.
|
||||
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}})
|
||||
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
|
||||
|
||||
# Attempt to find a stub that satisfies the method call and arguments.
|
||||
if %stub = _spectator_find_stub(%call)
|
||||
# Cast the stub or return value to the expected type.
|
||||
# This is necessary to match the expected return type of the original message.
|
||||
\{% if Messages.keys.includes?({{call.name.symbolize}}) %}
|
||||
_spectator_cast_stub_value(%stub, %call, \{{Messages[{{call.name.symbolize}}.id]}})
|
||||
\{% else %}
|
||||
# A method that was not defined during initialization was stubbed.
|
||||
# Even though all stubs will have a #call method, the compiler doesn't seem to agree.
|
||||
# Assert that it will (this should never fail).
|
||||
raise TypeCastError.new("Stub has no value") unless %stub.responds_to?(:call)
|
||||
|
||||
# Return the value of the stub as-is.
|
||||
# Might want to give a warning here, as this may produce a "bloated" union of all known stub types.
|
||||
%stub.call(%call)
|
||||
\{% end %}
|
||||
else
|
||||
# A stub wasn't found, invoke the fallback logic.
|
||||
\{% if Messages.keys.includes?({{call.name.symbolize}}.id) %}
|
||||
# Pass along the message type and a block to invoke it.
|
||||
_spectator_stub_fallback(%call, \{{Messages[{{call.name.symbolize}}.id]}}) { @messages[{{call.name.symbolize}}] }
|
||||
\{% else %}
|
||||
# Message received for a methods that isn't stubbed nor defined when initialized.
|
||||
_spectator_abstract_stub_fallback(%call)
|
||||
nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors".
|
||||
\{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,42 +0,0 @@
|
|||
require "./abstract_arguments"
|
||||
require "./arguments"
|
||||
require "./formal_arguments"
|
||||
|
||||
module Spectator
|
||||
# Stores information about a call to a method.
|
||||
class MethodCall
|
||||
# Name of the method.
|
||||
getter method : Symbol
|
||||
|
||||
# Arguments passed to the method.
|
||||
getter arguments : AbstractArguments
|
||||
|
||||
# Creates a method call.
|
||||
def initialize(@method : Symbol, @arguments : AbstractArguments = Arguments.none)
|
||||
end
|
||||
|
||||
# Creates a method call by splatting its arguments.
|
||||
def self.capture(method : Symbol, *args, **kwargs)
|
||||
arguments = Arguments.capture(*args, **kwargs).as(AbstractArguments)
|
||||
new(method, arguments)
|
||||
end
|
||||
|
||||
# Creates a method call from within a method.
|
||||
# Takes the same arguments as `FormalArguments.build` but with the method name first.
|
||||
def self.build(method : Symbol, *args, **kwargs)
|
||||
arguments = FormalArguments.build(*args, **kwargs).as(AbstractArguments)
|
||||
new(method, arguments)
|
||||
end
|
||||
|
||||
# Constructs a string containing the method name and arguments.
|
||||
def to_s(io : IO) : Nil
|
||||
io << '#' << method
|
||||
arguments.inspect(io)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def inspect(io : IO) : Nil
|
||||
to_s(io)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,227 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./mocked"
|
||||
require "./mock_registry"
|
||||
require "./reference_mock_registry"
|
||||
require "./stub"
|
||||
require "./stubbed_name"
|
||||
require "./stubbed_type"
|
||||
require "./value_mock_registry"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Module providing macros for defining new mocks from existing types and injecting mock features into concrete types.
|
||||
module Mock
|
||||
# Defines a type that inherits from another, existing type.
|
||||
# The newly defined subtype will have mocking functionality.
|
||||
#
|
||||
# Methods from the inherited type will be overridden to support stubs.
|
||||
# *base* is the keyword for the type being defined - class or struct.
|
||||
# *mocked_type* is the original type to inherit from.
|
||||
# *type_name* is the name of the new type to define.
|
||||
# An optional *name* of the mock can be provided.
|
||||
# Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type.
|
||||
#
|
||||
# A block can be provided to define additional methods and stubs.
|
||||
# The block is evaluated in the context of the derived type.
|
||||
#
|
||||
# ```
|
||||
# Mock.define_subtype(:class, SomeType, meth1: 42, meth2: "foobar") do
|
||||
# stub abstract def meth3 : Symbol
|
||||
#
|
||||
# # Default implementation with a dynamic value.
|
||||
# stub def meth4
|
||||
# Time.utc
|
||||
# end
|
||||
# end
|
||||
# ```
|
||||
macro define_subtype(base, mocked_type, type_name, name = nil, **value_methods, &block)
|
||||
{% begin %}
|
||||
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||
{% if base.id == :module.id %}
|
||||
{{base.id}} {{type_name.id}}
|
||||
include {{mocked_type.id}}
|
||||
|
||||
# Mock class that includes the mocked module {{mocked_type.id}}
|
||||
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||
private class ClassIncludingMock{{type_name.id}}
|
||||
include {{type_name.id}}
|
||||
end
|
||||
|
||||
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
|
||||
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
|
||||
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
|
||||
inst = ClassIncludingMock{{type_name.id}}.allocate
|
||||
inst.initialize(*args, **kwargs)
|
||||
inst
|
||||
end
|
||||
|
||||
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
|
||||
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
|
||||
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
|
||||
inst = ClassIncludingMock{{type_name.id}}.allocate
|
||||
inst.initialize(*args, **kwargs) { |*yargs| yield *yargs }
|
||||
inst
|
||||
end
|
||||
|
||||
{% else %}
|
||||
{{base.id}} {{type_name.id}} < {{mocked_type.id}}
|
||||
{% end %}
|
||||
include ::Spectator::Mocked
|
||||
extend ::Spectator::StubbedType
|
||||
|
||||
{% begin %}
|
||||
private getter(_spectator_stubs) do
|
||||
[
|
||||
{% for key, value in value_methods %}
|
||||
::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}),
|
||||
{% end %}
|
||||
] of ::Spectator::Stub
|
||||
end
|
||||
{% end %}
|
||||
|
||||
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
|
||||
@_spectator_stubs.try &.delete(stub)
|
||||
end
|
||||
|
||||
def _spectator_clear_stubs : ::Nil
|
||||
@_spectator_stubs = nil
|
||||
end
|
||||
|
||||
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub
|
||||
|
||||
class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
|
||||
|
||||
getter _spectator_calls = [] of ::Spectator::MethodCall
|
||||
|
||||
# Returns the mock's name formatted for user output.
|
||||
private def _spectator_stubbed_name : ::String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Mock {{mocked_type.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
private def self._spectator_stubbed_name : ::String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Class Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Class Mock {{mocked_type.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
macro finished
|
||||
stub_type {{mocked_type.id}}
|
||||
|
||||
{{block.body if block}}
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Injects mock functionality into an existing type.
|
||||
#
|
||||
# Generally this method of mocking should be avoiding.
|
||||
# It modifies types being tested, the mock functionality won't exist outside of tests.
|
||||
# This option should only be used when sub-types are not possible (e.g. concrete struct).
|
||||
#
|
||||
# Methods in the type will be overridden to support stubs.
|
||||
# The original method functionality will still be accessible, but pass through mock code first.
|
||||
# *base* is the keyword for the type being defined - class or struct.
|
||||
# *type_name* is the name of the type to inject mock functionality into.
|
||||
# This _must_ be full, resolvable path to the type.
|
||||
# An optional *name* of the mock can be provided.
|
||||
# Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type.
|
||||
#
|
||||
# A block can be provided to define additional methods and stubs.
|
||||
# The block is evaluated in the context of the derived type.
|
||||
#
|
||||
# ```
|
||||
# Mock.inject(:struct, SomeType, meth1: 42, meth2: "foobar") do
|
||||
# stub abstract def meth3 : Symbol
|
||||
#
|
||||
# # Default implementation with a dynamic value.
|
||||
# stub def meth4
|
||||
# Time.utc
|
||||
# end
|
||||
# end
|
||||
# ```
|
||||
macro inject(base, type_name, name = nil, **value_methods, &block)
|
||||
{% begin %}
|
||||
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||
{{base.id}} ::{{type_name.id}}
|
||||
include ::Spectator::Mocked
|
||||
extend ::Spectator::StubbedType
|
||||
|
||||
{% if base == :class %}
|
||||
@@_spectator_mock_registry = ::Spectator::ReferenceMockRegistry.new
|
||||
{% elsif base == :struct %}
|
||||
@@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new
|
||||
{% else %}
|
||||
@@_spectator_mock_registry = ::Spectator::MockRegistry.new
|
||||
{% end %}
|
||||
|
||||
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub
|
||||
|
||||
class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
|
||||
|
||||
private def _spectator_stubs
|
||||
entry = @@_spectator_mock_registry.fetch(self) do
|
||||
_spectator_default_stubs
|
||||
end
|
||||
entry.stubs
|
||||
end
|
||||
|
||||
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
|
||||
@@_spectator_mock_registry[self]?.try &.stubs.delete(stub)
|
||||
end
|
||||
|
||||
def _spectator_clear_stubs : ::Nil
|
||||
@@_spectator_mock_registry.delete(self)
|
||||
end
|
||||
|
||||
def _spectator_calls
|
||||
entry = @@_spectator_mock_registry.fetch(self) do
|
||||
_spectator_default_stubs
|
||||
end
|
||||
entry.calls
|
||||
end
|
||||
|
||||
private def _spectator_default_stubs
|
||||
{% begin %}
|
||||
[
|
||||
{% for key, value in value_methods %}
|
||||
::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}),
|
||||
{% end %}
|
||||
] of ::Spectator::Stub
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Returns the mock's name formatted for user output.
|
||||
private def _spectator_stubbed_name : ::String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Mock {{type_name.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
# Returns the mock's name formatted for user output.
|
||||
private def self._spectator_stubbed_name : ::String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Class Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Class Mock {{type_name.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
macro finished
|
||||
stub_type {{type_name.id}}
|
||||
|
||||
{{block.body if block}}
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
require "./mock_registry_entry"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Stores collections of stubs for mocked types.
|
||||
#
|
||||
# This type is intended for all mocked modules that have functionality "injected."
|
||||
# That is, the type itself has mock functionality bolted on.
|
||||
# Adding instance members should be avoided, for instance, it could mess up serialization.
|
||||
class MockRegistry
|
||||
@entry : MockRegistryEntry?
|
||||
|
||||
# Retrieves all stubs.
|
||||
def [](_object = nil)
|
||||
@entry.not_nil!
|
||||
end
|
||||
|
||||
# Retrieves all stubs.
|
||||
def []?(_object = nil)
|
||||
@entry
|
||||
end
|
||||
|
||||
# Retrieves all stubs.
|
||||
#
|
||||
# Yields to the block on the first retrieval.
|
||||
# This allows a mock to populate the registry with initial stubs.
|
||||
def fetch(object : Reference, & : -> Array(Stub))
|
||||
entry = @entry
|
||||
if entry.nil?
|
||||
entry = MockRegistryEntry.new
|
||||
entry.stubs = yield
|
||||
@entry = entry
|
||||
else
|
||||
entry
|
||||
end
|
||||
end
|
||||
|
||||
# Clears all stubs defined for a mocked object.
|
||||
def delete(object : Reference) : Nil
|
||||
@entry = nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Stubs and calls for a mock.
|
||||
private struct MockRegistryEntry
|
||||
# Retrieves all stubs defined for a mock.
|
||||
property stubs = [] of Stub
|
||||
|
||||
# Retrieves all calls to stubbed methods.
|
||||
getter calls = [] of MethodCall
|
||||
end
|
||||
end
|
|
@ -1,127 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./stubbable"
|
||||
require "./unexpected_message"
|
||||
|
||||
module Spectator
|
||||
# Mix-in used for mocked types.
|
||||
#
|
||||
# Bridges functionality between mocks and stubs
|
||||
# Implements the abstracts methods from `Stubbable`.
|
||||
#
|
||||
# Types including this module will need to implement `#_spectator_stubs`.
|
||||
# It should return a mutable list of stubs.
|
||||
# This is used to store the stubs for the mocked type.
|
||||
#
|
||||
# Additionally, the `#_spectator_calls` (getter with no arguments) must be implemented.
|
||||
# It should return a mutable list of method calls.
|
||||
# This is used to store the calls to stubs for the mocked type.
|
||||
module Mocked
|
||||
include Stubbable
|
||||
|
||||
# Retrieves an mutable collection of stubs.
|
||||
abstract def _spectator_stubs
|
||||
|
||||
def _spectator_define_stub(stub : ::Spectator::Stub) : Nil
|
||||
_spectator_stubs.unshift(stub)
|
||||
end
|
||||
|
||||
def _spectator_remove_stub(stub : Stub) : Nil
|
||||
_spectator_stubs.delete(stub)
|
||||
end
|
||||
|
||||
def _spectator_clear_stubs : Nil
|
||||
_spectator_stubs.clear
|
||||
end
|
||||
|
||||
private def _spectator_find_stub(call : ::Spectator::MethodCall) : ::Spectator::Stub?
|
||||
_spectator_stubs.find &.===(call)
|
||||
end
|
||||
|
||||
def _spectator_stub_for_method?(method : Symbol) : Bool
|
||||
_spectator_stubs.any? { |stub| stub.method == method }
|
||||
end
|
||||
|
||||
def _spectator_record_call(call : MethodCall) : Nil
|
||||
_spectator_calls << call
|
||||
end
|
||||
|
||||
def _spectator_calls(method : Symbol) : Enumerable(MethodCall)
|
||||
_spectator_calls.select { |call| call.method == method }
|
||||
end
|
||||
|
||||
def _spectator_clear_calls : Nil
|
||||
_spectator_calls.clear
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_stub_fallback(call : MethodCall, &)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Spectator::Log.info do # FIXME: Don't log to top-level Spectator logger (use mock or double logger).
|
||||
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
|
||||
end
|
||||
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
end
|
||||
|
||||
yield # Uninteresting message, allow through.
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_stub_fallback(call : MethodCall, type, &)
|
||||
value = _spectator_stub_fallback(call) { yield }
|
||||
|
||||
begin
|
||||
type.cast(value)
|
||||
rescue TypeCastError
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `#{value.inspect}`, but returned type must be `#{type}`.")
|
||||
end
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
Spectator::Log.info do # FIXME: Don't log to top-level Spectator logger (use mock or double logger).
|
||||
break unless _spectator_stub_for_method?(call.method)
|
||||
|
||||
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
|
||||
end
|
||||
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
value = _spectator_abstract_stub_fallback(call)
|
||||
|
||||
begin
|
||||
type.cast(value)
|
||||
rescue TypeCastError
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `#{value.inspect}`, but returned type must be `#{type}`.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,35 +0,0 @@
|
|||
require "../location"
|
||||
require "./arguments"
|
||||
require "./stub_modifiers"
|
||||
require "./typed_stub"
|
||||
|
||||
module Spectator
|
||||
# Stub that responds with a multiple values in succession.
|
||||
class MultiValueStub(T) < TypedStub(T)
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : T
|
||||
if @values.size == 1
|
||||
@values.first
|
||||
else
|
||||
@values.shift
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, @values, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, @values : Array(T), constraint : AbstractArguments? = nil, location : Location? = nil)
|
||||
super(method, constraint, location)
|
||||
end
|
||||
end
|
||||
|
||||
module StubModifiers
|
||||
# Returns a new stub that returns multiple values in succession.
|
||||
def and_return(value, *values)
|
||||
MultiValueStub.new(method, [value, *values], constraint, location)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,64 +0,0 @@
|
|||
require "./double"
|
||||
require "./method_call"
|
||||
require "./stubbed_name"
|
||||
require "./unexpected_message"
|
||||
|
||||
module Spectator
|
||||
# Stands in for an object for testing that a SUT calls expected methods.
|
||||
#
|
||||
# Handles all messages (method calls), but only responds to those configured.
|
||||
# Methods called that were not configured will return self.
|
||||
# Doubles should be defined with the `#define` macro.
|
||||
#
|
||||
# Use `#_spectator_define_stub` to override behavior of a method in the double.
|
||||
# Only methods defined in the double's type can have stubs.
|
||||
# New methods are not defines when a stub is added that doesn't have a matching method name.
|
||||
abstract class NullDouble < Double
|
||||
# Returns the double's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
{% if anno = @type.annotation(StubbedName) %}
|
||||
"#<NullDouble " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
|
||||
{% else %}
|
||||
"#<NullDouble Anonymous>"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
|
||||
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||
else
|
||||
Log.trace { "Fallback for #{call} - return self" }
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
# Specialization that matches when the return type matches self.
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall, _type : self)
|
||||
_spectator_abstract_stub_fallback(call)
|
||||
end
|
||||
|
||||
# Default implementation that raises a `TypeCastError` since the return type isn't self.
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
|
||||
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||
else
|
||||
raise TypeCastError.new("#{inspect} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.")
|
||||
end
|
||||
end
|
||||
|
||||
# Handles all undefined messages.
|
||||
# Returns stubbed values if available, otherwise delegates to `#_spectator_abstract_stub_fallback`.
|
||||
macro method_missing(call)
|
||||
# Capture information about the call.
|
||||
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}})
|
||||
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
|
||||
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
require "./typed_stub"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Stub that does nothing and returns nil.
|
||||
class NullStub < TypedStub(Nil)
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : Nil
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, constraint, location)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
require "../location"
|
||||
require "./arguments"
|
||||
require "./typed_stub"
|
||||
|
||||
module Spectator
|
||||
# Stub that responds with a value returned by calling a proc.
|
||||
class ProcStub(T) < TypedStub(T)
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : T
|
||||
@proc.call(call.arguments)
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, @proc, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, @proc : Proc(AbstractArguments, T), constraint : AbstractArguments? = nil, location : Location? = nil)
|
||||
super(method, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, constraint : AbstractArguments? = nil, location : Location? = nil, &block : Proc(AbstractArguments, T))
|
||||
initialize(method, block, constraint, location)
|
||||
end
|
||||
end
|
||||
|
||||
module StubModifiers
|
||||
# Returns a new stub with an argument constraint.
|
||||
def with(*args, **kwargs, &block : AbstractArguments -> T) forall T
|
||||
constraint = Arguments.new(args, kwargs).as(AbstractArguments)
|
||||
ProcStub(T).new(method, block, constraint, location)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,53 +0,0 @@
|
|||
require "./mock_registry_entry"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Stores collections of stubs for mocked reference (class) types.
|
||||
#
|
||||
# This type is intended for all mocked reference types that have functionality "injected."
|
||||
# That is, the type itself has mock functionality bolted on.
|
||||
# Adding instance members should be avoided, for instance, it could mess up serialization.
|
||||
# This registry works around that by mapping mocks (via their memory address) to a collection of stubs.
|
||||
# Doing so prevents adding data to the mocked type.
|
||||
class ReferenceMockRegistry
|
||||
@entries : Hash(Void*, MockRegistryEntry)
|
||||
|
||||
# Creates an empty registry.
|
||||
def initialize
|
||||
@entries = Hash(Void*, MockRegistryEntry).new do |hash, key|
|
||||
hash[key] = MockRegistryEntry.new
|
||||
end
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object.
|
||||
def [](object : Reference)
|
||||
key = Box.box(object)
|
||||
@entries[key]
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet.
|
||||
def []?(object : Reference)
|
||||
key = Box.box(object)
|
||||
@entries[key]?
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object.
|
||||
#
|
||||
# Yields to the block on the first retrieval.
|
||||
# This allows a mock to populate the registry with initial stubs.
|
||||
def fetch(object : Reference, & : -> Array(Stub))
|
||||
key = Box.box(object)
|
||||
@entries.fetch(key) do
|
||||
entry = MockRegistryEntry.new
|
||||
entry.stubs = yield
|
||||
@entries[key] = entry
|
||||
end
|
||||
end
|
||||
|
||||
# Clears all stubs defined for a mocked object.
|
||||
def delete(object : Reference) : Nil
|
||||
key = Box.box(object)
|
||||
@entries.delete(key)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,50 +0,0 @@
|
|||
require "./abstract_arguments"
|
||||
require "./arguments"
|
||||
require "./method_call"
|
||||
require "./stub_modifiers"
|
||||
|
||||
module Spectator
|
||||
# Untyped response to a method call (message).
|
||||
abstract class Stub
|
||||
include StubModifiers
|
||||
|
||||
# Name of the method this stub is for.
|
||||
getter method : Symbol
|
||||
|
||||
# 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?
|
||||
|
||||
# Location the stub was defined.
|
||||
getter location : Location?
|
||||
|
||||
# Creates the base of the stub.
|
||||
def initialize(@method : Symbol, @constraint : AbstractArguments? = nil, @location : Location? = nil)
|
||||
end
|
||||
|
||||
# String representation of the stub, formatted as a method call.
|
||||
def message(io : IO) : Nil
|
||||
io << "#" << method << (constraint || "(any args)")
|
||||
end
|
||||
|
||||
# String representation of the stub, formatted as a method call.
|
||||
def message
|
||||
String.build do |str|
|
||||
message(str)
|
||||
end
|
||||
end
|
||||
|
||||
# String representation of the stub, formatted as a method definition.
|
||||
def to_s(io : IO) : Nil
|
||||
message(io)
|
||||
end
|
||||
|
||||
# Checks if a method call should receive the response from this stub.
|
||||
def ===(call : MethodCall)
|
||||
return false if method != call.method
|
||||
return true unless constraint = @constraint
|
||||
|
||||
constraint === call.arguments
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
require "./arguments"
|
||||
|
||||
module Spectator
|
||||
# Mixin intended for `Stub` to return new, modified stubs.
|
||||
module StubModifiers
|
||||
# Returns a new stub of the same type with constrained arguments.
|
||||
abstract def with_constraint(constraint : AbstractArguments?)
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def with(constraint : AbstractArguments?)
|
||||
with_constraint(constraint)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def with(*args, **kwargs)
|
||||
constraint = Arguments.new(args, kwargs).as(AbstractArguments)
|
||||
self.with_constraint(constraint)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,567 +0,0 @@
|
|||
require "../dsl/reserved"
|
||||
require "./formal_arguments"
|
||||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./typed_stub"
|
||||
|
||||
module Spectator
|
||||
# Mix-in for mocks and doubles providing method stubs.
|
||||
#
|
||||
# Macros in this module can override existing methods.
|
||||
# Stubbed methods will look for stubs to evaluate in place of their original functionality.
|
||||
# The primary macro of interest is `#stub`.
|
||||
# The macros are intended to be called from within the type being stubbed.
|
||||
#
|
||||
# Types including this module must define `#_spectator_find_stub` and `#_spectator_stubbed_name`.
|
||||
# These are internal, reserved method names by Spectator, hence the `_spectator` prefix.
|
||||
# These methods can't (and shouldn't) be stubbed.
|
||||
module Stubbable
|
||||
# Attempts to find a stub that satisfies a method call.
|
||||
#
|
||||
# Returns a stub that matches the method *call*
|
||||
# or nil if no stubs satisfy it.
|
||||
abstract def _spectator_find_stub(call : MethodCall) : Stub?
|
||||
|
||||
# Utility method that looks for stubs for methods with the name specified.
|
||||
abstract def _spectator_stub_for_method?(method : Symbol) : Bool
|
||||
|
||||
# Defines a stub to change the behavior of a method.
|
||||
abstract def _spectator_define_stub(stub : Stub) : Nil
|
||||
|
||||
# Removes a specific, previously defined stub.
|
||||
abstract def _spectator_remove_stub(stub : Stub) : Nil
|
||||
|
||||
# Clears all previously defined stubs.
|
||||
abstract def _spectator_clear_stubs : Nil
|
||||
|
||||
# Saves a call that was made to a stubbed method.
|
||||
abstract def _spectator_record_call(call : MethodCall) : Nil
|
||||
|
||||
# Retrieves all previously saved calls.
|
||||
abstract def _spectator_calls
|
||||
|
||||
# Clears all previously saved calls.
|
||||
abstract def _spectator_clear_calls : Nil
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_stub_fallback(call : MethodCall, &)
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_stub_fallback(call : MethodCall, type, &)
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
|
||||
# Utility method returning the stubbed type's name formatted for user output.
|
||||
abstract def _spectator_stubbed_name : String
|
||||
|
||||
# Clears all previously defined calls and stubs.
|
||||
def _spectator_reset : Nil
|
||||
_spectator_clear_calls
|
||||
_spectator_clear_stubs
|
||||
end
|
||||
|
||||
# Redefines a method to accept stubs and provides a default response.
|
||||
#
|
||||
# The *method* must be a `Def`.
|
||||
# That is, a normal looking method definition should follow the `default_stub` keyword.
|
||||
#
|
||||
# ```
|
||||
# default_stub def stubbed_method
|
||||
# "foobar"
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# The method cannot be abstract, as this method requires a default (fallback) response if a stub isn't provided.
|
||||
#
|
||||
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
|
||||
# If no stub is found, then `#_spectator_stub_fallback` is called.
|
||||
# The block provided to `#_spectator_stub_fallback` will invoke the default response.
|
||||
# In other words, `#_spectator_stub_fallback` should yield if it's appropriate to return the default response.
|
||||
private macro default_stub(method)
|
||||
{% if method.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
method = method.exp
|
||||
else
|
||||
raise "`default_stub` requires a method definition"
|
||||
end %}
|
||||
{% raise "Cannot define a stub inside a method" if @def %}
|
||||
{% raise "Default stub cannot be an abstract method" if method.abstract? %}
|
||||
{% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %}
|
||||
|
||||
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
{{method.body}}
|
||||
end
|
||||
|
||||
{% original = "previous_def"
|
||||
# Workaround for Crystal not propagating block with previous_def/super.
|
||||
if method.accepts_block?
|
||||
original += "("
|
||||
if method.splat_index
|
||||
method.args.each_with_index do |arg, i|
|
||||
if i == method.splat_index
|
||||
if arg.internal_name && arg.internal_name.size > 0
|
||||
original += "*#{arg.internal_name}, "
|
||||
end
|
||||
original += "**#{method.double_splat}, " if method.double_splat
|
||||
elsif i > method.splat_index
|
||||
original += "#{arg.name}: #{arg.internal_name}, "
|
||||
else
|
||||
original += "#{arg.internal_name}, "
|
||||
end
|
||||
end
|
||||
else
|
||||
method.args.each do |arg|
|
||||
original += "#{arg.internal_name}, "
|
||||
end
|
||||
original += "**#{method.double_splat}, " if method.double_splat
|
||||
end
|
||||
# If the block is captured (i.e. `&block` syntax), it must be passed along as an argument.
|
||||
# Otherwise, use `yield` to forward the block.
|
||||
captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
|
||||
method.block_arg.name
|
||||
else
|
||||
nil
|
||||
end
|
||||
original += "&#{captured_block}" if captured_block
|
||||
original += ")"
|
||||
original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block
|
||||
end
|
||||
original = original.id %}
|
||||
|
||||
{% # Reconstruct the method signature.
|
||||
# I wish there was a better way of doing this, but there isn't (at least not that I'm aware of).
|
||||
# This chunk of code must reconstruct the method signature exactly as it was originally.
|
||||
# If it doesn't match, it doesn't override the method and the stubbing won't work.
|
||||
%}
|
||||
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
|
||||
# Capture information about the call.
|
||||
%call = ::Spectator::MethodCall.build(
|
||||
{{method.name.symbolize}},
|
||||
::NamedTuple.new(
|
||||
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
|
||||
),
|
||||
{% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
|
||||
::NamedTuple.new(
|
||||
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
|
||||
).merge({{method.double_splat}})
|
||||
)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
# Attempt to find a stub that satisfies the method call and arguments.
|
||||
# Finding a suitable stub is delegated to the type including the `Stubbable` module.
|
||||
if %stub = _spectator_find_stub(%call)
|
||||
# Cast the stub or return value to the expected type.
|
||||
# This is necessary to match the expected return type of the original method.
|
||||
_spectator_cast_stub_value(%stub, %call, typeof({{original}}),
|
||||
{{ if rt = method.return_type
|
||||
if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn
|
||||
:no_return
|
||||
else
|
||||
# Process as an enumerable type to reduce code repetition.
|
||||
rt = rt.is_a?(Union) ? rt.types : [rt]
|
||||
# Check if any types are nilable.
|
||||
nilable = rt.any? do |t|
|
||||
# These are all macro types that have the `resolve?` method.
|
||||
(t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) &&
|
||||
(resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil
|
||||
end
|
||||
if nilable
|
||||
:nil
|
||||
else
|
||||
:raise
|
||||
end
|
||||
end
|
||||
else
|
||||
:raise
|
||||
end }})
|
||||
else
|
||||
# Delegate missing stub behavior to concrete type.
|
||||
_spectator_stub_fallback(%call, typeof({{original}})) do
|
||||
# Use the default response for the method.
|
||||
{{original}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Redefines a method to require stubs.
|
||||
#
|
||||
# This macro is similar to `#default_stub` but requires that a stub is defined for the method if it's called.
|
||||
#
|
||||
# The *method* should be a `Def`.
|
||||
# That is, a normal looking method definition should follow the `stub` keyword.
|
||||
#
|
||||
# ```
|
||||
# abstract_stub def stubbed_method
|
||||
# "foobar"
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# The method being stubbed doesn't need to exist yet.
|
||||
# Its body of the method passed to this macro is ignored.
|
||||
# The method can be abstract.
|
||||
# It should have a return type annotation, otherwise the compiled return type will probably end up as a giant union.
|
||||
#
|
||||
# ```
|
||||
# abstract_stub abstract def stubbed_method : String
|
||||
# ```
|
||||
#
|
||||
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
|
||||
# If no stub is found, then `#_spectator_stub_fallback` or `#_spectator_abstract_stub_fallback` is called.
|
||||
private macro abstract_stub(method)
|
||||
{% if method.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
method = method.exp
|
||||
else
|
||||
raise "`abstract_stub` requires a method definition"
|
||||
end %}
|
||||
{% raise "Cannot define a stub inside a method" if @def %}
|
||||
{% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %}
|
||||
|
||||
{% # The logic in this macro follows mostly the same logic from `#default_stub`.
|
||||
# The main difference is that this macro cannot access the original method being stubbed.
|
||||
# It might exist or it might not.
|
||||
# The method could also be abstract.
|
||||
# For all intents and purposes, this macro defines logic that doesn't depend on an existing method.
|
||||
%}
|
||||
|
||||
{% unless method.abstract? %}
|
||||
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
{{method.body}}
|
||||
end
|
||||
|
||||
{% original = "previous_def"
|
||||
# Workaround for Crystal not propagating block with previous_def/super.
|
||||
if method.accepts_block?
|
||||
original += "("
|
||||
if method.splat_index
|
||||
method.args.each_with_index do |arg, i|
|
||||
if i == method.splat_index
|
||||
if arg.internal_name && arg.internal_name.size > 0
|
||||
original += "*#{arg.internal_name}, "
|
||||
end
|
||||
original += "**#{method.double_splat}, " if method.double_splat
|
||||
elsif i > method.splat_index
|
||||
original += "#{arg.name}: #{arg.internal_name}"
|
||||
else
|
||||
original += "#{arg.internal_name}, "
|
||||
end
|
||||
end
|
||||
else
|
||||
method.args.each do |arg|
|
||||
original += "#{arg.internal_name}, "
|
||||
end
|
||||
original += "**#{method.double_splat}, " if method.double_splat
|
||||
end
|
||||
# If the block is captured (i.e. `&block` syntax), it must be passed along as an argument.
|
||||
# Otherwise, use `yield` to forward the block.
|
||||
captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
|
||||
method.block_arg.name
|
||||
else
|
||||
nil
|
||||
end
|
||||
original += "&#{captured_block}" if captured_block
|
||||
original += ")"
|
||||
original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block
|
||||
end
|
||||
original = original.id %}
|
||||
|
||||
{% end %}
|
||||
|
||||
{% # Reconstruct the method signature.
|
||||
# I wish there was a better way of doing this, but there isn't (at least not that I'm aware of).
|
||||
# This chunk of code must reconstruct the method signature exactly as it was originally.
|
||||
# If it doesn't match, it doesn't override the method and the stubbing won't work.
|
||||
%}
|
||||
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
|
||||
# Capture information about the call.
|
||||
%call = ::Spectator::MethodCall.build(
|
||||
{{method.name.symbolize}},
|
||||
::NamedTuple.new(
|
||||
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
|
||||
),
|
||||
{% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
|
||||
::NamedTuple.new(
|
||||
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
|
||||
).merge({{method.double_splat}})
|
||||
)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
# Attempt to find a stub that satisfies the method call and arguments.
|
||||
# Finding a suitable stub is delegated to the type including the `Stubbable` module.
|
||||
if %stub = _spectator_find_stub(%call)
|
||||
# Cast the stub or return value to the expected type.
|
||||
# This is necessary to match the expected return type of the original method.
|
||||
{% if rt = method.return_type %}
|
||||
# Return type restriction takes priority since it can be a superset of the original implementation.
|
||||
_spectator_cast_stub_value(%stub, %call, {{method.return_type}},
|
||||
{{ if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn
|
||||
:no_return
|
||||
else
|
||||
# Process as an enumerable type to reduce code repetition.
|
||||
rt = rt.is_a?(Union) ? rt.types : [rt]
|
||||
# Check if any types are nilable.
|
||||
nilable = rt.any? do |t|
|
||||
# These are all macro types that have the `resolve?` method.
|
||||
(t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) &&
|
||||
(resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil
|
||||
end
|
||||
if nilable
|
||||
:nil
|
||||
else
|
||||
:raise
|
||||
end
|
||||
end }})
|
||||
{% elsif !method.abstract? %}
|
||||
# The method isn't abstract, infer the type it returns without calling it.
|
||||
_spectator_cast_stub_value(%stub, %call, typeof({{original}}))
|
||||
{% else %}
|
||||
# Stubbed method is abstract and there's no return type annotation.
|
||||
# The value of the stub could be returned as-is.
|
||||
# This may produce a "bloated" union of all known stub types,
|
||||
# and generally causes more annoying problems.
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{%call} but cannot resolve the return type. Please add a return type restriction.")
|
||||
{% end %}
|
||||
else
|
||||
# A stub wasn't found, invoke the type-specific fallback logic.
|
||||
{% if method.return_type %}
|
||||
# Pass along just the return type annotation.
|
||||
_spectator_abstract_stub_fallback(%call, {{method.return_type}})
|
||||
{% elsif !method.abstract? %}
|
||||
_spectator_abstract_stub_fallback(%call, typeof({{original}}))
|
||||
{% else %}
|
||||
# Stubbed method is abstract and there's no type annotation.
|
||||
_spectator_abstract_stub_fallback(%call)
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Redefines a method to require stubs.
|
||||
#
|
||||
# The *method* can be a `Def`.
|
||||
# That is, a normal looking method definition should follow the `stub` keyword.
|
||||
#
|
||||
# ```
|
||||
# stub def stubbed_method
|
||||
# "foobar"
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# If the *method* is abstract, then a stub must be provided otherwise attempts to call the method will raise `UnexpectedMessage`.
|
||||
#
|
||||
# ```
|
||||
# stub abstract def stubbed_method
|
||||
# ```
|
||||
#
|
||||
# A `Call` can also be specified.
|
||||
# In this case all methods in the stubbed type and its ancestors that match the call's signature are stubbed.
|
||||
#
|
||||
# ```
|
||||
# stub stubbed_method(arg)
|
||||
# ```
|
||||
#
|
||||
# The method being stubbed doesn't need to exist yet.
|
||||
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
|
||||
# If no stub is found, then `#_spectator_stub_fallback` or `#_spectator_abstract_stub_fallback` is called.
|
||||
macro stub(method)
|
||||
{% raise "Cannot define a stub inside a method" if @def %}
|
||||
|
||||
{% if method.is_a?(Def) %}
|
||||
{% if method.abstract? %}abstract_stub{% else %}default_stub{% end %} {{method}}
|
||||
{% elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def) %}
|
||||
{% if method.exp.abstract? %}abstract_stub{% else %}default_stub{% end %} {{method}}
|
||||
{% elsif method.is_a?(Call) %}
|
||||
{% raise "Stub on `Call` unsupported." %}
|
||||
{% else %}
|
||||
{% raise "Unrecognized syntax for `stub` - #{method}" %}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Redefines all methods and ones inherited from its parents and mixins to support stubs.
|
||||
private macro stub_type(type_name = @type)
|
||||
{% type = type_name.resolve
|
||||
definitions = [] of Nil
|
||||
scope = if type == @type
|
||||
:previous_def
|
||||
elsif type.module?
|
||||
type.name
|
||||
else
|
||||
:super
|
||||
end.id
|
||||
|
||||
# Add entries for methods in the target type and its class type.
|
||||
[[:self.id, type.class], [nil, type]].each do |(receiver, t)|
|
||||
t.methods.each do |method|
|
||||
definitions << {
|
||||
type: t,
|
||||
method: method,
|
||||
scope: scope,
|
||||
receiver: receiver,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Iterate through all ancestors and add their methods.
|
||||
type.ancestors.each do |ancestor|
|
||||
[[:self.id, ancestor.class], [nil, ancestor]].each do |(receiver, t)|
|
||||
t.methods.each do |method|
|
||||
# Skip methods already found to prevent redefining them multiple times.
|
||||
unless definitions.any? do |d|
|
||||
m = d[:method]
|
||||
m.name == method.name &&
|
||||
m.args == method.args &&
|
||||
m.splat_index == method.splat_index &&
|
||||
m.double_splat == method.double_splat &&
|
||||
m.block_arg == method.block_arg
|
||||
end
|
||||
definitions << {
|
||||
type: t,
|
||||
method: method,
|
||||
scope: :super.id,
|
||||
receiver: receiver,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
definitions = definitions.reject do |definition|
|
||||
name = definition[:method].name
|
||||
name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.symbolize)
|
||||
end %}
|
||||
|
||||
{% for definition in definitions %}
|
||||
{% original_type = definition[:type]
|
||||
method = definition[:method]
|
||||
scope = definition[:scope]
|
||||
receiver = definition[:receiver]
|
||||
rewrite_args = method.accepts_block?
|
||||
# Handle calling methods on other objects (primarily for mock modules).
|
||||
if scope != :super.id && scope != :previous_def.id
|
||||
if receiver == :self.id
|
||||
scope = "#{scope}.#{method.name}".id
|
||||
rewrite_args = true
|
||||
else
|
||||
scope = :super.id
|
||||
end
|
||||
end %}
|
||||
# Redefinition of {{original_type}}{{"#".id}}{{method.name}}
|
||||
{{(method.abstract? ? "abstract_stub abstract" : "default_stub").id}} {{method.visibility.id if method.visibility != :public}} def {{"#{receiver}.".id if receiver}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
{% unless method.abstract? %}
|
||||
{{scope}}{% if rewrite_args %}({% for arg, i in method.args %}
|
||||
{% if i == method.splat_index && arg.internal_name && arg.internal_name.size > 0 %}*{{arg.internal_name}}, {% if method.double_splat %}**{{method.double_splat}}, {% end %}{% end %}
|
||||
{% if method.splat_index && i > method.splat_index %}{{arg.name}}: {{arg.internal_name}}, {% end %}
|
||||
{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name}}, {% end %}{% end %}
|
||||
{% if !method.splat_index && method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
|
||||
method.block_arg.name
|
||||
else
|
||||
nil
|
||||
end %}
|
||||
{% if captured_block %}&{{captured_block}}{% end %}
|
||||
){% if !captured_block && method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}{% end %}
|
||||
end
|
||||
{% end %}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Utility macro for casting a stub (and its return value) to the correct type.
|
||||
#
|
||||
# *stub* is the variable holding the stub.
|
||||
# *call* is the variable holding the captured method call.
|
||||
# *type* is the expected type to cast the value to.
|
||||
# *fail_cast* indicates the behavior used when the value returned by the stub can't be cast to *type*.
|
||||
# - `:nil` - return nil.
|
||||
# - `:raise` - raise a `TypeCastError`.
|
||||
# - `:no_return` - raise as no value should be returned.
|
||||
private macro _spectator_cast_stub_value(stub, call, type, fail_cast = :nil)
|
||||
{% if fail_cast == :no_return %}
|
||||
{{stub}}.call({{call}})
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a value, but it shouldn't have returned (`NoReturn`).")
|
||||
{% else %}
|
||||
# Get the value as-is from the stub.
|
||||
# This will be compiled as a union of all known stubbed value types.
|
||||
%value = {{stub}}.call({{call}})
|
||||
%type = {{type}}
|
||||
|
||||
# Attempt to cast the value to the method's return type.
|
||||
# If successful, which it will be in most cases, return it.
|
||||
# The caller will receive a properly typed value without unions or other side-effects.
|
||||
%cast = %value.as?({{type}})
|
||||
|
||||
{% if fail_cast == :nil %}
|
||||
%cast
|
||||
{% elsif fail_cast == :raise %}
|
||||
# Check if nil was returned by the stub and if its okay to return it.
|
||||
if %value.nil? && %type.nilable?
|
||||
# Value was nil and nil is allowed to be returned.
|
||||
%type.cast(%cast)
|
||||
elsif %cast.nil?
|
||||
# The stubbed value was something else entirely and cannot be cast to the return type.
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{%type}`.")
|
||||
else
|
||||
# Types match and value can be returned as cast type.
|
||||
%cast
|
||||
end
|
||||
{% else %}
|
||||
{% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
module Spectator
|
||||
# Defines the name of a double or mock.
|
||||
#
|
||||
# When present on a stubbed type, this annotation indicates its name in output such as exceptions.
|
||||
# Must have one argument - the name of the double or mock.
|
||||
# This can be a symbol, string literal, or type name.
|
||||
annotation StubbedName
|
||||
end
|
||||
end
|
|
@ -1,68 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Defines stubbing functionality at the type level (classes and structs).
|
||||
#
|
||||
# This module is intended to be extended when a type includes `Stubbable`.
|
||||
module StubbedType
|
||||
private abstract def _spectator_stubs : Array(Stub)
|
||||
|
||||
def _spectator_find_stub(call : MethodCall) : Stub?
|
||||
_spectator_stubs.find &.===(call)
|
||||
end
|
||||
|
||||
def _spectator_stub_for_method?(method : Symbol) : Bool
|
||||
_spectator_stubs.any? { |stub| stub.method == method }
|
||||
end
|
||||
|
||||
def _spectator_define_stub(stub : Stub) : Nil
|
||||
_spectator_stubs.unshift(stub)
|
||||
end
|
||||
|
||||
def _spectator_remove_stub(stub : Stub) : Nil
|
||||
_spectator_stubs.delete(stub)
|
||||
end
|
||||
|
||||
def _spectator_clear_stubs : Nil
|
||||
_spectator_stubs.clear
|
||||
end
|
||||
|
||||
def _spectator_record_call(call : MethodCall) : Nil
|
||||
_spectator_calls << call
|
||||
end
|
||||
|
||||
def _spectator_clear_calls : Nil
|
||||
_spectator_calls.clear
|
||||
end
|
||||
|
||||
# Clears all previously defined calls and stubs.
|
||||
def _spectator_reset : Nil
|
||||
_spectator_clear_calls
|
||||
_spectator_clear_stubs
|
||||
end
|
||||
|
||||
def _spectator_stub_fallback(call : MethodCall, &)
|
||||
Log.trace { "Fallback for #{call} - call original" }
|
||||
yield
|
||||
end
|
||||
|
||||
def _spectator_stub_fallback(call : MethodCall, type, &)
|
||||
_spectator_stub_fallback(call) { yield }
|
||||
end
|
||||
|
||||
def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
Log.info do
|
||||
break unless _spectator_stub_for_method?(call.method)
|
||||
|
||||
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
|
||||
end
|
||||
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
end
|
||||
|
||||
def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
_spectator_abstract_stub_fallback(call)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,19 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Abstract type of stub that identifies the type of value produced by a stub.
|
||||
#
|
||||
# *T* is the type produced by the stub.
|
||||
# How the stub produces this value is up to subclasses.
|
||||
abstract class TypedStub(T) < Stub
|
||||
# Invokes the stubbed implementation.
|
||||
abstract def call(call : MethodCall) : T
|
||||
|
||||
# String representation of the stub, formatted as a method call.
|
||||
def to_s(io : IO) : Nil
|
||||
super
|
||||
io << " : " << T
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
module Spectator
|
||||
# Exception raised by a mock or double when a message is received that have been.
|
||||
class UnexpectedMessage < Exception
|
||||
end
|
||||
end
|
|
@ -1,70 +0,0 @@
|
|||
require "string_pool"
|
||||
require "./mock_registry_entry"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Stores collections of stubs for mocked value (struct) types.
|
||||
#
|
||||
# *T* is the type of value to track.
|
||||
#
|
||||
# This type is intended for all mocked struct types that have functionality "injected."
|
||||
# That is, the type itself has mock functionality bolted on.
|
||||
# Adding instance members should be avoided, for instance, it could mess up serialization.
|
||||
# This registry works around that by mapping mocks (via their raw memory content) to a collection of stubs.
|
||||
# Doing so prevents adding data to the mocked type.
|
||||
class ValueMockRegistry(T)
|
||||
@pool = StringPool.new # Used to de-dup values.
|
||||
@entries : Hash(String, MockRegistryEntry)
|
||||
|
||||
# Creates an empty registry.
|
||||
def initialize
|
||||
@entries = Hash(String, MockRegistryEntry).new do |hash, key|
|
||||
hash[key] = MockRegistryEntry.new
|
||||
end
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object.
|
||||
def [](object : T)
|
||||
key = value_bytes(object)
|
||||
@entries[key]
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet.
|
||||
def []?(object : T)
|
||||
key = value_bytes(object)
|
||||
@entries[key]?
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object.
|
||||
#
|
||||
# Yields to the block on the first retrieval.
|
||||
# This allows a mock to populate the registry with initial stubs.
|
||||
def fetch(object : T, & : -> Array(Stub))
|
||||
key = value_bytes(object)
|
||||
@entries.fetch(key) do
|
||||
entry = MockRegistryEntry.new
|
||||
entry.stubs = yield
|
||||
@entries[key] = entry
|
||||
end
|
||||
end
|
||||
|
||||
# Clears all stubs defined for a mocked object.
|
||||
def delete(object : T) : Nil
|
||||
key = value_bytes(object)
|
||||
@entries.delete(key)
|
||||
end
|
||||
|
||||
# Extracts heap-managed bytes for a value.
|
||||
#
|
||||
# Strings are used because a string pool is used.
|
||||
# However, the strings are treated as an array of bytes.
|
||||
@[AlwaysInline]
|
||||
private def value_bytes(value : T) : String
|
||||
# Get slice pointing to the memory used by the value (does not allocate).
|
||||
bytes = Bytes.new(pointerof(value).as(UInt8*), sizeof(T), read_only: true)
|
||||
|
||||
# De-dup the value (may allocate).
|
||||
@pool.get(bytes)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
require "../location"
|
||||
require "./arguments"
|
||||
require "./stub_modifiers"
|
||||
require "./typed_stub"
|
||||
|
||||
module Spectator
|
||||
# Stub that responds with a static value.
|
||||
class ValueStub(T) < TypedStub(T)
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : T
|
||||
@value
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, @value, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, @value : T, constraint : AbstractArguments? = nil, location : Location? = nil)
|
||||
super(method, constraint, location)
|
||||
end
|
||||
|
||||
# String representation of the stub, formatted as a method call and return value.
|
||||
def to_s(io : IO) : Nil
|
||||
super
|
||||
io << " # => "
|
||||
@value.inspect(io)
|
||||
end
|
||||
end
|
||||
|
||||
module StubModifiers
|
||||
# Returns a new stub that returns a static value.
|
||||
def and_return(value)
|
||||
ValueStub.new(method, value, constraint, location)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue