curb 1.3.7 → 1.4.1

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.
@@ -12,6 +12,110 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
12
12
  super
13
13
  end
14
14
 
15
+ def test_safe_bang_defaults_to_public_network_policy
16
+ assert_equal :public, Curl::SafetyConfig.new.network_policy
17
+ require_public_network_policy!
18
+ Curl.safe!
19
+
20
+ assert_raise(Curl::Err::UnsafeDestinationError) { Curl.get(TestServlet.url) }
21
+ easy = Curl::Easy.new(TestServlet.url)
22
+ easy.network_policy = :none
23
+ assert_raise(Curl::Err::UnsafeDestinationError) { easy.perform }
24
+ assert_equal :public, easy.network_policy
25
+ end
26
+
27
+ def test_safe_bang_default_blocks_private_and_metadata_destinations
28
+ require_public_network_policy!
29
+ Curl.safe!
30
+ ['10.0.0.1', '172.16.0.1', '192.168.0.1', '169.254.169.254', '[::1]'].each do |host|
31
+ assert_raise(Curl::Err::UnsafeDestinationError, host) do
32
+ Curl.get("http://#{host}/") do |easy|
33
+ easy.network_policy = :none
34
+ easy.connect_timeout_ms = 100
35
+ easy.timeout_ms = 200
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def test_safe_bang_default_applies_to_previously_added_multi_handles
42
+ require_public_network_policy!
43
+ easy = Curl::Easy.new(TestServlet.url)
44
+ multi = Curl::Multi.new
45
+ multi.add(easy)
46
+ Curl.safe!
47
+ multi.perform
48
+
49
+ assert_equal :public, easy.network_policy
50
+ assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
51
+ ensure
52
+ multi.close if multi
53
+ end
54
+
55
+ def test_safe_helpers_block_loopback_without_global_policy
56
+ require_public_network_policy!
57
+ [:get, :post, :put, :delete, :patch, :head, :options].each do |verb|
58
+ assert_raise(Curl::Err::UnsafeDestinationError, verb.to_s) do
59
+ Curl.public_send("safe_#{verb}", TestServlet.url) do |easy|
60
+ easy.network_policy = :none
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def test_safe_http_reapplies_public_policy_before_perform
67
+ require_public_network_policy!
68
+ easy = Curl::Easy.new(TestServlet.url)
69
+ assert_same easy, easy.safe_http!
70
+ assert_equal :public, easy.network_policy
71
+ easy.network_policy = :none
72
+
73
+ assert_raise(Curl::Err::UnsafeDestinationError) { easy.perform }
74
+ end
75
+
76
+ def test_safe_http_cannot_be_weakened_by_global_policy
77
+ require_public_network_policy!
78
+ Curl.safe! { |config| config.network_policy = :none }
79
+
80
+ assert_raise(Curl::Err::UnsafeDestinationError) { Curl.safe_get(TestServlet.url) }
81
+ end
82
+
83
+ def test_safe_helpers_block_private_and_metadata_destinations
84
+ require_public_network_policy!
85
+ ['10.0.0.1', '172.16.0.1', '192.168.0.1', '169.254.169.254', '[::1]'].each do |host|
86
+ assert_raise(Curl::Err::UnsafeDestinationError, host) do
87
+ Curl.safe_get("http://#{host}/") do |easy|
88
+ easy.connect_timeout_ms = 100
89
+ easy.timeout_ms = 200
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def test_safe_http_rejects_resolve_override_without_global_policy
96
+ require_public_network_policy!
97
+ assert_raise(Curl::Err::UnsafeDestinationError) do
98
+ Curl.safe_get('http://example.invalid/') do |easy|
99
+ easy.resolve = ['example.invalid:80:127.0.0.1']
100
+ end
101
+ end
102
+ end
103
+
104
+ def test_safe_http_applies_to_multi
105
+ require_public_network_policy!
106
+ easy = Curl::Easy.new(TestServlet.url)
107
+ easy.safe_http!
108
+ easy.network_policy = :none
109
+ multi = Curl::Multi.new
110
+ multi.add(easy)
111
+ multi.perform
112
+
113
+ assert_equal :public, easy.network_policy
114
+ assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
115
+ ensure
116
+ multi.close if multi
117
+ end
118
+
15
119
  def test_network_policy_defaults_to_none
16
120
  easy = Curl::Easy.new
17
121
 
@@ -95,6 +199,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
95
199
 
96
200
  with_host_redirect_server(allowed_host, allowed_host) do |port, hits|
97
201
  Curl.safe! do |config|
202
+ config.network_policy = :none # Isolate host filtering from peer filtering.
98
203
  config.allowed_hosts = [allowed_host]
99
204
  end
100
205
 
@@ -119,6 +224,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
119
224
 
120
225
  with_host_redirect_server(allowed_host, blocked_host) do |port, hits|
121
226
  Curl.safe! do |config|
227
+ config.network_policy = :none # Isolate host filtering from peer filtering.
122
228
  config.allowed_hosts = [allowed_host]
123
229
  end
124
230
 
@@ -142,6 +248,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
142
248
 
143
249
  def test_safe_bang_allowed_cidrs_requires_public_network_policy
144
250
  Curl.safe! do |config|
251
+ config.network_policy = :none # Isolate the host/CIDR policy from peer filtering.
145
252
  config.allowed_cidrs = ['1.1.1.0/24']
146
253
  end
147
254
 
@@ -1080,7 +1187,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
1080
1187
  multi = Curl::Multi.new
1081
1188
 
1082
1189
  multi.add(easy)
1083
- Curl.safe!
1190
+ Curl.safe! { |config| config.network_policy = :none }
1084
1191
  multi.perform
1085
1192
 
1086
1193
  assert_equal [['1']], request_headers
@@ -253,6 +253,143 @@ class TestCurbCurlPostfieldMultipartCoverage < Test::Unit::TestCase
253
253
  server_setup
254
254
  end
255
255
 
256
+ [:http_post, :http_patch, :http_put].each do |method|
257
+ [:close, :reset, :initialize, :perform, :http_post, :post_body=].each do |operation|
258
+ [:proc, :coercion].each do |source|
259
+ define_method("test_#{method}_guards_#{operation}_from_#{source}") do
260
+ curl = Curl::Easy.new(TestServlet.url)
261
+ curl.multipart_form_post = true
262
+ action = lambda do |*_|
263
+ if operation == :post_body=
264
+ curl.post_body = 'replacement'
265
+ elsif operation == :http_post
266
+ curl.http_post(Curl::PostField.content('nested', 'payload'))
267
+ else
268
+ curl.__send__(operation)
269
+ end
270
+ 'payload'
271
+ end
272
+ field = if source == :proc
273
+ Curl::PostField.content('trigger', &action)
274
+ else
275
+ value = Object.new
276
+ value.define_singleton_method(:to_str, &action)
277
+ Curl::PostField.content('trigger', value)
278
+ end
279
+
280
+ error = assert_raise(RuntimeError) do
281
+ curl.__send__(method, Curl::PostField.content('first', 'allocated'), field)
282
+ end
283
+ assert_match(/active curl handle/, error.message)
284
+
285
+ # Both partial forms and guards must unwind before reuse.
286
+ curl.multipart_form_post = false
287
+ curl.perform
288
+ assert_equal 'GET', curl.body_str
289
+ assert_nothing_raised { curl.reset }
290
+ assert_nothing_raised { curl.close }
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ [:content, :file].each do |kind|
297
+ [false, true].each do |dynamic|
298
+ define_method("test_multipart_snapshots_#{kind}_#{dynamic ? 'proc' : 'direct'}") do
299
+ name = +'original_name'
300
+ payload = +"original\0payload"
301
+ field = multipart_field(kind, dynamic, nil, payload)
302
+ field.name = name
303
+ later = Curl::PostField.content('later') do
304
+ Thread.new do
305
+ name.replace('changed_name' * 1000)
306
+ payload.replace('changed_payload' * 1000)
307
+ field.content = nil
308
+ field.name = nil
309
+ GC.start
310
+ GC.verify_compaction_references(double_heap: true, toward: :empty) if GC.respond_to?(:verify_compaction_references)
311
+ end.join
312
+ 'later_value'
313
+ end
314
+ curl = Curl::Easy.new(TestServlet.url)
315
+ curl.multipart_form_post = true
316
+ curl.http_post(field, later)
317
+
318
+ assert_include curl.body_str, 'name="original_name"'
319
+ assert_include curl.body_str, "original\0payload"
320
+ assert_include curl.body_str, 'filename="remote.txt"' if kind == :file
321
+ assert_not_include curl.body_str, 'changed_payload'
322
+ end
323
+ end
324
+ end
325
+
326
+ def test_multipart_snapshots_before_later_coercions_in_same_field
327
+ name = +'original_name'
328
+ payload = +'original_payload'
329
+ content_type = Object.new
330
+ content_type.define_singleton_method(:to_str) do
331
+ name.replace('changed_name' * 1000)
332
+ payload.replace('changed_payload' * 1000)
333
+ 'text/plain'
334
+ end
335
+ field = Curl::PostField.content(name, payload, content_type)
336
+ curl = Curl::Easy.new(TestServlet.url)
337
+ curl.multipart_form_post = true
338
+ curl.http_post(field)
339
+
340
+ assert_include curl.body_str, 'name="original_name"'
341
+ assert_include curl.body_str, 'original_payload'
342
+ end
343
+
344
+ # Exercise each pointer/length pair with and without a content type.
345
+ [:content, :file].each do |kind|
346
+ [false, true].each do |dynamic|
347
+ [nil, 'text/plain'].each do |content_type|
348
+ variant = "#{kind}_#{dynamic ? 'proc' : 'direct'}_#{content_type ? 'typed' : 'untyped'}"
349
+
350
+ define_method("test_multipart_coerces_#{variant}") do
351
+ payload = "before\0after"
352
+ string_like = Object.new
353
+ string_like.define_singleton_method(:to_str) { payload }
354
+ field = multipart_field(kind, dynamic, content_type, string_like)
355
+ curl = Curl::Easy.new(TestServlet.url)
356
+ curl.multipart_form_post = true
357
+
358
+ curl.http_post(field)
359
+
360
+ assert_include curl.body_str, payload
361
+ assert_equal payload, field.content unless dynamic
362
+ end
363
+
364
+ define_method("test_multipart_rejects_non_string_#{variant}") do
365
+ field = multipart_field(kind, dynamic, content_type, 5)
366
+ curl = Curl::Easy.new(TestServlet.url)
367
+ curl.multipart_form_post = true
368
+
369
+ assert_raise(TypeError) { curl.http_post(field) }
370
+ end
371
+ end
372
+ end
373
+ end
374
+
375
+ def multipart_field(kind, dynamic, content_type, value)
376
+ field = if dynamic
377
+ if kind == :file
378
+ Curl::PostField.file('upload', 'remote.txt') { value }
379
+ else
380
+ Curl::PostField.content('value') { value }
381
+ end
382
+ elsif kind == :file
383
+ upload = Curl::PostField.file('upload', 'remote.txt')
384
+ upload.content = value
385
+ upload
386
+ else
387
+ Curl::PostField.content('value', value)
388
+ end
389
+ field.content_type = content_type
390
+ field
391
+ end
392
+
256
393
  def test_multipart_content_variants_include_dynamic_and_typed_fields
257
394
  curl = Curl::Easy.new(TestServlet.url)
258
395
  curl.multipart_form_post = true
@@ -318,3 +455,42 @@ class TestCurbCurlPostfieldMultipartCoverage < Test::Unit::TestCase
318
455
  assert_match(/Content-Type: text\/plain/, body)
319
456
  end
320
457
  end
458
+
459
+ class TestCurbCurlPostfieldInvalidToS < Test::Unit::TestCase
460
+ class BadToS
461
+ def to_s
462
+ 42
463
+ end
464
+ end
465
+
466
+ def test_to_s_with_non_string_content_to_s_result_does_not_crash
467
+ pf = Curl::PostField.content('name', BadToS.new)
468
+ body = nil
469
+ assert_nothing_raised { body = pf.to_s }
470
+ assert_kind_of String, body
471
+ assert body.start_with?('name=')
472
+ end
473
+
474
+ def test_to_s_with_non_string_name_to_s_result_does_not_crash
475
+ pf = Curl::PostField.content(BadToS.new, 'value')
476
+ body = nil
477
+ assert_nothing_raised { body = pf.to_s }
478
+ assert_kind_of String, body
479
+ end
480
+
481
+ def test_to_s_with_content_proc_returning_non_string_does_not_crash
482
+ pf = Curl::PostField.content('name') { BadToS.new }
483
+ assert_nothing_raised { pf.to_s }
484
+ end
485
+
486
+ def test_to_s_content_proc_raising_error_still_propagates
487
+ pf = Curl::PostField.content('name') { raise 'boom' }
488
+ error = assert_raise(RuntimeError) { pf.to_s }
489
+ assert_equal 'boom', error.message
490
+ end
491
+
492
+ def test_to_s_still_coerces_objects_with_valid_to_s
493
+ pf = Curl::PostField.content('name', 42)
494
+ assert_equal 'name=42', pf.to_s
495
+ end
496
+ end
@@ -104,11 +104,10 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
104
104
  end
105
105
  end
106
106
 
107
- def test_safe_get_allows_http_protocol
108
- easy = Curl.safe_get(TestServlet.url)
109
-
110
- assert_equal 200, easy.response_code
111
- assert_match(/GET/, easy.body_str)
107
+ def test_safe_get_blocks_local_http_destination
108
+ assert_raise(Curl::Err::UnsafeDestinationError) do
109
+ Curl.safe_get(TestServlet.url)
110
+ end
112
111
  end
113
112
 
114
113
  def test_safe_bang_denies_file_protocol_for_easy_perform
@@ -159,8 +158,8 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
159
158
  end
160
159
  end
161
160
 
162
- def test_safe_bang_allows_http_protocol
163
- Curl.safe!
161
+ def test_safe_bang_explicit_none_allows_trusted_http_destination
162
+ Curl.safe! { |config| config.network_policy = :none }
164
163
  easy = Curl.get(TestServlet.url)
165
164
 
166
165
  assert_equal 200, easy.response_code
@@ -184,6 +183,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
184
183
  with_file_redirect_server do |url|
185
184
  easy = Curl::Easy.new(url)
186
185
  easy.follow_location = true
186
+ stub_public_network_policy_for_local_fixture(easy)
187
187
  easy.safe_http!
188
188
 
189
189
  assert_raises Curl::Err::UnsupportedProtocolError do
@@ -198,6 +198,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
198
198
 
199
199
  easy = Curl::Easy.new(url)
200
200
  easy.follow_location = true
201
+ stub_public_network_policy_for_local_fixture(easy)
201
202
 
202
203
  assert_raises Curl::Err::UnsupportedProtocolError do
203
204
  easy.perform
@@ -211,6 +212,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
211
212
  easy.follow_location = true
212
213
  easy.ssl_verify_peer = false
213
214
  easy.ssl_verify_host = false
215
+ stub_public_network_policy_for_local_fixture(easy)
214
216
  easy.safe_http!
215
217
 
216
218
  easy.perform
@@ -226,6 +228,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
226
228
  easy.follow_location = true
227
229
  easy.ssl_verify_peer = false
228
230
  easy.ssl_verify_host = false
231
+ stub_public_network_policy_for_local_fixture(easy)
229
232
  easy.safe_http!
230
233
 
231
234
  easy.perform
@@ -240,6 +243,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
240
243
  easy = Curl::Easy.new(url)
241
244
  easy.follow_location = true
242
245
  easy.max_redirects = 2
246
+ stub_public_network_policy_for_local_fixture(easy)
243
247
  easy.safe_http!
244
248
 
245
249
  assert_raises Curl::Err::TooManyRedirectsError do
@@ -252,6 +256,14 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
252
256
 
253
257
  private
254
258
 
259
+ # Isolate protocol/redirect behavior using local fixtures. Destination blocking
260
+ # is exercised without this stub in tc_curl_network_policy.rb.
261
+ def stub_public_network_policy_for_local_fixture(easy)
262
+ def easy.network_policy=(policy)
263
+ raise "unexpected policy: #{policy}" unless policy == :public
264
+ end
265
+ end
266
+
255
267
  def with_file_redirect_server
256
268
  port_socket = TCPServer.new('127.0.0.1', 0)
257
269
  port = port_socket.addr[1]
@@ -0,0 +1,112 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class TestCurbTransferGuard < Test::Unit::TestCase
4
+ class InterruptedTransfer < StandardError; end
5
+
6
+ [:perform, :_socket_perform].each do |drive|
7
+ define_method("test_#{drive}_rejects_cross_thread_handle_access") do
8
+ with_waiting_transfer(drive) do |easy, multi, worker, release|
9
+ operations = {
10
+ close: -> { easy.close },
11
+ reset: -> { easy.reset },
12
+ initialize: -> { easy.__send__(:initialize) },
13
+ clone: -> { easy.clone },
14
+ dup: -> { easy.dup },
15
+ setopt: -> { easy.setopt(Curl::CURLOPT_CUSTOMREQUEST, 'DELETE') },
16
+ post_body: -> { easy.post_body = 'replacement' },
17
+ url: -> { easy.url = 'http://127.0.0.1/' },
18
+ timeout: -> { easy.timeout = 1 },
19
+ multi_assignment: -> { easy.multi = nil },
20
+ multi_remove: -> { multi.remove(easy) },
21
+ multi_close: -> { multi.close },
22
+ multi_initialize: -> { multi.__send__(:initialize) },
23
+ multi_add: -> { multi.add(Curl::Easy.new('http://127.0.0.1/')) },
24
+ multi_option: -> { multi.max_connects = 1 }
25
+ }
26
+ operations.each do |name, operation|
27
+ error = assert_raise(RuntimeError, name.to_s, &operation)
28
+ assert_match(/another thread|active Curl::Multi/, error.message, name.to_s)
29
+ end
30
+ assert_equal [easy], multi.requests.values
31
+
32
+ release << true
33
+ assert worker.join(5), 'perform did not finish'
34
+ worker.value
35
+ assert_equal 'ok', easy.body_str
36
+ assert_nothing_raised { easy.clone.close }
37
+ assert_nothing_raised { easy.reset }
38
+ assert_nothing_raised { easy.close }
39
+ end
40
+ end
41
+
42
+ define_method("test_#{drive}_releases_guard_after_interruption") do
43
+ with_waiting_transfer(drive) do |easy, multi, worker, release|
44
+ worker.raise(InterruptedTransfer, 'stop')
45
+ assert worker.join(5), 'interrupted perform did not finish'
46
+ assert_kind_of InterruptedTransfer, worker[:transfer_error]
47
+ assert_nothing_raised { multi.remove(easy) }
48
+ assert_nothing_raised { easy.reset }
49
+ assert_nothing_raised { easy.close }
50
+ release << true
51
+ end
52
+ end
53
+ end
54
+
55
+ def with_waiting_transfer(drive)
56
+ if drive == :_socket_perform && !Curl::Multi.private_method_defined?(drive)
57
+ omit('socket-action perform path is not available')
58
+ end
59
+ server = TCPServer.new('127.0.0.1', 0)
60
+ started = Queue.new
61
+ release = Queue.new
62
+ server_thread = Thread.new do
63
+ socket = server.accept
64
+ begin
65
+ request = +''
66
+ request << socket.readpartial(4096) until request.include?("\r\n\r\n")
67
+ started << true
68
+ release.pop
69
+ socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
70
+ rescue Errno::EPIPE, Errno::ECONNRESET
71
+ # The interruption test intentionally closes the connection early.
72
+ ensure
73
+ socket.close
74
+ end
75
+ end
76
+ easy = Curl::Easy.new("http://127.0.0.1:#{server.addr[1]}/")
77
+ multi = Curl::Multi.new
78
+ # Exercise reuse after autoclose as well as the active-transfer checks.
79
+ previous_autoclose = Curl::Multi.autoclose
80
+ begin
81
+ Curl::Multi.autoclose = true
82
+ multi.add(Curl::Easy.new($TEST_URL))
83
+ multi.perform
84
+ ensure
85
+ Curl::Multi.autoclose = previous_autoclose
86
+ end
87
+ multi.add(easy)
88
+ worker = Thread.new do
89
+ Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
90
+ begin
91
+ multi.__send__(drive)
92
+ rescue InterruptedTransfer => error
93
+ # Keep join non-raising so the test can inspect the exception explicitly.
94
+ Thread.current[:transfer_error] = error
95
+ end
96
+ end
97
+ Timeout.timeout(5) { started.pop }
98
+ yield easy, multi, worker, release
99
+ ensure
100
+ release << true if release
101
+ if worker && worker.alive?
102
+ worker.kill
103
+ worker.join(5)
104
+ end
105
+ multi.close if multi
106
+ server.close if server && !server.closed?
107
+ if server_thread
108
+ server_thread.join(5)
109
+ server_thread.kill if server_thread.alive?
110
+ end
111
+ end
112
+ end
@@ -118,7 +118,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
118
118
 
119
119
  duration = Time.now - started
120
120
 
121
- assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
121
+ assert_ran_concurrently(duration)
122
122
  assert_equal ITERS, results.size
123
123
  assert_equal ITERS.times.map {200}, results
124
124
  end
@@ -149,7 +149,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
149
149
 
150
150
  duration = Time.now - started
151
151
 
152
- assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
152
+ assert_ran_concurrently(duration)
153
153
  assert_equal ITERS, results.size
154
154
  assert_equal ITERS.times.map {200}, results
155
155
  end
@@ -734,6 +734,97 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
734
734
  end
735
735
  end
736
736
 
737
+ # Records unblock calls in place of a real scheduler.
738
+ class UnblockRecorder
739
+ attr_reader :unblocked
740
+
741
+ def initialize
742
+ @unblocked = []
743
+ end
744
+
745
+ def unblock(blocker, _fiber)
746
+ @unblocked << blocker
747
+ end
748
+ end
749
+
750
+ # Enumerating every waiter on every drive-loop pass made release cost
751
+ # O(waiters) per pass; only the queued completions should be visited.
752
+ class NonEnumerableWaiters < Hash
753
+ %i[each each_pair each_value each_key].each do |name|
754
+ define_method(name) { |*| raise 'release_scheduler_waiters must not enumerate all waiters' }
755
+ end
756
+ end
757
+
758
+ def scheduler_test_waiter(scheduler)
759
+ {completed: true, done: false, error: nil, fiber: Fiber.new {}, scheduler: scheduler}
760
+ end
761
+
762
+ def test_release_scheduler_waiters_visits_only_queued_completions
763
+ state = Curl.scheduler_state
764
+ recorder = UnblockRecorder.new
765
+ waiters = NonEnumerableWaiters.new
766
+ 1_000.times { |i| waiters[i] = scheduler_test_waiter(recorder).merge(completed: false) }
767
+ finished = scheduler_test_waiter(recorder)
768
+ waiters[:finished] = finished
769
+ state[:waiters] = waiters
770
+ state[:completed] << [:finished, finished]
771
+
772
+ Curl.release_scheduler_waiters(state)
773
+
774
+ assert_equal [finished], recorder.unblocked
775
+ assert finished[:done]
776
+ assert_empty state[:completed]
777
+ ensure
778
+ cleanup_scheduler_state
779
+ end
780
+
781
+ # A waiter whose perform already returned (it raised) is unregistered and its
782
+ # fiber may be blocked on something else, so a late completion must not
783
+ # unblock it.
784
+ def test_release_scheduler_waiters_skips_unregistered_waiter
785
+ state = Curl.scheduler_state
786
+ recorder = UnblockRecorder.new
787
+ stale = scheduler_test_waiter(recorder)
788
+ state[:completed] << [:stale, stale]
789
+
790
+ Curl.release_scheduler_waiters(state)
791
+
792
+ assert_empty recorder.unblocked
793
+ assert_equal false, stale[:done]
794
+ assert_empty state[:completed]
795
+ ensure
796
+ cleanup_scheduler_state
797
+ end
798
+
799
+ # The transfer whose callback raised the deferred exception receives that
800
+ # error rather than a normal completion; it stays queued so it is completed
801
+ # normally if the exception source changes.
802
+ def test_release_scheduler_waiters_holds_back_deferred_exception_source
803
+ state = Curl.scheduler_state
804
+ recorder = UnblockRecorder.new
805
+ source = scheduler_test_waiter(recorder)
806
+ sibling = scheduler_test_waiter(recorder)
807
+ state[:waiters][:source] = source
808
+ state[:waiters][:sibling] = sibling
809
+ state[:completed] << [:source, source] << [:sibling, sibling]
810
+ state[:multi].instance_variable_set(:@__curb_deferred_exception_source_id, :source)
811
+
812
+ Curl.release_scheduler_waiters(state)
813
+
814
+ assert_equal [sibling], recorder.unblocked
815
+ assert_equal false, source[:done]
816
+ assert_equal [[:source, source]], state[:completed]
817
+
818
+ state[:multi].remove_instance_variable(:@__curb_deferred_exception_source_id)
819
+ Curl.release_scheduler_waiters(state)
820
+
821
+ assert_equal [sibling, source], recorder.unblocked
822
+ assert source[:done]
823
+ assert_empty state[:completed]
824
+ ensure
825
+ cleanup_scheduler_state
826
+ end
827
+
737
828
  def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
738
829
  state = Curl.scheduler_state
739
830
  easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
@@ -849,6 +940,14 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
849
940
  false
850
941
  end
851
942
 
943
+ # Wall-clock bounds are not reliable under valgrind, which slows and
944
+ # serializes threads unpredictably; there the requests and their results are
945
+ # still checked under memcheck, only the duration bound is skipped.
946
+ def assert_ran_concurrently(duration)
947
+ return if curb_under_valgrind?
948
+ assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
949
+ end
950
+
852
951
  def socket_perform_supported?
853
952
  Curl::Multi.private_method_defined?(:_socket_perform)
854
953
  end