dnsruby 1.74.0 → 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 +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -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 +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- 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 +19 -0
- 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/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 +979 -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 +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -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 +105 -10
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class TestResolverConcurrency < Minitest::Test
|
|
4
|
+
def test_busy_resolver_dispatches_a_new_query_without_waiting_for_a_tick
|
|
5
|
+
isolated(<<~'RUBY')
|
|
6
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
7
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
8
|
+
writer = resolver.single_resolvers.first
|
|
9
|
+
sent = []
|
|
10
|
+
writer.define_singleton_method(:send_async) { |_message, _queue, id| sent << id }
|
|
11
|
+
slow_results, fast_results = Queue.new, Queue.new
|
|
12
|
+
resolver.send_async(query('pending.example.'), slow_results, :pending)
|
|
13
|
+
raise 'first query not dispatched' unless sent.size == 1
|
|
14
|
+
resolver.query_timeout = 0.1
|
|
15
|
+
resolver.send_async(query('fast.example.'), fast_results, :fast)
|
|
16
|
+
raise 'busy resolver left new query waiting for a tick' unless sent.size == 2
|
|
17
|
+
fast = SELECTOR.query_settings_for_id(sent.last)
|
|
18
|
+
answer = fast.query.clone
|
|
19
|
+
answer.header.qr = true
|
|
20
|
+
SELECTOR.push_response_for_query(fast, answer, fast.query, writer)
|
|
21
|
+
SELECTOR.send_queued_responses
|
|
22
|
+
raise 'fast query did not finish independently' unless fast_results.size == 1 &&
|
|
23
|
+
fast_results.pop.last.nil? && slow_results.empty?
|
|
24
|
+
SELECTOR.send_tick_to_observers
|
|
25
|
+
raise 'selector tick resent initial attempt' unless sent.size == 2
|
|
26
|
+
resolver.close
|
|
27
|
+
RUBY
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_concurrent_admission_and_selector_ticks_dispatch_each_attempt_once
|
|
31
|
+
isolated(<<~'RUBY')
|
|
32
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
33
|
+
counts, lock = Hash.new(0), Mutex.new
|
|
34
|
+
resolver.single_resolvers.first.define_singleton_method(:send_async) do |_message, _queue, id|
|
|
35
|
+
lock.synchronize { counts[id[2]] += 1 }
|
|
36
|
+
Thread.pass
|
|
37
|
+
end
|
|
38
|
+
gate = Queue.new
|
|
39
|
+
callers = 20.times.map do |index|
|
|
40
|
+
Thread.new { gate.pop; resolver.send_async(query("parallel#{index}.example."), Queue.new, index) }
|
|
41
|
+
end
|
|
42
|
+
ticking = Thread.new { 100.times { SELECTOR.send_tick_to_observers; Thread.pass } }
|
|
43
|
+
callers.length.times { gate << true }
|
|
44
|
+
callers.each(&:value)
|
|
45
|
+
ticking.value
|
|
46
|
+
raise 'lost or duplicate initial attempts' unless counts.size == callers.size && counts.values.all? { |count| count == 1 }
|
|
47
|
+
resolver.close
|
|
48
|
+
RUBY
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_concurrent_first_use_publishes_one_complete_configuration_and_engine
|
|
52
|
+
isolated(<<~'RUBY')
|
|
53
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
54
|
+
Dnsruby::Config.define_singleton_method(:default_config_hash) { { nameserver: ['127.0.0.1'] } }
|
|
55
|
+
original = Dnsruby::PacketSender.method(:new)
|
|
56
|
+
entered, release = Queue.new, Queue.new
|
|
57
|
+
creations = 0
|
|
58
|
+
Dnsruby::PacketSender.define_singleton_method(:new) do |*args|
|
|
59
|
+
creations += 1
|
|
60
|
+
entered << true
|
|
61
|
+
release.pop
|
|
62
|
+
original.call(*args).tap do |sender|
|
|
63
|
+
sender.define_singleton_method(:send_async) do |message, queue, id|
|
|
64
|
+
answer = message.clone
|
|
65
|
+
answer.header.qr = true
|
|
66
|
+
SELECTOR.push_response_to_select(id, queue, answer, message, self)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
resolver = Dnsruby::Resolver.new
|
|
71
|
+
resolver.dnssec = false
|
|
72
|
+
queues = [Queue.new, Queue.new]
|
|
73
|
+
first = Thread.new { resolver.send_async(query('first.example.'), queues[0], :first) }
|
|
74
|
+
entered.pop
|
|
75
|
+
second = Thread.new { resolver.send_async(query('second.example.'), queues[1], :second) }
|
|
76
|
+
Thread.pass until second.status == 'sleep'
|
|
77
|
+
raise 'partial configuration published' if resolver.instance_variable_get(:@configured)
|
|
78
|
+
release << true
|
|
79
|
+
first.value
|
|
80
|
+
second.value
|
|
81
|
+
SELECTOR.send_tick_to_observers
|
|
82
|
+
SELECTOR.send_queued_responses
|
|
83
|
+
raise 'initialized more than once' unless creations == 1 && resolver.single_resolvers.length == 1
|
|
84
|
+
queues.each do |queue|
|
|
85
|
+
raise 'missing or duplicate completion' unless queue.size == 1
|
|
86
|
+
raise 'cold query failed' unless queue.pop.last.nil?
|
|
87
|
+
end
|
|
88
|
+
resolver.close
|
|
89
|
+
RUBY
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_sender_publication_uses_settings_changed_during_construction
|
|
93
|
+
isolated(<<~'RUBY')
|
|
94
|
+
original = Dnsruby::PacketSender.method(:new)
|
|
95
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
96
|
+
entered, release = Queue.new, Queue.new
|
|
97
|
+
Dnsruby::PacketSender.define_singleton_method(:new) do |*args|
|
|
98
|
+
original.call(*args).tap do
|
|
99
|
+
entered << true
|
|
100
|
+
release.pop
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
reconfigure = Thread.new { resolver.nameserver = '127.0.0.2' }
|
|
104
|
+
entered.pop
|
|
105
|
+
resolver.port = 1053
|
|
106
|
+
resolver.use_tcp = true
|
|
107
|
+
resolver.no_tcp = true
|
|
108
|
+
resolver.tcp_pipelining = true
|
|
109
|
+
resolver.tcp_pipelining_max_queries = 11
|
|
110
|
+
resolver.packet_timeout = 3
|
|
111
|
+
resolver.tsig = ['example.', 'c2VjcmV0']
|
|
112
|
+
resolver.ignore_truncation = true
|
|
113
|
+
resolver.src_address = '127.0.0.3'
|
|
114
|
+
resolver.src_address6 = '::1'
|
|
115
|
+
resolver.src_port = 50123
|
|
116
|
+
resolver.recurse = false
|
|
117
|
+
resolver.udp_size = 1452
|
|
118
|
+
resolver.dnssec = true
|
|
119
|
+
release << true
|
|
120
|
+
reconfigure.value
|
|
121
|
+
sender = resolver.single_resolvers.first
|
|
122
|
+
[:port, :use_tcp, :no_tcp, :tcp_pipelining, :tcp_pipelining_max_queries,
|
|
123
|
+
:packet_timeout, :ignore_truncation, :src_address, :src_port, :recurse,
|
|
124
|
+
:udp_size, :dnssec].each do |option|
|
|
125
|
+
raise "stale #{option}" unless sender.public_send(option) == resolver.public_send(option)
|
|
126
|
+
end
|
|
127
|
+
raise 'stale IPv6 source' unless sender.instance_variable_get(:@src_address6) == '::1'
|
|
128
|
+
raise 'stale TSIG key' unless sender.tsig.name.to_s == 'example' &&
|
|
129
|
+
sender.tsig.algorithm.to_s == 'hmac-sha256'
|
|
130
|
+
resolver.close
|
|
131
|
+
RUBY
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_initialization_failure_is_shared_and_an_explicit_reconfiguration_recovers
|
|
135
|
+
isolated(<<~'RUBY')
|
|
136
|
+
Thread.report_on_exception = false
|
|
137
|
+
Dnsruby::Config.define_singleton_method(:default_config_hash) { { nameserver: ['127.0.0.1'] } }
|
|
138
|
+
original = Dnsruby::PacketSender.method(:new)
|
|
139
|
+
entered, release = Queue.new, Queue.new
|
|
140
|
+
creations = 0
|
|
141
|
+
Dnsruby::PacketSender.define_singleton_method(:new) do |*|
|
|
142
|
+
creations += 1
|
|
143
|
+
entered << true
|
|
144
|
+
release.pop
|
|
145
|
+
raise ArgumentError, 'unusable nameserver'
|
|
146
|
+
end
|
|
147
|
+
resolver = Dnsruby::Resolver.new
|
|
148
|
+
resolver.query_timeout = 0.02
|
|
149
|
+
queues = [Queue.new, Queue.new]
|
|
150
|
+
first = Thread.new { resolver.send_async(query, queues[0], :first) }
|
|
151
|
+
entered.pop
|
|
152
|
+
second = Thread.new { resolver.send_async(query, queues[1], :second) }
|
|
153
|
+
Thread.pass until second.status == 'sleep'
|
|
154
|
+
release << true
|
|
155
|
+
first.value
|
|
156
|
+
second.value
|
|
157
|
+
raise 'failed initialization repeated' unless creations == 1
|
|
158
|
+
sleep 0.03
|
|
159
|
+
SELECTOR.send_tick_to_observers
|
|
160
|
+
queues.each do |queue|
|
|
161
|
+
raise 'failed configuration changed async completion' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
162
|
+
end
|
|
163
|
+
SELECTOR.send_tick_to_observers
|
|
164
|
+
raise 'initialization failure completed twice' unless queues.all?(&:empty?)
|
|
165
|
+
Dnsruby::PacketSender.define_singleton_method(:new, original)
|
|
166
|
+
resolver.nameserver = '127.0.0.1'
|
|
167
|
+
raise 'explicit reconfiguration failed' unless resolver.single_resolvers.length == 1
|
|
168
|
+
raise 'failure retained' if resolver.instance_variable_get(:@configuration_error)
|
|
169
|
+
RUBY
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_an_explicitly_empty_resolver_completes_each_query_once_without_a_timer_thread
|
|
173
|
+
isolated(<<~'RUBY')
|
|
174
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
175
|
+
resolver = Dnsruby::Resolver.new
|
|
176
|
+
resolver.single_resolvers = []
|
|
177
|
+
resolver.query_timeout = 0.02
|
|
178
|
+
queue = Queue.new
|
|
179
|
+
resolver.send_async(query, queue, :empty)
|
|
180
|
+
sleep 0.03
|
|
181
|
+
SELECTOR.send_tick_to_observers
|
|
182
|
+
raise 'empty resolver did not time out once' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
183
|
+
SELECTOR.send_tick_to_observers
|
|
184
|
+
raise 'empty resolver completed twice' unless queue.empty?
|
|
185
|
+
resolver.close
|
|
186
|
+
RUBY
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_hostname_reconfiguration_does_not_hold_the_selector_callback_lock
|
|
190
|
+
isolated(<<~'RUBY')
|
|
191
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
192
|
+
parent = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
193
|
+
parent.single_resolvers.first.define_singleton_method(:send_async) { |*| }
|
|
194
|
+
parent.send_async(query('pending.example.'), Queue.new, :pending)
|
|
195
|
+
bootstrap = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
196
|
+
queued = Queue.new
|
|
197
|
+
bootstrap.single_resolvers.first.define_singleton_method(:send_async) do |message, queue, id|
|
|
198
|
+
answer = message.clone
|
|
199
|
+
answer.header.qr = true
|
|
200
|
+
answer.add_answer(Dnsruby::RR.create(name: message.question.first.qname, type: 'A', address: '127.0.0.2'))
|
|
201
|
+
SELECTOR.push_response_to_select(id, queue, answer, message, self)
|
|
202
|
+
queued << true
|
|
203
|
+
end
|
|
204
|
+
Dnsruby::Config.define_singleton_method(:nameserver_resolver) { bootstrap }
|
|
205
|
+
reconfigure = Thread.new { parent.nameserver = 'resolver.example.' }
|
|
206
|
+
queued.pop
|
|
207
|
+
# A selector loop ticks observers before dispatching the bootstrap answer.
|
|
208
|
+
# Holding the parent query mutex while joining the constructor deadlocks.
|
|
209
|
+
SELECTOR.send_tick_to_observers
|
|
210
|
+
SELECTOR.send_queued_responses
|
|
211
|
+
reconfigure.value
|
|
212
|
+
raise 'hostname was not configured' unless parent.single_resolvers.first.server == '127.0.0.2'
|
|
213
|
+
parent.close
|
|
214
|
+
bootstrap.close
|
|
215
|
+
RUBY
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def test_every_configuration_entry_point_recovers_an_initially_empty_resolver
|
|
219
|
+
isolated(<<~'RUBY')
|
|
220
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
221
|
+
[:add_server, :single_resolvers=, :nameserver=].each do |entry|
|
|
222
|
+
resolver = Dnsruby::Resolver.new(nameserver: [], dnssec: false)
|
|
223
|
+
raise 'fixture did not record empty configuration' unless resolver.instance_variable_get(:@configuration_error)
|
|
224
|
+
value = entry == :single_resolvers= ? [Dnsruby::PacketSender.new(server: '127.0.0.1')] : '127.0.0.1'
|
|
225
|
+
resolver.public_send(entry, value)
|
|
226
|
+
resolver.add_config_nameservers
|
|
227
|
+
raise 'failed initialization retained after recovery' if resolver.instance_variable_get(:@configuration_error)
|
|
228
|
+
raise 'server missing after recovery' unless resolver.single_resolvers.size == 1
|
|
229
|
+
resolver.single_resolvers.first.define_singleton_method(:send_async) do |message, queue, id|
|
|
230
|
+
answer = message.clone
|
|
231
|
+
answer.header.qr = true
|
|
232
|
+
SELECTOR.push_response_to_select(id, queue, answer, message, self)
|
|
233
|
+
end
|
|
234
|
+
queue = Queue.new
|
|
235
|
+
resolver.send_async(query, queue, :recovered)
|
|
236
|
+
SELECTOR.send_queued_responses
|
|
237
|
+
raise 'recovered query failed' unless queue.size == 1 && queue.pop.last.nil?
|
|
238
|
+
resolver.close
|
|
239
|
+
end
|
|
240
|
+
RUBY
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def test_close_cannot_observe_a_query_before_its_selector_observer_exists
|
|
244
|
+
isolated(<<~'RUBY')
|
|
245
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
246
|
+
resolver.single_resolvers.first.define_singleton_method(:send_async) { |*| }
|
|
247
|
+
entered, release, errors = Queue.new, Queue.new, Queue.new
|
|
248
|
+
add_observer = SELECTOR.method(:add_observer)
|
|
249
|
+
SELECTOR.define_singleton_method(:add_observer) do |*args|
|
|
250
|
+
entered << true
|
|
251
|
+
release.pop
|
|
252
|
+
add_observer.call(*args)
|
|
253
|
+
end
|
|
254
|
+
queue = Queue.new
|
|
255
|
+
sending = Thread.new { resolver.send_async(query, queue, :closing) }
|
|
256
|
+
entered.pop
|
|
257
|
+
closing = Thread.new do
|
|
258
|
+
resolver.close
|
|
259
|
+
rescue StandardError => error
|
|
260
|
+
errors << error
|
|
261
|
+
end
|
|
262
|
+
Thread.pass until !closing.alive? || closing.status == 'sleep'
|
|
263
|
+
raise 'close saw an unpublished observer' unless closing.alive? && errors.empty?
|
|
264
|
+
release << true
|
|
265
|
+
sending.value
|
|
266
|
+
closing.value
|
|
267
|
+
raise 'concurrent close raised' unless errors.empty?
|
|
268
|
+
raise 'query did not complete once' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
|
|
269
|
+
raise 'observer retained after close' unless K.class_variable_get(:@@observers).empty?
|
|
270
|
+
RUBY
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def test_close_and_id_reuse_ignore_an_old_received_response
|
|
274
|
+
assert_stale_query_event_is_ignored(:RECEIVED)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def test_close_and_id_reuse_ignore_an_old_received_error
|
|
278
|
+
assert_stale_query_event_is_ignored(:RECEIVED, :ServFail)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_close_and_id_reuse_ignore_an_old_transport_error
|
|
282
|
+
assert_stale_query_event_is_ignored(:ERROR, :ServFail)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def test_close_and_id_reuse_ignore_an_old_validated_response
|
|
286
|
+
assert_stale_query_event_is_ignored(:VALIDATED)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_close_and_id_reuse_ignore_an_old_validation_error
|
|
290
|
+
assert_stale_query_event_is_ignored(:VALIDATED, :ServFail)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
private
|
|
294
|
+
|
|
295
|
+
def assert_stale_query_event_is_ignored(event_type, error_type = nil)
|
|
296
|
+
isolated(<<~RUBY + <<~'RUBY')
|
|
297
|
+
stale_event_type = Dnsruby::Resolver::EventType::#{event_type}
|
|
298
|
+
stale_error = #{error_type ? "Dnsruby::#{error_type}.new" : 'nil'}
|
|
299
|
+
RUBY
|
|
300
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
301
|
+
Thread.report_on_exception = false
|
|
302
|
+
options = { nameserver: '127.0.0.1', dnssec: false, do_caching: false,
|
|
303
|
+
packet_timeout: 60, query_timeout: 60 }
|
|
304
|
+
resolver = Dnsruby::Resolver.new(options)
|
|
305
|
+
unrelated = Dnsruby::Resolver.new(options)
|
|
306
|
+
sends = Queue.new
|
|
307
|
+
[resolver, unrelated].each do |parent|
|
|
308
|
+
parent.single_resolvers.first.define_singleton_method(:send_async) do |message, queue, id|
|
|
309
|
+
sends << [message, queue, id, self]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
queue, keeper_queue, unrelated_queue = Queue.new, Queue.new, Queue.new
|
|
313
|
+
resolver.send_async(query('old.example.'), queue, :reused)
|
|
314
|
+
old_message, old_queue, old_id, = sends.pop
|
|
315
|
+
response = old_message.clone
|
|
316
|
+
response.header.qr = true
|
|
317
|
+
response.header.rcode = Dnsruby::RCode::SERVFAIL if stale_error
|
|
318
|
+
old_queue << [old_id, stale_event_type, response, stale_error]
|
|
319
|
+
|
|
320
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
321
|
+
original = engine.method(:handle_queue_event)
|
|
322
|
+
entered, release = Queue.new, Queue.new
|
|
323
|
+
engine.define_singleton_method(:handle_queue_event) do |*args|
|
|
324
|
+
if args.first.equal?(old_queue)
|
|
325
|
+
entered << true
|
|
326
|
+
release.pop
|
|
327
|
+
end
|
|
328
|
+
original.call(*args)
|
|
329
|
+
end
|
|
330
|
+
callback = Thread.new do
|
|
331
|
+
# Exercise the real observer lookup, then pause the callback before it
|
|
332
|
+
# can acquire the resolver lock. close cannot retract that lookup.
|
|
333
|
+
SELECTOR.notify_queue_observers(old_queue, old_id)
|
|
334
|
+
raise 'stale event completed the replacement query' unless queue.empty?
|
|
335
|
+
pending = engine.instance_variable_get(:@timeouts)[:reused]
|
|
336
|
+
replacement = engine.instance_variable_get(:@query_list)[:reused]
|
|
337
|
+
raise 'stale event stopped the replacement send' unless pending && replacement &&
|
|
338
|
+
replacement[3].size == 1 && SELECTOR.query_settings_for_id(replacement[3].first)
|
|
339
|
+
|
|
340
|
+
# The same selector dispatch must still serve the replacement and a
|
|
341
|
+
# different Resolver. No sockets or timing-dependent timeout is needed.
|
|
342
|
+
SELECTOR.send_tick_to_observers
|
|
343
|
+
until sends.empty?
|
|
344
|
+
message, select_queue, id, sender = sends.pop
|
|
345
|
+
answer = message.clone
|
|
346
|
+
answer.header.qr = true
|
|
347
|
+
SELECTOR.push_response_to_select(id, select_queue, answer, message, sender)
|
|
348
|
+
end
|
|
349
|
+
SELECTOR.send_queued_responses
|
|
350
|
+
end
|
|
351
|
+
entered.pop
|
|
352
|
+
resolver.close
|
|
353
|
+
raise 'close did not complete the old query once' unless
|
|
354
|
+
queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
|
|
355
|
+
|
|
356
|
+
# Both new queries dispatch immediately, even while the old observer's
|
|
357
|
+
# callback is paused. That stale callback must preserve their attempts.
|
|
358
|
+
resolver.send_async(query('keeper.example.'), keeper_queue, :keeper)
|
|
359
|
+
resolver.send_async(query('new.example.'), queue, :reused)
|
|
360
|
+
unrelated.send_async(query('unrelated.example.'), unrelated_queue, :unrelated)
|
|
361
|
+
release << true
|
|
362
|
+
callback.value
|
|
363
|
+
[[queue, :reused, 'new.example'], [keeper_queue, :keeper, 'keeper.example'],
|
|
364
|
+
[unrelated_queue, :unrelated, 'unrelated.example']].each do |results, expected_id, name|
|
|
365
|
+
raise 'query did not complete exactly once' unless results.size == 1
|
|
366
|
+
id, answer, error = results.pop
|
|
367
|
+
raise 'wrong query completion' unless id == expected_id && error.nil? &&
|
|
368
|
+
answer.question.first.qname.to_s == name
|
|
369
|
+
end
|
|
370
|
+
raise 'completed queries retained selector ownership' unless K.class_variable_get(:@@query_hash).empty?
|
|
371
|
+
resolver.close
|
|
372
|
+
unrelated.close
|
|
373
|
+
RUBY
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def isolated(body)
|
|
377
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
378
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
379
|
+
File.expand_path('../lib', __dir__), '-e', "#{TestPipelineSend::FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend")
|
|
380
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
381
|
+
assert_empty(stderr)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
|
|
4
|
+
class ResolverConfigurationContractsTest < Minitest::Test
|
|
5
|
+
def test_single_resolver_keeps_only_its_configured_server
|
|
6
|
+
Dnsruby::Config.stub(:default_config_hash, {nameserver: %w[192.0.2.1 192.0.2.2]}) do
|
|
7
|
+
resolver = Dnsruby::SingleResolver.new
|
|
8
|
+
assert_equal(['192.0.2.1'], resolver.single_resolvers.map(&:server))
|
|
9
|
+
resolver.server = '198.51.100.1'
|
|
10
|
+
assert_equal(['198.51.100.1'], resolver.single_resolvers.map(&:server))
|
|
11
|
+
assert_equal('198.51.100.1', resolver.server)
|
|
12
|
+
assert_equal(['198.51.100.1'], resolver.config.nameserver)
|
|
13
|
+
ensure
|
|
14
|
+
resolver&.close
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_zero_attempt_plan_completes_once_without_sending
|
|
19
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', retry_times: 0, query_timeout: 0, do_caching: false)
|
|
20
|
+
queue = Queue.new
|
|
21
|
+
resolver.single_resolvers.first.define_singleton_method(:prepare_send_async) { |*| raise 'Unexpected send' }
|
|
22
|
+
resolver.send_async(Dnsruby::Message.new('example.'), queue, :empty)
|
|
23
|
+
id, response, error = Timeout.timeout(1) { queue.pop }
|
|
24
|
+
assert_equal(:empty, id)
|
|
25
|
+
assert_nil(response)
|
|
26
|
+
assert_kind_of(Dnsruby::ResolvTimeout, error)
|
|
27
|
+
assert(queue.empty?)
|
|
28
|
+
ensure
|
|
29
|
+
resolver&.close
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class TestResolverValidation < Minitest::Test
|
|
4
|
+
FIXTURE = <<~'RUBY'
|
|
5
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
6
|
+
clock = 1000.0
|
|
7
|
+
# Advance slightly on reads so an immediately scheduled send is due on
|
|
8
|
+
# the next tick, without sleeps or dependence on the host's clock.
|
|
9
|
+
Dnsruby::TransportClock.define_singleton_method(:now) { clock += 0.0001 }
|
|
10
|
+
validators = []
|
|
11
|
+
Dnsruby::ValidatorThread.define_method(:run) { validators << self }
|
|
12
|
+
Dnsruby::Dnssec.define_singleton_method(:validate_with_query) do |_query, answer|
|
|
13
|
+
answer.security_level = Dnsruby::Message::SecurityLevel::SECURE
|
|
14
|
+
end
|
|
15
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['127.0.0.1', '127.0.0.2'],
|
|
16
|
+
dnssec: true, do_validation: true, do_caching: false,
|
|
17
|
+
retry_times: 2, retry_delay: 1, query_timeout: 10)
|
|
18
|
+
resolver.single_resolvers.each { |writer| writer.define_singleton_method(:send_async) { |*| } }
|
|
19
|
+
results = Queue.new
|
|
20
|
+
|
|
21
|
+
def start_query(resolver, results, name = 'validation.example.')
|
|
22
|
+
request = query(name)
|
|
23
|
+
request.do_validation = true
|
|
24
|
+
resolver.send_async(request, results, :client)
|
|
25
|
+
SELECTOR.send_tick_to_observers
|
|
26
|
+
request
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def accept_answer(settings, rcode = Dnsruby::RCode::NOERROR)
|
|
30
|
+
answer = settings.query.clone
|
|
31
|
+
answer.header.qr = true
|
|
32
|
+
answer.header.rcode = rcode
|
|
33
|
+
SELECTOR.push_response_for_query(settings, answer, settings.query, settings.single_resolver)
|
|
34
|
+
SELECTOR.send_queued_responses
|
|
35
|
+
answer
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def attempts
|
|
39
|
+
K.class_variable_get(:@@query_hash).values
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def expect_completion(results, error_class = nil, name = 'validation.example')
|
|
43
|
+
raise 'missing or duplicate completion' unless results.size == 1
|
|
44
|
+
id, answer, error = results.pop
|
|
45
|
+
raise 'wrong completion id' unless id == :client
|
|
46
|
+
if error_class
|
|
47
|
+
raise "wrong error: #{error.inspect}" unless error.is_a?(error_class)
|
|
48
|
+
else
|
|
49
|
+
raise "unexpected error: #{error.inspect}" if error
|
|
50
|
+
raise 'wrong answer' unless answer.question.first.qname.to_s == name
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def expect_clean(resolver)
|
|
55
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
56
|
+
tables = [:@query_list, :@timeouts]
|
|
57
|
+
tables << :@validation_queries if engine.instance_variable_defined?(:@validation_queries)
|
|
58
|
+
tables.each do |table|
|
|
59
|
+
raise "retained #{table}" unless engine.instance_variable_get(table).empty?
|
|
60
|
+
end
|
|
61
|
+
raise 'retained attempts' unless attempts.empty?
|
|
62
|
+
raise 'retained observer' unless K.class_variable_get(:@@observers).empty?
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
|
|
66
|
+
def test_total_deadline_applies_during_positive_and_nxdomain_validation
|
|
67
|
+
[0, 3].each do |rcode|
|
|
68
|
+
isolated("rcode = #{rcode}\n" + <<~'RUBY')
|
|
69
|
+
start_query(resolver, results)
|
|
70
|
+
accept_answer(attempts.first, rcode)
|
|
71
|
+
raise 'validation not pending' unless validators.size == 1 && results.empty?
|
|
72
|
+
clock += 11
|
|
73
|
+
SELECTOR.send_tick_to_observers
|
|
74
|
+
expect_completion(results, Dnsruby::ResolvTimeout)
|
|
75
|
+
validators.shift.do_validate
|
|
76
|
+
SELECTOR.send_queued_validation_responses
|
|
77
|
+
raise 'late validation completed twice' unless results.empty?
|
|
78
|
+
expect_clean(resolver)
|
|
79
|
+
RUBY
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_validation_receives_the_original_transmission_budget
|
|
84
|
+
isolated(<<~'RUBY')
|
|
85
|
+
start_query(resolver, results)
|
|
86
|
+
settings = attempts.first
|
|
87
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
88
|
+
original = engine.instance_variable_get(:@transmission_budgets).fetch(:client)
|
|
89
|
+
accept_answer(settings)
|
|
90
|
+
raise 'validation lost transmission history' unless
|
|
91
|
+
validators.first.instance_variable_get(:@transmission_budget).equal?(original)
|
|
92
|
+
raise 'replacement queue inherited old transmission history' if
|
|
93
|
+
engine.validation_transmission_budget(Queue.new, settings.client_query_id)
|
|
94
|
+
resolver.close
|
|
95
|
+
RUBY
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_validation_finishing_after_deadline_times_out_even_before_next_tick
|
|
99
|
+
isolated(<<~'RUBY')
|
|
100
|
+
start_query(resolver, results)
|
|
101
|
+
accept_answer(attempts.first)
|
|
102
|
+
clock += 11
|
|
103
|
+
validators.shift.do_validate
|
|
104
|
+
SELECTOR.send_queued_validation_responses
|
|
105
|
+
expect_completion(results, Dnsruby::ResolvTimeout)
|
|
106
|
+
expect_clean(resolver)
|
|
107
|
+
RUBY
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_competing_error_or_answer_cannot_replace_the_accepted_validator
|
|
111
|
+
[:servfail, :socket_error, :positive, :nxdomain, :validation_error].each do |event|
|
|
112
|
+
isolated("competing = :#{event}\n" + <<~'RUBY')
|
|
113
|
+
start_query(resolver, results)
|
|
114
|
+
clock += 0.6
|
|
115
|
+
SELECTOR.send_tick_to_observers
|
|
116
|
+
first, second = attempts
|
|
117
|
+
raise 'second server was not queried' unless first && second
|
|
118
|
+
accept_answer(first)
|
|
119
|
+
raise 'competing sends not canceled' unless attempts.empty?
|
|
120
|
+
clock += 3
|
|
121
|
+
SELECTOR.send_tick_to_observers
|
|
122
|
+
raise 'retry continued during validation' unless attempts.empty?
|
|
123
|
+
|
|
124
|
+
# A reply/error may already have been claimed and queued when the first
|
|
125
|
+
# answer wins. Exercise those callbacks even though its send is canceled.
|
|
126
|
+
response = second.query.clone
|
|
127
|
+
response.header.qr = true
|
|
128
|
+
kind = Dnsruby::Resolver::EventType::RECEIVED
|
|
129
|
+
error = case competing
|
|
130
|
+
when :servfail then Dnsruby::ServFail.new
|
|
131
|
+
when :socket_error
|
|
132
|
+
kind = Dnsruby::Resolver::EventType::ERROR
|
|
133
|
+
Dnsruby::OtherResolvError.new('peer closed')
|
|
134
|
+
when :nxdomain
|
|
135
|
+
response.header.rcode = Dnsruby::RCode::NXDOMAIN
|
|
136
|
+
Dnsruby::NXDomain.new
|
|
137
|
+
when :validation_error
|
|
138
|
+
kind = Dnsruby::Resolver::EventType::VALIDATED
|
|
139
|
+
Dnsruby::VerifyError.new('other answer failed')
|
|
140
|
+
end
|
|
141
|
+
second.client_queue << [second.client_query_id, kind, response, error]
|
|
142
|
+
SELECTOR.notify_queue_observers(second.client_queue, second.client_query_id)
|
|
143
|
+
if competing == :positive || competing == :nxdomain
|
|
144
|
+
SELECTOR.push_validation_response_to_select(second.client_query_id,
|
|
145
|
+
second.client_queue, response, nil, second.query, second.single_resolver)
|
|
146
|
+
SELECTOR.send_queued_validation_responses
|
|
147
|
+
end
|
|
148
|
+
raise 'competing event completed query' unless results.empty?
|
|
149
|
+
validators.shift.do_validate
|
|
150
|
+
SELECTOR.send_queued_validation_responses
|
|
151
|
+
expect_completion(results)
|
|
152
|
+
expect_clean(resolver)
|
|
153
|
+
RUBY
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_accepted_nxdomain_and_validation_failure_keep_their_error_semantics
|
|
158
|
+
[:nxdomain, :bogus].each do |outcome|
|
|
159
|
+
isolated("outcome = :#{outcome}\n" + <<~'RUBY')
|
|
160
|
+
start_query(resolver, results)
|
|
161
|
+
accept_answer(attempts.first, outcome == :nxdomain ? 3 : 0)
|
|
162
|
+
if outcome == :bogus
|
|
163
|
+
Dnsruby::Dnssec.define_singleton_method(:validate_with_query) { |*| raise Dnsruby::VerifyError, 'bad signature' }
|
|
164
|
+
end
|
|
165
|
+
validators.shift.do_validate
|
|
166
|
+
SELECTOR.send_queued_validation_responses
|
|
167
|
+
expect_completion(results, outcome == :nxdomain ? Dnsruby::NXDomain : Dnsruby::VerifyError)
|
|
168
|
+
expect_clean(resolver)
|
|
169
|
+
RUBY
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def test_timeout_or_close_and_id_reuse_ignore_the_old_validator
|
|
174
|
+
[:timeout, :close].each do |finish|
|
|
175
|
+
isolated("finish = :#{finish}\n" + <<~'RUBY')
|
|
176
|
+
start_query(resolver, results)
|
|
177
|
+
accept_answer(attempts.first)
|
|
178
|
+
old_validator = validators.shift
|
|
179
|
+
if finish == :timeout
|
|
180
|
+
clock += 11
|
|
181
|
+
SELECTOR.send_tick_to_observers
|
|
182
|
+
else
|
|
183
|
+
resolver.close
|
|
184
|
+
end
|
|
185
|
+
expect_completion(results, finish == :timeout ? Dnsruby::ResolvTimeout : Dnsruby::OtherResolvError)
|
|
186
|
+
expect_clean(resolver)
|
|
187
|
+
|
|
188
|
+
start_query(resolver, results, 'replacement.example.')
|
|
189
|
+
old_validator.do_validate
|
|
190
|
+
SELECTOR.send_queued_validation_responses
|
|
191
|
+
raise 'old validator completed reused id' unless results.empty?
|
|
192
|
+
accept_answer(attempts.first)
|
|
193
|
+
validators.shift.do_validate
|
|
194
|
+
SELECTOR.send_queued_validation_responses
|
|
195
|
+
expect_completion(results, nil, 'replacement.example')
|
|
196
|
+
expect_clean(resolver)
|
|
197
|
+
RUBY
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def isolated(body)
|
|
204
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
205
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
206
|
+
File.expand_path('../lib', __dir__), '-e',
|
|
207
|
+
"#{TestPipelineSend::FIXTURE}\nTimeout.timeout(10) do\n#{FIXTURE}\n#{body}\nend")
|
|
208
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
209
|
+
assert_empty(stderr)
|
|
210
|
+
end
|
|
211
|
+
end
|