Skip to content

Commit

Permalink
Fix memoized method subjects to preserve freezer option
Browse files Browse the repository at this point in the history
* If Memoizable alone is included in the subject scope, #memoize
  does not support any options.
* Adamantium extends Memoizable by adding support for configuring
  the freezer to be used as an option for #memoize.
* If Adamantium is included in the subject scope, the freezer that
  was either used implicitly or configured explicitly, needs to be
  preserved to keep original semantics intact.
* The AST generated for the call to #memoize exhibits semantics
  that are equivalent to the originally defined memoizer. It may
  not always lead to identical source when being unparsed though.
  This is because memoizable provides no API to reflect on whether
  the freezer was configured implicitly by the adamantium module
  or whether it was set explicitly via the :freezer option added
  by adamantium.
  • Loading branch information
snusnu committed Oct 29, 2019
1 parent 4bd37d8 commit 92310a8
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 11 deletions.
44 changes: 42 additions & 2 deletions lib/mutant/subject/method/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ def prepare
class Memoized < self
include AST::Sexp

FREEZER_OPTION_VALUES = {
Adamantium::Freezer::Deep => :deep,
Adamantium::Freezer::Flat => :flat,
Adamantium::Freezer::Noop => :noop
}.freeze

private_constant(*constants(false))

# Prepare subject for mutation insertion
#
# @return [self]
def prepare
scope.__send__(:memoized_methods).instance_variable_get(:@memory).delete(name)
memory.delete(name)
super()
end

Expand All @@ -39,8 +47,40 @@ def prepare
#
# @return [Parser::AST::Node]
def wrap_node(mutant)
s(:begin, mutant, s(:send, nil, :memoize, s(:args, s(:sym, name))))
s(:begin, mutant, s(:send, nil, :memoize, s(:args, s(:sym, name), *options)))
end

# The optional AST node for adamantium memoization options
#
# @return [Array(Parser::AST::Node), nil]
def options
# rubocop:disable Style/GuardClause
if FREEZER_OPTION_VALUES.key?(freezer)
[
s(:hash,
s(:pair,
s(:sym, :freezer),
s(:sym, FREEZER_OPTION_VALUES.fetch(freezer))))
]
end
# rubocop:enable Style/GuardClause
end

# The freezer used for memoization
#
# @return [Object]
def freezer
memory.fetch(name).instance_variable_get(:@freezer)
end
memoize :freezer, freezer: :noop

# The memory used for memoization
#
# @return [ThreadSafe::Cache]
def memory
scope.__send__(:memoized_methods).instance_variable_get(:@memory)
end
memoize :memory, freezer: :noop

end # Memoized
end # Instance
Expand Down
118 changes: 109 additions & 9 deletions spec/unit/mutant/subject/method/instance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ def self.name
)
end

let(:context) { double('Context') }
let(:context) do
Mutant::Context.new(scope, double('Source Path'))
end

let(:warnings) { instance_double(Mutant::Warnings) }

let(:node) do
Expand All @@ -111,18 +114,31 @@ def self.name
allow(warnings).to receive(:call).and_yield
end

describe '#prepare' do
let(:context) do
Mutant::Context.new(scope, double('Source Path'))
end

shared_context 'memoizable scope setup' do
let(:scope) do
Class.new do
include Memoizable
def foo; end
memoize :foo
end
end
end

shared_context 'adamantium scope setup' do
let(:scope) do
memoize_options = self.memoize_options
memoize_provider = self.memoize_provider

Class.new do
include memoize_provider
def foo; end
memoize :foo, **memoize_options
end
end
end

describe '#prepare' do
include_context 'memoizable scope setup'

subject { object.prepare }

Expand Down Expand Up @@ -162,15 +178,99 @@ def foo; end
end

let(:memoize_node) do
s(:send, nil, :memoize, s(:args, s(:sym, :foo)))
s(:send, nil, :memoize, s(:args, s(:sym, :foo), *options_node))
end

it { should eql(expected) }
let(:options_node) { nil }

context 'when Memoizable is included in scope' do
include_context 'memoizable scope setup'

it { should eql(expected) }
end

context 'when Adamantium is included in scope' do
include_context 'adamantium scope setup'

{
Adamantium => :deep,
Adamantium::Flat => :flat
}.each do |memoize_provider, default_freezer_option|
context "as include #{memoize_provider}" do
let(:memoize_provider) { memoize_provider }
let(:default_freezer_option) { default_freezer_option }

let(:options_node) do
[s(:hash, s(:pair, s(:sym, :freezer), s(:sym, freezer_option)))]
end

context 'when no memoize options are given' do
let(:memoize_options) { Mutant::EMPTY_HASH }
let(:freezer_option) { default_freezer_option }

it { should eql(expected) }
end

context 'when memoize options are given' do
let(:memoize_options) { { freezer: freezer_option } }

%i[deep flat noop].each do |option|
context "as #{option.inspect}" do
let(:freezer_option) { option }

it { should eql(expected) }
end
end
end
end
end
end
end

describe '#source' do
subject { object.source }

it { should eql("def foo\nend\nmemoize(:foo)") }
context 'when Memoizable is included in scope' do
include_context 'memoizable scope setup'

let(:source) { "def foo\nend\nmemoize(:foo)" }

it { should eql(source) }
end

context 'when Adamantium is included in scope' do
include_context 'adamantium scope setup'

let(:source) do
"def foo\nend\nmemoize(:foo, { freezer: #{freezer_option.inspect} })"
end

{
Adamantium => :deep,
Adamantium::Flat => :flat
}.each do |memoize_provider, default_freezer_option|
context "as include #{memoize_provider}" do
let(:memoize_provider) { memoize_provider }

context 'when no memoize options are given' do
let(:memoize_options) { Mutant::EMPTY_HASH }
let(:freezer_option) { default_freezer_option }

it { should eql(source) }
end

context 'when memoize options are given' do
%i[deep flat noop].each do |freezer_option|
context "as #{freezer_option.inspect}" do
let(:memoize_options) { { freezer: freezer_option } }
let(:freezer_option) { freezer_option }

it { should eql(source) }
end
end
end
end
end
end
end
end

0 comments on commit 92310a8

Please sign in to comment.