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 %} {% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
class {{type_name.id}} < {{@type.name}} class {{type_name.id}} < {{@type.name}}
{% for key, value in value_methods %} {% for key, value in value_methods %}
inject_stub def {{key.id}}(*%args, **%kwargs) default_stub def {{key.id}}(*%args, **%kwargs)
{{value}} {{value}}
end end
inject_stub def {{key.id}}(*%args, **%kwargs, &) default_stub def {{key.id}}(*%args, **%kwargs, &)
{{key.id}} {{key.id}}
end end
{% end %} {% end %}
{% if block %} {% if block %}{{block.body}}{% end %}
{% 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 %}
end end
end end
@ -148,7 +140,7 @@ module Spectator
# "Hide" existing methods and methods from ancestors by overriding them. # "Hide" existing methods and methods from ancestors by overriding them.
macro finished macro finished
stub_all {{@type.name(generic_args: false)}} stub_hierarchy {{@type.name(generic_args: false)}}
end end
# Handle all methods but only respond to configured messages. # Handle all methods but only respond to configured messages.

View file

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