curb 1.3.6 → 1.3.7
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/README.md +33 -13
- data/ext/curb.c +9 -0
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +107 -46
- data/ext/curb_multi.c +211 -130
- data/ext/extconf.rb +4 -0
- data/tests/helper.rb +178 -1
- data/tests/io_select_less_scheduler_probe.rb +105 -0
- data/tests/tc_curl_easy.rb +1 -0
- data/tests/tc_curl_easy_resolve.rb +2 -0
- data/tests/tc_curl_native_coverage.rb +1 -0
- data/tests/tc_fiber_scheduler.rb +332 -4
- data/tests/tc_ftp_options.rb +58 -0
- data/tests/tc_gc_compact.rb +53 -0
- data/tests/tc_test_server_methods.rb +15 -0
- metadata +4 -2
data/tests/tc_fiber_scheduler.rb
CHANGED
|
@@ -46,11 +46,12 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def kernel_sleep(duration = nil)
|
|
49
|
-
sleep
|
|
49
|
+
# A bare sleep here would re-enter this hook through the scheduler.
|
|
50
|
+
blocking_io { sleep(duration || 0) }
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def block(_blocker, timeout = nil)
|
|
53
|
-
sleep(timeout || 0)
|
|
54
|
+
blocking_io { sleep(timeout || 0) }
|
|
54
55
|
false
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -69,7 +70,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
69
70
|
if Fiber.respond_to?(:blocking)
|
|
70
71
|
Fiber.blocking(&block)
|
|
71
72
|
else
|
|
72
|
-
block.
|
|
73
|
+
Fiber.new(blocking: true, &block).resume
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
end
|
|
@@ -215,7 +216,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
215
216
|
c = Curl::Easy.new(url)
|
|
216
217
|
c.on_complete { result = c.code }
|
|
217
218
|
m.add(c)
|
|
218
|
-
unless
|
|
219
|
+
unless socket_perform_supported?
|
|
219
220
|
omit('socket-action perform path is not available in this build')
|
|
220
221
|
end
|
|
221
222
|
m.send(:_socket_perform)
|
|
@@ -231,6 +232,249 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
231
232
|
end
|
|
232
233
|
end
|
|
233
234
|
|
|
235
|
+
# Multi#perform must not fall back to the thread-blocking legacy fdset loop
|
|
236
|
+
# when the calling fiber runs under a scheduler: the waits have to go
|
|
237
|
+
# through the scheduler's io_wait/io_select hooks so sibling fibers run.
|
|
238
|
+
def test_multi_perform_waits_via_scheduler_hooks
|
|
239
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
240
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
241
|
+
|
|
242
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
243
|
+
scheduler = RecordingScheduler.new
|
|
244
|
+
result = nil
|
|
245
|
+
|
|
246
|
+
with_scheduler(scheduler) do
|
|
247
|
+
m = Curl::Multi.new
|
|
248
|
+
unless socket_perform_supported?
|
|
249
|
+
omit('socket-action perform path is not available in this build')
|
|
250
|
+
end
|
|
251
|
+
c = Curl::Easy.new(url)
|
|
252
|
+
c.on_complete { result = c.code }
|
|
253
|
+
m.add(c)
|
|
254
|
+
m.perform
|
|
255
|
+
ensure
|
|
256
|
+
m.close if m
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
assert_equal 200, result
|
|
260
|
+
assert_operator scheduler.io_wait_events.length + scheduler.io_select_calls, :>=, 1,
|
|
261
|
+
'Multi#perform under a fiber scheduler should wait through the scheduler hooks'
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def test_multi_perform_propagates_io_wrapper_errors
|
|
265
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
266
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
267
|
+
omit('socket-action perform path is not available in this build') unless socket_perform_supported?
|
|
268
|
+
|
|
269
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
270
|
+
scheduler = RecordingScheduler.new
|
|
271
|
+
wrapper_error = Class.new(StandardError)
|
|
272
|
+
original_for_fd = IO.method(:for_fd)
|
|
273
|
+
redefine_io_for_fd do |*_args|
|
|
274
|
+
raise wrapper_error, 'IO wrapper failed'
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
error = assert_raise(wrapper_error) do
|
|
278
|
+
with_scheduler(scheduler) do
|
|
279
|
+
m = Curl::Multi.new
|
|
280
|
+
m.add(Curl::Easy.new(url))
|
|
281
|
+
m.perform
|
|
282
|
+
ensure
|
|
283
|
+
m.close if m
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
assert_equal 'IO wrapper failed', error.message
|
|
288
|
+
ensure
|
|
289
|
+
redefine_io_for_fd(original_for_fd) if original_for_fd
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Probe-style regression tests: all previous concurrency assertions routed N
|
|
293
|
+
# requests through one multi handle, so libcurl provided the parallelism and
|
|
294
|
+
# they passed even when the wait loop blocked the whole thread. Here a
|
|
295
|
+
# sibling fiber ticks every 10ms while a single request is in flight; a
|
|
296
|
+
# blocking wait loop caps the ticks at roughly one per 100ms select chunk,
|
|
297
|
+
# while a scheduler-friendly loop lets most of the ~MIN_S/0.01 ticks happen.
|
|
298
|
+
def test_sibling_fibers_progress_during_multi_perform
|
|
299
|
+
return if skip_no_async
|
|
300
|
+
return if skip_no_socket_perform
|
|
301
|
+
|
|
302
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
303
|
+
result = nil
|
|
304
|
+
|
|
305
|
+
ticks = probe_sibling_ticks do
|
|
306
|
+
m = Curl::Multi.new
|
|
307
|
+
begin
|
|
308
|
+
c = Curl::Easy.new(url)
|
|
309
|
+
c.on_complete { result = c.code }
|
|
310
|
+
m.add(c)
|
|
311
|
+
m.perform
|
|
312
|
+
ensure
|
|
313
|
+
m.close
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
assert_equal 200, result
|
|
318
|
+
assert_operator ticks, :>=, SIBLING_TICKS_MIN,
|
|
319
|
+
"sibling fiber starved during Multi#perform: #{ticks} ticks in a #{MIN_S}s transfer (~#{SIBLING_TICKS_POSSIBLE} possible)"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_sibling_fibers_progress_during_easy_perform
|
|
323
|
+
return if skip_no_async
|
|
324
|
+
return if skip_no_socket_perform
|
|
325
|
+
|
|
326
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
327
|
+
result = nil
|
|
328
|
+
|
|
329
|
+
ticks = probe_sibling_ticks do
|
|
330
|
+
c = Curl::Easy.new(url)
|
|
331
|
+
c.perform
|
|
332
|
+
result = c.code
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
assert_equal 200, result
|
|
336
|
+
assert_operator ticks, :>=, SIBLING_TICKS_MIN,
|
|
337
|
+
"sibling fiber starved during Easy#perform: #{ticks} ticks in a #{MIN_S}s transfer (~#{SIBLING_TICKS_POSSIBLE} possible)"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_sibling_fibers_progress_during_multi_perform_with_multiple_sockets
|
|
341
|
+
return if skip_no_async
|
|
342
|
+
return if skip_no_socket_perform
|
|
343
|
+
|
|
344
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
345
|
+
results = []
|
|
346
|
+
|
|
347
|
+
ticks = probe_sibling_ticks do
|
|
348
|
+
m = Curl::Multi.new
|
|
349
|
+
begin
|
|
350
|
+
2.times do
|
|
351
|
+
c = Curl::Easy.new(url)
|
|
352
|
+
c.on_complete { results << c.code }
|
|
353
|
+
m.add(c)
|
|
354
|
+
end
|
|
355
|
+
m.perform
|
|
356
|
+
ensure
|
|
357
|
+
m.close
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
assert_equal [200, 200], results.sort
|
|
362
|
+
assert_operator ticks, :>=, SIBLING_TICKS_MIN,
|
|
363
|
+
"sibling fiber starved during multi-fd Multi#perform: #{ticks} ticks in a #{MIN_S}s transfer (~#{SIBLING_TICKS_POSSIBLE} possible)"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_scheduler_timeout_interrupts_single_socket_multi_perform
|
|
367
|
+
return if skip_no_async
|
|
368
|
+
return if skip_no_socket_perform
|
|
369
|
+
|
|
370
|
+
assert_scheduler_timeout_interrupts_multi_perform(1)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def test_scheduler_timeout_interrupts_multi_socket_multi_perform
|
|
374
|
+
return if skip_no_async
|
|
375
|
+
return if skip_no_socket_perform
|
|
376
|
+
|
|
377
|
+
assert_scheduler_timeout_interrupts_multi_perform(2)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def test_multi_perform_runs_work_added_from_final_idle_yield_under_scheduler
|
|
381
|
+
return if skip_no_async
|
|
382
|
+
return if skip_no_socket_perform
|
|
383
|
+
|
|
384
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
385
|
+
completions = []
|
|
386
|
+
empty_yields = 0
|
|
387
|
+
queued_added = false
|
|
388
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
389
|
+
Curl::Multi.autoclose = true
|
|
390
|
+
|
|
391
|
+
begin
|
|
392
|
+
async_run do
|
|
393
|
+
m = Curl::Multi.new
|
|
394
|
+
begin
|
|
395
|
+
first = Curl::Easy.new(url)
|
|
396
|
+
first.on_complete { completions << :first }
|
|
397
|
+
m.add(first)
|
|
398
|
+
|
|
399
|
+
m.perform do |performing_multi|
|
|
400
|
+
empty_yields += 1 if performing_multi.requests.empty?
|
|
401
|
+
if !queued_added && empty_yields >= 2
|
|
402
|
+
queued_added = true
|
|
403
|
+
queued = Curl::Easy.new(url)
|
|
404
|
+
queued.on_complete { completions << :queued }
|
|
405
|
+
performing_multi.add(queued)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
ensure
|
|
409
|
+
m.close
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
assert queued_added, 'test should add queued work from the final idle yield'
|
|
414
|
+
assert_equal [:first, :queued], completions
|
|
415
|
+
ensure
|
|
416
|
+
Curl::Multi.autoclose = previous_autoclose
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# A scheduler that implements io_wait but not the optional io_select hook
|
|
421
|
+
# must neither busy-loop nor fall back to a thread-blocking wait. The broken
|
|
422
|
+
# loop spins without an interrupt checkpoint, so the probe runs in a
|
|
423
|
+
# subprocess guarded by a parent-side SIGKILL fuse.
|
|
424
|
+
def test_socket_perform_progresses_when_scheduler_lacks_io_select
|
|
425
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
426
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
427
|
+
unless socket_perform_supported?
|
|
428
|
+
omit('socket-action perform path is not available in this build')
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
script = File.join(File.dirname(__FILE__), 'io_select_less_scheduler_probe.rb')
|
|
432
|
+
out_r, out_w = IO.pipe
|
|
433
|
+
pid = Process.spawn(RbConfig.ruby, '-I', $LIBDIR, '-I', $EXTDIR, script, @port.to_s,
|
|
434
|
+
:out => out_w, :err => out_w)
|
|
435
|
+
out_w.close
|
|
436
|
+
|
|
437
|
+
fuse_seconds = 30
|
|
438
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + fuse_seconds
|
|
439
|
+
status = nil
|
|
440
|
+
loop do
|
|
441
|
+
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
442
|
+
break if status
|
|
443
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
444
|
+
begin
|
|
445
|
+
Process.kill('KILL', pid)
|
|
446
|
+
rescue Errno::ESRCH
|
|
447
|
+
end
|
|
448
|
+
begin
|
|
449
|
+
Process.waitpid(pid)
|
|
450
|
+
rescue Errno::ECHILD
|
|
451
|
+
end
|
|
452
|
+
flunk "scheduler without io_select busy-looped _socket_perform (no exit within #{fuse_seconds}s; killed)"
|
|
453
|
+
end
|
|
454
|
+
sleep 0.05
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
output = out_r.read
|
|
458
|
+
assert status.success?, "io_select-less scheduler subprocess failed: #{output}"
|
|
459
|
+
match = /^OK ticks=(\d+)$/.match(output)
|
|
460
|
+
assert_not_nil match, "unexpected io_select-less scheduler output: #{output}"
|
|
461
|
+
assert_operator match[1].to_i, :>=, 2,
|
|
462
|
+
"scheduler without io_select did not make sibling-fiber progress: #{output}"
|
|
463
|
+
ensure
|
|
464
|
+
if pid && !status
|
|
465
|
+
begin
|
|
466
|
+
Process.kill('KILL', pid)
|
|
467
|
+
rescue Errno::ESRCH
|
|
468
|
+
end
|
|
469
|
+
begin
|
|
470
|
+
Process.waitpid(pid)
|
|
471
|
+
rescue Errno::ECHILD
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
out_r.close if out_r && !out_r.closed?
|
|
475
|
+
out_w.close if out_w && !out_w.closed?
|
|
476
|
+
end
|
|
477
|
+
|
|
234
478
|
def test_multi_reuse_after_scheduler_perform
|
|
235
479
|
unless HAS_ASYNC
|
|
236
480
|
warn 'Skipping fiber scheduler test (Async gem not available)'
|
|
@@ -508,7 +752,79 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
508
752
|
cleanup_scheduler_state
|
|
509
753
|
end
|
|
510
754
|
|
|
755
|
+
# Sibling-fiber probe bounds: ~MIN_S/0.01 ticks are possible while the
|
|
756
|
+
# request is in flight. A thread-blocking wait loop yields only one tick per
|
|
757
|
+
# 100ms select chunk (2-3 for MIN_S=0.25); require a comfortably higher
|
|
758
|
+
# count while leaving slack for slow CI hosts.
|
|
759
|
+
SIBLING_TICKS_POSSIBLE = (MIN_S / 0.01).to_i
|
|
760
|
+
SIBLING_TICKS_MIN = 8
|
|
761
|
+
|
|
511
762
|
private
|
|
763
|
+
def redefine_io_for_fd(callable = nil, &block)
|
|
764
|
+
previous_verbose = $VERBOSE
|
|
765
|
+
$VERBOSE = nil
|
|
766
|
+
if callable
|
|
767
|
+
IO.define_singleton_method(:for_fd, callable)
|
|
768
|
+
else
|
|
769
|
+
IO.define_singleton_method(:for_fd, &block)
|
|
770
|
+
end
|
|
771
|
+
ensure
|
|
772
|
+
$VERBOSE = previous_verbose
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def assert_scheduler_timeout_interrupts_multi_perform(easy_count)
|
|
776
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
777
|
+
timeout_error = nil
|
|
778
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
779
|
+
|
|
780
|
+
async_run do |task|
|
|
781
|
+
begin
|
|
782
|
+
task.with_timeout(0.05) do
|
|
783
|
+
m = Curl::Multi.new
|
|
784
|
+
begin
|
|
785
|
+
easy_count.times { m.add(Curl::Easy.new(url)) }
|
|
786
|
+
m.perform
|
|
787
|
+
ensure
|
|
788
|
+
m.close
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
rescue Async::TimeoutError => error
|
|
792
|
+
timeout_error = error
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
797
|
+
assert_kind_of Async::TimeoutError, timeout_error,
|
|
798
|
+
"scheduler timeout was swallowed for #{easy_count} easy handle(s)"
|
|
799
|
+
assert_operator elapsed, :<, MIN_S * 0.8,
|
|
800
|
+
"scheduler cancellation took #{elapsed}s for #{easy_count} easy handle(s)"
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Runs the block in one fiber while a sibling fiber ticks every 10ms;
|
|
804
|
+
# returns how many ticks the sibling managed before the block finished.
|
|
805
|
+
def probe_sibling_ticks
|
|
806
|
+
ticks = 0
|
|
807
|
+
done = false
|
|
808
|
+
|
|
809
|
+
async_run do |top|
|
|
810
|
+
ticker = top.async do
|
|
811
|
+
until done
|
|
812
|
+
sleep 0.01
|
|
813
|
+
ticks += 1 unless done
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
top.async do
|
|
818
|
+
yield
|
|
819
|
+
ensure
|
|
820
|
+
done = true
|
|
821
|
+
end.wait
|
|
822
|
+
ticker.wait
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
ticks
|
|
826
|
+
end
|
|
827
|
+
|
|
512
828
|
def skip_no_async
|
|
513
829
|
if WINDOWS
|
|
514
830
|
warn 'Skipping fiber scheduler tests on Windows'
|
|
@@ -525,6 +841,18 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
525
841
|
false
|
|
526
842
|
end
|
|
527
843
|
|
|
844
|
+
def skip_no_socket_perform
|
|
845
|
+
unless socket_perform_supported?
|
|
846
|
+
warn 'Skipping fiber scheduler test (socket-action perform path unavailable)'
|
|
847
|
+
return true
|
|
848
|
+
end
|
|
849
|
+
false
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def socket_perform_supported?
|
|
853
|
+
Curl::Multi.private_method_defined?(:_socket_perform)
|
|
854
|
+
end
|
|
855
|
+
|
|
528
856
|
def async_run(&block)
|
|
529
857
|
# Prefer newer Async.run to avoid deprecated scheduler.async path.
|
|
530
858
|
if defined?(Async) && Async.respond_to?(:run)
|
data/tests/tc_ftp_options.rb
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
|
2
2
|
|
|
3
3
|
class TestCurbFtpOptions < Test::Unit::TestCase
|
|
4
|
+
SCALAR_AND_STRING_OPTIONS = {
|
|
5
|
+
:ftpport => "-",
|
|
6
|
+
:dirlistonly => true,
|
|
7
|
+
:append => true,
|
|
8
|
+
:ftp_use_eprt => false,
|
|
9
|
+
:ftp_use_epsv => false,
|
|
10
|
+
:ftp_use_pret => true,
|
|
11
|
+
:ftp_create_missing_dirs => true,
|
|
12
|
+
:ftp_response_timeout => 30,
|
|
13
|
+
:ftp_alternative_to_user => "anonymous",
|
|
14
|
+
:ftp_skip_pasv_ip => true,
|
|
15
|
+
:ftpsslauth => 0,
|
|
16
|
+
:ftp_ssl_ccc => 0,
|
|
17
|
+
:ftp_account => "account",
|
|
18
|
+
:ftp_filemethod => 0
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
4
21
|
# Ensure FTP-related set(:option, ...) mappings are accepted and do not raise
|
|
5
22
|
# a TypeError (they used to be unsupported in setopt dispatch).
|
|
6
23
|
def test_can_set_ftp_listing_related_flags
|
|
@@ -15,6 +32,47 @@ class TestCurbFtpOptions < Test::Unit::TestCase
|
|
|
15
32
|
end
|
|
16
33
|
end
|
|
17
34
|
|
|
35
|
+
def test_can_set_detected_scalar_and_string_ftp_options
|
|
36
|
+
c = Curl::Easy.new('ftp://example.com/')
|
|
37
|
+
|
|
38
|
+
SCALAR_AND_STRING_OPTIONS.each do |name, value|
|
|
39
|
+
constant = "CURLOPT_#{name.to_s.upcase}"
|
|
40
|
+
next unless Curl.const_defined?(constant)
|
|
41
|
+
|
|
42
|
+
assert_nothing_raised("setting #{name}") { c.set(name, value) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_issue_481_ftp_create_missing_dirs_accessor_and_set
|
|
47
|
+
omit('CURLOPT_FTP_CREATE_MISSING_DIRS is not available in this build') unless
|
|
48
|
+
Curl.const_defined?(:CURLOPT_FTP_CREATE_MISSING_DIRS)
|
|
49
|
+
|
|
50
|
+
c = Curl::Easy.new('ftp://example.com/')
|
|
51
|
+
|
|
52
|
+
assert_respond_to(c, :ftp_create_missing_dirs=)
|
|
53
|
+
assert_respond_to(c, :ftp_create_missing_dirs)
|
|
54
|
+
assert_nil(c.ftp_create_missing_dirs)
|
|
55
|
+
|
|
56
|
+
c.ftp_create_missing_dirs = true
|
|
57
|
+
assert_equal(true, c.ftp_create_missing_dirs)
|
|
58
|
+
|
|
59
|
+
assert_equal(1, c.set(:ftp_create_missing_dirs, 1))
|
|
60
|
+
assert_equal(1, c.ftp_create_missing_dirs)
|
|
61
|
+
|
|
62
|
+
retry_mode = if Curl.const_defined?(:CURLFTP_CREATE_DIR_RETRY)
|
|
63
|
+
Curl::CURLFTP_CREATE_DIR_RETRY
|
|
64
|
+
else
|
|
65
|
+
2
|
|
66
|
+
end
|
|
67
|
+
c.set(:ftp_create_missing_dirs, retry_mode)
|
|
68
|
+
assert_equal(retry_mode, c.ftp_create_missing_dirs)
|
|
69
|
+
assert_equal(retry_mode, c.clone.ftp_create_missing_dirs)
|
|
70
|
+
|
|
71
|
+
settings = c.reset
|
|
72
|
+
assert_equal(retry_mode, settings[:ftp_create_missing_dirs])
|
|
73
|
+
assert_nil(c.ftp_create_missing_dirs)
|
|
74
|
+
end
|
|
75
|
+
|
|
18
76
|
# Setting ftp_commands remains supported for control-connection commands.
|
|
19
77
|
def test_can_assign_ftp_commands
|
|
20
78
|
c = Curl::Easy.new('ftp://example.com/')
|
data/tests/tc_gc_compact.rb
CHANGED
|
@@ -96,6 +96,59 @@ class TestGcCompact < Test::Unit::TestCase
|
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Regression: after compaction relocates a persistent Curl::Easy and its old
|
|
100
|
+
# heap slot is recycled by another T_DATA object, completion dispatch (which
|
|
101
|
+
# reads the rbce->self back-reference via CURLINFO_PRIVATE) must still
|
|
102
|
+
# resolve to the correct Curl::Easy. Without pinning in curl_easy_mark this
|
|
103
|
+
# raises "TypeError: wrong argument type Curl::Upload (expected Curl::Easy)"
|
|
104
|
+
# on the first round, or defers it onto the multi and raises it from a later
|
|
105
|
+
# request.
|
|
106
|
+
def test_completion_dispatch_survives_compact_and_slot_reuse
|
|
107
|
+
omit('needs GC.verify_compaction_references') unless GC.respond_to?(:verify_compaction_references)
|
|
108
|
+
|
|
109
|
+
# Heap-only reference: holding the easy in a local would pin it to the
|
|
110
|
+
# stack and mask the bug.
|
|
111
|
+
holder = { easy: Curl::Easy.new($TEST_URL) }
|
|
112
|
+
holder[:easy].timeout = 5
|
|
113
|
+
completed = nil
|
|
114
|
+
holder[:easy].on_complete { |e| completed = e }
|
|
115
|
+
holder[:easy].http(:GET)
|
|
116
|
+
assert_same holder[:easy], completed
|
|
117
|
+
expected_body = holder[:easy].body_str
|
|
118
|
+
refute_empty expected_body
|
|
119
|
+
|
|
120
|
+
ruby_version = RUBY_VERSION.split('.').first(2).map(&:to_i)
|
|
121
|
+
heap_expansion = if (ruby_version <=> [3, 2]) >= 0
|
|
122
|
+
{ expand_heap: true }
|
|
123
|
+
else
|
|
124
|
+
{ double_heap: true }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
3.times do
|
|
128
|
+
# Relocate every movable object, then recycle freed slots with other
|
|
129
|
+
# T_DATA instances so a stale back-reference cannot fail soft.
|
|
130
|
+
GC.verify_compaction_references(toward: :empty, **heap_expansion)
|
|
131
|
+
churn = Array.new(50_000) { Curl::Upload.new }
|
|
132
|
+
|
|
133
|
+
completed = nil
|
|
134
|
+
holder[:easy].http(:GET)
|
|
135
|
+
|
|
136
|
+
multi = holder[:easy].multi
|
|
137
|
+
if multi&.instance_variable_defined?(:@__curb_deferred_exception)
|
|
138
|
+
raise multi.instance_variable_get(:@__curb_deferred_exception)
|
|
139
|
+
end
|
|
140
|
+
assert_same holder[:easy], completed
|
|
141
|
+
assert_equal expected_body, holder[:easy].body_str
|
|
142
|
+
churn.clear
|
|
143
|
+
end
|
|
144
|
+
ensure
|
|
145
|
+
begin
|
|
146
|
+
holder[:easy]&.close
|
|
147
|
+
rescue StandardError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
99
152
|
private
|
|
100
153
|
|
|
101
154
|
def run_multi_perform_compact_iteration
|
|
@@ -107,4 +107,19 @@ class TestServerMethodsLockHandling < Test::Unit::TestCase
|
|
|
107
107
|
assert_equal false, File.exist?(locked_file)
|
|
108
108
|
assert_equal false, server_responding?(@__port)
|
|
109
109
|
end
|
|
110
|
+
|
|
111
|
+
def test_server_setup_reuses_the_process_server
|
|
112
|
+
server_setup(@__port)
|
|
113
|
+
first_server = @server
|
|
114
|
+
|
|
115
|
+
other = Object.new
|
|
116
|
+
other.extend(TestServerMethods)
|
|
117
|
+
lock_path = locked_file
|
|
118
|
+
other.define_singleton_method(:locked_file) { lock_path }
|
|
119
|
+
other.server_setup(@__port)
|
|
120
|
+
|
|
121
|
+
assert_same first_server, other.instance_variable_get(:@server)
|
|
122
|
+
ensure
|
|
123
|
+
other.stop_test_server if defined?(other) && other
|
|
124
|
+
end
|
|
110
125
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: curb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ross Bamford
|
|
8
8
|
- Todd A. Fisher
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
|
|
14
14
|
for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- tests/bug_require_last_or_segfault.rb
|
|
65
65
|
- tests/bugtests.rb
|
|
66
66
|
- tests/helper.rb
|
|
67
|
+
- tests/io_select_less_scheduler_probe.rb
|
|
67
68
|
- tests/leak_trace.rb
|
|
68
69
|
- tests/mem_check.rb
|
|
69
70
|
- tests/require_last_or_segfault_script.rb
|
|
@@ -133,6 +134,7 @@ test_files:
|
|
|
133
134
|
- tests/bug_require_last_or_segfault.rb
|
|
134
135
|
- tests/bugtests.rb
|
|
135
136
|
- tests/helper.rb
|
|
137
|
+
- tests/io_select_less_scheduler_probe.rb
|
|
136
138
|
- tests/leak_trace.rb
|
|
137
139
|
- tests/mem_check.rb
|
|
138
140
|
- tests/require_last_or_segfault_script.rb
|