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
data/test/tc_soak.rb CHANGED
@@ -1,57 +1,31 @@
1
- # --
2
- # Copyright 2007 Nominet UK
3
- #
4
- # Licensed under the Apache License, Version 2.0 (the "License");
5
- # you may not use this file except in compliance with the License.
6
- # You may obtain a copy of the License at
7
- #
8
- # http://www.apache.org/licenses/LICENSE-2.0
9
- #
10
- # Unless required by applicable law or agreed to in writing, software
11
- # distributed under the License is distributed on an "AS IS" BASIS,
12
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- # See the License for the specific language governing permissions and
14
- # limitations under the License.
15
- # ++
1
+ # frozen_string_literal: true
16
2
 
17
3
  require_relative 'spec_helper'
4
+ require_relative 'soak_server'
18
5
 
19
- # require_relative 'tc_single_resolver'
20
- require_relative 'tc_soak_base'
21
- require_relative 'test_dnsserver'
22
- require_relative 'localdns'
23
-
24
-
25
- # This class tries to soak test the Dnsruby library.
26
- # It can't do this very well, owing to the small number of sockets allowed to be open simultaneously.
27
- # @TODO@ Future versions of dnsruby will allow random streaming over a fixed number of (cycling) random sockets,
28
- # so this test can be beefed up considerably at that point.
29
-
6
+ # Defaults preserve the historical workload: 81,512 queries in eight cases.
7
+ # Increase the counts for a longer run. SOAK_TIMEOUT bounds each complete case.
30
8
  class TestSingleResolverSoak < Minitest::Test
31
-
32
- IP = SimpleTCPPipeliningUDPServer::IP
33
- PORT = SimpleTCPPipeliningUDPServer::PORT
34
-
35
- def initialize(arg)
36
- super(arg)
37
- end
38
-
39
- def teardown
40
- Celluloid.shutdown
41
- end
42
-
43
- SINGLE_RESOLVER_QUERY_TIMES = 63
9
+ SINGLE_RESOLVER_QUERY_TIMES = Integer(ENV.fetch('SOAK_ASYNC_ROUNDS', '63'))
10
+ THREAD_COUNT = Integer(ENV.fetch('SOAK_THREADS', RUBY_PLATFORM.include?('java') ? '50' : '250'))
11
+ ROUNDS = Integer(ENV.fetch('SOAK_ROUNDS', '40'))
12
+ CASE_TIMEOUT = Float(ENV.fetch('SOAK_TIMEOUT', '120'))
44
13
 
45
14
  def setup
46
- # Instantiate a local dns server
47
- pipe = IO.popen("./test/localdns.rb")
48
- @dnspid = pipe.pid
49
- sleep 1
15
+ @resolvers = []
16
+ @workers = []
17
+ @queues = []
18
+ raise ArgumentError, 'Soak counts and timeout must be positive' unless
19
+ [SINGLE_RESOLVER_QUERY_TIMES, THREAD_COUNT, ROUNDS, CASE_TIMEOUT].all?(&:positive?)
20
+ @server = SoakServer.new
50
21
  end
51
22
 
52
23
  def teardown
53
- Process.kill("KILL", @dnspid)
54
- sleep 1
24
+ @workers.each { |worker| worker.kill if worker.alive? }
25
+ @workers.each(&:join)
26
+ @resolvers.each(&:close)
27
+ @server&.stop
28
+ @queues.each { |queue| assert(queue.empty?, 'Unexpected extra/duplicate completion') }
55
29
  end
56
30
 
57
31
  def test_many_asynchronous_queries_one_single_resolver
@@ -79,178 +53,126 @@ class TestSingleResolverSoak < Minitest::Test
79
53
  end
80
54
 
81
55
  def run_many_asynch_queries_test_single_res(num_resolvers, tcp = false, pipelining = false)
82
- q = Queue.new
83
- timeout_count = 0
84
- resolvers = Array.new(num_resolvers) do
85
- Dnsruby::SingleResolver.new(server: IP,
86
- port: PORT,
87
- do_caching: false,
88
- do_validation: false,
89
- tcp_pipelining: pipelining,
90
- packet_timeout: 10,
91
- tcp_pipelining_max_queries: 5,
92
- use_tcp: tcp)
93
- end
94
- start = Time.now
95
-
96
- # @todo@ On windows, MAX_FILES is 256. This means that we have to limit
97
- # this test while we're not using single sockets.
98
- # We run four queries per iteration, so we're limited to 64 runs.
99
- messages = TestSoakBase::Rrs.map do |data|
100
- message = Dnsruby::Message.new(data[:name], data[:type])
101
- message.do_validation = false
102
- message.do_caching = false
103
- message
56
+ queue = tracked_queue
57
+ resolvers = Array.new(num_resolvers) { create_resolver(tcp, pipelining) }
58
+ count = SINGLE_RESOLVER_QUERY_TIMES * SoakServer::TYPES.length
59
+ timed_case(count) do
60
+ count.times { |id| resolvers[id % num_resolvers].send_async(request_for(id), queue, id) }
61
+ receive_all(queue, count)
104
62
  end
63
+ end
105
64
 
106
- query_count = SINGLE_RESOLVER_QUERY_TIMES * messages.count
107
-
108
- receive_thread = Thread.new do
109
- query_count.times do
110
- _id, ret, error = q.pop
111
- if error.is_a?(Dnsruby::ResolvTimeout)
112
- timeout_count+=1
113
- elsif ret.class != Dnsruby::Message
114
- p "ERROR RETURNED : #{error}"
115
- end
116
- end
65
+ def test_many_threads_on_one_single_resolver_synchronous
66
+ resolver = create_resolver
67
+ run_threaded_queries do |id, _queue|
68
+ [id, resolver.query(query_name(id), query_type(id)), nil]
117
69
  end
70
+ end
118
71
 
119
- resolver_cycler = resolvers.cycle
120
-
121
- SINGLE_RESOLVER_QUERY_TIMES.times do |i|
122
- rr_count = 0
123
- messages.each do | message |
124
- rr_count += 1
125
- resolver_cycler.next.send_async(message, q, rr_count + i * messages.count)
126
- # p "Sent #{i}, #{rr_count}, Queue #{q}"
127
- end
72
+ def test_many_threads_on_many_single_resolvers
73
+ resolvers = Array.new(THREAD_COUNT) { create_resolver }
74
+ run_threaded_queries do |id, queue|
75
+ resolver = resolvers[id / (ROUNDS * SoakServer::TYPES.length)]
76
+ resolver.send_async(request_for(id), queue, id)
77
+ completion = queue.pop
78
+ raise "Wrong completion ID: expected #{id}, got #{completion[0].inspect}" unless completion[0] == id
79
+ completion
128
80
  end
81
+ end
129
82
 
130
- receive_thread.join
83
+ private
131
84
 
132
- time_taken = Time.now - start
133
- puts "Query count : #{query_count}, #{timeout_count} timed out. #{time_taken} time taken"
134
- assert(timeout_count < query_count * 0.1, "#{timeout_count} of #{query_count} timed out!")
85
+ def tracked_queue
86
+ queue = Queue.new
87
+ @queues << queue
88
+ queue
135
89
  end
136
90
 
137
- def test_many_threads_on_one_single_resolver_synchronous
138
- # Test multi-threaded behaviour
139
- # Check the header IDs to make sure they're all different
140
- threads = Array.new
141
-
142
- res = create_default_single_resolver
143
- ids = []
144
- mutex = Mutex.new
145
- timeout_count = 0
146
- query_count = 0
147
- res.packet_timeout=4
148
- start=Time.now
149
- # Windows limits us to 256 sockets
150
- num_times=250
151
- if (/java/ =~ RUBY_PLATFORM)
152
- # JRuby threads are native threads, so let's not go too mad!
153
- num_times=50
154
- end
155
- num_times.times do |i|
156
- threads[i] = Thread.new{
157
- 40.times do |j|
158
- TestSoakBase::Rrs.each do |data|
159
- mutex.synchronize { query_count += 1 }
160
- packet=nil
161
- begin
162
- packet = res.query(data[:name], data[:type])
163
- rescue Dnsruby::ResolvTimeout
164
- mutex.synchronize { timeout_count += 1 }
165
- next
166
- end
167
- assert(packet)
168
- ids.push(packet.header.id)
169
- assert_equal(packet.question[0].qclass, 'IN', 'Class correct' )
170
- end
171
- end
172
- }
173
- end
174
- threads.each do |thread|
175
- thread.join
176
- end
177
- stop=Time.now
178
- time_taken=stop-start
179
- puts "Query count : #{query_count}, #{timeout_count} timed out. #{time_taken} time taken"
180
- # check_ids(ids) # only do this if we expect all different IDs - e.g. if we stream over a single socket
181
- assert(timeout_count < query_count * 0.1, "#{timeout_count} of #{query_count} timed out!")
91
+ def create_resolver(tcp = false, pipelining = false)
92
+ resolver = Dnsruby::SingleResolver.new(server: SoakServer::IP,
93
+ port: @server.port, do_caching: false, do_validation: false,
94
+ dnssec: false, use_tcp: tcp, packet_timeout: 10,
95
+ tcp_pipelining: pipelining, tcp_pipelining_max_queries: 5)
96
+ assert_equal(pipelining, resolver.single_resolvers.first.tcp_pipelining)
97
+ @resolvers << resolver
98
+ resolver
182
99
  end
183
100
 
184
- def check_ids(ids)
185
- ids.sort!
186
- count = 0
187
- ids.each do |id|
188
- count+=1
189
- if (count < ids.length-1)
190
- assert(ids[count+1] != id, "Two identical header ids used!")
191
- end
192
- end
101
+ def query_name(id)
102
+ "q#{id}.soak.test."
193
103
  end
194
104
 
195
- def test_many_threads_on_many_single_resolvers
196
- # Test multi-threaded behaviour
197
- # @todo@ Check the header IDs to make sure they're all different
198
- threads = Array.new
199
- mutex = Mutex.new
200
- timeout_count = 0
201
- query_count = 0
202
- start=Time.now
203
- num_times=250
204
- if (/java/ =~ RUBY_PLATFORM)
205
- # JRuby threads are native threads, so let's not go too mad!
206
- num_times=50
207
- end
208
- num_times.times do |i|
209
- threads[i] = Thread.new{
210
- res = create_default_single_resolver
211
- 40.times do |j|
212
- TestSoakBase::Rrs.each do |data|
213
- mutex.synchronize do
214
- query_count+=1
215
- end
216
- q = Queue.new
105
+ def query_type(id)
106
+ SoakServer::TYPES[id % SoakServer::TYPES.length]
107
+ end
217
108
 
218
- message = Dnsruby::Message.new(data[:name], data[:type])
219
- message.do_validation = false
220
- message.do_caching = false
109
+ def request_for(id)
110
+ request = Dnsruby::Message.new(query_name(id), query_type(id))
111
+ request.do_caching = false
112
+ request.do_validation = false
113
+ request
114
+ end
221
115
 
222
- res.send_async(message, q, [i,j])
116
+ def timed_case(count)
117
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
118
+ Timeout.timeout(CASE_TIMEOUT) { yield }
119
+ @server.check!
120
+ puts "Verified #{count} responses in #{(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).round(3)}s"
121
+ rescue Timeout::Error
122
+ @server.check!
123
+ flunk "Soak case exceeded #{CASE_TIMEOUT}s; a query or worker did not complete"
124
+ end
223
125
 
224
- id, packet, error = q.pop
225
- if (error.class == Dnsruby::ResolvTimeout)
226
- mutex.synchronize {
227
- timeout_count+=1
228
- }
229
- next
230
- elsif (packet.class!=Dnsruby::Message)
231
- puts "ERROR! #{error}"
126
+ def run_threaded_queries
127
+ per_thread = ROUNDS * SoakServer::TYPES.length
128
+ completions = tracked_queue
129
+ queues = Array.new(THREAD_COUNT) { tracked_queue }
130
+ timed_case(THREAD_COUNT * per_thread) do
131
+ THREAD_COUNT.times do |worker_id|
132
+ @workers << Thread.new do
133
+ Thread.current.abort_on_exception = false
134
+ Thread.current.report_on_exception = false
135
+ begin
136
+ per_thread.times do |iteration|
137
+ id = worker_id * per_thread + iteration
138
+ completions << yield(id, queues[worker_id])
232
139
  end
233
-
234
- assert(packet)
235
- assert_equal(packet.question[0].qclass, 'IN', 'Class correct' )
140
+ rescue StandardError => error
141
+ completions << [nil, nil, error]
236
142
  end
237
143
  end
238
- }
144
+ end
145
+ receive_all(completions, THREAD_COUNT * per_thread)
146
+ @workers.each(&:join)
239
147
  end
240
- # NOTE: For methods on the objects taking no params, we can use this shorthand.
241
- threads.each(&:join)
242
-
243
- time_taken = Time.now - start
244
- puts "Query count : #{query_count}, #{timeout_count} timed out. #{time_taken} time taken"
245
- assert(timeout_count < query_count * 0.1, "#{timeout_count} of #{query_count} timed out!")
246
148
  end
247
149
 
248
- def create_default_single_resolver
249
- Dnsruby::SingleResolver.new(server: IP,
250
- port: PORT,
251
- do_caching: false,
252
- do_validation: false,
253
- packet_timeout: 10)
254
-
150
+ def receive_all(queue, count)
151
+ seen = {}
152
+ count.times do
153
+ id, response, error = queue.pop
154
+ assert_nil(error, "Query #{id.inspect}: #{error.class}: #{error}")
155
+ assert(id.is_a?(Integer) && id >= 0 && id < count, "Unexpected query ID #{id.inspect}")
156
+ refute(seen.key?(id), "Duplicate completion #{id}")
157
+ seen[id] = true
158
+ assert_instance_of(Dnsruby::Message, response)
159
+ assert_equal('NOERROR', response.rcode.to_s)
160
+ assert_equal(1, response.question.length)
161
+ question = response.question.first
162
+ assert_equal([query_name(id), query_type(id), 'IN'],
163
+ [question.qname.to_s(true), question.qtype.to_s, question.qclass.to_s])
164
+ assert_equal(1, response.answer.length)
165
+ answer = response.answer.first
166
+ assert_equal([query_name(id), query_type(id), 'IN'],
167
+ [answer.name.to_s(true), answer.type.to_s, answer.klass.to_s])
168
+ case query_type(id)
169
+ when 'A' then assert_equal(SoakServer::ADDRESS, answer.address.to_s)
170
+ when 'MX' then assert_equal([10, SoakServer::EXCHANGE], [answer.preference, answer.exchange.to_s(true)])
171
+ when 'CNAME' then assert_equal(SoakServer::ALIAS, answer.domainname.to_s(true))
172
+ when 'TXT' then assert_equal([SoakServer::TEXT], answer.strings)
173
+ end
174
+ end
175
+ assert_equal(count, seen.length)
176
+ assert(queue.empty?, 'Extra/duplicate completion after the expected count')
255
177
  end
256
178
  end
@@ -0,0 +1,90 @@
1
+ require_relative 'spec_helper'
2
+ require_relative 'socket_helpers'
3
+ require 'minitest/mock'
4
+
5
+ class TestSocketHelpers < Minitest::Test
6
+ def setup
7
+ @tcp_sockets = []
8
+ @udp_sockets = []
9
+ end
10
+
11
+ def teardown
12
+ (@tcp_sockets + @udp_sockets).each { |socket| socket.close unless socket.closed? }
13
+ end
14
+
15
+ def test_collision_retries_without_releasing_the_successful_pair
16
+ blocker, first_tcp = TestSockets.udp_tcp_pair
17
+ blocked_port = blocker.addr[1]
18
+ tcp_new = TCPServer.method(:new)
19
+ udp_new = UDPSocket.method(:new)
20
+ tcp_factory = lambda do |*args|
21
+ socket = @tcp_sockets.empty? ? first_tcp : tcp_new.call(*args)
22
+ @tcp_sockets << socket
23
+ socket
24
+ end
25
+ udp_factory = ->(*args) { udp_new.call(*args).tap { |socket| @udp_sockets << socket } }
26
+ TCPServer.stub(:new, tcp_factory) do
27
+ UDPSocket.stub(:new, udp_factory) do
28
+ udp, tcp = TestSockets.udp_tcp_pair
29
+ assert_operator(@tcp_sockets.length, :>=, 2)
30
+ assert(first_tcp.closed?)
31
+ assert(@udp_sockets.first.closed?)
32
+ assert_equal(udp.addr[1], tcp.addr[1])
33
+ refute_equal(blocked_port, tcp.addr[1])
34
+ refute(udp.closed?)
35
+ refute(tcp.closed?)
36
+ refute(blocker.closed?)
37
+ end
38
+ end
39
+ ensure
40
+ blocker&.close unless blocker&.closed?
41
+ first_tcp&.close unless first_tcp&.closed?
42
+ end
43
+
44
+ def test_repeated_collisions_are_bounded_and_close_every_attempt
45
+ with_bind_error(Errno::EADDRINUSE) do
46
+ assert_raises(Errno::EADDRINUSE) { TestSockets.udp_tcp_pair }
47
+ end
48
+ assert_equal(10, @tcp_sockets.length)
49
+ assert_equal(10, @udp_sockets.length)
50
+ assert((@tcp_sockets + @udp_sockets).all?(&:closed?))
51
+ end
52
+
53
+ def test_other_bind_errors_are_not_retried
54
+ with_bind_error(Errno::EACCES) do
55
+ assert_raises(Errno::EACCES) { TestSockets.udp_tcp_pair }
56
+ end
57
+ assert_equal(1, @tcp_sockets.length)
58
+ assert_equal(1, @udp_sockets.length)
59
+ assert((@tcp_sockets + @udp_sockets).all?(&:closed?))
60
+ end
61
+
62
+ def test_udp_creation_failure_releases_the_tcp_listener
63
+ tcp_new = TCPServer.method(:new)
64
+ factory = ->(*args) { tcp_new.call(*args).tap { |socket| @tcp_sockets << socket } }
65
+ TCPServer.stub(:new, factory) do
66
+ UDPSocket.stub(:new, ->(*) { raise Errno::EMFILE }) do
67
+ assert_raises(Errno::EMFILE) { TestSockets.udp_tcp_pair }
68
+ end
69
+ end
70
+ assert_equal(1, @tcp_sockets.length)
71
+ assert(@tcp_sockets.first.closed?)
72
+ end
73
+
74
+ private
75
+
76
+ def with_bind_error(error)
77
+ tcp_new = TCPServer.method(:new)
78
+ udp_new = UDPSocket.method(:new)
79
+ tcp_factory = ->(*args) { tcp_new.call(*args).tap { |socket| @tcp_sockets << socket } }
80
+ udp_factory = lambda do |*args|
81
+ udp_new.call(*args).tap do |socket|
82
+ @udp_sockets << socket
83
+ socket.define_singleton_method(:bind) { |*| raise error }
84
+ end
85
+ end
86
+ TCPServer.stub(:new, tcp_factory) do
87
+ UDPSocket.stub(:new, udp_factory) { yield }
88
+ end
89
+ end
90
+ end