Support free variables in mocked types

This commit is contained in:
Michael Miller 2022-12-13 18:22:22 -07:00
parent 2985ef5919
commit 293faccd5c
No known key found for this signature in database
GPG Key ID: AC78B32D30CE34A2
3 changed files with 56 additions and 4 deletions

View File

@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- Fix macro logic to support free variables on stubbed methods.
### Changed
- Simplify string representation of mock-related types.
- Remove unnecessary redefinitions of methods when adding stub functionality to a type.

View File

@ -0,0 +1,46 @@
require "../spec_helper"
Spectator.describe "GitHub Issue #48" do
class Test
def return_this(thing : T) : T forall T
thing
end
def map(thing : T, & : T -> U) : U forall T, U
yield thing
end
def make_nilable(thing : T) : T? forall T
thing.as(T?)
end
end
mock Test, make_nilable: nil
let(fake) { mock(Test) }
it "handles free variables" do
allow(fake).to receive(:return_this).and_return("different")
expect(fake.return_this("test")).to eq("different")
end
it "raises on type cast error with free variables" do
allow(fake).to receive(:return_this).and_return(42)
expect { fake.return_this("test") }.to raise_error(TypeCastError, /String/)
end
it "handles free variables with a block" do
allow(fake).to receive(:map).and_return("stub")
expect(fake.map(:mapped, &.to_s)).to eq("stub")
end
it "raises on type cast error with a block and free variables" do
allow(fake).to receive(:map).and_return(42)
expect { fake.map(:mapped, &.to_s) }.to raise_error(TypeCastError, /String/)
end
it "handles nilable free variables" do
fake = mock(Test)
expect(fake.make_nilable("foo")).to be_nil
end
end

View File

@ -158,9 +158,11 @@ module Spectator
# 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 method.return_type && method.return_type.resolve == NoReturn
{{ if method.return_type && method.return_type.resolve? == NoReturn
:no_return
elsif method.return_type && method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
elsif method.return_type &&
((resolved = method.return_type.resolve?).is_a?(TypeNode) && resolved <= Nil) ||
(method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve?).includes?(Nil))
:nil
else
:raise
@ -262,9 +264,10 @@ module Spectator
{% if 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 method.return_type.resolve == NoReturn
{{ if method.return_type.resolve? == NoReturn
:no_return
elsif method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
elsif (method.return_type.resolve?.is_a?(TypeNode) && method.return_type.resolve <= Nil) ||
(method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve?).includes?(Nil))
:nil
else
:raise