Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix memoized method subjects to preserve freezer option #973

Merged
merged 5 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snusnu The fix for the last alive mutation was removing this overly specific overwrite!

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