memorb 0.1.0

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