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.
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ::Memorb::IntegratorClassMethods do
4
+ let(:integrator) { ::Class.new { extend ::Memorb } }
5
+ let(:integration) { ::Memorb::Integration[integrator] }
6
+ let(:instance) { integrator.new }
7
+
8
+ describe '::memorb' do
9
+ it 'returns the integration for the integrator' do
10
+ expect(integrator.memorb).to be(integration)
11
+ end
12
+ end
13
+ describe '::memorb!' do
14
+ it 'calls register with the same arguments' do
15
+ spy = double('spy', register: nil)
16
+ mod = Module.new
17
+ ::Memorb::RubyCompatibility.define_method(mod, :memorb) { spy }
18
+ integrator.singleton_class.prepend(mod)
19
+ block = Proc.new { nil }
20
+ expect(spy).to receive(:register).with(:a, &block)
21
+ integrator.memorb!(:a, &block)
22
+ end
23
+ end
24
+ describe '::inherited' do
25
+ it 'makes children of integrators get their own integration' do
26
+ child_integrator = ::Class.new(integrator)
27
+ integration = ::Memorb::Integration[child_integrator]
28
+ expect(integration).not_to be(nil)
29
+ expected_ancestry = [integration, child_integrator]
30
+ expect(child_integrator.ancestors).to start_with(*expected_ancestry)
31
+ end
32
+ end
33
+ describe '::method_added' do
34
+ let(:method_name) { :method_1 }
35
+
36
+ it 'retains upstream behavior' do
37
+ spy = double('spy', spy!: nil)
38
+ ::Memorb::RubyCompatibility
39
+ .define_method(integrator.singleton_class, :method_added) do |m|
40
+ spy.spy!(m)
41
+ end
42
+ expect(spy).to receive(:spy!).with(method_name)
43
+ ::Memorb::RubyCompatibility
44
+ .define_method(integrator, method_name) { nil }
45
+ end
46
+ context 'when the method has been registered' do
47
+ it 'enables the method' do
48
+ integration.register(method_name)
49
+ expect(integration.enabled_methods).not_to include(method_name)
50
+ expect(integration.public_instance_methods).not_to include(method_name)
51
+ ::Memorb::RubyCompatibility
52
+ .define_method(integrator, method_name) { nil }
53
+ expect(integration.enabled_methods).to include(method_name)
54
+ expect(integration.public_instance_methods).to include(method_name)
55
+ end
56
+ end
57
+ context 'when automatic registration is enabled' do
58
+ it 'registers and enables new methods' do
59
+ integration.send(:_auto_registration).increment
60
+ ::Memorb::RubyCompatibility
61
+ .define_method(integrator, method_name) { nil }
62
+ expect(integration.registered_methods).to include(method_name)
63
+ expect(integration.enabled_methods).to include(method_name)
64
+ expect(integration.public_instance_methods).to include(method_name)
65
+ end
66
+ end
67
+ end
68
+ describe '::method_removed' do
69
+ let(:method_name) { :method_1 }
70
+ let(:method_id) { ::Memorb::MethodIdentifier.new(method_name) }
71
+
72
+ it 'retains upstream behavior' do
73
+ ::Memorb::RubyCompatibility
74
+ .define_method(integrator, method_name) { nil }
75
+ spy = double('spy', spy!: nil)
76
+ ::Memorb::RubyCompatibility
77
+ .define_method(integrator.singleton_class, :method_removed) do |m|
78
+ spy.spy!(m)
79
+ end
80
+ expect(spy).to receive(:spy!).with(method_name)
81
+ ::Memorb::RubyCompatibility.remove_method(integrator, method_name)
82
+ end
83
+ it 'removes the override for the method' do
84
+ ::Memorb::RubyCompatibility
85
+ .define_method(integrator, method_name) { nil }
86
+ integration.register(method_name)
87
+ expect(integration.enabled_methods).to include(method_name)
88
+ expect(integration.public_instance_methods).to include(method_name)
89
+ ::Memorb::RubyCompatibility.remove_method(integrator, method_name)
90
+ expect(integration.enabled_methods).not_to include(method_name)
91
+ expect(integration.public_instance_methods).not_to include(method_name)
92
+ end
93
+ it 'clears cached data for the method in all instances' do
94
+ ::Memorb::RubyCompatibility
95
+ .define_method(integrator, method_name) { nil }
96
+ integration.register(method_name)
97
+ instance.send(method_name)
98
+ store = instance.memorb.method_store.read(method_id)
99
+ expect(store.keys).not_to be_empty
100
+ ::Memorb::RubyCompatibility.remove_method(integrator, method_name)
101
+ expect(store.keys).to be_empty
102
+ end
103
+ end
104
+ describe '::method_undefined' do
105
+ let(:method_name) { :method_1 }
106
+ let(:method_id) { ::Memorb::MethodIdentifier.new(method_name) }
107
+
108
+ it 'retains upstream behavior' do
109
+ ::Memorb::RubyCompatibility
110
+ .define_method(integrator, method_name) { nil }
111
+ spy = double('spy', spy!: nil)
112
+ ::Memorb::RubyCompatibility
113
+ .define_method(integrator.singleton_class, :method_undefined) do |m|
114
+ spy.spy!(m)
115
+ end
116
+ expect(spy).to receive(:spy!).with(method_name)
117
+ ::Memorb::RubyCompatibility.undef_method(integrator, method_name)
118
+ end
119
+ it 'undefines the override for the method' do
120
+ ::Memorb::RubyCompatibility
121
+ .define_method(integrator, method_name) { nil }
122
+ integration.register(method_name)
123
+ ::Memorb::RubyCompatibility.undef_method(integrator, method_name)
124
+ instance = integrator.new
125
+ expect(integration.enabled_methods).not_to include(method_name)
126
+ expect(integration.public_instance_methods).not_to include(method_name)
127
+ expect {
128
+ instance.send(method_name)
129
+ }.to raise_error(::NoMethodError, /undefined method/)
130
+ end
131
+ it 'clears cached data for the method in all instances' do
132
+ ::Memorb::RubyCompatibility
133
+ .define_method(integrator, method_name) { nil }
134
+ integration.register(method_name)
135
+ instance.send(method_name)
136
+ store = instance.memorb.method_store.read(method_id)
137
+ expect(store.keys).not_to be_empty
138
+ ::Memorb::RubyCompatibility.undef_method(integrator, method_name)
139
+ expect(store.keys).to be_empty
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ::Memorb::KeyValueStore do
4
+ let(:key) { :key }
5
+ let(:value) { 'value' }
6
+
7
+ describe '#write' do
8
+ it 'stores a value' do
9
+ subject.write(key, value)
10
+ data = subject.instance_variable_get(:@data)
11
+ expect(data).to include(key => value)
12
+ end
13
+ it 'returns the stored value' do
14
+ result = subject.write(key, value)
15
+ expect(result).to equal(value)
16
+ end
17
+ end
18
+ describe '#read' do
19
+ it 'retrieves a value for a given key' do
20
+ data = subject.instance_variable_get(:@data)
21
+ data[key] = value
22
+ result = subject.read(key)
23
+ expect(result).to equal(value)
24
+ end
25
+ context 'when a value has not been set' do
26
+ it 'returns nil' do
27
+ result = subject.read(key)
28
+ expect(result).to eq(nil)
29
+ end
30
+ end
31
+ end
32
+ describe '#has?' do
33
+ context 'when a value has not been set' do
34
+ it 'return false' do
35
+ result = subject.has?(:key)
36
+ expect(result).to eq(false)
37
+ end
38
+ end
39
+ context 'when a value has been set' do
40
+ it 'returns true' do
41
+ data = subject.instance_variable_get(:@data)
42
+ data[key] = value
43
+ result = subject.has?(:key)
44
+ expect(result).to be(true)
45
+ end
46
+ end
47
+ end
48
+ describe '#fetch' do
49
+ context 'when a value has not been set' do
50
+ it 'stores and returns the result of the block' do
51
+ result = subject.fetch(key) { value }
52
+ expect(result).to equal(value)
53
+ data = subject.instance_variable_get(:@data)
54
+ expect(data[key]).to equal(value)
55
+ end
56
+ end
57
+ context 'when a value has been set' do
58
+ it 'retrieves the value that was already set' do
59
+ data = subject.instance_variable_get(:@data)
60
+ data[key] = value
61
+ result = subject.fetch(key) { 'other' }
62
+ expect(result).to equal(value)
63
+ end
64
+ end
65
+ end
66
+ describe '#forget' do
67
+ it 'removes the entry for the given key' do
68
+ data = subject.instance_variable_get(:@data)
69
+ original = { :k1 => :v1, :k2 => :v2 }
70
+ addition = { :k3 => :v3 }
71
+ data.merge!(original).merge!(addition)
72
+ subject.forget(:k3)
73
+ expect(data).to eq(original)
74
+ end
75
+ end
76
+ describe '#reset!' do
77
+ it 'clears all data' do
78
+ data = subject.instance_variable_get(:@data)
79
+ data[key] = value
80
+ subject.reset!
81
+ expect(data).to be_empty
82
+ end
83
+ end
84
+ describe '#inspect' do
85
+ it 'displays the keys that it stores' do
86
+ [:symbol, 'string', 123, [:a, :b]].each { |k| subject.write(k, value) }
87
+ expectation = '#<Memorb::KeyValueStore keys=[:symbol, "string", 123, [:a, :b]]>'
88
+ expect(subject.inspect).to eq(expectation)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ::Memorb::MethodIdentifier do
4
+ let(:method_name) { :method_1 }
5
+ subject { described_class.new(method_name) }
6
+
7
+ describe '#hash' do
8
+ it 'does not equal the hash of the method name symbol' do
9
+ expect(subject.hash).not_to equal(method_name.hash)
10
+ end
11
+ context 'when given an other instance with the same method name' do
12
+ it 'shares its hash value with the other instance' do
13
+ subject_1 = described_class.new(method_name)
14
+ subject_2 = described_class.new(method_name)
15
+ expect(subject_1.hash).to equal(subject_2.hash)
16
+ end
17
+ end
18
+ context 'when given an other instance with a different method name' do
19
+ it 'does not share its hash value with the other instance' do
20
+ subject_1 = described_class.new(:method_1)
21
+ subject_2 = described_class.new(:method_2)
22
+ expect(subject_1.hash).not_to equal(subject_2.hash)
23
+ end
24
+ end
25
+ end
26
+ describe '#eql?' do
27
+ context 'when given an other instance with the same method name' do
28
+ it 'returns true' do
29
+ subject_1 = described_class.new(method_name)
30
+ subject_2 = described_class.new(method_name)
31
+ result = subject_1.eql?(subject_2)
32
+ expect(result).to be(true)
33
+ end
34
+ end
35
+ context 'when given an other instance with a different method name' do
36
+ it 'returns false' do
37
+ subject_1 = described_class.new(:method_1)
38
+ subject_2 = described_class.new(:method_2)
39
+ result = subject_1.eql?(subject_2)
40
+ expect(result).to be(false)
41
+ end
42
+ end
43
+ end
44
+ describe '#==' do
45
+ context 'when given an other instance with the same method name' do
46
+ it 'is considered equal to the other instance' do
47
+ subject_1 = described_class.new('method_1')
48
+ subject_2 = described_class.new('method_1')
49
+ expect(subject_1 == subject_2).to be(true)
50
+ end
51
+ end
52
+ context 'when given an other instance with a different method name' do
53
+ it 'is not considered equal to the other instance' do
54
+ subject_1 = described_class.new('method_1')
55
+ subject_2 = described_class.new('method_2')
56
+ expect(subject_1 == subject_2).to be(false)
57
+ end
58
+ end
59
+ end
60
+ describe '#to_s' do
61
+ it 'returns the originally provided method name as a string' do
62
+ expect(subject.to_s).to eq('method_1')
63
+ end
64
+ end
65
+ describe '#to_sym' do
66
+ it 'returns the originally provided method name as a symbol' do
67
+ expect(subject.to_sym).to be(:method_1)
68
+ end
69
+ end
70
+ context 'when used as a key in a hash' do
71
+ context 'accessing with different instances for the same method name' do
72
+ it 'shares the same key in the hash with the other instance' do
73
+ instance_1 = described_class.new(method_name)
74
+ instance_2 = described_class.new(method_name)
75
+ hash = { instance_1 => nil }
76
+ expect(hash).to have_key(instance_2)
77
+ end
78
+ end
79
+ it 'does not refer to the same key as the symbol of the method name' do
80
+ hash = { subject => nil }
81
+ expect(hash).not_to have_key(method_name)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe ::Memorb do
4
+ context 'when integrating improperly' do
5
+ let(:error) { ::Memorb::InvalidIntegrationError }
6
+ let(:error_message) { 'Memorb must be integrated using `extend`' }
7
+
8
+ context 'when included on a target' do
9
+ it 'raises an error' do
10
+ expect {
11
+ ::Class.new { include ::Memorb }
12
+ }.to raise_error(error, error_message)
13
+ end
14
+ end
15
+ context 'when prepended on a target' do
16
+ it 'raises an error' do
17
+ expect {
18
+ ::Class.new { prepend ::Memorb }
19
+ }.to raise_error(error, error_message)
20
+ end
21
+ end
22
+ end
23
+
24
+ describe 'integrators' do
25
+ shared_examples 'for ancestry verification' do
26
+ it 'has the correct ancestry' do
27
+ expect(integration).not_to be(nil)
28
+ ancestors = integrator.ancestors
29
+ expected_ancestry = [integration, integrator]
30
+ relevant_ancestors = ancestors.select { |a| expected_ancestry.include?(a) }
31
+ expect(relevant_ancestors).to match_array(expected_ancestry)
32
+ end
33
+ end
34
+
35
+ let(:integration) { ::Memorb::Integration[integrator] }
36
+ let(:instance) { integrator.new }
37
+
38
+ describe 'a basic integrator' do
39
+ let(:integrator) {
40
+ ::Class.new do
41
+ extend ::Memorb
42
+ end
43
+ }
44
+ include_examples 'for ancestry verification'
45
+ end
46
+
47
+ describe 'an integrator that includes Memorb more than once' do
48
+ let(:integrator) {
49
+ ::Class.new do
50
+ extend ::Memorb
51
+ extend ::Memorb
52
+ end
53
+ }
54
+ include_examples 'for ancestry verification'
55
+ end
56
+
57
+ describe 'a child of an integrator' do
58
+ let(:parent_integrator) {
59
+ ::Class.new do
60
+ extend ::Memorb
61
+ end
62
+ }
63
+ let(:integrator) {
64
+ ::Class.new(parent_integrator)
65
+ }
66
+ include_examples 'for ancestry verification'
67
+ end
68
+
69
+ describe 'a child of an integrator that includes Memorb again' do
70
+ let(:parent_integrator) {
71
+ ::Class.new do
72
+ extend ::Memorb
73
+ end
74
+ }
75
+ let(:integrator) {
76
+ ::Class.new(parent_integrator) do
77
+ extend ::Memorb
78
+ end
79
+ }
80
+ include_examples 'for ancestry verification'
81
+ end
82
+
83
+ describe 'an integrator that aliases a method after registration' do
84
+ let(:integrator) {
85
+ ::Class.new(::SpecHelper.basic_target_class) do
86
+ extend ::Memorb
87
+ memorb.register(:increment)
88
+ alias_method :other_increment, :increment
89
+ end
90
+ }
91
+
92
+ it 'implements caching for the aliased method' do
93
+ result_1 = instance.other_increment
94
+ result_2 = instance.other_increment
95
+ expect(result_1).to eq(result_2)
96
+ end
97
+ end
98
+
99
+ describe 'an integrator that aliases a method before registration' do
100
+ let(:integrator) {
101
+ ::Class.new(::SpecHelper.basic_target_class) do
102
+ extend ::Memorb
103
+ alias_method :other_increment, :increment
104
+ memorb.register(:increment)
105
+ end
106
+ }
107
+
108
+ it 'does not implement caching for the aliased method' do
109
+ result_1 = instance.other_increment
110
+ result_2 = instance.other_increment
111
+ expect(result_1).not_to eq(result_2)
112
+ end
113
+ end
114
+
115
+ describe 'an integrator that uses alias method chaining' do
116
+ let(:integrator) {
117
+ ::Class.new(::SpecHelper.basic_target_class) do
118
+ extend ::Memorb
119
+ memorb.register(:increment)
120
+ def new_increment; old_increment; end
121
+ alias_method :old_increment, :increment
122
+ alias_method :increment, :new_increment
123
+ end
124
+ }
125
+
126
+ it 'results in infinite recursion when the method is called' do
127
+ error = case ::RUBY_ENGINE
128
+ when 'jruby'
129
+ [java.lang.StackOverflowError]
130
+ else
131
+ [::SystemStackError, 'stack level too deep']
132
+ end
133
+ expect { instance.increment }.to raise_error(*error)
134
+ end
135
+ end
136
+
137
+ describe 'an integrator with a method that accepts a block' do
138
+ let(:integrator) {
139
+ ::Class.new(::SpecHelper.basic_target_class) do
140
+ extend ::Memorb
141
+ def with_block(&block); block ? block.call(self) : increment; end
142
+ memorb.register(:with_block)
143
+ end
144
+ }
145
+
146
+ it 'still gets the block as a parameter' do
147
+ result = instance.with_block { |x| x }
148
+ expect(result).to be(instance)
149
+ end
150
+
151
+ it 'considers calls with different blocks to have the same arguments' do
152
+ result_1 = instance.with_block { |x| x.increment }
153
+ result_2 = instance.with_block { |x| x.increment }
154
+ expect(result_1).to eq(result_2)
155
+ end
156
+
157
+ it 'considers calls with different proc-blocks to have the same arguments' do
158
+ proc_1 = ::Proc.new { |x| x.increment }
159
+ proc_2 = ::Proc.new { |x| x.increment }
160
+ result_1 = instance.with_block(&proc_1)
161
+ result_2 = instance.with_block(&proc_2)
162
+ expect(result_1).to eq(result_2)
163
+ end
164
+
165
+ it 'considers a call with a block to be the same as a call without one' do
166
+ result_1 = instance.with_block { |x| x.increment }
167
+ result_2 = instance.with_block
168
+ expect(result_1).to eq(result_2)
169
+ end
170
+ end
171
+
172
+ ::SpecHelper.for_testing_garbage_collection do
173
+ let(:integrator) {
174
+ ::Class.new(::SpecHelper.basic_target_class) do
175
+ extend ::Memorb
176
+ memorb.register(:noop)
177
+ end
178
+ }
179
+
180
+ describe 'a method argument for a memoized method' do
181
+ it 'allows the argument to be garbage collected' do
182
+ ref = ::WeakRef.new(Object.new)
183
+ instance.send(:noop, ref.__getobj__)
184
+ ::SpecHelper.force_garbage_collection
185
+ expect(ref.weakref_alive?).to be_falsey
186
+ end
187
+ end
188
+ describe 'a low-level cache fetch' do
189
+ it 'allows the cache key to be garbage collected' do
190
+ ref = ::WeakRef.new(Object.new)
191
+ instance.memorb.fetch(ref.__getobj__) { nil }
192
+ ::SpecHelper.force_garbage_collection
193
+ expect(ref.weakref_alive?).to be_falsey
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end