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,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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Memorb
4
+ VERSION = '0.1.0'
5
+ end
@@ -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