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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +21 -2
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/OPT.rb +2 -3
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +986 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +127 -14
- 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
|