curb 0.8.4 → 1.3.5

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +522 -0
  3. data/Rakefile +79 -26
  4. data/ext/banned.h +32 -0
  5. data/ext/curb.c +549 -230
  6. data/ext/curb.h +19 -10
  7. data/ext/curb_easy.c +1935 -366
  8. data/ext/curb_easy.h +16 -0
  9. data/ext/curb_errors.c +115 -18
  10. data/ext/curb_errors.h +8 -5
  11. data/ext/curb_macros.h +33 -21
  12. data/ext/curb_multi.c +1858 -272
  13. data/ext/curb_multi.h +10 -3
  14. data/ext/curb_postfield.c +149 -77
  15. data/ext/curb_postfield.h +1 -0
  16. data/ext/curb_upload.c +38 -11
  17. data/ext/curb_upload.h +2 -0
  18. data/ext/extconf.rb +353 -35
  19. data/lib/curb.rb +1 -0
  20. data/lib/curl/easy.rb +314 -78
  21. data/lib/curl/multi.rb +134 -28
  22. data/lib/curl.rb +217 -11
  23. data/tests/bug_crash_on_debug.rb +14 -28
  24. data/tests/bug_crash_on_progress.rb +32 -16
  25. data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
  26. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
  27. data/tests/bug_follow_redirect_288.rb +83 -0
  28. data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
  29. data/tests/bug_issue102.rb +1 -1
  30. data/tests/bug_issue_noproxy.rb +56 -0
  31. data/tests/bug_issue_post_redirect.rb +93 -0
  32. data/tests/bug_issue_spnego.rb +41 -0
  33. data/tests/bug_multi_segfault.rb +1 -0
  34. data/tests/bug_raise_on_callback.rb +30 -0
  35. data/tests/helper.rb +400 -44
  36. data/tests/leak_trace.rb +237 -0
  37. data/tests/mem_check.rb +3 -0
  38. data/tests/tc_curl.rb +31 -1
  39. data/tests/tc_curl_download.rb +12 -7
  40. data/tests/tc_curl_easy.rb +911 -63
  41. data/tests/tc_curl_easy_cookielist.rb +277 -0
  42. data/tests/tc_curl_easy_request_target.rb +41 -0
  43. data/tests/tc_curl_easy_resolve.rb +48 -0
  44. data/tests/tc_curl_maxfilesize.rb +12 -0
  45. data/tests/tc_curl_multi.rb +855 -53
  46. data/tests/tc_curl_native_coverage.rb +145 -0
  47. data/tests/tc_curl_postfield.rb +207 -30
  48. data/tests/tc_curl_protocols.rb +37 -0
  49. data/tests/tc_fiber_scheduler.rb +543 -0
  50. data/tests/tc_ftp_options.rb +39 -0
  51. data/tests/tc_gc_compact.rb +223 -0
  52. data/tests/tc_test_server_methods.rb +110 -0
  53. data/tests/timeout.rb +30 -6
  54. metadata +59 -36
  55. data/README +0 -194
@@ -1,19 +1,216 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+ require 'set'
3
+ require 'tmpdir'
2
4
 
3
5
  class TestCurbCurlMulti < Test::Unit::TestCase
4
6
  def teardown
5
7
  # get a better read on memory loss when running in valgrind
6
8
  ObjectSpace.garbage_collect
9
+ super
10
+ end
11
+
12
+ # for https://github.com/taf2/curb/issues/277
13
+ # must connect to an external
14
+ def test_connection_keepalive
15
+ omit('Not supported on Windows runners') if WINDOWS
16
+ # this test fails with libcurl 7.22.0. I didn't investigate, but it may be related
17
+ # to CURLOPT_MAXCONNECTS bug fixed in 7.30.0:
18
+ # https://github.com/curl/curl/commit/e87e76e2dc108efb1cae87df496416f49c55fca0
19
+ omit("Skip, libcurl too old (< 7.22.0)") if Curl::CURL_VERSION.to_f < 8 && Curl::CURL_VERSION.split('.')[1].to_i <= 22
20
+
21
+ port_socket = TCPServer.new('127.0.0.1', 0)
22
+ port = port_socket.addr[1]
23
+ port_socket.close
24
+
25
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
26
+ server.mount_proc(TestServlet.path) do |req, res|
27
+ res.body = "GET#{req.query_string}"
28
+ res['Content-Type'] = 'text/plain'
29
+ end
30
+ server_thread = Thread.new(server) { |srv| srv.start }
31
+ wait_for_server_ready(port, thread: server_thread)
32
+
33
+ test_url = "http://127.0.0.1:#{port}#{TestServlet.path}"
34
+ Curl::Multi.autoclose = true
35
+ assert Curl::Multi.autoclose
36
+
37
+ if `which ss`.strip.size == 0
38
+ # osx need lsof still :(
39
+ open_fds = lambda do
40
+ out = `/usr/sbin/lsof -nP -a -p #{Process.pid} -iTCP:#{port} -sTCP:ESTABLISHED`
41
+ [out.lines.drop(1).size, 0].max
42
+ end
43
+ else
44
+ ss = `which ss`.strip
45
+ open_fds = lambda do
46
+ `#{ss} -tn4 state established dport = :#{port} | wc -l`.strip.to_i
47
+ end
48
+ end
49
+ Curl::Multi.autoclose = false
50
+ before_open = open_fds.call
51
+ #puts "before_open: #{before_open.inspect}"
52
+ assert !Curl::Multi.autoclose
53
+ multi = Curl::Multi.new
54
+ multi.max_connects = 1 # limit to 1 connection within the multi handle
55
+
56
+ did_complete = false
57
+ 5.times do |n|
58
+ easy = Curl::Easy.new(test_url) do |curl|
59
+ curl.timeout = 5 # ensure we don't hang for ever connecting to an external host
60
+ curl.on_complete {
61
+ did_complete = true
62
+ }
63
+ end
64
+ multi.add(easy)
65
+ end
66
+
67
+ multi.perform
68
+ assert did_complete
69
+ after_open = open_fds.call
70
+ #puts "after_open: #{after_open} before_open: #{before_open.inspect}"
71
+ # Some CI/libcurl combinations tear down the loopback connection before
72
+ # ss/lsof observes it, so only assert that the multi cache does not retain
73
+ # more than one extra connection here.
74
+ assert (after_open - before_open) < 3, "with max connections set to 1 the multi handle should not retain more than one extra connection"
75
+ multi.close
76
+
77
+ after_open = open_fds.call
78
+ #puts "after_open: #{after_open} before_open: #{before_open.inspect}"
79
+ assert_equal 0, (after_open - before_open), "after closing the multi handle all connections should be closed"
80
+
81
+ Curl::Multi.autoclose = true
82
+ multi = Curl::Multi.new
83
+ did_complete = false
84
+ 5.times do |n|
85
+ easy = Curl::Easy.new(test_url) do |curl|
86
+ curl.timeout = 5 # ensure we don't hang for ever connecting to an external host
87
+ curl.on_complete {
88
+ did_complete = true
89
+ }
90
+ end
91
+ multi.add(easy)
92
+ end
93
+
94
+ multi.perform
95
+ assert did_complete
96
+ after_open = open_fds.call
97
+ #puts "after_open: #{after_open} before_open: #{before_open.inspect}"
98
+ assert_equal 0, (after_open - before_open), "auto close the connections"
99
+ ensure
100
+ server.shutdown if defined?(server) && server
101
+ server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
102
+ server_thread.kill if defined?(server_thread) && server_thread&.alive?
103
+ Curl::Multi.autoclose = false # restore default
104
+ end
105
+
106
+ def test_connection_autoclose
107
+ assert !Curl::Multi.autoclose
108
+ Curl::Multi.autoclose = true
109
+ assert Curl::Multi.autoclose
110
+ ensure
111
+ Curl::Multi.autoclose = false # restore default
112
+ end
113
+
114
+ def test_perform_can_reuse_multi_when_autoclose_is_enabled
115
+ Curl::Multi.autoclose = true
116
+
117
+ multi = Curl::Multi.new
118
+ results = []
119
+
120
+ first = Curl::Easy.new(TestServlet.url)
121
+ first.on_complete { results << first.code }
122
+ multi.add(first)
123
+ multi.perform
124
+
125
+ second = Curl::Easy.new(TestServlet.url)
126
+ second.on_complete { results << second.code }
127
+ multi.add(second)
128
+ multi.perform
129
+
130
+ assert_equal [200, 200], results
131
+ ensure
132
+ multi.close if defined?(multi) && multi
133
+ Curl::Multi.autoclose = false
134
+ end
135
+
136
+ def test_close_inside_completion_callback_is_rejected_without_invalidating_multi
137
+ multi = Curl::Multi.new
138
+ easy = Curl::Easy.new(TestServlet.url)
139
+
140
+ easy.on_complete { multi.close }
141
+ multi.add(easy)
142
+
143
+ error = assert_raise(Curl::Err::AbortedByCallbackError) { multi.perform }
144
+ assert_match(/Cannot close an active Curl::Multi handle/, error.message)
145
+ assert multi.idle?
146
+ assert_equal({}, multi.requests)
147
+
148
+ easy.on_complete { |_curl| }
149
+ multi.add(easy)
150
+ multi.perform
151
+
152
+ assert_equal "GET", easy.body_str
153
+ ensure
154
+ multi.close if defined?(multi) && multi
155
+ end
156
+
157
+ def test_close_inside_perform_block_is_rejected_without_invalidating_multi
158
+ multi = Curl::Multi.new
159
+ easy = Curl::Easy.new(TestServlet.url)
160
+ multi.add(easy)
161
+
162
+ error = assert_raise(RuntimeError) do
163
+ multi.perform { multi.close }
164
+ end
165
+ assert_match(/Cannot close an active Curl::Multi handle/, error.message)
166
+
167
+ assert_nothing_raised { multi.close }
168
+ multi = nil
169
+ ensure
170
+ multi.close if defined?(multi) && multi
171
+ end
172
+
173
+ def test_active_easy_cannot_be_added_to_another_multi
174
+ first_multi = Curl::Multi.new
175
+ second_multi = Curl::Multi.new
176
+ easy = Curl::Easy.new(TestServlet.url)
177
+
178
+ first_multi.add(easy)
179
+
180
+ error = assert_raise(RuntimeError) { second_multi.add(easy) }
181
+ assert_match(/active Curl::Easy/, error.message)
182
+ assert_same first_multi, easy.multi
183
+ assert_equal easy, first_multi.requests[easy.object_id]
184
+
185
+ assert_nothing_raised { first_multi.close }
186
+ first_multi = nil
187
+ ensure
188
+ first_multi.close if defined?(first_multi) && first_multi
189
+ second_multi.close if defined?(second_multi) && second_multi
190
+ end
191
+
192
+ def test_close_makes_multi_unusable
193
+ multi = Curl::Multi.new
194
+ multi.close
195
+
196
+ error = assert_raise(Curl::Err::MultiBadHandle) do
197
+ multi.add(Curl::Easy.new($TEST_URL))
198
+ end
199
+
200
+ assert_match(/Invalid multi handle/i, error.message)
201
+ assert_equal 0, multi.requests.length
202
+ ensure
203
+ multi.close if multi
7
204
  end
8
205
 
9
206
  def test_new_multi_01
10
- d1 = ""
207
+ d1 = String.new
11
208
  c1 = Curl::Easy.new($TEST_URL) do |curl|
12
209
  curl.headers["User-Agent"] = "myapp-0.0"
13
210
  curl.on_body {|d| d1 << d; d.length }
14
211
  end
15
212
 
16
- d2 = ""
213
+ d2 = String.new
17
214
  c2 = Curl::Easy.new($TEST_URL) do |curl|
18
215
  curl.headers["User-Agent"] = "myapp-0.0"
19
216
  curl.on_body {|d| d2 << d; d.length }
@@ -28,9 +225,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
28
225
 
29
226
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, d1)
30
227
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, d2)
31
-
32
- m = nil
33
-
228
+ ensure
229
+ m.close if m
34
230
  end
35
231
 
36
232
  def test_perform_block
@@ -49,9 +245,424 @@ class TestCurbCurlMulti < Test::Unit::TestCase
49
245
 
50
246
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, c1.body_str)
51
247
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, c2.body_str)
248
+ ensure
249
+ m.close if m
250
+ end
251
+
252
+ def test_multi_perform_runs_work_added_from_final_idle_yield
253
+ with_queue_refill_test_server do |port, hits|
254
+ previous_autoclose = Curl::Multi.autoclose
255
+ Curl::Multi.autoclose = true
256
+
257
+ multi = Curl::Multi.new
258
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
259
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
260
+ completions = []
261
+ empty_yields = 0
262
+ queued_added = false
263
+
264
+ slow.on_complete { completions << :slow }
265
+ queued.on_complete { completions << :queued }
266
+
267
+ multi.add(slow)
268
+ multi.perform do |performing_multi|
269
+ empty_yields += 1 if performing_multi.requests.empty?
270
+ if !queued_added && empty_yields >= 2
271
+ queued_added = true
272
+ performing_multi.add(queued)
273
+ end
274
+ end
275
+
276
+ assert queued_added, "test should add queued work from the final idle yield"
277
+ assert_equal [:slow, :queued], completions
278
+ assert_equal 1, hits[:slow]
279
+ assert_equal 1, hits[:queued]
280
+ assert_equal 0, multi.requests.length
281
+ ensure
282
+ Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
283
+ multi.close if defined?(multi) && multi
284
+ end
285
+ end
286
+
287
+ def test_multi_easy_get
288
+ n = 1
289
+ urls = []
290
+ n.times { urls << $TEST_URL }
291
+ Curl::Multi.get(urls, {timeout: 5}) {|easy|
292
+ assert_match(/file:/, easy.last_effective_url)
293
+ }
294
+ end
295
+
296
+ def test_multi_easy_get_with_error
297
+ omit('Path/line parsing differs on Windows') if WINDOWS
298
+ begin
299
+ did_raise = false
300
+ n = 3
301
+ urls = []
302
+ n.times { urls << $TEST_URL }
303
+ error_line_number_should_be = nil
304
+ Curl::Multi.get(urls, {timeout: 5}) {|easy|
305
+ # if we got this right the error will be reported to be on the line below our error_line_number_should_be
306
+ error_line_number_should_be = __LINE__
307
+ raise
308
+ }
309
+
310
+ rescue Curl::Err::AbortedByCallbackError => e
311
+ did_raise = true
312
+ in_file = e.backtrace.detect {|err| err.match?(File.basename(__FILE__)) }
313
+ #in_file_stack = e.backtrace.select {|err| err.match?(File.basename(__FILE__)) }
314
+ assert_match(__FILE__, in_file)
315
+ in_file.gsub!(__FILE__)
316
+ parts = in_file.split(':')
317
+ parts.shift
318
+ line_no = parts.shift.to_i
319
+ assert_equal error_line_number_should_be+1, line_no.to_i
320
+ end
321
+
322
+ assert did_raise, "we should have raised an exception"
323
+
324
+ end
325
+
326
+ def test_multi_perform_reraises_on_body_exception
327
+ multi = Curl::Multi.new
328
+ easy = Curl::Easy.new(TestServlet.url)
329
+ easy.on_body { raise "body blew up" }
330
+
331
+ error = assert_raise(RuntimeError) do
332
+ multi.add(easy)
333
+ multi.perform
334
+ end
335
+
336
+ assert_equal "body blew up", error.message
337
+ assert !easy.instance_variable_defined?(:@__curb_callback_error)
338
+ ensure
339
+ multi.close if multi
340
+ end
341
+
342
+ def test_multi_perform_reraises_on_body_exception_for_frozen_easy
343
+ multi = Curl::Multi.new
344
+ easy = Curl::Easy.new($TEST_URL)
345
+ callback_error = Class.new(RuntimeError)
346
+ easy.on_body { raise callback_error, "body blew up" }
347
+ easy.freeze
348
+
349
+ error = assert_raise(callback_error) do
350
+ multi.add(easy)
351
+ multi.perform
352
+ end
353
+
354
+ assert_equal "body blew up", error.message
355
+ ensure
356
+ begin
357
+ multi.close if multi
358
+ rescue StandardError
359
+ multi.instance_variable_set(:@requests, {})
360
+ multi._close
361
+ end
362
+ end
363
+
364
+ def test_multi_perform_reraises_on_header_exception
365
+ multi = Curl::Multi.new
366
+ easy = Curl::Easy.new(TestServlet.url)
367
+ easy.on_header { raise "header blew up" }
368
+
369
+ error = assert_raise(RuntimeError) do
370
+ multi.add(easy)
371
+ multi.perform
372
+ end
373
+
374
+ assert_equal "header blew up", error.message
375
+ assert !easy.instance_variable_defined?(:@__curb_callback_error)
376
+ ensure
377
+ multi.close if multi
378
+ end
379
+
380
+ def test_multi_perform_drains_completed_siblings_before_reraising_on_body_exception
381
+ multi = Curl::Multi.new
382
+ failed = Curl::Easy.new($TEST_URL)
383
+ completed = Curl::Easy.new($TEST_URL)
384
+ completions = []
385
+
386
+ failed.on_body { raise "body blew up" }
387
+ completed.on_complete { completions << :completed }
388
+
389
+ error = assert_raise(RuntimeError) do
390
+ multi.add(failed)
391
+ multi.add(completed)
392
+ multi.perform
393
+ end
394
+
395
+ assert_equal "body blew up", error.message
396
+ assert_equal [:completed], completions
397
+ assert multi.idle?, 'The multi handle should be idle after draining the completed batch'
398
+ assert_equal 0, multi.requests.length
399
+ assert_nil failed.multi
400
+ assert_nil completed.multi
401
+ assert !failed.instance_variable_defined?(:@__curb_callback_error)
402
+ ensure
403
+ multi.close if multi
404
+ end
405
+
406
+ def test_multi_perform_waits_until_idle_before_reraising_on_complete_exception
407
+ port_socket = TCPServer.new('127.0.0.1', 0)
408
+ port = port_socket.addr[1]
409
+ port_socket.close
410
+
411
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
412
+ server.mount_proc('/fast') do |_req, res|
413
+ res['Content-Type'] = 'text/plain'
414
+ res.body = 'fast'
415
+ end
416
+ server.mount_proc('/slow') do |_req, res|
417
+ res['Content-Type'] = 'text/plain'
418
+ sleep 0.2
419
+ res.body = 'slow'
420
+ end
421
+ server_thread = Thread.new(server) { |srv| srv.start }
422
+ wait_for_server_ready(port, thread: server_thread)
423
+
424
+ multi = Curl::Multi.new
425
+ fast = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
426
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
427
+ completions = []
428
+
429
+ fast.on_complete do
430
+ completions << :fast
431
+ raise "complete blew up"
432
+ end
433
+ slow.on_complete { completions << :slow }
434
+
435
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
436
+ multi.add(fast)
437
+ multi.add(slow)
438
+ multi.perform
439
+ end
440
+
441
+ assert_equal "complete blew up", error.message
442
+ assert_equal [:fast, :slow], completions
443
+ assert multi.idle?, 'The multi handle should be idle before the deferred exception is raised'
444
+ assert_equal 0, multi.requests.length
445
+ assert_nil fast.multi
446
+ assert_nil slow.multi
447
+ ensure
448
+ multi.close if multi
449
+ server.shutdown if defined?(server) && server
450
+ server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
451
+ server_thread.kill if defined?(server_thread) && server_thread&.alive?
452
+ end
453
+
454
+ def test_multi_perform_keeps_yielding_block_while_draining_deferred_on_complete_exception
455
+ with_queue_refill_test_server do |port, hits|
456
+ multi = Curl::Multi.new
457
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
458
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
459
+ failure_seen = false
460
+ slow_completed = false
461
+ yielded_during_drain = false
462
+
463
+ failed.on_complete do
464
+ failure_seen = true
465
+ raise "complete blew up"
466
+ end
467
+ slow.on_complete { slow_completed = true }
468
+
469
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
470
+ multi.add(failed)
471
+ multi.add(slow)
472
+ multi.perform do
473
+ yielded_during_drain ||= failure_seen && !slow_completed
474
+ end
475
+ end
476
+
477
+ assert_equal "complete blew up", error.message
478
+ assert yielded_during_drain,
479
+ "perform block should continue yielding while draining a slow sibling after a deferred callback exception"
480
+ assert_equal 1, hits[:fail]
481
+ assert_equal 1, hits[:slow]
482
+ ensure
483
+ multi.close if multi
484
+ end
485
+ end
486
+
487
+ def test_multi_perform_does_not_start_queued_work_after_on_complete_exception
488
+ with_queue_refill_test_server do |port, hits|
489
+ multi = Curl::Multi.new
490
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
491
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
492
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
493
+ failure_seen = false
494
+
495
+ failed.on_complete do
496
+ failure_seen = true
497
+ raise "complete blew up"
498
+ end
499
+
500
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
501
+ multi.add(failed)
502
+ multi.add(slow)
503
+ multi.perform do
504
+ next unless failure_seen
505
+
506
+ multi.add(queued)
507
+ failure_seen = false
508
+ end
509
+ end
510
+
511
+ assert_equal "complete blew up", error.message
512
+ assert_equal 1, hits[:fail]
513
+ assert_equal 1, hits[:slow]
514
+ assert_equal 0, hits[:queued], "queued request should not start once a deferred callback exception is pending"
515
+ ensure
516
+ multi.close if multi
517
+ end
518
+ end
519
+
520
+ def test_multi_perform_does_not_start_work_added_within_failing_on_complete_callback
521
+ with_queue_refill_test_server do |port, hits|
522
+ multi = Curl::Multi.new
523
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
524
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
525
+
526
+ failed.on_complete do
527
+ multi.add(queued)
528
+ raise "complete blew up"
529
+ end
530
+
531
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
532
+ multi.add(failed)
533
+ multi.perform
534
+ end
535
+
536
+ assert_equal "complete blew up", error.message
537
+ assert_equal 1, hits[:fail]
538
+ assert_equal 0, hits[:queued],
539
+ "request added from a failing on_complete callback should not start once the multi begins aborting"
540
+ ensure
541
+ multi.close if multi
542
+ end
543
+ end
544
+
545
+ def test_multi_perform_does_not_restart_sibling_removed_and_readded_from_failing_on_complete_callback
546
+ with_queue_refill_test_server(wait_fail_until_slow: true) do |port, hits|
547
+ multi = Curl::Multi.new
548
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
549
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
550
+
551
+ failed.on_complete do
552
+ multi.remove(slow)
553
+ multi.add(slow)
554
+ raise "complete blew up"
555
+ end
556
+
557
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
558
+ multi.add(failed)
559
+ multi.add(slow)
560
+ multi.perform
561
+ end
52
562
 
53
- m = nil
563
+ assert_equal "complete blew up", error.message
564
+ assert_equal 1, hits[:fail]
565
+ assert_equal 1, hits[:slow],
566
+ "sibling removed and re-added from a failing on_complete callback should be treated as replacement work and not restarted"
567
+ ensure
568
+ multi.close if multi
569
+ end
570
+ end
571
+
572
+ def test_multi_perform_does_not_start_work_added_within_on_complete_after_on_body_exception
573
+ with_queue_refill_test_server do |port, hits|
574
+ multi = Curl::Multi.new
575
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
576
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
577
+
578
+ failed.on_body { raise "body blew up" }
579
+ failed.on_complete { multi.add(queued) }
580
+
581
+ error = assert_raise(RuntimeError) do
582
+ multi.add(failed)
583
+ multi.perform
584
+ end
585
+
586
+ assert_equal "body blew up", error.message
587
+ assert_equal 1, hits[:fail]
588
+ assert_equal 0, hits[:queued],
589
+ "request added from on_complete after a body callback exception should not start once the multi begins aborting"
590
+ ensure
591
+ multi.close if multi
592
+ end
593
+ end
594
+
595
+ def test_multi_http_does_not_fetch_queued_url_after_on_body_exception
596
+ with_queue_refill_test_server do |port, hits|
597
+ urls = [
598
+ { :url => "http://127.0.0.1:#{port}/queued", :method => :get },
599
+ { :url => "http://127.0.0.1:#{port}/slow", :method => :get },
600
+ { :url => "http://127.0.0.1:#{port}/fail", :method => :get, :on_body => proc { raise "body blew up" } }
601
+ ]
602
+
603
+ error = assert_raise(RuntimeError) do
604
+ Curl::Multi.http(urls, {:max_connects => 2}) { |_easy, _code, _method| }
605
+ end
606
+
607
+ assert_equal "body blew up", error.message
608
+ assert_equal 1, hits[:fail]
609
+ assert_equal 1, hits[:slow]
610
+ assert_equal 0, hits[:queued], "Curl::Multi.http should not refill queued urls after a callback abort"
611
+ end
612
+ end
613
+
614
+ def test_multi_perform_runs_status_callbacks_after_on_body_exception
615
+ multi = Curl::Multi.new
616
+ easy = Curl::Easy.new(TestServlet.url)
617
+ callbacks = []
618
+
619
+ easy.on_body { raise "body blew up" }
620
+ easy.on_complete { callbacks << :complete }
621
+ easy.on_failure do |_completed_easy, error_info|
622
+ callbacks << :failure
623
+ assert_equal Curl::Err::WriteError, error_info.first
624
+ end
625
+
626
+ error = assert_raise(RuntimeError) do
627
+ multi.add(easy)
628
+ multi.perform
629
+ end
54
630
 
631
+ assert_equal "body blew up", error.message
632
+ assert_equal [:complete, :failure], callbacks
633
+ assert multi.idle?
634
+ assert_equal 0, multi.requests.length
635
+ ensure
636
+ multi.close if multi
637
+ end
638
+
639
+ def test_multi_perform_preserves_on_body_exception_when_status_callbacks_raise
640
+ multi = Curl::Multi.new
641
+ easy = Curl::Easy.new(TestServlet.url)
642
+ callbacks = []
643
+
644
+ easy.on_body { raise "body blew up" }
645
+ easy.on_complete do
646
+ callbacks << :complete
647
+ raise "complete blew up"
648
+ end
649
+ easy.on_failure do |_completed_easy, error_info|
650
+ callbacks << :failure
651
+ assert_equal Curl::Err::WriteError, error_info.first
652
+ raise "failure blew up"
653
+ end
654
+
655
+ error = assert_raise(RuntimeError) do
656
+ multi.add(easy)
657
+ multi.perform
658
+ end
659
+
660
+ assert_equal "body blew up", error.message
661
+ assert_equal [:complete, :failure], callbacks
662
+ assert multi.idle?
663
+ assert_equal 0, multi.requests.length
664
+ ensure
665
+ multi.close if multi
55
666
  end
56
667
 
57
668
  # NOTE: if this test runs slowly on Mac OSX, it is probably due to the use of a port install curl+ssl+ares install
@@ -61,7 +672,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
61
672
  m = Curl::Multi.new
62
673
  responses = []
63
674
  n.times do|i|
64
- responses[i] = ""
675
+ responses[i] = String.new
65
676
  c = Curl::Easy.new($TEST_URL) do|curl|
66
677
  curl.on_body{|data| responses[i] << data; data.size }
67
678
  end
@@ -74,8 +685,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
74
685
  n.times do|i|
75
686
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, responses[i], "response #{i}")
76
687
  end
77
-
78
- m = nil
688
+ ensure
689
+ m.close if m
79
690
  end
80
691
 
81
692
  def test_n_requests_with_break
@@ -85,7 +696,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
85
696
  5.times do|it|
86
697
  responses = []
87
698
  n.times do|i|
88
- responses[i] = ""
699
+ responses[i] = String.new
89
700
  c = Curl::Easy.new($TEST_URL) do|curl|
90
701
  curl.on_body{|data| responses[i] << data; data.size }
91
702
  end
@@ -98,9 +709,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
98
709
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, responses[i], "response #{i}")
99
710
  end
100
711
  end
101
-
102
- m = nil
103
-
712
+ ensure
713
+ m.close if m
104
714
  end
105
715
 
106
716
  def test_idle_check
@@ -109,19 +719,25 @@ class TestCurbCurlMulti < Test::Unit::TestCase
109
719
 
110
720
  assert(m.idle?, 'A new Curl::Multi handle should be idle')
111
721
 
722
+ assert_nil e.multi
723
+
112
724
  m.add(e)
113
725
 
726
+ assert_not_nil e.multi
727
+
114
728
  assert((not m.idle?), 'A Curl::Multi handle with a request should not be idle')
115
729
 
116
730
  m.perform
117
731
 
118
732
  assert(m.idle?, 'A Curl::Multi handle should be idle after performing its requests')
733
+ ensure
734
+ m.close if m
119
735
  end
120
736
 
121
737
  def test_requests
122
738
  m = Curl::Multi.new
123
739
 
124
- assert_equal([], m.requests, 'A new Curl::Multi handle should have no requests')
740
+ assert_equal(0, m.requests.length, 'A new Curl::Multi handle should have no requests')
125
741
 
126
742
  10.times do
127
743
  m.add(Curl::Easy.new($TEST_URL))
@@ -131,7 +747,46 @@ class TestCurbCurlMulti < Test::Unit::TestCase
131
747
 
132
748
  m.perform
133
749
 
134
- assert_equal([], m.requests, 'A new Curl::Multi handle should have no requests after a perform')
750
+ assert_equal(0, m.requests.length, 'A new Curl::Multi handle should have no requests after a perform')
751
+ ensure
752
+ m.close if m
753
+ end
754
+
755
+ def test_easy_multi_is_cleared_after_perform
756
+ m = Curl::Multi.new
757
+ c = Curl::Easy.new($TEST_URL)
758
+
759
+ m.add(c)
760
+ assert_equal m, c.multi
761
+
762
+ m.perform
763
+
764
+ assert_nil c.multi
765
+ ensure
766
+ m.close if m
767
+ end
768
+
769
+ def test_easy_multi_is_available_during_completion_callbacks
770
+ multi = Curl::Multi.new
771
+ easy = Curl::Easy.new($TEST_URL)
772
+ seen_multi = {}
773
+
774
+ easy.on_complete do |completed_easy|
775
+ seen_multi[:complete] = completed_easy.multi
776
+ end
777
+
778
+ easy.on_success do |completed_easy|
779
+ seen_multi[:success] = completed_easy.multi
780
+ end
781
+
782
+ multi.add(easy)
783
+ multi.perform
784
+
785
+ assert_same multi, seen_multi[:complete]
786
+ assert_same multi, seen_multi[:success]
787
+ assert_nil easy.multi
788
+ ensure
789
+ multi.close if multi
135
790
  end
136
791
 
137
792
  def test_cancel
@@ -144,7 +799,9 @@ class TestCurbCurlMulti < Test::Unit::TestCase
144
799
 
145
800
  m.cancel!
146
801
 
147
- assert_equal([], m.requests, 'A new Curl::Multi handle should have no requests after being canceled')
802
+ assert_equal(0, m.requests.size, 'A new Curl::Multi handle should have no requests after being canceled')
803
+ ensure
804
+ m.close if m
148
805
  end
149
806
 
150
807
  def test_with_success
@@ -175,8 +832,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
175
832
 
176
833
  assert success_called2
177
834
  assert success_called1
178
-
179
- m = nil
835
+ ensure
836
+ m.close if m
180
837
  end
181
838
 
182
839
  def test_with_success_cb_with_404
@@ -188,7 +845,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
188
845
  c1.on_success do|c|
189
846
  success_called1 = true
190
847
  #puts "success 1 called: #{c.body_str.inspect}"
191
- #assert_match(/^# DO NOT REMOVE THIS COMMENT/, c.body_str)
848
+ assert_match(/^# DO NOT REMOVE THIS COMMENT/, c.body_str)
192
849
  end
193
850
 
194
851
  c1.on_failure do|c,rc|
@@ -218,8 +875,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
218
875
 
219
876
  assert success_called2
220
877
  assert !success_called1
221
-
222
- m = nil
878
+ ensure
879
+ m.close if m
223
880
  end
224
881
 
225
882
  # This tests whether, ruby's GC will trash an out of scope easy handle
@@ -227,7 +884,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
227
884
  attr_reader :buf
228
885
 
229
886
  def t_method
230
- @buf = ""
887
+ @buf = String.new
231
888
  @m = Curl::Multi.new
232
889
  10.times do|i|
233
890
  c = Curl::Easy.new($TEST_URL)
@@ -243,6 +900,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
243
900
  @m.perform do
244
901
  ObjectSpace.garbage_collect
245
902
  end
903
+ ensure
904
+ @m.close if @m
246
905
  end
247
906
 
248
907
  def self.test
@@ -273,8 +932,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
273
932
  m = Curl::Multi.new
274
933
  # add a few easy handles
275
934
  requests.each do |url|
276
- responses[url] = ""
277
- responses["#{url}-header"] = ""
935
+ responses[url] = String.new
936
+ responses["#{url}-header"] = String.new
278
937
  c = Curl::Easy.new(url) do|curl|
279
938
  curl.follow_location = true
280
939
  curl.on_header{|data| responses["#{url}-header"] << data; data.size }
@@ -314,24 +973,22 @@ class TestCurbCurlMulti < Test::Unit::TestCase
314
973
  urls = []
315
974
  downloads = []
316
975
  file_info = {}
317
- FileUtils.mkdir("tmp/")
318
976
 
319
- # for each file store the size by file name
320
- Dir[File.dirname(__FILE__) + "/../ext/*.c"].each do|path|
321
- urls << (root_uri + File.basename(path))
322
- downloads << "tmp/" + File.basename(path)
323
- file_info[File.basename(path)] = {:size => File.size(path), :path => path}
324
- end
977
+ Dir.mktmpdir('curb-download-') do |download_dir|
978
+ # for each file store the size by file name
979
+ Dir[File.dirname(__FILE__) + "/../ext/*.c"].each do|path|
980
+ urls << (root_uri + File.basename(path))
981
+ downloads << File.join(download_dir, File.basename(path))
982
+ file_info[File.basename(path)] = {:size => File.size(path), :path => path}
983
+ end
325
984
 
326
- # start downloads
327
- Curl::Multi.download(urls,{},{},downloads) do|curl,download_path|
328
- assert_equal 200, curl.response_code
329
- assert File.exist?(download_path)
330
- store = file_info[File.basename(download_path)]
331
- assert_equal file_info[File.basename(download_path)][:size], File.size(download_path), "incomplete download: #{download_path}"
985
+ # start downloads
986
+ Curl::Multi.download(urls,{},{},downloads) do|curl,download_path|
987
+ assert_equal 200, curl.response_code
988
+ assert File.exist?(download_path)
989
+ assert_equal file_info[File.basename(download_path)][:size], File.size(download_path), "incomplete download: #{download_path}"
990
+ end
332
991
  end
333
- ensure
334
- FileUtils.rm_rf("tmp/")
335
992
  end
336
993
 
337
994
  def test_multi_easy_post_01
@@ -342,7 +999,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
342
999
  ]
343
1000
  Curl::Multi.post(urls, {:follow_location => true, :multipart_form_post => true}, {:pipeline => true}) do|easy|
344
1001
  str = easy.body_str
345
- assert_match /POST/, str
1002
+ assert_match(/POST/, str)
346
1003
  fields = {}
347
1004
  str.gsub(/POST\n/,'').split('&').map{|sv| k, v = sv.split('='); fields[k] = v }
348
1005
  expected = urls.find{|s| s[:url] == easy.last_effective_url }
@@ -357,8 +1014,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
357
1014
  { :url => TestServlet.url, :method => :put, :put_data => "message",
358
1015
  :headers => {'Content-Type' => 'application/json' } }]
359
1016
  Curl::Multi.put(urls, {}, {:pipeline => true}) do|easy|
360
- assert_match /PUT/, easy.body_str
361
- assert_match /message/, easy.body_str
1017
+ assert_match(/PUT/, easy.body_str)
1018
+ assert_match(/message/, easy.body_str)
362
1019
  end
363
1020
  end
364
1021
 
@@ -372,38 +1029,86 @@ class TestCurbCurlMulti < Test::Unit::TestCase
372
1029
  { :url => TestServlet.url, :method => :get }
373
1030
  ]
374
1031
  Curl::Multi.http(urls, {:pipeline => true}) do|easy, code, method|
375
- assert_equal nil, code
1032
+ assert_equal 200, code
376
1033
  case method
377
1034
  when :post
378
- assert_match /POST/, easy.body_str
1035
+ assert_match(/POST/, easy.body_str)
379
1036
  when :get
380
- assert_match /GET/, easy.body_str
1037
+ assert_match(/GET/, easy.body_str)
381
1038
  when :put
382
- assert_match /PUT/, easy.body_str
1039
+ assert_match(/PUT/, easy.body_str)
383
1040
  end
384
1041
  #puts "#{easy.body_str.inspect}, #{method.inspect}, #{code.inspect}"
385
1042
  end
386
1043
  end
387
1044
 
388
1045
  def test_multi_easy_http_with_max_connects
389
- urls = [
1046
+ urls = [
390
1047
  { :url => TestServlet.url + '?q=1', :method => :get },
391
1048
  { :url => TestServlet.url + '?q=2', :method => :get },
392
1049
  { :url => TestServlet.url + '?q=3', :method => :get }
393
1050
  ]
394
1051
  Curl::Multi.http(urls, {:pipeline => true, :max_connects => 1}) do|easy, code, method|
395
- assert_equal nil, code
1052
+ assert_equal 200, code
396
1053
  case method
397
1054
  when :post
398
- assert_match /POST/, easy.body_str
1055
+ assert_match(/POST/, easy.body)
399
1056
  when :get
400
- assert_match /GET/, easy.body_str
1057
+ assert_match(/GET/, easy.body)
401
1058
  when :put
402
- assert_match /PUT/, easy.body_str
1059
+ assert_match(/PUT/, easy.body)
403
1060
  end
404
1061
  end
405
1062
  end
406
1063
 
1064
+ def test_multi_easy_http_with_max_host_connections
1065
+ urls = [
1066
+ { :url => TestServlet.url + '?q=1', :method => :get },
1067
+ { :url => TestServlet.url + '?q=2', :method => :get },
1068
+ { :url => TestServlet.url + '?q=3', :method => :get }
1069
+ ]
1070
+ Curl::Multi.http(urls, {:pipeline => true, :max_host_connections => 1}) do|easy, code, method|
1071
+ assert_equal 200, code
1072
+ case method
1073
+ when :post
1074
+ assert_match(/POST/, easy.body)
1075
+ when :get
1076
+ assert_match(/GET/, easy.body)
1077
+ when :put
1078
+ assert_match(/PUT/, easy.body)
1079
+ end
1080
+ end
1081
+ end
1082
+
1083
+ # Regression test for issue #267 (2015): ensure that when reusing
1084
+ # easy handles with constrained concurrency, the callback receives
1085
+ # the correct URL for each completed request rather than repeating
1086
+ # the first URL.
1087
+ def test_multi_easy_http_urls_unique_across_max_connects
1088
+ urls = [
1089
+ { :url => TestServlet.url + '?q=1', :method => :get },
1090
+ { :url => TestServlet.url + '?q=2', :method => :get },
1091
+ { :url => TestServlet.url + '?q=3', :method => :get }
1092
+ ]
1093
+
1094
+ [1, 2, 3].each do |max|
1095
+ results = []
1096
+ Curl::Multi.http(urls.dup, {:pipeline => true, :max_connects => max}) do |easy, code, method|
1097
+ assert_equal 200, code
1098
+ assert_equal :get, method
1099
+ results << easy.last_effective_url
1100
+ end
1101
+
1102
+ # Ensure we saw one completion per input URL
1103
+ assert_equal urls.size, results.size, "expected #{urls.size} results with max_connects=#{max}"
1104
+
1105
+ # And that each URL completed exactly once (no accidental reuse/mis-reporting)
1106
+ expected_urls = urls.map { |u| u[:url] }
1107
+ assert_equal expected_urls.to_set, results.to_set, "unexpected URLs for max_connects=#{max}: #{results.inspect}"
1108
+ assert_equal expected_urls.size, results.uniq.size, "duplicate URLs seen for max_connects=#{max}: #{results.inspect}"
1109
+ end
1110
+ end
1111
+
407
1112
  def test_multi_recieves_500
408
1113
  m = Curl::Multi.new
409
1114
  e = Curl::Easy.new("http://127.0.0.1:9129/methods")
@@ -422,6 +1127,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
422
1127
  failure = false
423
1128
  assert !failure
424
1129
  assert_equal "POST\nhello=world", e2.body_str
1130
+ ensure
1131
+ m.close if m
425
1132
  end
426
1133
 
427
1134
  def test_remove_exception_is_descriptive
@@ -431,6 +1138,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
431
1138
  rescue => e
432
1139
  assert_equal 'CURLError: Invalid easy handle', e.message
433
1140
  assert_equal 0, m.requests.size
1141
+ ensure
1142
+ m.close if m
434
1143
  end
435
1144
 
436
1145
  def test_retry_easy_handle
@@ -454,6 +1163,50 @@ class TestCurbCurlMulti < Test::Unit::TestCase
454
1163
  m.perform
455
1164
  assert_equal 0, tries
456
1165
  assert_equal 0, m.requests.size
1166
+ ensure
1167
+ m.close if m
1168
+ end
1169
+
1170
+ def test_close_in_on_missing_callback_is_blocked
1171
+ m = Curl::Multi.new
1172
+ c = Curl::Easy.new(TestServlet.url + '/not_here')
1173
+ did_raise = false
1174
+
1175
+ c.on_missing do |easy, _error|
1176
+ begin
1177
+ easy.close
1178
+ rescue RuntimeError
1179
+ did_raise = true
1180
+ end
1181
+ end
1182
+
1183
+ m.add(c)
1184
+ m.perform
1185
+
1186
+ assert did_raise
1187
+ ensure
1188
+ m.close if m
1189
+ end
1190
+
1191
+ def test_close_in_on_redirect_callback_is_blocked
1192
+ m = Curl::Multi.new
1193
+ c = Curl::Easy.new(TestServlet.url + '/redirect')
1194
+ did_raise = false
1195
+
1196
+ c.on_redirect do |easy, _error|
1197
+ begin
1198
+ easy.close
1199
+ rescue RuntimeError
1200
+ did_raise = true
1201
+ end
1202
+ end
1203
+
1204
+ m.add(c)
1205
+ m.perform
1206
+
1207
+ assert did_raise
1208
+ ensure
1209
+ m.close if m
457
1210
  end
458
1211
 
459
1212
  def test_reusing_handle
@@ -466,7 +1219,9 @@ class TestCurbCurlMulti < Test::Unit::TestCase
466
1219
  m.add(c)
467
1220
  m.add(c)
468
1221
  rescue => e
469
- assert_equal Curl::Err::MultiBadEasyHandle, e.class
1222
+ assert Curl::Err::MultiBadEasyHandle == e.class || Curl::Err::MultiAddedAlready == e.class
1223
+ ensure
1224
+ m.close if m
470
1225
  end
471
1226
 
472
1227
  def test_multi_default_timeout
@@ -476,10 +1231,57 @@ class TestCurbCurlMulti < Test::Unit::TestCase
476
1231
  assert_equal 100, (Curl::Multi.default_timeout = 100)
477
1232
  end
478
1233
 
1234
+ def with_queue_refill_test_server(wait_fail_until_slow: false)
1235
+ port_socket = TCPServer.new('127.0.0.1', 0)
1236
+ port = port_socket.addr[1]
1237
+ port_socket.close
1238
+
1239
+ hits = Hash.new(0)
1240
+ slow_started = false
1241
+ slow_started_lock = Mutex.new
1242
+ slow_started_condition = ConditionVariable.new
1243
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
1244
+ server.mount_proc('/fail') do |_req, res|
1245
+ if wait_fail_until_slow
1246
+ slow_started_lock.synchronize do
1247
+ slow_started_condition.wait(slow_started_lock, 1) unless slow_started
1248
+ end
1249
+ end
1250
+
1251
+ hits[:fail] += 1
1252
+ res['Content-Type'] = 'text/plain'
1253
+ res.body = 'fail'
1254
+ end
1255
+ server.mount_proc('/slow') do |_req, res|
1256
+ slow_started_lock.synchronize do
1257
+ hits[:slow] += 1
1258
+ slow_started = true
1259
+ slow_started_condition.broadcast
1260
+ end
1261
+
1262
+ res['Content-Type'] = 'text/plain'
1263
+ sleep 0.3
1264
+ res.body = 'slow'
1265
+ end
1266
+ server.mount_proc('/queued') do |_req, res|
1267
+ hits[:queued] += 1
1268
+ res['Content-Type'] = 'text/plain'
1269
+ res.body = 'queued'
1270
+ end
1271
+
1272
+ server_thread = Thread.new(server) { |srv| srv.start }
1273
+ wait_for_server_ready(port, thread: server_thread)
1274
+
1275
+ yield port, hits
1276
+ ensure
1277
+ server.shutdown if defined?(server) && server
1278
+ server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
1279
+ server_thread.kill if defined?(server_thread) && server_thread&.alive?
1280
+ end
1281
+
479
1282
  include TestServerMethods
480
1283
 
481
1284
  def setup
482
1285
  server_setup
483
1286
  end
484
-
485
1287
  end