Fix resolution issue when mocked types use custom types

GitLab issue 51 is affected.
https://gitlab.com/arctic-fox/spectator/-/issues/51
Private types cannot be referenced with mocks.
This commit is contained in:
Michael Miller 2022-12-17 20:56:16 -07:00
parent c3e7edc700
commit 4b68b8e3de
No known key found for this signature in database
GPG key ID: 32B47AE8F388A1FF
4 changed files with 133 additions and 35 deletions

View file

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48) - Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48)
- Fix method stubs used on methods that capture blocks. - Fix method stubs used on methods that capture blocks.
- Fix type name resolution for when using custom types in a mocked typed.
### Changed ### Changed
- Simplify string representation of mock-related types. - Simplify string representation of mock-related types.

View file

@ -1,31 +1,33 @@
require "../spec_helper" require "../spec_helper"
private class Foo module GitLabIssue51
def call(str : String) : String? class Foo
"" def call(str : String) : String?
""
end
def alt1_call(str : String) : String?
nil
end
def alt2_call(str : String) : String?
[str, nil].sample
end
end end
def alt1_call(str : String) : String? class Bar
nil def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call.
end a_foo.call("")
a_foo.alt1_call("")
def alt2_call(str : String) : String? a_foo.alt2_call("")
[str, nil].sample end
end end
end end
private class Bar Spectator.describe GitLabIssue51::Bar do
def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call. mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: ""
a_foo.call("")
a_foo.alt1_call("")
a_foo.alt2_call("")
end
end
Spectator.describe Bar do let(:foo) { mock(GitLabIssue51::Foo) }
mock Foo, call: "", alt1_call: "", alt2_call: ""
let(:foo) { mock(Foo) }
subject(:call) { described_class.new.call(foo) } subject(:call) { described_class.new.call(foo) }
describe "#call" do describe "#call" do

View file

@ -29,8 +29,18 @@ Spectator.describe Spectator::Mock do
@_spectator_invocations << :method3 @_spectator_invocations << :method3
"original" "original"
end end
def method4 : Thing
self
end
def method5 : OtherThing
OtherThing.new
end
end end
class OtherThing; end
Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method1: 123) do Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method1: 123) do
stub def method2 stub def method2
:stubbed :stubbed
@ -104,6 +114,20 @@ Spectator.describe Spectator::Mock do
mock.method3 mock.method3
expect(mock._spectator_invocations).to contain_exactly(:method3) expect(mock._spectator_invocations).to contain_exactly(:method3)
end end
it "can reference its own type" do
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:method4, new_mock)
mock._spectator_define_stub(stub)
expect(mock.method4).to be(new_mock)
end
it "can reference other types in the original namespace" do
other = OtherThing.new
stub = Spectator::ValueStub.new(:method5, other)
mock._spectator_define_stub(stub)
expect(mock.method5).to be(other)
end
end end
context "with an abstract class" do context "with an abstract class" do
@ -120,8 +144,14 @@ Spectator.describe Spectator::Mock do
end end
abstract def method4 abstract def method4
abstract def method4 : Thing
abstract def method5 : OtherThing
end end
class OtherThing; end
Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method2: :stubbed) do Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method2: :stubbed) do
stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent. stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent.
123 123
@ -199,6 +229,20 @@ Spectator.describe Spectator::Mock do
mock.method3 mock.method3
expect(mock._spectator_invocations).to contain_exactly(:method3) expect(mock._spectator_invocations).to contain_exactly(:method3)
end end
it "can reference its own type" do
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:method4, new_mock)
mock._spectator_define_stub(stub)
expect(mock.method4).to be(new_mock)
end
it "can reference other types in the original namespace" do
other = OtherThing.new
stub = Spectator::ValueStub.new(:method5, other)
mock._spectator_define_stub(stub)
expect(mock.method5).to be(other)
end
end end
context "with an abstract struct" do context "with an abstract struct" do
@ -215,8 +259,14 @@ Spectator.describe Spectator::Mock do
end end
abstract def method4 abstract def method4
abstract def method4 : Thing
abstract def method5 : OtherThing
end end
class OtherThing; end
Spectator::Mock.define_subtype(:struct, Thing, MockThing, :mock_name, method2: :stubbed) do Spectator::Mock.define_subtype(:struct, Thing, MockThing, :mock_name, method2: :stubbed) do
stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent. stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent.
123 123
@ -286,6 +336,22 @@ Spectator.describe Spectator::Mock do
mock.method3 mock.method3
expect(mock._spectator_invocations).to contain_exactly(:method3) expect(mock._spectator_invocations).to contain_exactly(:method3)
end end
it "can reference its own type" do
mock = self.mock # FIXME: Workaround for passing by value messing with stubs.
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:method4, new_mock)
mock._spectator_define_stub(stub)
expect(mock.method4).to be_a(Thing)
end
it "can reference other types in the original namespace" do
mock = self.mock # FIXME: Workaround for passing by value messing with stubs.
other = OtherThing.new
stub = Spectator::ValueStub.new(:method5, other)
mock._spectator_define_stub(stub)
expect(mock.method5).to be(other)
end
end end
context "class method stubs" do context "class method stubs" do
@ -301,8 +367,18 @@ Spectator.describe Spectator::Mock do
def self.baz(arg) def self.baz(arg)
yield yield
end end
def self.thing : Thing
new
end
def self.other : OtherThing
OtherThing.new
end
end end
class OtherThing; end
Spectator::Mock.define_subtype(:class, Thing, MockThing) do Spectator::Mock.define_subtype(:class, Thing, MockThing) do
stub def self.foo stub def self.foo
:stub :stub
@ -367,6 +443,20 @@ Spectator.describe Spectator::Mock do
expect(restricted(mock)).to eq(:stub) expect(restricted(mock)).to eq(:stub)
end end
it "can reference its own type" do
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:thing, new_mock)
mock._spectator_define_stub(stub)
expect(mock.thing).to be(new_mock)
end
it "can reference other types in the original namespace" do
other = OtherThing.new
stub = Spectator::ValueStub.new(:other, other)
mock._spectator_define_stub(stub)
expect(mock.other).to be(other)
end
describe "._spectator_clear_stubs" do describe "._spectator_clear_stubs" do
before { mock._spectator_define_stub(foo_stub) } before { mock._spectator_define_stub(foo_stub) }

View file

@ -218,24 +218,29 @@ module Spectator::DSL
# end # end
# ``` # ```
private macro def_mock(type, name = nil, **value_methods, &block) private macro def_mock(type, name = nil, **value_methods, &block)
{% # Construct a unique type name for the mock by using the number of defined types. {% resolved = type.resolve
index = ::Spectator::DSL::Mocks::TYPES.size # Construct a unique type name for the mock by using the number of defined types.
mock_type_name = "Mock#{index}".id index = ::Spectator::DSL::Mocks::TYPES.size
# The type is nested under the original so that any type names from the original can be resolved.
mock_type_name = "Mock#{index}".id
# Store information about how the mock is defined and its context. # Store information about how the mock is defined and its context.
# This is important for constructing an instance of the mock later. # This is important for constructing an instance of the mock later.
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, mock_type_name.symbolize} ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "::#{resolved.name}::#{mock_type_name}".id.symbolize}
resolved = type.resolve base = if resolved.class?
base = if resolved.class? :class
:class elsif resolved.struct?
elsif resolved.struct? :struct
:struct else
else :module
:module end %}
end %}
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}} {% begin %}
{{base.id}} ::{{resolved.name}}
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}}
end
{% end %}
end end
# Instantiates a mock. # Instantiates a mock.