memorb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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