memorb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +52 -0
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +7 -0
- data/README.md +258 -0
- data/Rakefile +4 -0
- data/lib/memorb.rb +34 -0
- data/lib/memorb/agent.rb +30 -0
- data/lib/memorb/errors.rb +11 -0
- data/lib/memorb/integration.rb +277 -0
- data/lib/memorb/integrator_class_methods.rb +44 -0
- data/lib/memorb/key_value_store.rb +88 -0
- data/lib/memorb/method_identifier.rb +33 -0
- data/lib/memorb/ruby_compatibility.rb +55 -0
- data/lib/memorb/version.rb +5 -0
- data/memorb.gemspec +27 -0
- data/spec/memorb/agent_spec.rb +45 -0
- data/spec/memorb/integration_spec.rb +529 -0
- data/spec/memorb/integrator_class_methods_spec.rb +142 -0
- data/spec/memorb/key_value_store_spec.rb +91 -0
- data/spec/memorb/method_identifier_spec.rb +84 -0
- data/spec/memorb_spec.rb +198 -0
- data/spec/spec_helper.rb +69 -0
- metadata +111 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Memorb
|
4
|
+
module IntegratorClassMethods
|
5
|
+
|
6
|
+
def memorb
|
7
|
+
Integration[self]
|
8
|
+
end
|
9
|
+
|
10
|
+
def memorb!(*args, &block)
|
11
|
+
memorb.register(*args, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def inherited(child)
|
17
|
+
super.tap do
|
18
|
+
Integration.integrate_with!(child)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_added(name)
|
23
|
+
super.tap do
|
24
|
+
memorb.register(name) if memorb.auto_register?
|
25
|
+
memorb.enable(name)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_removed(name)
|
30
|
+
super.tap do
|
31
|
+
memorb.disable(name)
|
32
|
+
memorb.purge(name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_undefined(name)
|
37
|
+
super.tap do
|
38
|
+
memorb.disable(name)
|
39
|
+
memorb.purge(name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
|
5
|
+
module Memorb
|
6
|
+
class KeyValueStore
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@data = ::Hash.new
|
10
|
+
@lock = ::Concurrent::ReentrantReadWriteLock.new
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :lock
|
14
|
+
|
15
|
+
def write(key, value)
|
16
|
+
@lock.with_write_lock { _write(key, value) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def read(key)
|
20
|
+
@lock.with_read_lock { _read(key) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def has?(key)
|
24
|
+
@lock.with_read_lock { _has?(key) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch(key, &fallback)
|
28
|
+
@lock.with_read_lock do
|
29
|
+
return _read(key) if _has?(key)
|
30
|
+
end
|
31
|
+
# Concurrent readers could all see no entry if none were able to
|
32
|
+
# write before the others checked for the key, so they need to be
|
33
|
+
# synchronized below to ensure that only one of them actually
|
34
|
+
# executes the fallback block and writes the resulting value.
|
35
|
+
@lock.with_write_lock do
|
36
|
+
# The first thread to acquire the write lock will write the value
|
37
|
+
# for the key causing the other aforementioned threads that may
|
38
|
+
# also want to write to now see the key and return it.
|
39
|
+
_has?(key) ? _read(key) : _write(key, fallback.call)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def forget(key)
|
44
|
+
@lock.with_write_lock { _forget(key) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset!
|
48
|
+
@lock.with_write_lock { _reset! }
|
49
|
+
end
|
50
|
+
|
51
|
+
def keys
|
52
|
+
@lock.with_read_lock { _keys }
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"#<#{ self.class.name } keys=#{ keys.inspect }>"
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def _write(key, value)
|
62
|
+
@data[key] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
def _read(key)
|
66
|
+
@data[key]
|
67
|
+
end
|
68
|
+
|
69
|
+
def _has?(key)
|
70
|
+
@data.include?(key)
|
71
|
+
end
|
72
|
+
|
73
|
+
def _forget(key)
|
74
|
+
@data.delete(key)
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def _reset!
|
79
|
+
@data.clear
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def _keys
|
84
|
+
@data.keys
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Memorb
|
4
|
+
class MethodIdentifier
|
5
|
+
|
6
|
+
def initialize(method_name)
|
7
|
+
@method_name = method_name.to_sym
|
8
|
+
end
|
9
|
+
|
10
|
+
def hash
|
11
|
+
self.class.hash ^ method_name.hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
hash === other.hash
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :eql?, :==
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
method_name.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_sym
|
25
|
+
method_name
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
attr_reader :method_name
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Memorb
|
4
|
+
# When Memorb code needs to be changed to accomodate older Ruby versions,
|
5
|
+
# that code should live here. This has a few advantages:
|
6
|
+
# - it will be clear that there was a need to change the code for Ruby
|
7
|
+
# compatibility reasons
|
8
|
+
# - when the Ruby versions that required the altered code will no longer
|
9
|
+
# be supported, locating the places to refactor is easy
|
10
|
+
# - the specific approach needed to accommodate older version of Ruby
|
11
|
+
# and any explanatory comments need only be defined in one place
|
12
|
+
module RubyCompatibility
|
13
|
+
class << self
|
14
|
+
|
15
|
+
# MRI < 2.5
|
16
|
+
# These methods are `private` and require the use of `send`.
|
17
|
+
%i[ define_method remove_method undef_method ].each do |m|
|
18
|
+
eval(<<~RUBY, binding, __FILE__, __LINE__ + 1)
|
19
|
+
def #{ m }(receiver, *args, &block)
|
20
|
+
receiver.send(__method__, *args, &block)
|
21
|
+
end
|
22
|
+
RUBY
|
23
|
+
end
|
24
|
+
|
25
|
+
# JRuby *
|
26
|
+
# JRuby doesn't work well with module singleton constants, so an
|
27
|
+
# instance variable is used instead.
|
28
|
+
def module_constant(receiver, key)
|
29
|
+
receiver.instance_variable_get(ivar_name(key))
|
30
|
+
end
|
31
|
+
def module_constant_set(receiver, key, value)
|
32
|
+
n = ivar_name(key)
|
33
|
+
if receiver.instance_variable_defined?(n)
|
34
|
+
raise "Memorb internal error! Reassignment of constant at #{ n }"
|
35
|
+
end
|
36
|
+
receiver.instance_variable_set(n, value)
|
37
|
+
end
|
38
|
+
|
39
|
+
# JRuby *
|
40
|
+
# JRuby does not yet support `receiver` on `NameError`.
|
41
|
+
# https://github.com/jruby/jruby/issues/5576
|
42
|
+
def name_error_matches(error, expected_name, expected_receiver)
|
43
|
+
return false unless error.name.to_s == expected_name.to_s
|
44
|
+
::RUBY_ENGINE == 'jruby' || error.receiver.equal?(expected_receiver)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def ivar_name(key)
|
50
|
+
:"@#{ key }"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/memorb.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../lib/memorb/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'memorb'
|
7
|
+
s.version = Memorb::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.license = 'MIT'
|
10
|
+
s.authors = ['Patrick Rebsch']
|
11
|
+
s.email = ['pjrebsch@gmail.com']
|
12
|
+
s.homepage = 'https://github.com/pjrebsch/memorb'
|
13
|
+
s.summary = 'Memoization made easy'
|
14
|
+
s.description = <<~TXT
|
15
|
+
Memorb makes instance method memoization easy to set up and use.
|
16
|
+
TXT
|
17
|
+
|
18
|
+
s.required_ruby_version = '~> 2.3'
|
19
|
+
s.required_rubygems_version = '>= 2.6'
|
20
|
+
|
21
|
+
s.add_development_dependency 'bundler', '~> 2.0'
|
22
|
+
s.add_development_dependency 'rspec', '~> 3.9'
|
23
|
+
s.add_dependency 'concurrent-ruby', '~> 1.1'
|
24
|
+
|
25
|
+
s.require_paths = ['lib']
|
26
|
+
s.files = `git ls-files`.split("\n")
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe ::Memorb::Agent do
|
4
|
+
let(:target) { ::SpecHelper.basic_target_class }
|
5
|
+
let(:integrator) { ::Class.new(target) { extend ::Memorb } }
|
6
|
+
let(:integrator_instance) { integrator.new }
|
7
|
+
subject { described_class.new(integrator_instance.object_id) }
|
8
|
+
|
9
|
+
describe '#id' do
|
10
|
+
it 'returns the value provided upon initialization' do
|
11
|
+
expect(subject.id).to equal(integrator_instance.object_id)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
describe '#method_store' do
|
15
|
+
it 'returns a key-values store' do
|
16
|
+
expect(subject.method_store).to be_an_instance_of(::Memorb::KeyValueStore)
|
17
|
+
end
|
18
|
+
context 'when called more than once' do
|
19
|
+
it 'returns the same store each time' do
|
20
|
+
store_1 = subject.method_store
|
21
|
+
store_2 = subject.method_store
|
22
|
+
expect(store_1).to be(store_2)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
describe '#value_store' do
|
27
|
+
it 'returns a key-values store' do
|
28
|
+
expect(subject.value_store).to be_an_instance_of(::Memorb::KeyValueStore)
|
29
|
+
end
|
30
|
+
context 'when called more than once' do
|
31
|
+
it 'returns the same store each time' do
|
32
|
+
store_1 = subject.value_store
|
33
|
+
store_2 = subject.value_store
|
34
|
+
expect(store_1).to be(store_2)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
describe '#fetch' do
|
39
|
+
it 'delegates to the value store' do
|
40
|
+
subject.fetch([:a, :b]) { :c }
|
41
|
+
value = subject.value_store.read([:a, :b].hash)
|
42
|
+
expect(value).to eq(:c)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,529 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe ::Memorb::Integration do
|
4
|
+
let(:target) { ::SpecHelper.basic_target_class }
|
5
|
+
|
6
|
+
describe '::integrate_with!' do
|
7
|
+
it 'returns the integration for the given class' do
|
8
|
+
result = described_class.integrate_with!(target)
|
9
|
+
expect(result).not_to be(nil)
|
10
|
+
end
|
11
|
+
context 'when called more than once for a given class' do
|
12
|
+
it 'returns the same integration every time' do
|
13
|
+
result1 = described_class.integrate_with!(target)
|
14
|
+
result2 = described_class.integrate_with!(target)
|
15
|
+
expect(result1).to be(result2)
|
16
|
+
end
|
17
|
+
it 'includes the integration with the integrator only once' do
|
18
|
+
described_class.integrate_with!(target)
|
19
|
+
integration = described_class.integrate_with!(target)
|
20
|
+
ancestors = target.ancestors
|
21
|
+
integrations = ancestors.select { |a| a.equal? integration }
|
22
|
+
expect(integrations).to contain_exactly(integration)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
context 'when given a regular object' do
|
26
|
+
it 'raises an error' do
|
27
|
+
obj = ::Object.new
|
28
|
+
error = ::Memorb::InvalidIntegrationError
|
29
|
+
error_message = 'integration target must be a class'
|
30
|
+
expect { obj.extend(::Memorb) }.to raise_error(error, error_message)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
describe '::integrated?' do
|
35
|
+
context 'when a given class has not integrated Memorb' do
|
36
|
+
it 'returns false' do
|
37
|
+
result = described_class.integrated?(target)
|
38
|
+
expect(result).to be(false)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
context 'when a given class has integrated with Memorb' do
|
42
|
+
it 'returns true' do
|
43
|
+
described_class.integrate_with!(target)
|
44
|
+
result = described_class.integrated?(target)
|
45
|
+
expect(result).to be(true)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
describe '::[]' do
|
50
|
+
it 'returns the integration for the given class' do
|
51
|
+
integration = described_class.integrate_with!(target)
|
52
|
+
result = described_class[target]
|
53
|
+
expect(result).to be(integration)
|
54
|
+
end
|
55
|
+
context 'when given a class that has not been called with integrate!' do
|
56
|
+
it 'returns nil' do
|
57
|
+
result = described_class[target]
|
58
|
+
expect(result).to be(nil)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'an integration' do
|
64
|
+
let(:integrator) { target.tap { |x| x.extend(::Memorb) } }
|
65
|
+
let(:integrator_singleton) { integrator.singleton_class }
|
66
|
+
let(:instance) { integrator.new }
|
67
|
+
let(:agent_registry) { subject.send(:_agents) }
|
68
|
+
subject { described_class[integrator] }
|
69
|
+
|
70
|
+
describe 'integrator instance methods' do
|
71
|
+
describe '#initialize' do
|
72
|
+
it 'retains the behavior of the instance' do
|
73
|
+
expect(instance.counter).to be(0)
|
74
|
+
end
|
75
|
+
it 'initializes the agent with the object ID of the instance' do
|
76
|
+
agent = instance.memorb
|
77
|
+
expect(agent.id).to equal(instance.object_id)
|
78
|
+
end
|
79
|
+
it 'adds the agent to the global registry' do
|
80
|
+
agent = instance.memorb
|
81
|
+
expect(agent_registry.keys).to contain_exactly(agent.id)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
describe '#memorb' do
|
85
|
+
it 'returns the agent for the instance' do
|
86
|
+
agent = instance.memorb
|
87
|
+
expect(agent).to be_an_instance_of(::Memorb::Agent)
|
88
|
+
end
|
89
|
+
it 'does not share the agent across instances' do
|
90
|
+
agent_1 = integrator.new.memorb
|
91
|
+
agent_2 = integrator.new.memorb
|
92
|
+
expect(agent_1).not_to equal(agent_2)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
describe '::integrator' do
|
97
|
+
it 'returns its integrating class' do
|
98
|
+
expect(subject.integrator).to be(integrator)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
describe '::register' do
|
102
|
+
let(:method_name) { :increment }
|
103
|
+
|
104
|
+
context 'when called with method names as arguments' do
|
105
|
+
it 'caches the registered method' do
|
106
|
+
subject.register(method_name)
|
107
|
+
result1 = instance.send(method_name)
|
108
|
+
result2 = instance.send(method_name)
|
109
|
+
expect(result1).to eq(result2)
|
110
|
+
end
|
111
|
+
it 'records the registration of the method' do
|
112
|
+
subject.register(method_name)
|
113
|
+
expect(subject.registered_methods).to include(method_name)
|
114
|
+
end
|
115
|
+
context 'when providing a method name as a string' do
|
116
|
+
it 'registers the given method' do
|
117
|
+
subject.register('a')
|
118
|
+
expect(subject.registered_methods).to include(:a)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
context 'when providing multiple method names' do
|
122
|
+
it 'registers each method' do
|
123
|
+
subject.register(:a, :b)
|
124
|
+
expect(subject.registered_methods).to include(:a, :b)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
context 'when providing arrays of method names' do
|
128
|
+
it 'registers all methods in those arrays' do
|
129
|
+
subject.register([:a, :b], [:c])
|
130
|
+
expect(subject.registered_methods).to include(:a, :b, :c)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
context 'when registering a method multiple times' do
|
134
|
+
before(:each) { 2.times { subject.register(method_name) } }
|
135
|
+
|
136
|
+
it 'still caches the registered method' do
|
137
|
+
result1 = instance.send(method_name)
|
138
|
+
result2 = instance.send(method_name)
|
139
|
+
expect(result1).to eq(result2)
|
140
|
+
end
|
141
|
+
it 'records registration of the method once' do
|
142
|
+
expect(subject.registered_methods).to contain_exactly(method_name)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
context 'when registering a method that does not exist' do
|
146
|
+
let(:target) { ::Class.new }
|
147
|
+
|
148
|
+
it 'still allows the method to be registered' do
|
149
|
+
subject.register(method_name)
|
150
|
+
expect(subject.registered_methods).to include(method_name)
|
151
|
+
end
|
152
|
+
it 'does not enable the method' do
|
153
|
+
subject.register(method_name)
|
154
|
+
expect(subject.enabled_methods).not_to include(method_name)
|
155
|
+
end
|
156
|
+
it 'an integrator instance does not respond to the method' do
|
157
|
+
subject.register(method_name)
|
158
|
+
expect(instance).not_to respond_to(method_name)
|
159
|
+
end
|
160
|
+
it 'raises an error when trying to call it' do
|
161
|
+
subject.register(method_name)
|
162
|
+
expect { instance.send(method_name) }.to raise_error(::NoMethodError)
|
163
|
+
end
|
164
|
+
context 'once the method is defined' do
|
165
|
+
it 'responds to the the method' do
|
166
|
+
subject.register(method_name)
|
167
|
+
::Memorb::RubyCompatibility
|
168
|
+
.define_method(integrator, method_name) { nil }
|
169
|
+
expect(instance).to respond_to(method_name)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
context 'when called with only a block' do
|
175
|
+
it 'adds the methods defined in the block to the integrator' do
|
176
|
+
subject.register do
|
177
|
+
def method_1; end
|
178
|
+
def method_2; end
|
179
|
+
end
|
180
|
+
methods = integrator.public_instance_methods(false)
|
181
|
+
expect(methods).to include(:method_1, :method_2)
|
182
|
+
end
|
183
|
+
it 'registers and enables the methods defined in that block' do
|
184
|
+
subject.register do
|
185
|
+
def method_1; end
|
186
|
+
def method_2; end
|
187
|
+
end
|
188
|
+
expect(subject.registered_methods).to include(:method_1, :method_2)
|
189
|
+
expect(subject.enabled_methods).to include(:method_1, :method_2)
|
190
|
+
end
|
191
|
+
context 'when an error is raised in the provided block' do
|
192
|
+
it 'still disables automatic registration' do
|
193
|
+
begin
|
194
|
+
subject.register { raise }
|
195
|
+
rescue ::RuntimeError
|
196
|
+
end
|
197
|
+
expect(subject.auto_register?).to be(false)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
context 'when called with arguments and a block' do
|
202
|
+
it 'raises an error' do
|
203
|
+
expect {
|
204
|
+
subject.register(:method_1) { nil }
|
205
|
+
}.to raise_error(::ArgumentError)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
context 'when called without arguments or a block' do
|
209
|
+
it 'raises an error' do
|
210
|
+
expect { subject.register }.to raise_error(::ArgumentError)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
describe '::registered?' do
|
215
|
+
it 'preserves the visibility of the method that it overrides' do
|
216
|
+
visibilities = [:public, :protected, :private]
|
217
|
+
method_names = visibilities.map { |vis| :"#{ vis }_method" }
|
218
|
+
|
219
|
+
integrator = ::Class.new.tap do |target|
|
220
|
+
eval_string = visibilities.map.with_index do |vis, i|
|
221
|
+
"#{ vis }; def #{ method_names[i] }; end;"
|
222
|
+
end.join("\n")
|
223
|
+
target.class_eval(eval_string)
|
224
|
+
target.extend(::Memorb)
|
225
|
+
end
|
226
|
+
|
227
|
+
subject = described_class[integrator]
|
228
|
+
|
229
|
+
method_names.each do |m|
|
230
|
+
subject.register(m)
|
231
|
+
end
|
232
|
+
|
233
|
+
visibilities.each.with_index do |vis, i|
|
234
|
+
overrides = subject.send(:"#{ vis }_instance_methods", false)
|
235
|
+
expect(overrides).to include(method_names[i])
|
236
|
+
other_methods = method_names.reject { |m| m === method_names[i] }
|
237
|
+
expect(overrides).not_to include(*other_methods)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
::SpecHelper.test_method_name(:increment) do |method_name, provided_name|
|
241
|
+
context 'when the named method is registered' do
|
242
|
+
it 'returns true' do
|
243
|
+
subject.register(method_name)
|
244
|
+
result = subject.registered?(provided_name)
|
245
|
+
expect(result).to be(true)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
context 'when the named method is not registered' do
|
249
|
+
it 'returns false' do
|
250
|
+
result = subject.registered?(provided_name)
|
251
|
+
expect(result).to be(false)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
describe '::enable' do
|
257
|
+
::SpecHelper.test_method_name(:increment) do |method_name, provided_name|
|
258
|
+
it 'records the cache key correctly' do
|
259
|
+
method_id = ::Memorb::MethodIdentifier.new(provided_name)
|
260
|
+
subject.register(method_name)
|
261
|
+
instance.send(method_name)
|
262
|
+
store = instance.memorb.method_store
|
263
|
+
expect(store.keys).to contain_exactly(method_id)
|
264
|
+
end
|
265
|
+
context 'when the method is registered' do
|
266
|
+
it 'overrides the method' do
|
267
|
+
subject.register(method_name)
|
268
|
+
subject.disable(method_name)
|
269
|
+
subject.enable(provided_name)
|
270
|
+
expect(subject.enabled_methods).to include(method_name)
|
271
|
+
end
|
272
|
+
it 'returns the visibility of the method' do
|
273
|
+
subject.register(method_name)
|
274
|
+
result = subject.enable(provided_name)
|
275
|
+
expect(result).to be(:public)
|
276
|
+
end
|
277
|
+
context 'when the method is not defined' do
|
278
|
+
let(:target) { ::Class.new }
|
279
|
+
|
280
|
+
it 'does not override the method' do
|
281
|
+
subject.register(method_name)
|
282
|
+
subject.enable(provided_name)
|
283
|
+
expect(subject.enabled_methods).not_to include(method_name)
|
284
|
+
end
|
285
|
+
it 'returns nil' do
|
286
|
+
subject.register(method_name)
|
287
|
+
result = subject.enable(provided_name)
|
288
|
+
expect(result).to be(nil)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
context 'when the method is not registered' do
|
293
|
+
it 'does not override the method' do
|
294
|
+
subject.enable(provided_name)
|
295
|
+
expect(subject.enabled_methods).not_to include(method_name)
|
296
|
+
end
|
297
|
+
it 'returns nil' do
|
298
|
+
result = subject.enable(provided_name)
|
299
|
+
expect(result).to be(nil)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
describe '::disable' do
|
305
|
+
::SpecHelper.test_method_name(:increment) do |method_name, provided_name|
|
306
|
+
it 'removes the override method for the given method' do
|
307
|
+
subject.register(method_name)
|
308
|
+
subject.disable(provided_name)
|
309
|
+
expect(subject.enabled_methods).not_to include(method_name)
|
310
|
+
end
|
311
|
+
context 'when there is no override method defined' do
|
312
|
+
let(:target) { ::Class.new }
|
313
|
+
|
314
|
+
it 'does not raise an error' do
|
315
|
+
expect { subject.disable(provided_name) }.not_to raise_error
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
describe '::registered_methods' do
|
321
|
+
it 'returns an array of registered methods' do
|
322
|
+
methods = [:increment, :decrement]
|
323
|
+
methods.each { |m| subject.register(m) }
|
324
|
+
expect(subject.registered_methods).to match_array(methods)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
describe '::enabled_methods' do
|
328
|
+
it 'returns an array of enabled methods' do
|
329
|
+
methods = [:increment, :double]
|
330
|
+
methods.each { |m| subject.register(m) }
|
331
|
+
expect(subject.enabled_methods).to match_array(methods)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
describe '::disabled_methods' do
|
335
|
+
it 'returns an array of methods that are not enabled' do
|
336
|
+
methods = [:a, :increment, :b, :double, :c]
|
337
|
+
methods.each { |m| subject.register(m) }
|
338
|
+
expect(subject.disabled_methods).to contain_exactly(:a, :b, :c)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
describe '::enabled?' do
|
342
|
+
::SpecHelper.test_method_name(:increment) do |method_name, provided_name|
|
343
|
+
context 'when the named method is enabled' do
|
344
|
+
it 'returns true' do
|
345
|
+
subject.register(method_name)
|
346
|
+
result = subject.enabled?(provided_name)
|
347
|
+
expect(result).to be(true)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
context 'when the named method is not enabled' do
|
351
|
+
it 'returns false' do
|
352
|
+
result = subject.enabled?(provided_name)
|
353
|
+
expect(result).to be(false)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
describe '::purge' do
|
359
|
+
let(:method_name) { :increment }
|
360
|
+
let(:method_id) { ::Memorb::MethodIdentifier.new(method_name) }
|
361
|
+
|
362
|
+
it 'clears cached data for the given method in all instances' do
|
363
|
+
subject.register(method_name)
|
364
|
+
instance.send(method_name)
|
365
|
+
store = instance.memorb.method_store.read(method_id)
|
366
|
+
expect(store.keys).not_to be_empty
|
367
|
+
subject.purge(method_name)
|
368
|
+
expect(store.keys).to be_empty
|
369
|
+
end
|
370
|
+
context 'when the given method has no cache record' do
|
371
|
+
it 'does not raise an error' do
|
372
|
+
subject.register(method_name)
|
373
|
+
store = instance.memorb.method_store.read(method_id)
|
374
|
+
expect(store).to be(nil)
|
375
|
+
expect { subject.purge(method_name) }.not_to raise_error
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
describe '::auto_register?' do
|
380
|
+
context 'by default' do
|
381
|
+
it 'returns false' do
|
382
|
+
expect(subject.auto_register?).to be(false)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
context 'when turned on' do
|
386
|
+
it 'returns true' do
|
387
|
+
subject.send(:_auto_registration).increment
|
388
|
+
expect(subject.auto_register?).to be(true)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
describe '::auto_register!' do
|
393
|
+
context 'when not given a block' do
|
394
|
+
it 'raises an error' do
|
395
|
+
expect {
|
396
|
+
subject.auto_register!
|
397
|
+
}.to raise_error(::ArgumentError, 'a block must be provided')
|
398
|
+
end
|
399
|
+
end
|
400
|
+
it 'enables automatic registration of methods defined in the block' do
|
401
|
+
subject.auto_register! do
|
402
|
+
::Memorb::RubyCompatibility.define_method(integrator, :a) { nil }
|
403
|
+
end
|
404
|
+
expect(subject.registered_methods).to include(:a)
|
405
|
+
end
|
406
|
+
it 'returns the return value of the given block' do
|
407
|
+
result = subject.auto_register! { 1 }
|
408
|
+
expect(result).to be(1)
|
409
|
+
end
|
410
|
+
context 'when an error is raised in the given block' do
|
411
|
+
it 'still disables automatic registration' do
|
412
|
+
begin
|
413
|
+
subject.auto_register! { raise }
|
414
|
+
rescue ::RuntimeError
|
415
|
+
end
|
416
|
+
expect(subject.auto_register?).to be(false)
|
417
|
+
end
|
418
|
+
it 'returns nil' do
|
419
|
+
begin
|
420
|
+
result = subject.auto_register! { raise }
|
421
|
+
rescue ::RuntimeError
|
422
|
+
end
|
423
|
+
expect(result).to be(nil)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
context 'when nested' do
|
427
|
+
it 'preserves the setting until the outer block ends' do
|
428
|
+
subject.auto_register! do
|
429
|
+
subject.auto_register! do
|
430
|
+
nil
|
431
|
+
end
|
432
|
+
expect(subject.auto_register?).to be(true)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
context 'if the internal counter goes below zero' do
|
437
|
+
it 'be corrected on subsequent calls' do
|
438
|
+
subject.send(:_auto_registration).decrement
|
439
|
+
subject.auto_register! do
|
440
|
+
expect(subject.auto_register?).to be(true)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
describe '::name' do
|
446
|
+
it 'includes the name of the integrating class' do
|
447
|
+
name = 'IntegratingKlass'
|
448
|
+
expectation = "Memorb:#{ name }"
|
449
|
+
::Memorb::RubyCompatibility
|
450
|
+
.define_method(integrator_singleton, :name) { name }
|
451
|
+
expect(subject.name).to eq(expectation)
|
452
|
+
end
|
453
|
+
context 'when integrating class does not have a name' do
|
454
|
+
it 'uses the inspection of the integrating class' do
|
455
|
+
expectation = "Memorb:#{ integrator.inspect }"
|
456
|
+
::Memorb::RubyCompatibility
|
457
|
+
.define_method(integrator_singleton, :name) { nil }
|
458
|
+
expect(subject.name).to eq(expectation)
|
459
|
+
::Memorb::RubyCompatibility.undef_method(integrator_singleton, :name)
|
460
|
+
expect(subject.name).to eq(expectation)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
context 'when integrating class does not have an inspection' do
|
464
|
+
it 'uses the object ID of the integrating class' do
|
465
|
+
expectation = "Memorb:#{ integrator.object_id }"
|
466
|
+
::Memorb::RubyCompatibility
|
467
|
+
.define_method(integrator_singleton, :inspect) { nil }
|
468
|
+
expect(subject.name).to eq(expectation)
|
469
|
+
::Memorb::RubyCompatibility.undef_method(integrator_singleton, :inspect)
|
470
|
+
expect(subject.name).to eq(expectation)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
describe '::create_agent' do
|
475
|
+
it 'returns a agent object' do
|
476
|
+
agent = subject.create_agent(instance)
|
477
|
+
expect(agent).to be_an_instance_of(::Memorb::Agent)
|
478
|
+
end
|
479
|
+
it 'writes the agent to the global agent registry' do
|
480
|
+
agent = subject.create_agent(instance)
|
481
|
+
registry = subject.send(:_agents)
|
482
|
+
expect(registry.keys).to contain_exactly(agent.id)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
it 'supports regularly invalid method names' do
|
486
|
+
invalid_starting_chars = [0x00..0x40, 0x5b..0x60, 0x7b..0xff]
|
487
|
+
method_name = invalid_starting_chars
|
488
|
+
.map(&:to_a)
|
489
|
+
.flatten
|
490
|
+
.map(&:chr)
|
491
|
+
.shuffle(random: ::SpecHelper.prng)
|
492
|
+
.join
|
493
|
+
.to_sym
|
494
|
+
subject.register(method_name)
|
495
|
+
::Memorb::RubyCompatibility
|
496
|
+
.define_method(integrator, method_name) { nil }
|
497
|
+
expect(subject.registered_methods).to include(method_name)
|
498
|
+
expect(subject.enabled_methods).to include(method_name)
|
499
|
+
expect { instance.send(method_name) }.not_to raise_error
|
500
|
+
end
|
501
|
+
context 'when prepending on another class' do
|
502
|
+
it 'raises an error' do
|
503
|
+
klass = ::Class.new.singleton_class
|
504
|
+
error = ::Memorb::MismatchedTargetError
|
505
|
+
expect { klass.prepend(subject) }.to raise_error(error)
|
506
|
+
end
|
507
|
+
end
|
508
|
+
context 'when including with any class' do
|
509
|
+
it 'raises an error' do
|
510
|
+
klass = subject.integrator
|
511
|
+
error = ::Memorb::InvalidIntegrationError
|
512
|
+
error_message = 'an integration must be applied with `prepend`, not `include`'
|
513
|
+
expect { klass.include(subject) }.to raise_error(error, error_message)
|
514
|
+
end
|
515
|
+
end
|
516
|
+
::SpecHelper.for_testing_garbage_collection do
|
517
|
+
context 'when freed by the garbage collector' do
|
518
|
+
it 'removes its agent from the global registry' do
|
519
|
+
ref = ::WeakRef.new(integrator.new)
|
520
|
+
agent = ref.__getobj__.memorb
|
521
|
+
expect(agent_registry.keys).to include(agent.id)
|
522
|
+
::SpecHelper.force_garbage_collection
|
523
|
+
expect(ref.weakref_alive?).to be_falsey
|
524
|
+
expect(agent_registry.keys).to be_empty
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|