Rework stubbing type hierarchy

No longer need to "inject" stubs for new methods.
No weird lookup of super/previous_def.
Handle visibility modifier of def.
This commit is contained in:
Michael Miller 2022-04-28 22:07:12 -06:00
parent 8f5f3becb4
commit 307c679609
No known key found for this signature in database
GPG key ID: AC78B32D30CE34A2
3 changed files with 72 additions and 69 deletions

View file

@ -50,24 +50,16 @@ module Spectator
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
class {{type_name.id}} < {{@type.name}}
{% for key, value in value_methods %}
inject_stub def {{key.id}}(*%args, **%kwargs)
default_stub def {{key.id}}(*%args, **%kwargs)
{{value}}
end
inject_stub def {{key.id}}(*%args, **%kwargs, &)
default_stub def {{key.id}}(*%args, **%kwargs, &)
{{key.id}}
end
{% end %}
{% if block %}
{% for expr in block.body.is_a?(Expressions) ? block.body.expressions : [block.body] %}
{% if expr.is_a?(Call) && expr.name == :stub.id %}
inject_{{expr}}
{% else %}
{{expr}}
{% end %}
{% end %}
{% end %}
{% if block %}{{block.body}}{% end %}
end
end
@ -148,7 +140,7 @@ module Spectator
# "Hide" existing methods and methods from ancestors by overriding them.
macro finished
stub_all {{@type.name(generic_args: false)}}
stub_hierarchy {{@type.name(generic_args: false)}}
end
# Handle all methods but only respond to configured messages.

View file

@ -41,7 +41,7 @@ module Spectator
end
macro finished
stub_all {{mocked_type.id}}
stub_hierarchy {{mocked_type.id}}
end
{% if block %}{{block.body}}{% end %}

View file

@ -85,21 +85,34 @@ module Spectator
# 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` requires a method definition" unless method.is_a?(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) %}
{{method}}
{{visibility.id if visibility != :public}} def {{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#{" { |*_spectator_yargs| yield *_spectator_yargs }" if method.accepts_block?}".id %}
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".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.
%}
{% if method.visibility != :public %}{{method.visibility.id}}{% end %} def {{method.receiver}}{{method.name}}(
{{visibility.id if visibility != :public}} def {{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 %}
@ -153,8 +166,15 @@ module Spectator
# 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 "abstract_stub requires a method definition" if !method.is_a?(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`.
@ -164,47 +184,24 @@ module Spectator
# For all intents and purposes, this macro defines logic that doesn't depend on an existing method.
%}
{% if method.abstract? %}
{% original = if @type.methods.includes?(method)
:previous_def
elsif @type.ancestors.any? &.methods.includes?(method)
:super
# sigh... sometimes the method won't match with a simple check.
# It seems to be from a difference with the body attribute.
# Manually check most attributes.
elsif @type.ancestors.any? do |ancestor|
ancestor.methods.any? do |meth|
meth.name == method.name &&
meth.args == method.args &&
meth.accepts_block? == method.accepts_block? &&
meth.block_arg == method.block_arg &&
meth.double_splat == method.double_splat &&
meth.free_vars == method.free_vars &&
meth.receiver == method.receiver &&
meth.return_type == method.return_type &&
meth.splat_index == method.splat_index &&
meth.visibility == method.visibility
end
end
:super
else
:previous_def # raise "Could not find original implementation of `#{method.name}` for stubbing"
end.id
if method.accepts_block?
original = "#{original} { |*_spectator_yargs| yield *_spectator_yargs }".id
end %}
{% else %}
{{method}}
{% unless method.abstract? %}
{{visibility.id if visibility != :public}} def {{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#{" { |*_spectator_yargs| yield *_spectator_yargs }" if method.accepts_block?}".id %}
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".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.
%}
{% if method.visibility != :public %}{{method.visibility.id}}{% end %} def {{method.receiver}}{{method.name}}(
{{visibility.id if visibility != :public}} def {{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 %}
@ -237,17 +234,14 @@ module Spectator
{% end %}
else
# A stub wasn't found, invoke the type-specific fallback logic.
{% if method.return_type && method.abstract? %}
{% if method.return_type %}
# Pass along just the return type annotation.
_spectator_abstract_stub_fallback(%call, {{method.return_type}})
{% elsif method.abstract? %}
{% 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)
{% else %}
# Pass along the type of the original method and a block to invoke it.
_spectator_stub_fallback(%call, typeof({{original}})) do
{{original}}
end
{% end %}
end
end
@ -257,8 +251,13 @@ module Spectator
{% 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
@ -302,21 +301,33 @@ module Spectator
end
end
# Redefines all methods on a type to conditionally respond to messages.
# Methods will raise `UnexpectedMessage` if they're called when they shouldn't be.
# Otherwise, they'll return the configured response.
private macro stub_all(type_name, *, with style = :stub)
{% type = type_name.resolve %}
{% if type.superclass %}
stub_all({{type.superclass}}, with: {{style}})
# Redefines all methods and ones inherited from its parents and mixins to support stubs.
private macro stub_hierarchy(type_name = @type)
{% type = type_name.resolve
# Reverse order of ancestors (there's currently no reverse method for ArrayLiteral).
count = type.ancestors.size
ancestors = type.ancestors.map_with_index { |_, i| type.ancestors[count - i - 1] } %}
{% for ancestor in ancestors %}
stub_type {{ancestor}}
{% end %}
stub_type {{type_name}}
end
private macro stub_type(type_name = @type)
{% type = type_name.resolve %}
{% for method in type.methods.reject do |meth|
meth.name.starts_with?("_spectator") ||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
end %}
{{style.id}} {{method}}
{% end %}
{{(method.abstract? ? :abstract_stub : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{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 %}
{{ type == @type ? :previous_def.id : :super.id }}{{ " { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block? }}
end
{% end %}
end
end
end