debian-mirror-gitlab/spec/lib/gitlab/utils/strong_memoize_spec.rb
2023-06-20 00:43:36 +05:30

374 lines
10 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-benchmark'
require 'rspec-parameterized'
require 'active_support/testing/time_helpers'
RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end
RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
include ActiveSupport::Testing::TimeHelpers
let(:klass) do
strong_memoize_class = described_class
Struct.new(:value) do
include strong_memoize_class
def self.method_added_list
@method_added_list ||= []
end
def self.method_added(name)
method_added_list << name
end
def method_name
strong_memoize(:method_name) do # rubocop: disable Gitlab/StrongMemoizeAttr
trace << value
value
end
end
def method_name_with_expiration
strong_memoize_with_expiration(:method_name_with_expiration, 1) do
trace << value
value
end
end
def method_name_attr
trace << value
value
end
strong_memoize_attr :method_name_attr
def enabled?
trace << value
value
end
strong_memoize_attr :enabled?
def method_name_with_args(*args)
strong_memoize_with(:method_name_with_args, args) do
trace << [value, args]
value
end
end
def trace
@trace ||= []
end
protected
def private_method; end
private :private_method
strong_memoize_attr :private_method
public
def protected_method; end
protected :protected_method
strong_memoize_attr :protected_method
private
def public_method; end
public :public_method
strong_memoize_attr :public_method
end
end
subject(:object) { klass.new(value) }
shared_examples 'caching the value' do
let(:member_name) { described_class.normalize_key(method_name) }
it 'only calls the block once' do
value0 = object.send(method_name)
value1 = object.send(method_name)
expect(value0).to eq(value)
expect(value1).to eq(value)
expect(object.trace).to contain_exactly(value)
end
it 'returns and defines the instance variable for the exact value' do
returned_value = object.send(method_name)
memoized_value = object.instance_variable_get(:"@#{member_name}")
expect(returned_value).to eql(value)
expect(memoized_value).to eql(value)
end
end
describe '#strong_memoize' do
[nil, false, true, 'value', 0, [0]].each do |value|
context "with value #{value}" do
let(:value) { value }
let(:method_name) { :method_name }
it_behaves_like 'caching the value'
it 'raises exception for invalid type as key' do
expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/
end
it 'raises exception for invalid characters in key' do
expect { object.strong_memoize(:enabled?) { 20 } }
.to raise_error /is not allowed as an instance variable name/
end
end
end
context "memory allocation", type: :benchmark do
let(:value) { 'aaa' }
before do
object.method_name # warmup
end
[:method_name, "method_name"].each do |argument|
context "for #{argument.class}" do
it 'does allocate exactly one string when fetching value' do
expect do
object.strong_memoize(argument) { 10 }
end.to perform_allocation(1)
end
it 'does allocate exactly one string when storing value' do
object.clear_memoization(:method_name) # clear to force set
expect do
object.strong_memoize(argument) { 10 }
end.to perform_allocation(1)
end
end
end
end
end
describe '#strong_memoize_with_expiration' do
[nil, false, true, 'value', 0, [0]].each do |value|
context "with value #{value}" do
let(:value) { value }
let(:method_name) { :method_name_with_expiration }
it_behaves_like 'caching the value'
it 'raises exception for invalid type as key' do
expect { object.strong_memoize_with_expiration(10, 1) { 20 } }.to raise_error /Invalid type of '10'/
end
it 'raises exception for invalid characters in key' do
expect { object.strong_memoize_with_expiration(:enabled?, 1) { 20 } }
.to raise_error /is not allowed as an instance variable name/
end
end
end
context 'value memoization test' do
let(:value) { 'value' }
it 'caches the value for specified number of seconds' do
object.method_name_with_expiration
object.method_name_with_expiration
expect(object.trace.count).to eq(1)
travel_to(Time.current + 2.seconds) do
object.method_name_with_expiration
expect(object.trace.count).to eq(2)
end
end
end
end
describe '#strong_memoize_with' do
[nil, false, true, 'value', 0, [0]].each do |value|
context "with value #{value}" do
let(:value) { value }
it 'only calls the block once' do
value0 = object.method_name_with_args(1)
value1 = object.method_name_with_args(1)
value2 = object.method_name_with_args([2, 3])
value3 = object.method_name_with_args([2, 3])
expect(value0).to eq(value)
expect(value1).to eq(value)
expect(value2).to eq(value)
expect(value3).to eq(value)
expect(object.trace).to contain_exactly([value, [1]], [value, [[2, 3]]])
end
it 'returns and defines the instance variable for the exact value' do
returned_value = object.method_name_with_args(1, 2, 3)
memoized_value = object.instance_variable_get(:@method_name_with_args)
expect(returned_value).to eql(value)
expect(memoized_value).to eql({ [[1, 2, 3]] => value })
end
end
end
end
describe '#strong_memoized?' do
shared_examples 'memoization check' do |method_name|
context "for #{method_name}" do
let(:value) { :anything }
subject { object.strong_memoized?(method_name) }
it 'returns false if the value is uncached' do
is_expected.to be(false)
end
it 'returns true if the value is cached' do
object.public_send(method_name)
is_expected.to be(true)
end
end
end
it_behaves_like 'memoization check', :method_name
it_behaves_like 'memoization check', :enabled?
end
describe '#clear_memoization' do
shared_examples 'clearing memoization' do |method_name|
let(:member_name) { described_class.normalize_key(method_name) }
let(:value) { 'mepmep' }
it 'removes the instance variable' do
object.public_send(method_name)
object.clear_memoization(method_name)
expect(object.instance_variable_defined?(:"@#{member_name}")).to be(false)
end
end
it_behaves_like 'clearing memoization', :method_name
it_behaves_like 'clearing memoization', :enabled?
end
describe '.strong_memoize_attr' do
[nil, false, true, 'value', 0, [0]].each do |value|
context "with value '#{value}'" do
let(:value) { value }
context 'memoized after method definition' do
let(:method_name) { :method_name_attr }
it_behaves_like 'caching the value'
it 'calls the existing .method_added' do
expect(klass.method_added_list).to include(:method_name_attr)
end
it 'retains method arity' do
expect(klass.instance_method(method_name).arity).to eq(0)
end
end
end
end
describe 'method visibility' do
it 'sets private visibility' do
expect(klass.private_instance_methods).to include(:private_method)
expect(klass.protected_instance_methods).not_to include(:private_method)
expect(klass.public_instance_methods).not_to include(:private_method)
end
it 'sets protected visibility' do
expect(klass.private_instance_methods).not_to include(:protected_method)
expect(klass.protected_instance_methods).to include(:protected_method)
expect(klass.public_instance_methods).not_to include(:protected_method)
end
it 'sets public visibility' do
expect(klass.private_instance_methods).not_to include(:public_method)
expect(klass.protected_instance_methods).not_to include(:public_method)
expect(klass.public_instance_methods).to include(:public_method)
end
end
context "when method doesn't exist" do
let(:klass) do
strong_memoize_class = described_class
Struct.new(:value) do
include strong_memoize_class
end
end
subject { klass.strong_memoize_attr(:nonexistent_method) }
it 'fails when strong-memoizing a nonexistent method' do
expect { subject }.to raise_error(NameError, %r{undefined method `nonexistent_method' for class})
end
end
context 'when memoized method has parameters' do
it 'raises an error' do
expected_message = /Using `strong_memoize_attr` on methods with parameters is not supported/
expect do
strong_memoize_class = described_class
Class.new do
include strong_memoize_class
def method_with_parameters(params); end
strong_memoize_attr :method_with_parameters
end
end.to raise_error(RuntimeError, expected_message)
end
end
end
describe '.normalize_key' do
using RSpec::Parameterized::TableSyntax
subject { described_class.normalize_key(input) }
where(:input, :output, :valid) do
:key | :key | true
"key" | "key" | true
:key? | "key" | true
"key?" | "key" | true
:key! | "key" | true
"key!" | "key" | true
# invalid cases caught elsewhere
:"ke?y" | :"ke?y" | false
"ke?y" | "ke?y" | false
:"ke!y" | :"ke!y" | false
"ke!y" | "ke!y" | false
end
with_them do
let(:ivar) { "@#{output}" }
it { is_expected.to eq(output) }
if params[:valid]
it 'is a valid ivar name' do
expect { instance_variable_defined?(ivar) }.not_to raise_error
end
else
it 'raises a NameError error' do
expect { instance_variable_defined?(ivar) }
.to raise_error(NameError, /not allowed as an instance/)
end
end
end
end
end