dnsruby 1.73.1 → 2.0.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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. data/.coveralls.yml +0 -2
@@ -0,0 +1,852 @@
1
+ require_relative 'spec_helper'
2
+ require 'weakref'
3
+ require 'timeout'
4
+ require 'open3'
5
+ require 'rbconfig'
6
+
7
+ class TestGenericCache < Minitest::Test
8
+ Generic = Dnsruby::RR::Generic
9
+ Cache = Dnsruby::RR::GenericCache
10
+
11
+ def evict
12
+ (50_000...(50_000 + Cache::LIMIT + 2)).each { |type| Dnsruby::RR.get_class(type, 9) }
13
+ end
14
+
15
+ def test_live_frozen_class_and_record_survive_eviction
16
+ klass = Dnsruby::RR.get_class(61_001, 1)
17
+ klass.class_eval { def client_method; :preserved; end }
18
+ record = klass.new("\x00\xffx".b)
19
+ klass.freeze
20
+ evict
21
+ assert_same(klass, Dnsruby::RR.get_class(61_001, 1))
22
+ assert_same(klass, Generic.const_get('Type61001_Class1'))
23
+ assert_equal(:preserved, record.client_method)
24
+ assert_equal([0, 255, 120], record.data.bytes)
25
+ end
26
+
27
+ def test_names_have_native_mutability_and_class_copies_are_anonymous
28
+ klass = Dnsruby::RR.get_class(61_002, 1)
29
+ assert(klass.name.frozen?)
30
+ [:to_s, :inspect].each do |method|
31
+ original = klass.public_send(method)
32
+ refute(original.frozen?)
33
+ original << ' changed'
34
+ refute_equal(original, klass.public_send(method))
35
+ end
36
+ [klass.dup, klass.clone].each do |copy|
37
+ assert_nil(copy.name)
38
+ assert_match(/\A#<Class:0x[0-9a-f]+>\z/, copy.to_s)
39
+ assert_equal(61_002, copy::TypeValue)
40
+ end
41
+ end
42
+
43
+ def test_explicit_factory_preserves_names_arguments_errors_and_registration
44
+ klass = Generic.create('61003', '1')
45
+ assert_equal('61003', klass::TypeValue)
46
+ assert_equal('1', klass::ClassValue)
47
+ assert_same(klass, Dnsruby::ClassHash[['61003', '1']])
48
+ assert_equal('Dnsruby::RR::Generic::Type61003_Class1', Module.instance_method(:name).bind(klass).call)
49
+ nonnumeric = Generic.create('Alpha', 'Beta')
50
+ assert_same(nonnumeric, Generic.const_get('TypeAlpha_ClassBeta'))
51
+ assert_raises(NameError) { Generic.create(-1, 1) }
52
+ assert_raises(NameError) { Generic.create('bad-name', 1) }
53
+ evict
54
+ assert_same(klass, Dnsruby::ClassHash[['61003', '1']])
55
+ assert_same(nonnumeric, Dnsruby::ClassHash[['Alpha', 'Beta']])
56
+ end
57
+
58
+ def test_explicit_registration_overrides_live_automatic_class
59
+ automatic = Dnsruby::RR.get_class(61_004, 1)
60
+ explicit = Generic.create(61_004, 1)
61
+ refute_same(automatic, explicit)
62
+ evict
63
+ assert_same(explicit, Dnsruby::RR.get_class(61_004, 1))
64
+ assert_same(explicit, Generic.const_get('Type61004_Class1'))
65
+ custom = Object.new
66
+ Dnsruby::ClassHash[[61_004, 1]] = custom
67
+ assert_same(custom, Dnsruby::RR.find_class(61_004, 1))
68
+ ensure
69
+ Dnsruby::ClassHash.delete([61_004, 1])
70
+ end
71
+
72
+ def test_subclass_noninherited_lookup_preserves_native_constant_boundaries
73
+ name = 'Type62170_Class1'
74
+ explicit = Generic.create(62_170, 1)
75
+ parent = Class.new(Generic)
76
+ child = Class.new(parent)
77
+ [parent, child].each do |receiver|
78
+ assert_same(explicit, receiver.const_get(name))
79
+ [false, nil].each do |inherit|
80
+ assert_raises(NameError) { receiver.const_get(name, inherit) }
81
+ refute(receiver.const_defined?(name, inherit))
82
+ end
83
+ end
84
+ own = Class.new
85
+ parent.const_set(name, own)
86
+ assert_same(own, parent.const_get(name, false))
87
+ assert_same(own, child.const_get(name))
88
+ assert_raises(NameError) { child.const_get(name, false) }
89
+ # The documented virtual-name fallback remains available to subclasses.
90
+ virtual = Dnsruby::RR.get_class(62_171, 1)
91
+ assert_same(virtual, child.const_get('Type62171_Class1', false))
92
+ refute(child.const_defined?('Type62171_Class1', false))
93
+ ensure
94
+ Generic.send(:remove_const, name) if name && Generic.const_defined?(name, false)
95
+ Dnsruby::ClassHash.delete([62_170, 1])
96
+ end
97
+
98
+ def test_external_deletion_is_different_from_fifo_eviction
99
+ klass = Dnsruby::RR.get_class(61_005, 1)
100
+ Dnsruby::ClassHash.delete([61_005, 1])
101
+ refute_same(klass, Dnsruby::RR.get_class(61_005, 1))
102
+ held = Dnsruby::RR.get_class(61_006, 1)
103
+ evict
104
+ assert_same(held, Dnsruby::RR.get_class(61_006, 1))
105
+ evict
106
+ Dnsruby::ClassHash.delete([61_006, 1])
107
+ refute_same(held, Dnsruby::RR.get_class(61_006, 1))
108
+ end
109
+
110
+ def test_deleted_live_class_stays_named_without_becoming_reusable
111
+ [false, true].each_with_index do |explicit, index|
112
+ pair = [62_210 + index, 1]
113
+ name = "Type#{pair[0]}_Class1"
114
+ held = Dnsruby::RR.get_class(*pair)
115
+ Dnsruby::ClassHash[pair] = held if explicit
116
+ Dnsruby::ClassHash.delete(pair)
117
+ evict
118
+ Dnsruby::ClassHash.rehash
119
+ Dnsruby::ClassHash.transform_values! { |value| value }
120
+ assert_same(held, Generic.const_get(name))
121
+ refute(Dnsruby::ClassHash.key?(pair))
122
+ replacement = Dnsruby::RR.get_class(*pair)
123
+ refute_same(held, replacement)
124
+ assert_same(replacement, Generic.const_get(name))
125
+ evict
126
+ assert_same(replacement, Dnsruby::RR.get_class(*pair))
127
+ end
128
+ end
129
+
130
+ def test_bulk_registry_removal_and_replacement_invalidate_lookup
131
+ [:delete_if, :reject!, :keep_if, :select!, :filter!, :transform_keys!, :transform_values!].each_with_index do |operation, index|
132
+ type = 61_100 + index
133
+ pair = [type, 1]
134
+ held = Dnsruby::RR.get_class(*pair)
135
+ if operation == :transform_keys!
136
+ Dnsruby::ClassHash.transform_keys! { |key| key == pair ? [type, 2] : key }
137
+ elsif operation == :transform_values!
138
+ Dnsruby::ClassHash.transform_values! { |value| value.equal?(held) ? nil : value }
139
+ Dnsruby::ClassHash.compact!
140
+ else
141
+ keep = [:keep_if, :select!, :filter!].include?(operation)
142
+ Dnsruby::ClassHash.public_send(operation) { |key, _| keep ? key != pair : key == pair }
143
+ end
144
+ refute_same(held, Dnsruby::RR.get_class(*pair), operation.to_s)
145
+ end
146
+ end
147
+
148
+ def test_inherited_callback_can_wait_for_another_thread
149
+ parent = Generic.singleton_class
150
+ original = parent.instance_method(:inherited)
151
+ calls = []
152
+ parent.send(:define_method, :inherited) do |subclass|
153
+ worker = Thread.new { Dnsruby::RR.get_class(1, 1) }
154
+ raise 'lookup blocked by factory lock' unless worker.join(2)
155
+ calls << worker.value
156
+ original.bind(self).call(subclass)
157
+ end
158
+ Dnsruby::RR.get_class(61_007, 1)
159
+ assert_equal([Dnsruby::RR::IN::A], calls)
160
+ ensure
161
+ parent.send(:remove_method, :inherited) if parent
162
+ end
163
+
164
+ def test_simultaneous_lookups_publish_one_class
165
+ start = Queue.new
166
+ threads = Array.new(8) do
167
+ Thread.new { start.pop; 100.times.map { Dnsruby::RR.get_class(61_008, 1) } }
168
+ end
169
+ 8.times { start << true }
170
+ classes = threads.flat_map(&:value)
171
+ assert_equal(1, classes.map(&:object_id).uniq.length)
172
+ held = classes.first
173
+ threads = Array.new(4) do |worker|
174
+ Thread.new do
175
+ 300.times do |index|
176
+ Dnsruby::RR.get_class(30_000 + worker * 300 + index, 9)
177
+ raise 'live Class identity lost' unless held.equal?(Dnsruby::RR.get_class(61_008, 1))
178
+ end
179
+ end
180
+ end
181
+ threads.each(&:value)
182
+ assert_same(held, Dnsruby::RR.get_class(61_008, 1))
183
+ end
184
+
185
+ def test_constant_inspection_does_not_allocate_or_reactivate
186
+ held = Dnsruby::RR.get_class(61_009, 1)
187
+ held.const_set(:ClientConstant, 42)
188
+ evict
189
+ refute(Dnsruby::ClassHash.key?([61_009, 1]))
190
+ assert(Generic.const_defined?('Type61009_Class1::ClientConstant'))
191
+ assert(Generic.const_defined?('Type61010_Class1::TypeValue'))
192
+ refute(Dnsruby::ClassHash.key?([61_009, 1]))
193
+ refute(Dnsruby::ClassHash.key?([61_010, 1]))
194
+ refute(Generic.const_defined?('Type61009_Class1::Object'))
195
+ child = Class.new(Generic)
196
+ refute(child.const_defined?('Type61009_Class1', false))
197
+ assert(child.const_defined?('Type61009_Class1'))
198
+ end
199
+ def test_registry_copies_cannot_change_the_cache
200
+ [:dup, :clone].each_with_index do |copy_method, index|
201
+ pair = [61_020 + index, 1]
202
+ held = Dnsruby::RR.get_class(*pair)
203
+ copy = Dnsruby::ClassHash.public_send(copy_method)
204
+ copy.delete(pair)
205
+ copy[pair] = Object.new
206
+ copy.reject! { |key, _| key == pair }
207
+ copy.clear
208
+ assert_same(held, Dnsruby::ClassHash[pair])
209
+ evict
210
+ refute(Dnsruby::ClassHash.key?(pair), copy_method.to_s)
211
+ assert_same(held, Dnsruby::RR.get_class(*pair))
212
+ end
213
+ end
214
+
215
+ def test_two_argument_super_factory_is_invoked_and_stays_anonymous
216
+ calls = []
217
+ wrapper = Module.new do
218
+ define_method(:create) do |type, klass|
219
+ calls << [type, klass]
220
+ super(type, klass)
221
+ end
222
+ end
223
+ Generic.singleton_class.prepend(wrapper)
224
+ generated = Dnsruby::RR.get_class(61_030, 1)
225
+ assert_equal([[61_030, 1]], calls)
226
+ assert_nil(Module.instance_method(:name).bind(generated).call)
227
+ explicit = Generic.create(61_031, 1)
228
+ assert_equal('Dnsruby::RR::Generic::Type61031_Class1', Module.instance_method(:name).bind(explicit).call)
229
+ ensure
230
+ wrapper.send(:remove_method, :create) if wrapper
231
+ end
232
+
233
+ def test_alias_factory_and_unrelated_direct_create
234
+ singleton = Generic.singleton_class
235
+ singleton.send(:alias_method, :client_saved_create, :create)
236
+ direct = nil
237
+ singleton.send(:define_method, :create) do |type, klass|
238
+ direct = client_saved_create(61_033, 1)
239
+ client_saved_create(type, klass)
240
+ end
241
+ generated = Dnsruby::RR.get_class(61_032, 1)
242
+ assert_nil(Module.instance_method(:name).bind(generated).call)
243
+ assert_equal('Dnsruby::RR::Generic::Type61033_Class1', Module.instance_method(:name).bind(direct).call)
244
+ ensure
245
+ if singleton
246
+ singleton.send(:alias_method, :create, :client_saved_create)
247
+ singleton.send(:remove_method, :client_saved_create)
248
+ end
249
+ end
250
+
251
+ def test_custom_factory_result_and_exception_context_cleanup
252
+ wrapper = Module.new do
253
+ define_method(:create) do |type, klass|
254
+ raise 'client factory error' if type == 61_034
255
+ return Dnsruby::RR::IN::TXT if type == 61_035
256
+ super(type, klass)
257
+ end
258
+ end
259
+ Generic.singleton_class.prepend(wrapper)
260
+ assert_raises(RuntimeError) { Dnsruby::RR.get_class(61_034, 1) }
261
+ assert_nil(Thread.current[:dnsruby_generic_factory])
262
+ assert_same(Dnsruby::RR::IN::TXT, Dnsruby::RR.get_class(61_035, 1))
263
+ assert_nil(Thread.current[:dnsruby_generic_factory])
264
+ ensure
265
+ wrapper.send(:remove_method, :create) if wrapper
266
+ end
267
+
268
+ def test_nested_same_pair_inherited_lookup_and_display_customization
269
+ singleton = Generic.singleton_class
270
+ original = singleton.instance_method(:inherited)
271
+ inside = false
272
+ nested = nil
273
+ singleton.send(:define_method, :inherited) do |subclass|
274
+ unless inside
275
+ inside = true
276
+ nested = Dnsruby::RR.get_class(61_036, 1)
277
+ end
278
+ subclass.define_singleton_method(:to_s) { 'client display' }
279
+ original.bind(self).call(subclass)
280
+ end
281
+ generated = Dnsruby::RR.get_class(61_036, 1)
282
+ assert_same(nested, generated)
283
+ assert_equal('client display', generated.to_s)
284
+ assert_nil(Module.instance_method(:name).bind(generated).call)
285
+ assert_nil(Thread.current[:dnsruby_generic_factory])
286
+ ensure
287
+ singleton.send(:remove_method, :inherited) if singleton
288
+ end
289
+
290
+ def test_registry_default_callback_does_not_run_under_the_cache_lock
291
+ previous = Dnsruby::ClassHash.default_proc
292
+ calls = 0
293
+ Dnsruby::ClassHash.default_proc = proc do |hash, pair|
294
+ worker = Thread.new { Dnsruby::RR.get_class(1, 1) }
295
+ raise 'default proc blocked by cache lock' unless worker.join(2)
296
+ calls += 1
297
+ nil
298
+ end
299
+ Dnsruby::RR.get_class(61_037, 1)
300
+ assert_equal(1, calls)
301
+ ensure
302
+ Dnsruby::ClassHash.default_proc = previous
303
+ end
304
+
305
+ def test_explicit_merge_registration_of_the_same_class_is_retained
306
+ [:merge!, :update].each_with_index do |operation, index|
307
+ pair = [61_040 + index, 1]
308
+ held = Dnsruby::RR.get_class(*pair)
309
+ Dnsruby::ClassHash.public_send(operation, {pair => held})
310
+ evict
311
+ assert_same(held, Dnsruby::ClassHash[pair])
312
+ end
313
+ end
314
+
315
+ def test_partial_registry_mutation_is_observed_when_client_block_raises
316
+ held = Dnsruby::RR.get_class(61_042, 1)
317
+ Dnsruby::RR.get_class(61_043, 1)
318
+ assert_raises(RuntimeError) do
319
+ Dnsruby::ClassHash.delete_if do |pair, _|
320
+ raise 'client callback' if pair == [61_043, 1]
321
+ pair == [61_042, 1]
322
+ end
323
+ end
324
+ refute_same(held, Dnsruby::RR.get_class(61_042, 1))
325
+ end
326
+
327
+ def test_one_byte_wire_records_do_not_permanently_retain_classes_or_names
328
+ weak = Thread.new do
329
+ first = nil
330
+ (3 * Cache::LIMIT).times do |index|
331
+ type, klass = 40_000 + index, 12
332
+ wire = [0, 0x8000, 0, 1, 0, 0].pack('n6') + "\0" + [type, klass, 0, 1].pack('nnNn') + 'x'
333
+ record = Dnsruby::Message.decode(wire).answer.first
334
+ raise 'opaque byte changed' unless record.data == 'x'
335
+ first ||= WeakRef.new(record.class)
336
+ end
337
+ first
338
+ end.value
339
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
340
+ refute(weak.weakref_alive?)
341
+ assert_equal(Cache::LIMIT, Dnsruby::ClassHash.keys.count { |pair| pair[1] == 12 })
342
+ assert_equal(0, Generic.constants(false).count { |name| name.to_s.match?(/\AType4\d+_Class12\z/) })
343
+ assert_equal(0, Symbol.all_symbols.count { |name| name.to_s.match?(/\AType4\d+_Class12\z/) })
344
+ assert_operator(ObjectSpace.each_object(String).count { |name| name.ascii_only? && name.match?(/\A(?:Dnsruby::RR::Generic::)?Type4\d+_Class12\z/) }, :<=, 2 * Cache::LIMIT)
345
+ end
346
+
347
+ def test_replacement_remains_indexed_after_old_class_is_collected
348
+ old, replacement = Thread.new do
349
+ previous = Dnsruby::RR.get_class(61_044, 1)
350
+ Dnsruby::ClassHash.delete([61_044, 1])
351
+ [WeakRef.new(previous), Dnsruby::RR.get_class(61_044, 1)]
352
+ end.value
353
+ evict
354
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
355
+ refute(old.weakref_alive?)
356
+ assert_same(replacement, Generic.const_get('Type61044_Class1'))
357
+ assert_same(replacement, Dnsruby::RR.get_class(61_044, 1))
358
+ end
359
+
360
+ def test_name_and_index_snapshots_do_not_retain_obsolete_classes_or_lose_replacements
361
+ [false, true].each_with_index do |hold_name, offset|
362
+ type = 62_223 + offset
363
+ weak_class, retained = Thread.new do
364
+ generated = Dnsruby::RR.get_class(type, 1)
365
+ entry = generated.instance_variable_get(:@generic_cache_entry)
366
+ # A caller can retain a name, and registry cleanup temporarily retains
367
+ # index cells. Neither must keep an obsolete class alive.
368
+ snapshot = hold_name ? entry.name : entry.index
369
+ Dnsruby::ClassHash.delete([type, 1])
370
+ [WeakRef.new(generated), snapshot]
371
+ end.value
372
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
373
+ refute(weak_class.weakref_alive?)
374
+ assert_instance_of(hold_name ? String : Cache::NameIndex, retained)
375
+ Dnsruby::ClassHash.rehash
376
+ Dnsruby::ClassHash.transform_values! { |value| value }
377
+ replacement = Dnsruby::RR.get_class(type, 1)
378
+ evict
379
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
380
+ assert_same(replacement, Generic.const_get("Type#{type}_Class1"))
381
+ assert_same(replacement, Dnsruby::RR.get_class(type, 1))
382
+ end
383
+ end
384
+
385
+ def test_retired_name_index_and_key_remain_collectible
386
+ references = Thread.new do
387
+ generated = Dnsruby::RR.get_class(62_225, 1)
388
+ entry = generated.instance_variable_get(:@generic_cache_entry)
389
+ Dnsruby::ClassHash.delete([62_225, 1])
390
+ [generated, entry.index, entry.name].map { |object| WeakRef.new(object) }
391
+ end.value
392
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
393
+ references.each { |reference| refute(reference.weakref_alive?) }
394
+ end
395
+
396
+ def test_older_frozen_class_does_not_retain_its_replacement
397
+ held, replacement = Thread.new do
398
+ previous = Dnsruby::RR.get_class(62_220, 1).freeze
399
+ Dnsruby::ClassHash.delete([62_220, 1])
400
+ current = Dnsruby::RR.get_class(62_220, 1)
401
+ Dnsruby::ClassHash.delete([62_220, 1])
402
+ [previous, WeakRef.new(current)]
403
+ end.value
404
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
405
+ refute(replacement.weakref_alive?)
406
+ assert(held.frozen?)
407
+ assert_nil(Cache.named('Type62220_Class1'))
408
+
409
+ current = Dnsruby::RR.get_class(62_220, 1)
410
+ refute_same(held, current)
411
+ evict
412
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
413
+ assert_same(current, Generic.const_get('Type62220_Class1'))
414
+ assert_same(current, Dnsruby::RR.get_class(62_220, 1))
415
+ end
416
+
417
+ def test_repeated_replacement_does_not_retain_obsolete_weak_maps
418
+ # Ruby 3.0-3.2 WeakMap finalizers can retain an abandoned map while any
419
+ # indexed object remains alive. A held class plus delete/recreate churn
420
+ # must not retain a new complete map for every replacement.
421
+ script = <<~RUBY
422
+ require 'dnsruby'
423
+ held = Dnsruby::RR.get_class(62_221, 1).freeze
424
+ GC.start(full_mark: true, immediate_sweep: true)
425
+ before = ObjectSpace.each_object(ObjectSpace::WeakMap).count
426
+ Thread.new do
427
+ 200.times do
428
+ Dnsruby::ClassHash.delete([62_222, 1])
429
+ Dnsruby::RR.get_class(62_222, 1)
430
+ end
431
+ end.value
432
+ 5.times { GC.start(full_mark: true, immediate_sweep: true) }
433
+ after = ObjectSpace.each_object(ObjectSpace::WeakMap).count
434
+ raise "obsolete WeakMaps retained: \#{before} -> \#{after}" if after > before + 2
435
+ raise 'held class identity changed' unless held.equal?(Dnsruby::RR.get_class(62_221, 1))
436
+ RUBY
437
+ stdout, stderr, status = Open3.capture3(
438
+ RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), '-e', script)
439
+ assert(status.success?, "#{stdout}\n#{stderr}")
440
+ assert_empty(stderr)
441
+ end
442
+
443
+ def after_native_hash_return(operation, concurrent)
444
+ owner = Thread.current
445
+ fired = false
446
+ trace = TracePoint.new(:c_return) do |event|
447
+ next if fired || Thread.current != owner || event.defined_class != Hash
448
+ next unless event.self.equal?(Dnsruby::ClassHash) && event.method_id == operation
449
+ fired = true
450
+ Thread.new(&concurrent).value
451
+ end
452
+ result = trace.enable { yield }
453
+ assert(fired, "native #{operation} was observed")
454
+ result
455
+ ensure
456
+ trace.disable if trace
457
+ end
458
+
459
+ def test_concurrent_deletion_does_not_orphan_republished_classes
460
+ 32.times do |index|
461
+ pair = [62_000 + index, 1]
462
+ Dnsruby::RR.get_class(*pair)
463
+ after_native_hash_return(:delete, -> { Dnsruby::RR.get_class(*pair) }) do
464
+ Dnsruby::ClassHash.delete(pair)
465
+ end
466
+ end
467
+ evict
468
+ assert_equal(0, Dnsruby::ClassHash.keys.count { |pair| (62_000...62_032).cover?(pair[0]) })
469
+ end
470
+
471
+ def test_concurrent_clear_and_replace_keep_new_publications_tracked
472
+ automatic_keys = Cache.snapshot.keys.map(&:registry_key)
473
+ saved = Dnsruby::ClassHash.reject { |key, _| automatic_keys.any? { |item| item.equal?(key) } }
474
+ [:clear, :replace].each_with_index do |operation, index|
475
+ pair = [62_050 + index, 1]
476
+ Dnsruby::RR.get_class(*pair)
477
+ after_native_hash_return(operation, -> { Dnsruby::RR.get_class(*pair) }) do
478
+ operation == :clear ? Dnsruby::ClassHash.clear : Dnsruby::ClassHash.replace({})
479
+ end
480
+ evict
481
+ refute(Dnsruby::ClassHash.key?(pair), operation.to_s)
482
+ end
483
+ ensure
484
+ Dnsruby::ClassHash.replace(saved) if saved
485
+ end
486
+
487
+ def test_array_subclass_registration_and_deletion
488
+ subclass = Class.new(Array) do
489
+ def length; raise 'must use native Array contents'; end
490
+ def to_a; raise 'must use native Array contents'; end
491
+ end
492
+ key = subclass.new([62_100, 1])
493
+ held = Dnsruby::RR.get_class(62_100, 1)
494
+ Dnsruby::ClassHash[key] = held
495
+ evict
496
+ assert_same(held, Dnsruby::ClassHash[[62_100, 1]])
497
+ Dnsruby::ClassHash.delete(key)
498
+ refute_same(held, Dnsruby::RR.get_class(62_100, 1))
499
+ end
500
+
501
+ def test_array_singleton_methods_do_not_change_registration_or_deletion
502
+ [:zero, :raise, :reenter].each_with_index do |mode, index|
503
+ pair = [62_103 + index, 1]
504
+ held = Dnsruby::RR.get_class(*pair)
505
+ callbacks = 0
506
+ pair.define_singleton_method(:length) do
507
+ callbacks += 1
508
+ case mode
509
+ when :zero then 0
510
+ when :raise then raise 'must use native Array contents'
511
+ when :reenter
512
+ Dnsruby::RR.get_class(45_000 + callbacks, 41)
513
+ 2
514
+ end
515
+ end
516
+ pair.define_singleton_method(:[]) { |*_| raise 'must use native Array contents' }
517
+ pair.define_singleton_method(:to_a) { raise 'must use native Array contents' }
518
+ Dnsruby::ClassHash[pair] = held
519
+ evict
520
+ assert_same(held, Dnsruby::ClassHash[[62_103 + index, 1]], mode.to_s)
521
+ assert_same(held, Dnsruby::ClassHash.delete(pair), mode.to_s)
522
+ refute_same(held, Dnsruby::RR.get_class(62_103 + index, 1), mode.to_s)
523
+ assert_equal(0, callbacks, mode.to_s)
524
+ end
525
+ end
526
+
527
+ def test_rehash_moves_mutable_keys_even_after_fifo_eviction
528
+ [false, true].each_with_index do |churn, index|
529
+ pair = [62_110 + 2 * index, 1]
530
+ held = Dnsruby::RR.get_class(*pair)
531
+ key = Dnsruby::ClassHash.keys.find { |item| item == pair }
532
+ key[0] += 1
533
+ evict if churn
534
+ assert_same(Dnsruby::ClassHash, Dnsruby::ClassHash.rehash)
535
+ refute_same(held, Dnsruby::RR.get_class(*pair))
536
+ assert_same(held, Dnsruby::RR.get_class(pair[0] + 1, 1))
537
+ end
538
+ end
539
+
540
+ def test_explicit_registration_survives_concurrent_fifo_eviction
541
+ [:[]=, :merge!, :update].each_with_index do |operation, index|
542
+ pair = [62_120 + index, 1]
543
+ held = Dnsruby::RR.get_class(*pair)
544
+ after_native_hash_return(operation, -> { evict }) do
545
+ operation == :[]= ? Dnsruby::ClassHash[pair] = held : Dnsruby::ClassHash.public_send(operation, {pair => held})
546
+ end
547
+ evict
548
+ assert_same(held, Dnsruby::ClassHash[pair], operation.to_s)
549
+ end
550
+ end
551
+
552
+ def test_merge_callback_deletion_is_not_resurrected
553
+ [:merge!, :update].each_with_index do |operation, index|
554
+ pair = [62_130 + index, 1]
555
+ held = Dnsruby::RR.get_class(*pair)
556
+ Dnsruby::ClassHash.public_send(operation, {pair => held}) do |key, _, value|
557
+ Dnsruby::ClassHash.delete(key)
558
+ value
559
+ end
560
+ refute(Dnsruby::ClassHash.key?(pair))
561
+ evict
562
+ refute(Dnsruby::ClassHash.key?(pair))
563
+ end
564
+ end
565
+
566
+ def test_merge_callback_cached_lookups_are_visible_and_remain_bounded
567
+ [:merge!, :update].each_with_index do |operation, index|
568
+ pair = [62_140 + 2 * index, 1]
569
+ lookup_pair = [pair[0] + 1, 1]
570
+ held = Dnsruby::RR.get_class(*pair)
571
+ cached = Dnsruby::RR.get_class(*lookup_pair)
572
+ Dnsruby::ClassHash.public_send(operation, {pair => held}) do |_, _, value|
573
+ worker = Thread.new do
574
+ assert_same(cached, Dnsruby::RR.get_class(*lookup_pair))
575
+ assert_same(cached, Dnsruby::ClassHash[lookup_pair])
576
+ assert_same(cached, Generic.const_get("Type#{lookup_pair[0]}_Class1"))
577
+ end
578
+ raise 'lookup blocked by registry mutation' unless worker.join(2)
579
+ worker.value
580
+ value
581
+ end
582
+ evict
583
+ assert_same(held, Dnsruby::ClassHash[pair])
584
+ refute(Dnsruby::ClassHash.key?(lookup_pair))
585
+ assert_same(cached, Dnsruby::RR.get_class(*lookup_pair))
586
+ end
587
+ end
588
+
589
+ def test_merge_callback_new_lookups_follow_native_hash_mutation_rules
590
+ [:merge!, :update].each_with_index do |operation, index|
591
+ # Recent Ruby releases forbid new keys while a merge conflict block is
592
+ # running. Probe native Hash behavior so older supported Rubies retain
593
+ # coverage too, without bypassing the registry's native mutation rules.
594
+ native = {existing: true}
595
+ native_error = begin
596
+ native.public_send(operation, {existing: true}) { native[:added] = true }
597
+ nil
598
+ rescue RuntimeError => error
599
+ error
600
+ end
601
+ pair = [62_144 + 2 * index, 1]
602
+ lookup_pair = [pair[0] + 1, 1]
603
+ held = Dnsruby::RR.get_class(*pair)
604
+ generated = nil
605
+ merge = lambda do
606
+ Dnsruby::ClassHash.public_send(operation, {pair => held}) do |_, _, value|
607
+ worker = Thread.new do
608
+ Thread.current.report_on_exception = false
609
+ Dnsruby::RR.get_class(*lookup_pair)
610
+ end
611
+ raise 'lookup blocked by registry mutation' unless worker.join(2)
612
+ generated = worker.value
613
+ assert_same(generated, Dnsruby::ClassHash[lookup_pair])
614
+ value
615
+ end
616
+ end
617
+ if native_error
618
+ error = assert_raises(native_error.class, &merge)
619
+ assert_equal(native_error.message, error.message)
620
+ refute(Dnsruby::ClassHash.key?(lookup_pair))
621
+ else
622
+ assert_same(Dnsruby::ClassHash, merge.call)
623
+ end
624
+ generated ||= Dnsruby::RR.get_class(*lookup_pair)
625
+ assert_same(generated, Dnsruby::ClassHash[lookup_pair])
626
+ evict
627
+ refute(Dnsruby::ClassHash.key?(lookup_pair))
628
+ assert_same(generated, Dnsruby::RR.get_class(*lookup_pair))
629
+ end
630
+ end
631
+
632
+ def test_rejected_registry_publication_can_be_retried
633
+ pair = [62_148, 1]
634
+ error = assert_raises(RuntimeError) do
635
+ Dnsruby::ClassHash.each_pair { Dnsruby::RR.get_class(*pair) }
636
+ end
637
+ assert_match(/can't add a new key into hash during iteration/, error.message)
638
+ refute(Dnsruby::ClassHash.key?(pair))
639
+ generated = Dnsruby::RR.get_class(*pair)
640
+ assert_same(generated, Dnsruby::ClassHash[pair])
641
+ evict
642
+ refute(Dnsruby::ClassHash.key?(pair))
643
+ assert_same(generated, Dnsruby::RR.get_class(*pair))
644
+ end
645
+
646
+ def test_hash_and_equality_callbacks_run_outside_the_cache_lock
647
+ subclass = Class.new(Array) do
648
+ def check_callback
649
+ worker = Thread.new { Dnsruby::RR.get_class(62_151, 1) }
650
+ raise 'Hash key callback blocked by registry lock' unless worker.join(2)
651
+ worker.value
652
+ end
653
+ def hash; check_callback; super; end
654
+ def eql?(other); check_callback; super; end
655
+ end
656
+ pair = subclass.new([62_150, 1])
657
+ held = Dnsruby::RR.get_class(62_150, 1)
658
+ Dnsruby::ClassHash[pair] = held
659
+ evict
660
+ assert_same(held, Dnsruby::ClassHash[pair])
661
+ end
662
+
663
+ def test_replace_conversion_callbacks_remain_visible_but_removed_entries_are_invalidated
664
+ held = nil
665
+ replacement = Object.new
666
+ replacement.define_singleton_method(:to_hash) do
667
+ held = Dnsruby::RR.get_class(62_160, 1)
668
+ raise 'factory publication hidden from callback' unless Dnsruby::ClassHash[[62_160, 1]].equal?(held)
669
+ {}
670
+ end
671
+ automatic_keys = Cache.snapshot.keys.map(&:registry_key)
672
+ saved = Dnsruby::ClassHash.reject { |key, _| automatic_keys.any? { |item| item.equal?(key) } }
673
+ Dnsruby::ClassHash.replace(replacement)
674
+ refute_same(held, Dnsruby::RR.get_class(62_160, 1))
675
+ ensure
676
+ Dnsruby::ClassHash.replace(saved) if saved
677
+ end
678
+
679
+ def test_replace_registers_classes_created_during_argument_conversion
680
+ automatic_keys = Cache.snapshot.keys.map(&:registry_key)
681
+ saved = Dnsruby::ClassHash.reject { |key, _| automatic_keys.any? { |item| item.equal?(key) } }
682
+ [false, true].each_with_index do |concurrent_churn, index|
683
+ pair = [62_162 + index, 1]
684
+ held = nil
685
+ conversions = 0
686
+ replacement = Object.new
687
+ replacement.define_singleton_method(:to_hash) do
688
+ conversions += 1
689
+ held = Dnsruby::RR.get_class(*pair)
690
+ {pair => held}
691
+ end
692
+ if concurrent_churn
693
+ after_native_hash_return(:replace, -> { evict }) do
694
+ assert_same(Dnsruby::ClassHash, Dnsruby::ClassHash.replace(replacement))
695
+ end
696
+ else
697
+ assert_same(Dnsruby::ClassHash, Dnsruby::ClassHash.replace(replacement))
698
+ end
699
+ assert_equal(1, conversions)
700
+ evict
701
+ assert_same(held, Dnsruby::ClassHash[pair])
702
+ assert_same(held, Dnsruby::RR.get_class(*pair))
703
+ end
704
+ ensure
705
+ Dnsruby::ClassHash.replace(saved) if saved
706
+ end
707
+
708
+ def test_replace_preserves_native_hash_conversion_defaults_and_identity
709
+ automatic_keys = Cache.snapshot.keys.map(&:registry_key)
710
+ saved = Dnsruby::ClassHash.reject { |key, _| automatic_keys.any? { |item| item.equal?(key) } }
711
+ registry = Dnsruby::ClassHash
712
+ source = Hash.new(:missing)
713
+ source[:client] = :registered
714
+ assert_same(registry, registry.replace(source))
715
+ assert_equal(:missing, registry[:absent])
716
+ assert_equal(:registered, registry[:client])
717
+ assert_same(registry, registry.replace(registry))
718
+ assert_equal(source, registry)
719
+ default = ->(hash, key) { [hash, key] }
720
+ registry.replace(Hash.new(&default))
721
+ assert_same(default, registry.default_proc)
722
+ assert_equal([registry, :absent], registry[:absent])
723
+ key = [62_164, 1]
724
+ identity = {}.compare_by_identity
725
+ identity[key] = :identity_value
726
+ registry.replace(identity)
727
+ assert(registry.compare_by_identity?)
728
+ assert_equal(:identity_value, registry[key])
729
+ assert_nil(registry[key.dup])
730
+ invalid = Object.new
731
+ conversions = 0
732
+ invalid.define_singleton_method(:to_hash) { conversions += 1; [] }
733
+ assert_raises(TypeError) { registry.replace(invalid) }
734
+ assert_equal(1, conversions)
735
+ assert(registry.compare_by_identity?)
736
+ assert_equal(:identity_value, registry[key])
737
+ frozen = registry.dup.freeze
738
+ assert_raises(FrozenError) { frozen.replace(invalid) }
739
+ assert_equal(1, conversions)
740
+ ensure
741
+ Dnsruby::ClassHash.replace(saved) if saved
742
+ end
743
+
744
+ def test_frozen_registry_replace_does_not_coerce_its_argument
745
+ script = <<~RUBY
746
+ require 'dnsruby'
747
+ source = Object.new
748
+ source.define_singleton_method(:to_hash) { raise 'argument converted before frozen check' }
749
+ Dnsruby::ClassHash.freeze
750
+ begin
751
+ Dnsruby::ClassHash.replace(source)
752
+ raise 'frozen replacement succeeded'
753
+ rescue FrozenError
754
+ end
755
+ RUBY
756
+ stdout, stderr, status = Open3.capture3(
757
+ RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), '-e', script)
758
+ assert(status.success?, "#{stdout}\n#{stderr}")
759
+ assert_empty(stderr)
760
+ end
761
+
762
+ def test_regeneration_removes_stale_native_binding_without_client_callbacks
763
+ old = Generic.create(62_165, 1)
764
+ Dnsruby::ClassHash.delete([62_165, 1])
765
+ Generic.define_singleton_method(:remove_const) { |_| raise 'client callback under cache lock' }
766
+ current = Dnsruby::RR.get_class(62_165, 1)
767
+ refute_same(old, current)
768
+ assert_same(current, Generic.const_get('Type62165_Class1'))
769
+ refute(Module.instance_method(:const_defined?).bind(Generic).call('Type62165_Class1', false))
770
+ ensure
771
+ Generic.singleton_class.send(:remove_method, :remove_const)
772
+ end
773
+
774
+ def test_identity_hash_uses_distinct_classes_and_keeps_fifo_bound
775
+ automatic_keys = Cache.snapshot.keys.map(&:registry_key)
776
+ saved = Dnsruby::ClassHash.reject { |key, _| automatic_keys.any? { |item| item.equal?(key) } }
777
+ Dnsruby::ClassHash.clear.compare_by_identity
778
+ classes = 300.times.map { Dnsruby::RR.get_class(62_170, 1) }
779
+ assert_equal(300, classes.map(&:object_id).uniq.length)
780
+ assert_equal(Cache::LIMIT, Dnsruby::ClassHash.size)
781
+ key = Dnsruby::ClassHash.keys.last
782
+ held = Dnsruby::ClassHash[key]
783
+ Dnsruby::ClassHash[key] = held
784
+ 300.times { Dnsruby::RR.get_class(62_171, 1) }
785
+ assert_same(held, Dnsruby::ClassHash[key])
786
+ assert_equal(Cache::LIMIT + 1, Dnsruby::ClassHash.size)
787
+ Dnsruby::ClassHash.delete(key)
788
+ refute(Dnsruby::ClassHash.key?(key))
789
+ custom = Object.new
790
+ custom.define_singleton_method(:hash) { raise "identity hash must not call hash" }
791
+ custom.define_singleton_method(:eql?) { |_| raise "identity hash must not call eql?" }
792
+ Dnsruby::ClassHash[custom] = held
793
+ assert_same(held, Dnsruby::ClassHash.delete(custom))
794
+ ensure
795
+ Dnsruby::ClassHash.replace(saved) if saved
796
+ end
797
+
798
+ def test_array_subclass_hash_miss_does_not_detach_canonical_entry
799
+ subclass = Class.new(Array) { def hash; super ^ 1; end }
800
+ pair = [62_180, 1]
801
+ held = Dnsruby::RR.get_class(*pair)
802
+ assert_nil(Dnsruby::ClassHash.delete(subclass.new(pair)))
803
+ assert_same(held, Dnsruby::ClassHash[pair])
804
+ evict
805
+ refute(Dnsruby::ClassHash.key?(pair))
806
+ assert_same(held, Dnsruby::RR.get_class(*pair))
807
+ end
808
+
809
+ def test_distinct_array_subclass_updates_do_not_pin_canonical_entry
810
+ subclass = Class.new(Array) { def hash; super ^ 1; end }
811
+ [:[]=, :merge!, :update].each_with_index do |operation, index|
812
+ pair = [62_190 + index, 1]
813
+ held = Dnsruby::RR.get_class(*pair)
814
+ custom = Class.new(Generic)
815
+ 2.times do
816
+ key = subclass.new(pair)
817
+ operation == :[]= ? Dnsruby::ClassHash[key] = custom : Dnsruby::ClassHash.public_send(operation, {key => custom})
818
+ end
819
+ evict
820
+ refute(Dnsruby::ClassHash.key?(pair), operation.to_s)
821
+ assert_same(custom, Dnsruby::ClassHash[subclass.new(pair)])
822
+ assert_same(held, Dnsruby::RR.get_class(*pair))
823
+ end
824
+ end
825
+
826
+ def test_custom_hash_aliases_address_the_native_pair
827
+ [Array, Object].each_with_index do |base, index|
828
+ [:same_class, :replacement].each_with_index do |mode, offset|
829
+ pair = [62_200 + 2 * index + offset, 1]
830
+ key_class = Class.new(base) do
831
+ define_method(:method) { |_| raise "client method override" }
832
+ define_method(:hash) { pair.hash }
833
+ define_method(:eql?) { |other| other == pair }
834
+ end
835
+ key = base == Array ? key_class.new([pair[0] + 100, 1]) : key_class.new
836
+ held = Dnsruby::RR.get_class(*pair)
837
+ if mode == :same_class
838
+ Dnsruby::ClassHash[key] = held
839
+ evict
840
+ assert_same(held, Dnsruby::ClassHash[pair])
841
+ else
842
+ custom = Class.new(Generic)
843
+ Dnsruby::ClassHash[key] = custom
844
+ assert_same(custom, Dnsruby::ClassHash[pair])
845
+ end
846
+ Dnsruby::ClassHash.delete(key)
847
+ refute_same(held, Dnsruby::RR.get_class(*pair))
848
+ end
849
+ end
850
+ end
851
+
852
+ end