curb 0.8.5 → 1.3.6

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +579 -0
  3. data/Rakefile +85 -27
  4. data/doc.rb +48 -8
  5. data/ext/banned.h +32 -0
  6. data/ext/curb.c +563 -233
  7. data/ext/curb.h +19 -10
  8. data/ext/curb_easy.c +3229 -368
  9. data/ext/curb_easy.h +42 -0
  10. data/ext/curb_errors.c +117 -18
  11. data/ext/curb_errors.h +9 -5
  12. data/ext/curb_macros.h +33 -21
  13. data/ext/curb_multi.c +1904 -272
  14. data/ext/curb_multi.h +11 -3
  15. data/ext/curb_postfield.c +149 -77
  16. data/ext/curb_postfield.h +1 -0
  17. data/ext/curb_upload.c +38 -11
  18. data/ext/curb_upload.h +2 -0
  19. data/ext/extconf.rb +355 -35
  20. data/lib/curb.rb +1 -0
  21. data/lib/curl/download.rb +160 -0
  22. data/lib/curl/easy.rb +422 -88
  23. data/lib/curl/multi.rb +295 -56
  24. data/lib/curl.rb +676 -11
  25. data/tests/bug_crash_on_debug.rb +14 -28
  26. data/tests/bug_crash_on_progress.rb +32 -16
  27. data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
  28. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
  29. data/tests/bug_follow_redirect_288.rb +83 -0
  30. data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
  31. data/tests/bug_issue102.rb +17 -0
  32. data/tests/bug_issue_noproxy.rb +56 -0
  33. data/tests/bug_issue_post_redirect.rb +93 -0
  34. data/tests/bug_issue_spnego.rb +41 -0
  35. data/tests/bug_multi_segfault.rb +1 -0
  36. data/tests/bug_poison.rb +29 -0
  37. data/tests/bug_raise_on_callback.rb +30 -0
  38. data/tests/helper.rb +400 -44
  39. data/tests/leak_trace.rb +237 -0
  40. data/tests/mem_check.rb +3 -0
  41. data/tests/tc_curl.rb +31 -1
  42. data/tests/tc_curl_download.rb +98 -7
  43. data/tests/tc_curl_easy.rb +985 -66
  44. data/tests/tc_curl_easy_cookielist.rb +277 -0
  45. data/tests/tc_curl_easy_request_target.rb +41 -0
  46. data/tests/tc_curl_easy_resolve.rb +48 -0
  47. data/tests/tc_curl_maxfilesize.rb +212 -0
  48. data/tests/tc_curl_multi.rb +1111 -51
  49. data/tests/tc_curl_native_coverage.rb +145 -0
  50. data/tests/tc_curl_network_policy.rb +1475 -0
  51. data/tests/tc_curl_postfield.rb +207 -30
  52. data/tests/tc_curl_protocols.rb +388 -0
  53. data/tests/tc_fiber_scheduler.rb +584 -0
  54. data/tests/tc_ftp_options.rb +39 -0
  55. data/tests/tc_gc_compact.rb +223 -0
  56. data/tests/tc_test_server_methods.rb +110 -0
  57. data/tests/timeout.rb +30 -6
  58. metadata +66 -31
  59. data/README +0 -194
@@ -1,19 +1,233 @@
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_easy_multi_assignment_detaches_from_old_multi
193
+ multi = Curl::Multi.new
194
+ easy = Curl::Easy.new(TestServlet.url)
195
+
196
+ multi.add(easy)
197
+ easy.multi = nil
198
+
199
+ assert_nil easy.multi
200
+ assert_equal({}, multi.requests)
201
+ assert_nothing_raised { easy.close }
202
+ assert_nothing_raised { multi.close }
203
+
204
+ multi = nil
205
+ ensure
206
+ multi.close if defined?(multi) && multi
207
+ end
208
+
209
+ def test_close_makes_multi_unusable
210
+ multi = Curl::Multi.new
211
+ multi.close
212
+
213
+ error = assert_raise(Curl::Err::MultiBadHandle) do
214
+ multi.add(Curl::Easy.new($TEST_URL))
215
+ end
216
+
217
+ assert_match(/Invalid multi handle/i, error.message)
218
+ assert_equal 0, multi.requests.length
219
+ ensure
220
+ multi.close if multi
7
221
  end
8
222
 
9
223
  def test_new_multi_01
10
- d1 = ""
224
+ d1 = String.new
11
225
  c1 = Curl::Easy.new($TEST_URL) do |curl|
12
226
  curl.headers["User-Agent"] = "myapp-0.0"
13
227
  curl.on_body {|d| d1 << d; d.length }
14
228
  end
15
229
 
16
- d2 = ""
230
+ d2 = String.new
17
231
  c2 = Curl::Easy.new($TEST_URL) do |curl|
18
232
  curl.headers["User-Agent"] = "myapp-0.0"
19
233
  curl.on_body {|d| d2 << d; d.length }
@@ -28,9 +242,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
28
242
 
29
243
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, d1)
30
244
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, d2)
31
-
32
- m = nil
33
-
245
+ ensure
246
+ m.close if m
34
247
  end
35
248
 
36
249
  def test_perform_block
@@ -49,11 +262,426 @@ class TestCurbCurlMulti < Test::Unit::TestCase
49
262
 
50
263
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, c1.body_str)
51
264
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, c2.body_str)
265
+ ensure
266
+ m.close if m
267
+ end
268
+
269
+ def test_multi_perform_runs_work_added_from_final_idle_yield
270
+ with_queue_refill_test_server do |port, hits|
271
+ previous_autoclose = Curl::Multi.autoclose
272
+ Curl::Multi.autoclose = true
273
+
274
+ multi = Curl::Multi.new
275
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
276
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
277
+ completions = []
278
+ empty_yields = 0
279
+ queued_added = false
280
+
281
+ slow.on_complete { completions << :slow }
282
+ queued.on_complete { completions << :queued }
283
+
284
+ multi.add(slow)
285
+ multi.perform do |performing_multi|
286
+ empty_yields += 1 if performing_multi.requests.empty?
287
+ if !queued_added && empty_yields >= 2
288
+ queued_added = true
289
+ performing_multi.add(queued)
290
+ end
291
+ end
292
+
293
+ assert queued_added, "test should add queued work from the final idle yield"
294
+ assert_equal [:slow, :queued], completions
295
+ assert_equal 1, hits[:slow]
296
+ assert_equal 1, hits[:queued]
297
+ assert_equal 0, multi.requests.length
298
+ ensure
299
+ Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
300
+ multi.close if defined?(multi) && multi
301
+ end
302
+ end
303
+
304
+ def test_multi_easy_get
305
+ n = 1
306
+ urls = []
307
+ n.times { urls << $TEST_URL }
308
+ Curl::Multi.get(urls, {timeout: 5}) {|easy|
309
+ assert_match(/file:/, easy.last_effective_url)
310
+ }
311
+ end
312
+
313
+ def test_multi_easy_get_with_error
314
+ omit('Path/line parsing differs on Windows') if WINDOWS
315
+ begin
316
+ did_raise = false
317
+ n = 3
318
+ urls = []
319
+ n.times { urls << $TEST_URL }
320
+ error_line_number_should_be = nil
321
+ Curl::Multi.get(urls, {timeout: 5}) {|easy|
322
+ # if we got this right the error will be reported to be on the line below our error_line_number_should_be
323
+ error_line_number_should_be = __LINE__
324
+ raise
325
+ }
52
326
 
53
- m = nil
327
+ rescue Curl::Err::AbortedByCallbackError => e
328
+ did_raise = true
329
+ in_file = e.backtrace.detect {|err| err.match?(File.basename(__FILE__)) }
330
+ #in_file_stack = e.backtrace.select {|err| err.match?(File.basename(__FILE__)) }
331
+ assert_match(__FILE__, in_file)
332
+ in_file.gsub!(__FILE__)
333
+ parts = in_file.split(':')
334
+ parts.shift
335
+ line_no = parts.shift.to_i
336
+ assert_equal error_line_number_should_be+1, line_no.to_i
337
+ end
338
+
339
+ assert did_raise, "we should have raised an exception"
54
340
 
55
341
  end
56
342
 
343
+ def test_multi_perform_reraises_on_body_exception
344
+ multi = Curl::Multi.new
345
+ easy = Curl::Easy.new(TestServlet.url)
346
+ easy.on_body { raise "body blew up" }
347
+
348
+ error = assert_raise(RuntimeError) do
349
+ multi.add(easy)
350
+ multi.perform
351
+ end
352
+
353
+ assert_equal "body blew up", error.message
354
+ assert !easy.instance_variable_defined?(:@__curb_callback_error)
355
+ ensure
356
+ multi.close if multi
357
+ end
358
+
359
+ def test_multi_perform_reraises_on_body_exception_for_frozen_easy
360
+ multi = Curl::Multi.new
361
+ easy = Curl::Easy.new($TEST_URL)
362
+ callback_error = Class.new(RuntimeError)
363
+ easy.on_body { raise callback_error, "body blew up" }
364
+ easy.freeze
365
+
366
+ error = assert_raise(callback_error) do
367
+ multi.add(easy)
368
+ multi.perform
369
+ end
370
+
371
+ assert_equal "body blew up", error.message
372
+ ensure
373
+ begin
374
+ multi.close if multi
375
+ rescue StandardError
376
+ multi.instance_variable_set(:@requests, {})
377
+ multi._close
378
+ end
379
+ end
380
+
381
+ def test_multi_perform_reraises_on_header_exception
382
+ multi = Curl::Multi.new
383
+ easy = Curl::Easy.new(TestServlet.url)
384
+ easy.on_header { raise "header blew up" }
385
+
386
+ error = assert_raise(RuntimeError) do
387
+ multi.add(easy)
388
+ multi.perform
389
+ end
390
+
391
+ assert_equal "header blew up", error.message
392
+ assert !easy.instance_variable_defined?(:@__curb_callback_error)
393
+ ensure
394
+ multi.close if multi
395
+ end
396
+
397
+ def test_multi_perform_drains_completed_siblings_before_reraising_on_body_exception
398
+ multi = Curl::Multi.new
399
+ failed = Curl::Easy.new($TEST_URL)
400
+ completed = Curl::Easy.new($TEST_URL)
401
+ completions = []
402
+
403
+ failed.on_body { raise "body blew up" }
404
+ completed.on_complete { completions << :completed }
405
+
406
+ error = assert_raise(RuntimeError) do
407
+ multi.add(failed)
408
+ multi.add(completed)
409
+ multi.perform
410
+ end
411
+
412
+ assert_equal "body blew up", error.message
413
+ assert_equal [:completed], completions
414
+ assert multi.idle?, 'The multi handle should be idle after draining the completed batch'
415
+ assert_equal 0, multi.requests.length
416
+ assert_nil failed.multi
417
+ assert_nil completed.multi
418
+ assert !failed.instance_variable_defined?(:@__curb_callback_error)
419
+ ensure
420
+ multi.close if multi
421
+ end
422
+
423
+ def test_multi_perform_waits_until_idle_before_reraising_on_complete_exception
424
+ port_socket = TCPServer.new('127.0.0.1', 0)
425
+ port = port_socket.addr[1]
426
+ port_socket.close
427
+
428
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
429
+ server.mount_proc('/fast') do |_req, res|
430
+ res['Content-Type'] = 'text/plain'
431
+ res.body = 'fast'
432
+ end
433
+ server.mount_proc('/slow') do |_req, res|
434
+ res['Content-Type'] = 'text/plain'
435
+ sleep 0.2
436
+ res.body = 'slow'
437
+ end
438
+ server_thread = Thread.new(server) { |srv| srv.start }
439
+ wait_for_server_ready(port, thread: server_thread)
440
+
441
+ multi = Curl::Multi.new
442
+ fast = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
443
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
444
+ completions = []
445
+
446
+ fast.on_complete do
447
+ completions << :fast
448
+ raise "complete blew up"
449
+ end
450
+ slow.on_complete { completions << :slow }
451
+
452
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
453
+ multi.add(fast)
454
+ multi.add(slow)
455
+ multi.perform
456
+ end
457
+
458
+ assert_equal "complete blew up", error.message
459
+ assert_equal [:fast, :slow], completions
460
+ assert multi.idle?, 'The multi handle should be idle before the deferred exception is raised'
461
+ assert_equal 0, multi.requests.length
462
+ assert_nil fast.multi
463
+ assert_nil slow.multi
464
+ ensure
465
+ multi.close if multi
466
+ server.shutdown if defined?(server) && server
467
+ server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
468
+ server_thread.kill if defined?(server_thread) && server_thread&.alive?
469
+ end
470
+
471
+ def test_multi_perform_keeps_yielding_block_while_draining_deferred_on_complete_exception
472
+ with_queue_refill_test_server do |port, hits|
473
+ multi = Curl::Multi.new
474
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
475
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
476
+ failure_seen = false
477
+ slow_completed = false
478
+ yielded_during_drain = false
479
+
480
+ failed.on_complete do
481
+ failure_seen = true
482
+ raise "complete blew up"
483
+ end
484
+ slow.on_complete { slow_completed = true }
485
+
486
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
487
+ multi.add(failed)
488
+ multi.add(slow)
489
+ multi.perform do
490
+ yielded_during_drain ||= failure_seen && !slow_completed
491
+ end
492
+ end
493
+
494
+ assert_equal "complete blew up", error.message
495
+ assert yielded_during_drain,
496
+ "perform block should continue yielding while draining a slow sibling after a deferred callback exception"
497
+ assert_equal 1, hits[:fail]
498
+ assert_equal 1, hits[:slow]
499
+ ensure
500
+ multi.close if multi
501
+ end
502
+ end
503
+
504
+ def test_multi_perform_does_not_start_queued_work_after_on_complete_exception
505
+ with_queue_refill_test_server do |port, hits|
506
+ multi = Curl::Multi.new
507
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
508
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
509
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
510
+ failure_seen = false
511
+
512
+ failed.on_complete do
513
+ failure_seen = true
514
+ raise "complete blew up"
515
+ end
516
+
517
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
518
+ multi.add(failed)
519
+ multi.add(slow)
520
+ multi.perform do
521
+ next unless failure_seen
522
+
523
+ multi.add(queued)
524
+ failure_seen = false
525
+ end
526
+ end
527
+
528
+ assert_equal "complete blew up", error.message
529
+ assert_equal 1, hits[:fail]
530
+ assert_equal 1, hits[:slow]
531
+ assert_equal 0, hits[:queued], "queued request should not start once a deferred callback exception is pending"
532
+ ensure
533
+ multi.close if multi
534
+ end
535
+ end
536
+
537
+ def test_multi_perform_does_not_start_work_added_within_failing_on_complete_callback
538
+ with_queue_refill_test_server do |port, hits|
539
+ multi = Curl::Multi.new
540
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
541
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
542
+
543
+ failed.on_complete do
544
+ multi.add(queued)
545
+ raise "complete blew up"
546
+ end
547
+
548
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
549
+ multi.add(failed)
550
+ multi.perform
551
+ end
552
+
553
+ assert_equal "complete blew up", error.message
554
+ assert_equal 1, hits[:fail]
555
+ assert_equal 0, hits[:queued],
556
+ "request added from a failing on_complete callback should not start once the multi begins aborting"
557
+ ensure
558
+ multi.close if multi
559
+ end
560
+ end
561
+
562
+ def test_multi_perform_does_not_restart_sibling_removed_and_readded_from_failing_on_complete_callback
563
+ with_queue_refill_test_server(wait_fail_until_slow: true) do |port, hits|
564
+ multi = Curl::Multi.new
565
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
566
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
567
+
568
+ failed.on_complete do
569
+ multi.remove(slow)
570
+ multi.add(slow)
571
+ raise "complete blew up"
572
+ end
573
+
574
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
575
+ multi.add(failed)
576
+ multi.add(slow)
577
+ multi.perform
578
+ end
579
+
580
+ assert_equal "complete blew up", error.message
581
+ assert_equal 1, hits[:fail]
582
+ assert_equal 1, hits[:slow],
583
+ "sibling removed and re-added from a failing on_complete callback should be treated as replacement work and not restarted"
584
+ ensure
585
+ multi.close if multi
586
+ end
587
+ end
588
+
589
+ def test_multi_perform_does_not_start_work_added_within_on_complete_after_on_body_exception
590
+ with_queue_refill_test_server do |port, hits|
591
+ multi = Curl::Multi.new
592
+ failed = Curl::Easy.new("http://127.0.0.1:#{port}/fail")
593
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
594
+
595
+ failed.on_body { raise "body blew up" }
596
+ failed.on_complete { multi.add(queued) }
597
+
598
+ error = assert_raise(RuntimeError) do
599
+ multi.add(failed)
600
+ multi.perform
601
+ end
602
+
603
+ assert_equal "body blew up", error.message
604
+ assert_equal 1, hits[:fail]
605
+ assert_equal 0, hits[:queued],
606
+ "request added from on_complete after a body callback exception should not start once the multi begins aborting"
607
+ ensure
608
+ multi.close if multi
609
+ end
610
+ end
611
+
612
+ def test_multi_http_does_not_fetch_queued_url_after_on_body_exception
613
+ with_queue_refill_test_server do |port, hits|
614
+ urls = [
615
+ { :url => "http://127.0.0.1:#{port}/queued", :method => :get },
616
+ { :url => "http://127.0.0.1:#{port}/slow", :method => :get },
617
+ { :url => "http://127.0.0.1:#{port}/fail", :method => :get, :on_body => proc { raise "body blew up" } }
618
+ ]
619
+
620
+ error = assert_raise(RuntimeError) do
621
+ Curl::Multi.http(urls, {:max_connects => 2}) { |_easy, _code, _method| }
622
+ end
623
+
624
+ assert_equal "body blew up", error.message
625
+ assert_equal 1, hits[:fail]
626
+ assert_equal 1, hits[:slow]
627
+ assert_equal 0, hits[:queued], "Curl::Multi.http should not refill queued urls after a callback abort"
628
+ end
629
+ end
630
+
631
+ def test_multi_perform_runs_status_callbacks_after_on_body_exception
632
+ multi = Curl::Multi.new
633
+ easy = Curl::Easy.new(TestServlet.url)
634
+ callbacks = []
635
+
636
+ easy.on_body { raise "body blew up" }
637
+ easy.on_complete { callbacks << :complete }
638
+ easy.on_failure do |_completed_easy, error_info|
639
+ callbacks << :failure
640
+ assert_equal Curl::Err::WriteError, error_info.first
641
+ end
642
+
643
+ error = assert_raise(RuntimeError) do
644
+ multi.add(easy)
645
+ multi.perform
646
+ end
647
+
648
+ assert_equal "body blew up", error.message
649
+ assert_equal [:complete, :failure], callbacks
650
+ assert multi.idle?
651
+ assert_equal 0, multi.requests.length
652
+ ensure
653
+ multi.close if multi
654
+ end
655
+
656
+ def test_multi_perform_preserves_on_body_exception_when_status_callbacks_raise
657
+ multi = Curl::Multi.new
658
+ easy = Curl::Easy.new(TestServlet.url)
659
+ callbacks = []
660
+
661
+ easy.on_body { raise "body blew up" }
662
+ easy.on_complete do
663
+ callbacks << :complete
664
+ raise "complete blew up"
665
+ end
666
+ easy.on_failure do |_completed_easy, error_info|
667
+ callbacks << :failure
668
+ assert_equal Curl::Err::WriteError, error_info.first
669
+ raise "failure blew up"
670
+ end
671
+
672
+ error = assert_raise(RuntimeError) do
673
+ multi.add(easy)
674
+ multi.perform
675
+ end
676
+
677
+ assert_equal "body blew up", error.message
678
+ assert_equal [:complete, :failure], callbacks
679
+ assert multi.idle?
680
+ assert_equal 0, multi.requests.length
681
+ ensure
682
+ multi.close if multi
683
+ end
684
+
57
685
  # NOTE: if this test runs slowly on Mac OSX, it is probably due to the use of a port install curl+ssl+ares install
58
686
  # on my MacBook, this causes curl_easy_init to take nearly 0.01 seconds / * 100 below is 1 second too many!
59
687
  def test_n_requests
@@ -61,7 +689,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
61
689
  m = Curl::Multi.new
62
690
  responses = []
63
691
  n.times do|i|
64
- responses[i] = ""
692
+ responses[i] = String.new
65
693
  c = Curl::Easy.new($TEST_URL) do|curl|
66
694
  curl.on_body{|data| responses[i] << data; data.size }
67
695
  end
@@ -74,8 +702,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
74
702
  n.times do|i|
75
703
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, responses[i], "response #{i}")
76
704
  end
77
-
78
- m = nil
705
+ ensure
706
+ m.close if m
79
707
  end
80
708
 
81
709
  def test_n_requests_with_break
@@ -85,7 +713,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
85
713
  5.times do|it|
86
714
  responses = []
87
715
  n.times do|i|
88
- responses[i] = ""
716
+ responses[i] = String.new
89
717
  c = Curl::Easy.new($TEST_URL) do|curl|
90
718
  curl.on_body{|data| responses[i] << data; data.size }
91
719
  end
@@ -98,9 +726,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
98
726
  assert_match(/^# DO NOT REMOVE THIS COMMENT/, responses[i], "response #{i}")
99
727
  end
100
728
  end
101
-
102
- m = nil
103
-
729
+ ensure
730
+ m.close if m
104
731
  end
105
732
 
106
733
  def test_idle_check
@@ -109,19 +736,25 @@ class TestCurbCurlMulti < Test::Unit::TestCase
109
736
 
110
737
  assert(m.idle?, 'A new Curl::Multi handle should be idle')
111
738
 
739
+ assert_nil e.multi
740
+
112
741
  m.add(e)
113
742
 
743
+ assert_not_nil e.multi
744
+
114
745
  assert((not m.idle?), 'A Curl::Multi handle with a request should not be idle')
115
746
 
116
747
  m.perform
117
748
 
118
749
  assert(m.idle?, 'A Curl::Multi handle should be idle after performing its requests')
750
+ ensure
751
+ m.close if m
119
752
  end
120
753
 
121
754
  def test_requests
122
755
  m = Curl::Multi.new
123
756
 
124
- assert_equal([], m.requests, 'A new Curl::Multi handle should have no requests')
757
+ assert_equal(0, m.requests.length, 'A new Curl::Multi handle should have no requests')
125
758
 
126
759
  10.times do
127
760
  m.add(Curl::Easy.new($TEST_URL))
@@ -131,7 +764,78 @@ class TestCurbCurlMulti < Test::Unit::TestCase
131
764
 
132
765
  m.perform
133
766
 
134
- assert_equal([], m.requests, 'A new Curl::Multi handle should have no requests after a perform')
767
+ assert_equal(0, m.requests.length, 'A new Curl::Multi handle should have no requests after a perform')
768
+ ensure
769
+ m.close if m
770
+ end
771
+
772
+ def test_native_safety_signatures_are_pruned_after_perform
773
+ m = Curl::Multi.new
774
+ easy = Curl::Easy.new($TEST_URL)
775
+
776
+ m.add(easy)
777
+ signatures = m.__send__(:__curb_native_safety_signatures)
778
+ assert_equal easy, m.requests[easy.object_id]
779
+ assert signatures.key?(easy.object_id)
780
+
781
+ m.perform
782
+
783
+ assert_equal 0, m.requests.length
784
+ assert_empty signatures
785
+ ensure
786
+ m.close if m
787
+ end
788
+
789
+ def test_easy_multi_is_cleared_after_perform
790
+ m = Curl::Multi.new
791
+ c = Curl::Easy.new($TEST_URL)
792
+
793
+ m.add(c)
794
+ assert_equal m, c.multi
795
+
796
+ m.perform
797
+
798
+ assert_nil c.multi
799
+ ensure
800
+ m.close if m
801
+ end
802
+
803
+ def test_frozen_idle_easy_multi_reference_is_cleared_on_close
804
+ multi = Curl::Multi.new
805
+ easy = Curl::Easy.new($TEST_URL)
806
+ easy.freeze
807
+
808
+ easy.multi = multi
809
+
810
+ assert_same multi, easy.multi
811
+ assert_nothing_raised { multi.close }
812
+ assert_nil easy.multi
813
+ multi = nil
814
+ ensure
815
+ multi.close if multi
816
+ end
817
+
818
+ def test_easy_multi_is_available_during_completion_callbacks
819
+ multi = Curl::Multi.new
820
+ easy = Curl::Easy.new($TEST_URL)
821
+ seen_multi = {}
822
+
823
+ easy.on_complete do |completed_easy|
824
+ seen_multi[:complete] = completed_easy.multi
825
+ end
826
+
827
+ easy.on_success do |completed_easy|
828
+ seen_multi[:success] = completed_easy.multi
829
+ end
830
+
831
+ multi.add(easy)
832
+ multi.perform
833
+
834
+ assert_same multi, seen_multi[:complete]
835
+ assert_same multi, seen_multi[:success]
836
+ assert_nil easy.multi
837
+ ensure
838
+ multi.close if multi
135
839
  end
136
840
 
137
841
  def test_cancel
@@ -144,7 +848,9 @@ class TestCurbCurlMulti < Test::Unit::TestCase
144
848
 
145
849
  m.cancel!
146
850
 
147
- assert_equal([], m.requests, 'A new Curl::Multi handle should have no requests after being canceled')
851
+ assert_equal(0, m.requests.size, 'A new Curl::Multi handle should have no requests after being canceled')
852
+ ensure
853
+ m.close if m
148
854
  end
149
855
 
150
856
  def test_with_success
@@ -175,8 +881,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
175
881
 
176
882
  assert success_called2
177
883
  assert success_called1
178
-
179
- m = nil
884
+ ensure
885
+ m.close if m
180
886
  end
181
887
 
182
888
  def test_with_success_cb_with_404
@@ -188,7 +894,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
188
894
  c1.on_success do|c|
189
895
  success_called1 = true
190
896
  #puts "success 1 called: #{c.body_str.inspect}"
191
- #assert_match(/^# DO NOT REMOVE THIS COMMENT/, c.body_str)
897
+ assert_match(/^# DO NOT REMOVE THIS COMMENT/, c.body_str)
192
898
  end
193
899
 
194
900
  c1.on_failure do|c,rc|
@@ -218,8 +924,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
218
924
 
219
925
  assert success_called2
220
926
  assert !success_called1
221
-
222
- m = nil
927
+ ensure
928
+ m.close if m
223
929
  end
224
930
 
225
931
  # This tests whether, ruby's GC will trash an out of scope easy handle
@@ -227,7 +933,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
227
933
  attr_reader :buf
228
934
 
229
935
  def t_method
230
- @buf = ""
936
+ @buf = String.new
231
937
  @m = Curl::Multi.new
232
938
  10.times do|i|
233
939
  c = Curl::Easy.new($TEST_URL)
@@ -243,6 +949,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
243
949
  @m.perform do
244
950
  ObjectSpace.garbage_collect
245
951
  end
952
+ ensure
953
+ @m.close if @m
246
954
  end
247
955
 
248
956
  def self.test
@@ -273,8 +981,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
273
981
  m = Curl::Multi.new
274
982
  # add a few easy handles
275
983
  requests.each do |url|
276
- responses[url] = ""
277
- responses["#{url}-header"] = ""
984
+ responses[url] = String.new
985
+ responses["#{url}-header"] = String.new
278
986
  c = Curl::Easy.new(url) do|curl|
279
987
  curl.follow_location = true
280
988
  curl.on_header{|data| responses["#{url}-header"] << data; data.size }
@@ -314,24 +1022,209 @@ class TestCurbCurlMulti < Test::Unit::TestCase
314
1022
  urls = []
315
1023
  downloads = []
316
1024
  file_info = {}
317
- FileUtils.mkdir("tmp/")
318
1025
 
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}
1026
+ Dir.mktmpdir('curb-download-') do |download_dir|
1027
+ # for each file store the size by file name
1028
+ Dir[File.dirname(__FILE__) + "/../ext/*.c"].each do|path|
1029
+ urls << (root_uri + File.basename(path))
1030
+ downloads << File.join(download_dir, File.basename(path))
1031
+ file_info[File.basename(path)] = {:size => File.size(path), :path => path}
1032
+ end
1033
+
1034
+ # start downloads
1035
+ Curl::Multi.download(urls,{},{},downloads) do|curl,download_path|
1036
+ assert_equal 200, curl.response_code
1037
+ assert File.exist?(download_path)
1038
+ assert_equal file_info[File.basename(download_path)][:size], File.size(download_path), "incomplete download: #{download_path}"
1039
+ end
324
1040
  end
1041
+ end
325
1042
 
326
- # start downloads
327
- Curl::Multi.download(urls,{},{},downloads) do|curl,download_path|
328
- assert_equal 200, curl.response_code
1043
+ def test_multi_download_refuses_existing_explicit_download_path
1044
+ root_uri = 'http://127.0.0.1:9129/ext/'
1045
+
1046
+ Dir.mktmpdir('curb-download-') do |download_dir|
1047
+ download_path = File.join(download_dir, 'curb_easy.c')
1048
+ File.write(download_path, 'sentinel')
1049
+
1050
+ assert_raise(Curl::DownloadTargetExistsError) do
1051
+ Curl::Multi.download([root_uri + 'curb_easy.c'], {}, {}, [download_path])
1052
+ end
1053
+
1054
+ assert_equal 'sentinel', File.read(download_path)
1055
+ end
1056
+ end
1057
+
1058
+ def test_multi_download_failed_transfer_does_not_publish_file_without_block
1059
+ Dir.mktmpdir('curb-download-') do |download_dir|
1060
+ download_path = File.join(download_dir, 'missing')
1061
+
1062
+ error = assert_raise(Curl::Multi::DownloadError) do
1063
+ Curl::Multi.download(['file:///curb/missing'], {}, {}, [download_path])
1064
+ end
1065
+
1066
+ assert !error.errors.empty?
1067
+ assert !File.exist?(download_path)
1068
+ end
1069
+ end
1070
+
1071
+ def test_multi_download_failed_transfer_does_not_publish_file_or_call_block
1072
+ called = false
1073
+
1074
+ Dir.mktmpdir('curb-download-') do |download_dir|
1075
+ download_path = File.join(download_dir, 'missing')
1076
+
1077
+ error = assert_raise(Curl::Multi::DownloadError) do
1078
+ Curl::Multi.download(['file:///curb/missing'], {}, {}, [download_path]) do |_curl, _path|
1079
+ called = true
1080
+ end
1081
+ end
1082
+
1083
+ assert !error.errors.empty?
1084
+ assert_equal false, called
1085
+ assert !File.exist?(download_path)
1086
+ end
1087
+ end
1088
+
1089
+ def test_multi_download_failed_transfer_closes_io_destination
1090
+ Dir.mktmpdir('curb-download-') do |download_dir|
1091
+ download_path = File.join(download_dir, 'missing')
1092
+ io = File.open(download_path, 'wb')
1093
+
1094
+ error = assert_raise(Curl::Multi::DownloadError) do
1095
+ Curl::Multi.download(['file:///curb/missing'], {}, {}, [io])
1096
+ end
1097
+
1098
+ assert !error.errors.any? { |e| e.is_a?(ArgumentError) }
1099
+ assert io.closed?
1100
+ assert_equal '', File.read(download_path)
1101
+ end
1102
+ end
1103
+
1104
+ def test_multi_download_preserves_body_limit_callback_error
1105
+ with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n#{'x' * 64}") do |url|
1106
+ Dir.mktmpdir('curb-download-') do |download_dir|
1107
+ download_path = File.join(download_dir, 'too-large')
1108
+
1109
+ error = assert_raise(Curl::Err::FileSizeExceededError) do
1110
+ Curl::Multi.download([url], { :max_body_bytes => 10 }, {}, [download_path])
1111
+ end
1112
+
1113
+ assert_match(/Maximum body size exceeded/, error.message)
1114
+ assert !File.exist?(download_path)
1115
+ end
1116
+ end
1117
+ end
1118
+
1119
+ def test_multi_download_preserves_output_write_callback_error
1120
+ writer_error = Class.new(StandardError)
1121
+
1122
+ with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata") do |url|
1123
+ Dir.mktmpdir('curb-download-') do |download_dir|
1124
+ writer = File.open(File.join(download_dir, 'writer'), 'wb')
1125
+ writer.define_singleton_method(:write) do |_data|
1126
+ raise writer_error, 'writer failed'
1127
+ end
1128
+
1129
+ error = assert_raise(writer_error) do
1130
+ Curl::Multi.download([url], {}, {}, [writer])
1131
+ end
1132
+
1133
+ assert_equal 'writer failed', error.message
1134
+ assert writer.closed?
1135
+ end
1136
+ end
1137
+ end
1138
+
1139
+ def test_multi_download_safe_directory_rejects_unsafe_names
1140
+ root_uri = 'http://127.0.0.1:9129/ext/'
1141
+
1142
+ Dir.mktmpdir('curb-download-') do |download_dir|
1143
+ ['../escape.c', '.env', 'nested/file', '/tmp/escape.c', "bad\0name"].each do |name|
1144
+ assert_raise(ArgumentError, "expected #{name.inspect} to be rejected") do
1145
+ Curl::Multi.download([root_uri + 'curb_easy.c'], {}, {}, [name], :download_dir => download_dir)
1146
+ end
1147
+ end
1148
+ end
1149
+ end
1150
+
1151
+ def test_multi_download_rejects_duplicate_destinations
1152
+ root_uri = 'http://127.0.0.1:9129/ext/'
1153
+
1154
+ Dir.mktmpdir('curb-download-') do |download_dir|
1155
+ assert_raise(ArgumentError) do
1156
+ Curl::Multi.download(
1157
+ [root_uri + 'curb_easy.c?one=1', root_uri + 'curb_easy.c?two=2'],
1158
+ {},
1159
+ {},
1160
+ nil,
1161
+ :download_dir => download_dir
1162
+ )
1163
+ end
1164
+
1165
+ assert !File.exist?(File.join(download_dir, 'curb_easy.c'))
1166
+ assert_equal [], Dir[File.join(download_dir, '.curb-download-*')]
1167
+ end
1168
+ end
1169
+
1170
+ def test_multi_download_safe_directory_strips_query_from_url_basename
1171
+ root_uri = 'http://127.0.0.1:9129/ext/'
1172
+
1173
+ Dir.mktmpdir('curb-download-') do |download_dir|
1174
+ Curl::Multi.download([root_uri + 'curb_easy.c?cache=1'], {}, {}, nil, :download_dir => download_dir)
1175
+
1176
+ download_path = File.join(download_dir, 'curb_easy.c')
329
1177
  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}"
1178
+ assert !File.exist?(File.join(download_dir, 'curb_easy.c?cache=1'))
1179
+ assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(download_path)
1180
+ end
1181
+ end
1182
+
1183
+ def test_multi_download_hash_url_config_uses_hash_url
1184
+ root_uri = 'http://127.0.0.1:9129/ext/'
1185
+
1186
+ Dir.mktmpdir('curb-download-') do |download_dir|
1187
+ download_path = File.join(download_dir, 'curb_easy.c')
1188
+
1189
+ Curl::Multi.download([{ :url => root_uri + 'curb_easy.c', :method => :get }], {}, {}, [download_path]) do |curl, path|
1190
+ assert_equal 200, curl.response_code
1191
+ assert_equal download_path, path
1192
+ end
1193
+
1194
+ assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(download_path)
1195
+ end
1196
+ end
1197
+
1198
+ def test_multi_download_duplicate_urls_use_distinct_explicit_paths
1199
+ url = "file://#{File.expand_path(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c'))}"
1200
+
1201
+ Dir.mktmpdir('curb-download-') do |download_dir|
1202
+ paths = [
1203
+ File.join(download_dir, 'a.c'),
1204
+ File.join(download_dir, 'b.c')
1205
+ ]
1206
+ completed_paths = []
1207
+
1208
+ Curl::Multi.download([url, url], {}, {}, paths) do |_curl, path|
1209
+ completed_paths << path
1210
+ end
1211
+
1212
+ assert_equal paths.sort, completed_paths.sort
1213
+ assert_equal File.read(paths[0]), File.read(paths[1])
1214
+ end
1215
+ end
1216
+
1217
+ def test_multi_download_accepts_indexed_hash_download_paths
1218
+ root_uri = 'http://127.0.0.1:9129/ext/'
1219
+
1220
+ Dir.mktmpdir('curb-download-') do |download_dir|
1221
+ download_path = File.join(download_dir, 'curb_easy.c')
1222
+
1223
+ Curl::Multi.download([root_uri + 'curb_easy.c'], {}, {}, {0 => download_path})
1224
+
1225
+ assert File.exist?(download_path)
1226
+ assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(download_path)
332
1227
  end
333
- ensure
334
- FileUtils.rm_rf("tmp/")
335
1228
  end
336
1229
 
337
1230
  def test_multi_easy_post_01
@@ -342,7 +1235,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
342
1235
  ]
343
1236
  Curl::Multi.post(urls, {:follow_location => true, :multipart_form_post => true}, {:pipeline => true}) do|easy|
344
1237
  str = easy.body_str
345
- assert_match /POST/, str
1238
+ assert_match(/POST/, str)
346
1239
  fields = {}
347
1240
  str.gsub(/POST\n/,'').split('&').map{|sv| k, v = sv.split('='); fields[k] = v }
348
1241
  expected = urls.find{|s| s[:url] == easy.last_effective_url }
@@ -357,8 +1250,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
357
1250
  { :url => TestServlet.url, :method => :put, :put_data => "message",
358
1251
  :headers => {'Content-Type' => 'application/json' } }]
359
1252
  Curl::Multi.put(urls, {}, {:pipeline => true}) do|easy|
360
- assert_match /PUT/, easy.body_str
361
- assert_match /message/, easy.body_str
1253
+ assert_match(/PUT/, easy.body_str)
1254
+ assert_match(/message/, easy.body_str)
362
1255
  end
363
1256
  end
364
1257
 
@@ -372,35 +1265,83 @@ class TestCurbCurlMulti < Test::Unit::TestCase
372
1265
  { :url => TestServlet.url, :method => :get }
373
1266
  ]
374
1267
  Curl::Multi.http(urls, {:pipeline => true}) do|easy, code, method|
375
- assert_equal nil, code
1268
+ assert_equal 200, code
376
1269
  case method
377
1270
  when :post
378
- assert_match /POST/, easy.body_str
1271
+ assert_match(/POST/, easy.body_str)
379
1272
  when :get
380
- assert_match /GET/, easy.body_str
1273
+ assert_match(/GET/, easy.body_str)
381
1274
  when :put
382
- assert_match /PUT/, easy.body_str
1275
+ assert_match(/PUT/, easy.body_str)
383
1276
  end
384
1277
  #puts "#{easy.body_str.inspect}, #{method.inspect}, #{code.inspect}"
385
1278
  end
386
1279
  end
387
1280
 
388
1281
  def test_multi_easy_http_with_max_connects
389
- urls = [
1282
+ urls = [
390
1283
  { :url => TestServlet.url + '?q=1', :method => :get },
391
1284
  { :url => TestServlet.url + '?q=2', :method => :get },
392
1285
  { :url => TestServlet.url + '?q=3', :method => :get }
393
1286
  ]
394
1287
  Curl::Multi.http(urls, {:pipeline => true, :max_connects => 1}) do|easy, code, method|
395
- assert_equal nil, code
1288
+ assert_equal 200, code
396
1289
  case method
397
1290
  when :post
398
- assert_match /POST/, easy.body_str
1291
+ assert_match(/POST/, easy.body)
399
1292
  when :get
400
- assert_match /GET/, easy.body_str
1293
+ assert_match(/GET/, easy.body)
401
1294
  when :put
402
- assert_match /PUT/, easy.body_str
1295
+ assert_match(/PUT/, easy.body)
1296
+ end
1297
+ end
1298
+ end
1299
+
1300
+ def test_multi_easy_http_with_max_host_connections
1301
+ urls = [
1302
+ { :url => TestServlet.url + '?q=1', :method => :get },
1303
+ { :url => TestServlet.url + '?q=2', :method => :get },
1304
+ { :url => TestServlet.url + '?q=3', :method => :get }
1305
+ ]
1306
+ Curl::Multi.http(urls, {:pipeline => true, :max_host_connections => 1}) do|easy, code, method|
1307
+ assert_equal 200, code
1308
+ case method
1309
+ when :post
1310
+ assert_match(/POST/, easy.body)
1311
+ when :get
1312
+ assert_match(/GET/, easy.body)
1313
+ when :put
1314
+ assert_match(/PUT/, easy.body)
1315
+ end
1316
+ end
1317
+ end
1318
+
1319
+ # Regression test for issue #267 (2015): ensure that when reusing
1320
+ # easy handles with constrained concurrency, the callback receives
1321
+ # the correct URL for each completed request rather than repeating
1322
+ # the first URL.
1323
+ def test_multi_easy_http_urls_unique_across_max_connects
1324
+ urls = [
1325
+ { :url => TestServlet.url + '?q=1', :method => :get },
1326
+ { :url => TestServlet.url + '?q=2', :method => :get },
1327
+ { :url => TestServlet.url + '?q=3', :method => :get }
1328
+ ]
1329
+
1330
+ [1, 2, 3].each do |max|
1331
+ results = []
1332
+ Curl::Multi.http(urls.dup, {:pipeline => true, :max_connects => max}) do |easy, code, method|
1333
+ assert_equal 200, code
1334
+ assert_equal :get, method
1335
+ results << easy.last_effective_url
403
1336
  end
1337
+
1338
+ # Ensure we saw one completion per input URL
1339
+ assert_equal urls.size, results.size, "expected #{urls.size} results with max_connects=#{max}"
1340
+
1341
+ # And that each URL completed exactly once (no accidental reuse/mis-reporting)
1342
+ expected_urls = urls.map { |u| u[:url] }
1343
+ assert_equal expected_urls.to_set, results.to_set, "unexpected URLs for max_connects=#{max}: #{results.inspect}"
1344
+ assert_equal expected_urls.size, results.uniq.size, "duplicate URLs seen for max_connects=#{max}: #{results.inspect}"
404
1345
  end
405
1346
  end
406
1347
 
@@ -422,6 +1363,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
422
1363
  failure = false
423
1364
  assert !failure
424
1365
  assert_equal "POST\nhello=world", e2.body_str
1366
+ ensure
1367
+ m.close if m
425
1368
  end
426
1369
 
427
1370
  def test_remove_exception_is_descriptive
@@ -431,6 +1374,8 @@ class TestCurbCurlMulti < Test::Unit::TestCase
431
1374
  rescue => e
432
1375
  assert_equal 'CURLError: Invalid easy handle', e.message
433
1376
  assert_equal 0, m.requests.size
1377
+ ensure
1378
+ m.close if m
434
1379
  end
435
1380
 
436
1381
  def test_retry_easy_handle
@@ -454,6 +1399,50 @@ class TestCurbCurlMulti < Test::Unit::TestCase
454
1399
  m.perform
455
1400
  assert_equal 0, tries
456
1401
  assert_equal 0, m.requests.size
1402
+ ensure
1403
+ m.close if m
1404
+ end
1405
+
1406
+ def test_close_in_on_missing_callback_is_blocked
1407
+ m = Curl::Multi.new
1408
+ c = Curl::Easy.new(TestServlet.url + '/not_here')
1409
+ did_raise = false
1410
+
1411
+ c.on_missing do |easy, _error|
1412
+ begin
1413
+ easy.close
1414
+ rescue RuntimeError
1415
+ did_raise = true
1416
+ end
1417
+ end
1418
+
1419
+ m.add(c)
1420
+ m.perform
1421
+
1422
+ assert did_raise
1423
+ ensure
1424
+ m.close if m
1425
+ end
1426
+
1427
+ def test_close_in_on_redirect_callback_is_blocked
1428
+ m = Curl::Multi.new
1429
+ c = Curl::Easy.new(TestServlet.url + '/redirect')
1430
+ did_raise = false
1431
+
1432
+ c.on_redirect do |easy, _error|
1433
+ begin
1434
+ easy.close
1435
+ rescue RuntimeError
1436
+ did_raise = true
1437
+ end
1438
+ end
1439
+
1440
+ m.add(c)
1441
+ m.perform
1442
+
1443
+ assert did_raise
1444
+ ensure
1445
+ m.close if m
457
1446
  end
458
1447
 
459
1448
  def test_reusing_handle
@@ -466,7 +1455,9 @@ class TestCurbCurlMulti < Test::Unit::TestCase
466
1455
  m.add(c)
467
1456
  m.add(c)
468
1457
  rescue => e
469
- assert_equal Curl::Err::MultiBadEasyHandle, e.class
1458
+ assert Curl::Err::MultiBadEasyHandle == e.class || Curl::Err::MultiAddedAlready == e.class
1459
+ ensure
1460
+ m.close if m
470
1461
  end
471
1462
 
472
1463
  def test_multi_default_timeout
@@ -476,10 +1467,79 @@ class TestCurbCurlMulti < Test::Unit::TestCase
476
1467
  assert_equal 100, (Curl::Multi.default_timeout = 100)
477
1468
  end
478
1469
 
1470
+ def with_queue_refill_test_server(wait_fail_until_slow: false)
1471
+ port_socket = TCPServer.new('127.0.0.1', 0)
1472
+ port = port_socket.addr[1]
1473
+ port_socket.close
1474
+
1475
+ hits = Hash.new(0)
1476
+ slow_started = false
1477
+ slow_started_lock = Mutex.new
1478
+ slow_started_condition = ConditionVariable.new
1479
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
1480
+ server.mount_proc('/fail') do |_req, res|
1481
+ if wait_fail_until_slow
1482
+ slow_started_lock.synchronize do
1483
+ slow_started_condition.wait(slow_started_lock, 1) unless slow_started
1484
+ end
1485
+ end
1486
+
1487
+ hits[:fail] += 1
1488
+ res['Content-Type'] = 'text/plain'
1489
+ res.body = 'fail'
1490
+ end
1491
+ server.mount_proc('/slow') do |_req, res|
1492
+ slow_started_lock.synchronize do
1493
+ hits[:slow] += 1
1494
+ slow_started = true
1495
+ slow_started_condition.broadcast
1496
+ end
1497
+
1498
+ res['Content-Type'] = 'text/plain'
1499
+ sleep 0.3
1500
+ res.body = 'slow'
1501
+ end
1502
+ server.mount_proc('/queued') do |_req, res|
1503
+ hits[:queued] += 1
1504
+ res['Content-Type'] = 'text/plain'
1505
+ res.body = 'queued'
1506
+ end
1507
+
1508
+ server_thread = Thread.new(server) { |srv| srv.start }
1509
+ wait_for_server_ready(port, thread: server_thread)
1510
+
1511
+ yield port, hits
1512
+ ensure
1513
+ server.shutdown if defined?(server) && server
1514
+ server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
1515
+ server_thread.kill if defined?(server_thread) && server_thread&.alive?
1516
+ end
1517
+
1518
+ def with_raw_http_response(response)
1519
+ server = TCPServer.new('127.0.0.1', 0)
1520
+ port = server.addr[1]
1521
+ thread = Thread.new do
1522
+ socket = server.accept
1523
+ begin
1524
+ socket.readpartial(4096)
1525
+ rescue EOFError
1526
+ end
1527
+ socket.write(response)
1528
+ socket.close
1529
+ end
1530
+
1531
+ yield "http://127.0.0.1:#{port}/"
1532
+ ensure
1533
+ server.close if defined?(server) && server && !server.closed?
1534
+ if defined?(thread) && thread
1535
+ thread.join(5)
1536
+ thread.kill if thread.alive?
1537
+ end
1538
+ end
1539
+
479
1540
  include TestServerMethods
480
1541
 
481
1542
  def setup
482
1543
  server_setup
483
1544
  end
484
-
485
1545
  end