From d74a772f438911cecc6015048dac8d1b21813189 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 26 Dec 2023 18:46:19 -0700 Subject: [PATCH] WIP replace mocks with Mocks shard --- shard.yml | 4 + spec/matchers/receive_matcher_spec.cr | 10 +- .../spectator/dsl/mocks/allow_receive_spec.cr | 6 +- spec/spectator/dsl/mocks/double_spec.cr | 2 +- .../dsl/mocks/expect_receive_spec.cr | 6 +- spec/spectator/dsl/mocks/mock_spec.cr | 24 +- spec/spectator/mocks/double_spec.cr | 12 +- spec/spectator/mocks/null_double_spec.cr | 12 +- src/spectator.cr | 1 + src/spectator/dsl/mocks.cr | 110 ++-- src/spectator/expectation.cr | 38 +- src/spectator/includes.cr | 1 - src/spectator/matchers/receive_matcher.cr | 33 +- src/spectator/mocks.cr | 7 - src/spectator/mocks/abstract_arguments.cr | 58 -- src/spectator/mocks/allow.cr | 28 - src/spectator/mocks/arguments.cr | 110 ---- src/spectator/mocks/double.cr | 214 ------- src/spectator/mocks/exception_stub.cr | 55 -- src/spectator/mocks/formal_arguments.cr | 133 ---- src/spectator/mocks/lazy_double.cr | 91 --- src/spectator/mocks/method_call.cr | 42 -- src/spectator/mocks/mock.cr | 227 ------- src/spectator/mocks/mock_registry.cr | 43 -- src/spectator/mocks/mock_registry_entry.cr | 13 - src/spectator/mocks/mocked.cr | 127 ---- src/spectator/mocks/multi_value_stub.cr | 35 -- src/spectator/mocks/null_double.cr | 64 -- src/spectator/mocks/null_stub.cr | 16 - src/spectator/mocks/proc_stub.cr | 36 -- .../mocks/reference_mock_registry.cr | 53 -- src/spectator/mocks/stub.cr | 50 -- src/spectator/mocks/stub_modifiers.cr | 21 - src/spectator/mocks/stubbable.cr | 567 ------------------ src/spectator/mocks/stubbed_name.cr | 9 - src/spectator/mocks/stubbed_type.cr | 68 --- src/spectator/mocks/typed_stub.cr | 19 - src/spectator/mocks/unexpected_message.cr | 5 - src/spectator/mocks/value_mock_registry.cr | 70 --- src/spectator/mocks/value_stub.cr | 38 -- 40 files changed, 110 insertions(+), 2348 deletions(-) delete mode 100644 src/spectator/mocks.cr delete mode 100644 src/spectator/mocks/abstract_arguments.cr delete mode 100644 src/spectator/mocks/allow.cr delete mode 100644 src/spectator/mocks/arguments.cr delete mode 100644 src/spectator/mocks/double.cr delete mode 100644 src/spectator/mocks/exception_stub.cr delete mode 100644 src/spectator/mocks/formal_arguments.cr delete mode 100644 src/spectator/mocks/lazy_double.cr delete mode 100644 src/spectator/mocks/method_call.cr delete mode 100644 src/spectator/mocks/mock.cr delete mode 100644 src/spectator/mocks/mock_registry.cr delete mode 100644 src/spectator/mocks/mock_registry_entry.cr delete mode 100644 src/spectator/mocks/mocked.cr delete mode 100644 src/spectator/mocks/multi_value_stub.cr delete mode 100644 src/spectator/mocks/null_double.cr delete mode 100644 src/spectator/mocks/null_stub.cr delete mode 100644 src/spectator/mocks/proc_stub.cr delete mode 100644 src/spectator/mocks/reference_mock_registry.cr delete mode 100644 src/spectator/mocks/stub.cr delete mode 100644 src/spectator/mocks/stub_modifiers.cr delete mode 100644 src/spectator/mocks/stubbable.cr delete mode 100644 src/spectator/mocks/stubbed_name.cr delete mode 100644 src/spectator/mocks/stubbed_type.cr delete mode 100644 src/spectator/mocks/typed_stub.cr delete mode 100644 src/spectator/mocks/unexpected_message.cr delete mode 100644 src/spectator/mocks/value_mock_registry.cr delete mode 100644 src/spectator/mocks/value_stub.cr diff --git a/shard.yml b/shard.yml index d7b6cff..048953c 100644 --- a/shard.yml +++ b/shard.yml @@ -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 diff --git a/spec/matchers/receive_matcher_spec.cr b/spec/matchers/receive_matcher_spec.cr index b5addc5..fc532bc 100644 --- a/spec/matchers/receive_matcher_spec.cr +++ b/spec/matchers/receive_matcher_spec.cr @@ -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 diff --git a/spec/spectator/dsl/mocks/allow_receive_spec.cr b/spec/spectator/dsl/mocks/allow_receive_spec.cr index 473c74b..10cd761 100644 --- a/spec/spectator/dsl/mocks/allow_receive_spec.cr +++ b/spec/spectator/dsl/mocks/allow_receive_spec.cr @@ -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 diff --git a/spec/spectator/dsl/mocks/double_spec.cr b/spec/spectator/dsl/mocks/double_spec.cr index 5547ae0..4d99391 100644 --- a/spec/spectator/dsl/mocks/double_spec.cr +++ b/spec/spectator/dsl/mocks/double_spec.cr @@ -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 diff --git a/spec/spectator/dsl/mocks/expect_receive_spec.cr b/spec/spectator/dsl/mocks/expect_receive_spec.cr index a249ad3..fec462a 100644 --- a/spec/spectator/dsl/mocks/expect_receive_spec.cr +++ b/spec/spectator/dsl/mocks/expect_receive_spec.cr @@ -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 diff --git a/spec/spectator/dsl/mocks/mock_spec.cr b/spec/spectator/dsl/mocks/mock_spec.cr index cd57cdc..5651969 100644 --- a/spec/spectator/dsl/mocks/mock_spec.cr +++ b/spec/spectator/dsl/mocks/mock_spec.cr @@ -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 diff --git a/spec/spectator/mocks/double_spec.cr b/spec/spectator/mocks/double_spec.cr index e55c549..d195e26 100644 --- a/spec/spectator/mocks/double_spec.cr +++ b/spec/spectator/mocks/double_spec.cr @@ -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) } diff --git a/spec/spectator/mocks/null_double_spec.cr b/spec/spectator/mocks/null_double_spec.cr index a6fc7d2..6ebca50 100644 --- a/spec/spectator/mocks/null_double_spec.cr +++ b/spec/spectator/mocks/null_double_spec.cr @@ -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) } diff --git a/src/spectator.cr b/src/spectator.cr index a05a7e2..9ab920e 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,5 +1,6 @@ require "colorize" require "log" +require "mocks" require "./spectator/includes" # Module that contains all functionality related to Spectator. diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index ab05636..1d5f996 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -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 diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 79d8473..039a935 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -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 diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 10c1698..0e78ca6 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -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" diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 560cabd..323925d 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -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 diff --git a/src/spectator/mocks.cr b/src/spectator/mocks.cr deleted file mode 100644 index f50bf9e..0000000 --- a/src/spectator/mocks.cr +++ /dev/null @@ -1,7 +0,0 @@ -require "./mocks/*" - -module Spectator - # Functionality for mocking existing types. - module Mocks - end -end diff --git a/src/spectator/mocks/abstract_arguments.cr b/src/spectator/mocks/abstract_arguments.cr deleted file mode 100644 index 442c1d2..0000000 --- a/src/spectator/mocks/abstract_arguments.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/allow.cr b/src/spectator/mocks/allow.cr deleted file mode 100644 index 4754690..0000000 --- a/src/spectator/mocks/allow.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr deleted file mode 100644 index f15a0ba..0000000 --- a/src/spectator/mocks/arguments.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr deleted file mode 100644 index e5e43f4..0000000 --- a/src/spectator/mocks/double.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/exception_stub.cr b/src/spectator/mocks/exception_stub.cr deleted file mode 100644 index e7b6cb9..0000000 --- a/src/spectator/mocks/exception_stub.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/formal_arguments.cr b/src/spectator/mocks/formal_arguments.cr deleted file mode 100644 index 1c0ca69..0000000 --- a/src/spectator/mocks/formal_arguments.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/lazy_double.cr b/src/spectator/mocks/lazy_double.cr deleted file mode 100644 index 75fe30c..0000000 --- a/src/spectator/mocks/lazy_double.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/method_call.cr b/src/spectator/mocks/method_call.cr deleted file mode 100644 index 9c5fd01..0000000 --- a/src/spectator/mocks/method_call.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/mock.cr b/src/spectator/mocks/mock.cr deleted file mode 100644 index 87e8e23..0000000 --- a/src/spectator/mocks/mock.cr +++ /dev/null @@ -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) %} - "#" - \{% else %} - "#" - \{% end %} - end - - private def self._spectator_stubbed_name : ::String - \{% if anno = @type.annotation(::Spectator::StubbedName) %} - "#" - \{% else %} - "#" - \{% 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) %} - "#" - \{% else %} - "#" - \{% end %} - end - - # Returns the mock's name formatted for user output. - private def self._spectator_stubbed_name : ::String - \{% if anno = @type.annotation(::Spectator::StubbedName) %} - "#" - \{% else %} - "#" - \{% end %} - end - - macro finished - stub_type {{type_name.id}} - - {{block.body if block}} - end - end - {% end %} - end - end -end diff --git a/src/spectator/mocks/mock_registry.cr b/src/spectator/mocks/mock_registry.cr deleted file mode 100644 index 29390c6..0000000 --- a/src/spectator/mocks/mock_registry.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/mock_registry_entry.cr b/src/spectator/mocks/mock_registry_entry.cr deleted file mode 100644 index 9c6670a..0000000 --- a/src/spectator/mocks/mock_registry_entry.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/mocked.cr b/src/spectator/mocks/mocked.cr deleted file mode 100644 index 280eef8..0000000 --- a/src/spectator/mocks/mocked.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/multi_value_stub.cr b/src/spectator/mocks/multi_value_stub.cr deleted file mode 100644 index 8e64db5..0000000 --- a/src/spectator/mocks/multi_value_stub.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/null_double.cr b/src/spectator/mocks/null_double.cr deleted file mode 100644 index 587f4ab..0000000 --- a/src/spectator/mocks/null_double.cr +++ /dev/null @@ -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) %} - "#" - {% else %} - "#" - {% 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 diff --git a/src/spectator/mocks/null_stub.cr b/src/spectator/mocks/null_stub.cr deleted file mode 100644 index f5f0854..0000000 --- a/src/spectator/mocks/null_stub.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/proc_stub.cr b/src/spectator/mocks/proc_stub.cr deleted file mode 100644 index 7bdd0f2..0000000 --- a/src/spectator/mocks/proc_stub.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/reference_mock_registry.cr b/src/spectator/mocks/reference_mock_registry.cr deleted file mode 100644 index 84227d1..0000000 --- a/src/spectator/mocks/reference_mock_registry.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/stub.cr b/src/spectator/mocks/stub.cr deleted file mode 100644 index 606894d..0000000 --- a/src/spectator/mocks/stub.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/stub_modifiers.cr b/src/spectator/mocks/stub_modifiers.cr deleted file mode 100644 index a545634..0000000 --- a/src/spectator/mocks/stub_modifiers.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/stubbable.cr b/src/spectator/mocks/stubbable.cr deleted file mode 100644 index 3385ad4..0000000 --- a/src/spectator/mocks/stubbable.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/stubbed_name.cr b/src/spectator/mocks/stubbed_name.cr deleted file mode 100644 index df3cfc6..0000000 --- a/src/spectator/mocks/stubbed_name.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/stubbed_type.cr b/src/spectator/mocks/stubbed_type.cr deleted file mode 100644 index 5362b84..0000000 --- a/src/spectator/mocks/stubbed_type.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/typed_stub.cr b/src/spectator/mocks/typed_stub.cr deleted file mode 100644 index eabbcb9..0000000 --- a/src/spectator/mocks/typed_stub.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/unexpected_message.cr b/src/spectator/mocks/unexpected_message.cr deleted file mode 100644 index f769ee8..0000000 --- a/src/spectator/mocks/unexpected_message.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/value_mock_registry.cr b/src/spectator/mocks/value_mock_registry.cr deleted file mode 100644 index 1efd0b0..0000000 --- a/src/spectator/mocks/value_mock_registry.cr +++ /dev/null @@ -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 diff --git a/src/spectator/mocks/value_stub.cr b/src/spectator/mocks/value_stub.cr deleted file mode 100644 index 7a84d19..0000000 --- a/src/spectator/mocks/value_stub.cr +++ /dev/null @@ -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