Skip to content

Commit

Permalink
Merge pull request #973 from mbj/change/preserve-adamantium-freezer-o…
Browse files Browse the repository at this point in the history
…ption

Fix memoized method subjects to preserve freezer option
  • Loading branch information
mbj authored Aug 24, 2020
2 parents cb2db0a + a11ba51 commit a2bf9df
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 25 deletions.
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.9.10 unreleased

* Fix memoized subjects to preserve freezer option [#973](https://github.com/mbj/mutant/pull/973).

# v0.9.9 2020-09-25

+ Add support for mutating methods inside eigenclasses `class <<`. [#1009](https://github.com/mbj/mutant/pull/1009)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
mutant (0.9.8)
mutant (0.9.9)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
anima (~> 0.3.1)
Expand Down
43 changes: 41 additions & 2 deletions lib/mutant/subject/method/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,57 @@ 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

private

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

end # Memoized
Expand Down
139 changes: 117 additions & 22 deletions spec/unit/mutant/subject/method/instance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@ def self.name
end

describe '#prepare' do
let(:context) do
Mutant::Context.new(scope, instance_double(Pathname))
end

subject { object.prepare }

it 'undefines method on scope' do
Expand Down Expand Up @@ -104,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 @@ -115,19 +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 All @@ -142,40 +153,124 @@ def foo; end
it_should_behave_like 'a command method'
end

describe '#mutations', mutant_expression: 'Mutant::Subject#mutations' do
describe '#mutations' do
subject { object.mutations }

let(:expected) do
[
Mutant::Mutation::Neutral.new(
object,
s(:begin,
s(:def, :foo, s(:args)), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))
s(:begin, s(:def, :foo, s(:args)), memoize_node)
),
Mutant::Mutation::Evil.new(
object,
s(:begin,
s(:def, :foo, s(:args), s(:send, nil, :raise)), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))
s(:begin, s(:def, :foo, s(:args), s(:send, nil, :raise)), memoize_node)
),
Mutant::Mutation::Evil.new(
object,
s(:begin,
s(:def, :foo, s(:args), s(:zsuper)), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))
s(:begin, s(:def, :foo, s(:args), s(:zsuper)), memoize_node)
),
Mutant::Mutation::Evil.new(
object,
s(:begin,
s(:def, :foo, s(:args), nil), s(:send, nil, :memoize, s(:args, s(:sym, :foo))))
s(:begin, s(:def, :foo, s(:args), nil), memoize_node)
)
]
end

it { should eql(expected) }
let(:memoize_node) do
s(:send, nil, :memoize, s(:args, s(:sym, :foo), *options_node))
end

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 a2bf9df

Please sign in to comment.