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
@@ -0,0 +1,543 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ begin
4
+ require 'async'
5
+ rescue LoadError
6
+ HAS_ASYNC = false
7
+ else
8
+ HAS_ASYNC = true
9
+ end
10
+
11
+ # This test verifies that Curl::Easy#perform cooperates with Ruby's Fiber scheduler
12
+ # by running multiple requests concurrently in a single thread using the Async gem.
13
+ class TestCurbFiberScheduler < Test::Unit::TestCase
14
+ include BugTestServerSetupTeardown
15
+ include TestServerMethods
16
+
17
+ class RecordingScheduler
18
+ attr_reader :io_wait_events, :io_select_calls
19
+
20
+ def initialize
21
+ @io_wait_events = []
22
+ @io_select_calls = 0
23
+ end
24
+
25
+ def fiber(&block)
26
+ Fiber.new(blocking: false, &block)
27
+ end
28
+
29
+ def io_wait(io, events, timeout = nil)
30
+ @io_wait_events << events
31
+ raise TypeError, "expected Integer events, got #{events.class}" unless events.is_a?(Integer)
32
+
33
+ readers = (events & IO::READABLE) != 0 ? [io] : nil
34
+ writers = (events & IO::WRITABLE) != 0 ? [io] : nil
35
+ readable, writable = blocking_io { IO.select(readers, writers, nil, timeout) }
36
+
37
+ ready = 0
38
+ ready |= IO::READABLE if readable && !readable.empty?
39
+ ready |= IO::WRITABLE if writable && !writable.empty?
40
+ ready.zero? ? false : ready
41
+ end
42
+
43
+ def io_select(readers, writers, excepts, timeout = nil)
44
+ @io_select_calls += 1
45
+ blocking_io { IO.select(readers, writers, excepts, timeout) }
46
+ end
47
+
48
+ def kernel_sleep(duration = nil)
49
+ sleep(duration || 0)
50
+ end
51
+
52
+ def block(_blocker, timeout = nil)
53
+ sleep(timeout || 0)
54
+ false
55
+ end
56
+
57
+ def unblock(*)
58
+ end
59
+
60
+ def close
61
+ end
62
+
63
+ def fiber_interrupt(*)
64
+ end
65
+
66
+ private
67
+
68
+ def blocking_io(&block)
69
+ if Fiber.respond_to?(:blocking)
70
+ Fiber.blocking(&block)
71
+ else
72
+ block.call
73
+ end
74
+ end
75
+ end
76
+
77
+ ITERS = 4
78
+ MIN_S = 0.25
79
+ # Each request sleeps 0.25s; two concurrent requests should be ~0.25–0.5s.
80
+ # Allow some jitter in CI environments.
81
+ THRESHOLD = ((MIN_S*(ITERS/2.0)) + (MIN_S/2.0)) # add more jitter for slower CI environments
82
+ SERIAL_TIME_WOULD_BE_ABOUT = MIN_S * ITERS
83
+
84
+ def setup
85
+ @port = unused_local_port
86
+
87
+ @response_proc = lambda do |res|
88
+ res['Content-Type'] = 'text/plain'
89
+ sleep MIN_S
90
+ res.body = '200'
91
+ end
92
+ super
93
+ end
94
+
95
+ def test_multi_is_scheduler_friendly
96
+ if skip_no_async
97
+ return
98
+ end
99
+
100
+ url = "http://127.0.0.1:#{@port}/test"
101
+
102
+ started = Time.now
103
+ results = []
104
+
105
+
106
+ async_run do
107
+ m = Curl::Multi.new
108
+ ITERS.times.each do
109
+ c = Curl::Easy.new(url)
110
+ c.on_complete { results << c.code }
111
+ m.add(c)
112
+ end
113
+ m.perform
114
+ ensure
115
+ m.close if m
116
+ end
117
+
118
+ duration = Time.now - started
119
+
120
+ assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
121
+ assert_equal ITERS, results.size
122
+ assert_equal ITERS.times.map {200}, results
123
+ end
124
+
125
+ def test_easy_perform_is_scheduler_friendly
126
+ if skip_no_async
127
+ return
128
+ end
129
+
130
+ url = "http://127.0.0.1:#{@port}/test"
131
+
132
+ started = Time.now
133
+ results = []
134
+
135
+ async_run do |top|
136
+ tasks = ITERS.times.map do
137
+ top.async do
138
+ #t = Time.now.to_i
139
+ #puts "starting fiber [#{results.size}] -> #{t}"
140
+ c = Curl.get(url)
141
+ #puts "received result: #{results.size} -> #{Time.now.to_f - t.to_f}"
142
+ results << c.code
143
+
144
+ end
145
+ end
146
+ tasks.each(&:wait)
147
+ end
148
+
149
+ duration = Time.now - started
150
+
151
+ assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
152
+ assert_equal ITERS, results.size
153
+ assert_equal ITERS.times.map {200}, results
154
+ end
155
+
156
+ def test_multi_perform_yields_block_under_scheduler
157
+ if skip_no_async
158
+ return
159
+ end
160
+
161
+ url = "http://127.0.0.1:#{@port}/test"
162
+ yielded = 0
163
+ results = []
164
+
165
+ async_run do
166
+ m = Curl::Multi.new
167
+ ITERS.times do
168
+ c = Curl::Easy.new(url)
169
+ c.on_complete { results << c.code }
170
+ m.add(c)
171
+ end
172
+ m.perform do
173
+ yielded += 1
174
+ end
175
+ ensure
176
+ m.close if m
177
+ end
178
+
179
+ assert_operator yielded, :>=, 1, 'perform did not yield block while waiting under scheduler'
180
+ assert_equal ITERS, results.size
181
+ assert_equal ITERS.times.map {200}, results
182
+ end
183
+
184
+ def test_multi_single_request_scheduler_path
185
+ if skip_no_async
186
+ return
187
+ end
188
+
189
+ url = "http://127.0.0.1:#{@port}/test"
190
+ result = nil
191
+
192
+ async_run do
193
+ m = Curl::Multi.new
194
+ c = Curl::Easy.new(url)
195
+ c.on_complete { result = c.code }
196
+ m.add(c)
197
+ m.perform
198
+ ensure
199
+ m.close if m
200
+ end
201
+
202
+ assert_equal 200, result
203
+ end
204
+
205
+ def test_multi_single_request_socket_perform_passes_integer_events_to_io_wait
206
+ omit('Skipping custom scheduler test on Windows') if WINDOWS
207
+ omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
208
+
209
+ url = "http://127.0.0.1:#{@port}/test"
210
+ scheduler = RecordingScheduler.new
211
+ result = nil
212
+
213
+ with_scheduler(scheduler) do
214
+ m = Curl::Multi.new
215
+ c = Curl::Easy.new(url)
216
+ c.on_complete { result = c.code }
217
+ m.add(c)
218
+ unless m.respond_to?(:_socket_perform, true)
219
+ omit('socket-action perform path is not available in this build')
220
+ end
221
+ m.send(:_socket_perform)
222
+ ensure
223
+ m.close if m
224
+ end
225
+
226
+ assert_equal 200, result
227
+ assert_operator scheduler.io_wait_events.length + scheduler.io_select_calls, :>=, 1
228
+ unless scheduler.io_wait_events.empty?
229
+ assert scheduler.io_wait_events.all? { |events| events.is_a?(Integer) }
230
+ assert scheduler.io_wait_events.any? { |events| (events & (IO::READABLE | IO::WRITABLE)) != 0 }
231
+ end
232
+ end
233
+
234
+ def test_multi_reuse_after_scheduler_perform
235
+ unless HAS_ASYNC
236
+ warn 'Skipping fiber scheduler test (Async gem not available)'
237
+ return
238
+ end
239
+
240
+ url = "http://127.0.0.1:#{@port}/test"
241
+ results = []
242
+
243
+ async_run do
244
+ m = Curl::Multi.new
245
+ # First round
246
+ c1 = Curl::Easy.new(url)
247
+ c1.on_complete { results << c1.code }
248
+ m.add(c1)
249
+ m.perform
250
+
251
+ # Second round on same multi
252
+ c2 = Curl::Easy.new(url)
253
+ c2.on_complete { results << c2.code }
254
+ m.add(c2)
255
+ m.perform
256
+ ensure
257
+ m.close if m
258
+ end
259
+
260
+ assert_equal [200, 200], results
261
+ end
262
+
263
+ def test_easy_perform_reuses_scheduler_multi_when_autoclose_is_enabled
264
+ if skip_no_async
265
+ return
266
+ end
267
+
268
+ url = "http://127.0.0.1:#{@port}/test"
269
+ results = []
270
+ previous_autoclose = Curl::Multi.autoclose
271
+ Curl::Multi.autoclose = true
272
+
273
+ async_run do
274
+ 2.times do
275
+ easy = Curl::Easy.new(url)
276
+ easy.perform
277
+ results << easy.code
278
+ end
279
+ end
280
+
281
+ assert_equal [200, 200], results
282
+ ensure
283
+ Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
284
+ cleanup_scheduler_state
285
+ end
286
+
287
+ def test_easy_perform_reraises_on_body_exception_under_scheduler
288
+ if skip_no_async
289
+ return
290
+ end
291
+
292
+ url = "http://127.0.0.1:#{@port}/test"
293
+
294
+ async_run do |top|
295
+ task = top.async do
296
+ c = Curl::Easy.new(url)
297
+ c.on_body { raise "body blew up" }
298
+ c.on_complete { sleep 0 }
299
+ c.perform
300
+ end
301
+
302
+ error = assert_raise(RuntimeError) do
303
+ task.wait
304
+ end
305
+
306
+ assert_equal "body blew up", error.message
307
+ end
308
+ end
309
+
310
+ def test_easy_perform_reraises_on_header_exception_under_scheduler
311
+ if skip_no_async
312
+ return
313
+ end
314
+
315
+ url = "http://127.0.0.1:#{@port}/test"
316
+
317
+ async_run do |top|
318
+ task = top.async do
319
+ c = Curl::Easy.new(url)
320
+ c.on_header { raise "header blew up" }
321
+ c.on_complete { sleep 0 }
322
+ c.perform
323
+ end
324
+
325
+ error = assert_raise(RuntimeError) do
326
+ task.wait
327
+ end
328
+
329
+ assert_equal "header blew up", error.message
330
+ end
331
+ end
332
+
333
+ def test_easy_perform_does_not_reraise_sibling_on_complete_exception_under_scheduler
334
+ if skip_no_async
335
+ return
336
+ end
337
+
338
+ with_ephemeral_http_server do |port, hits|
339
+ results = {}
340
+
341
+ async_run do |top|
342
+ successful = top.async do
343
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
344
+ easy.perform
345
+ results[:successful] = easy.response_code
346
+ rescue => e
347
+ results[:successful] = e
348
+ end
349
+
350
+ failing = top.async do
351
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
352
+ easy.on_complete { raise "boom" }
353
+ easy.perform
354
+ results[:failing] = :returned
355
+ rescue => e
356
+ results[:failing] = e
357
+ end
358
+
359
+ successful.wait
360
+ failing.wait
361
+ end
362
+
363
+ assert_equal 200, results[:successful], "successful scheduler peer should return normally"
364
+ assert_kind_of Curl::Err::AbortedByCallbackError, results[:failing]
365
+ assert_equal "boom", results[:failing].message
366
+ assert_equal 1, hits[:fast]
367
+ assert_equal 1, hits[:slow]
368
+ end
369
+ end
370
+
371
+ def test_easy_perform_does_not_reraise_fast_on_complete_exception_into_slow_successful_peer_under_scheduler
372
+ if skip_no_async
373
+ return
374
+ end
375
+
376
+ with_ephemeral_http_server do |port, hits|
377
+ results = {}
378
+
379
+ async_run do |top|
380
+ failing = top.async do
381
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
382
+ easy.on_complete { raise "boom" }
383
+ easy.perform
384
+ results[:failing] = :returned
385
+ rescue => e
386
+ results[:failing] = e
387
+ end
388
+
389
+ successful = top.async do
390
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
391
+ easy.perform
392
+ results[:successful] = easy.response_code
393
+ rescue => e
394
+ results[:successful] = e
395
+ end
396
+
397
+ failing.wait
398
+ successful.wait
399
+ end
400
+
401
+ assert_equal 200, results[:successful], "slow successful scheduler peer should not inherit a fast sibling callback error"
402
+ assert_kind_of Curl::Err::AbortedByCallbackError, results[:failing]
403
+ assert_equal "boom", results[:failing].message
404
+ assert_equal 1, hits[:fast]
405
+ assert_equal 1, hits[:slow]
406
+ end
407
+ end
408
+
409
+ def test_easy_perform_keeps_scheduler_timers_running_while_draining_deferred_on_complete_exception
410
+ if skip_no_async
411
+ return
412
+ end
413
+
414
+ with_ephemeral_http_server do |port, hits|
415
+ marks = {}
416
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
417
+
418
+ async_run do |top|
419
+ timer = top.async do
420
+ sleep 0.05
421
+ marks[:timer] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
422
+ end
423
+
424
+ failing = top.async do
425
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
426
+ easy.on_complete { raise "boom" }
427
+ easy.perform
428
+ marks[:failing] = :returned
429
+ rescue => e
430
+ marks[:failing] = e
431
+ end
432
+
433
+ successful = top.async do
434
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
435
+ easy.perform
436
+ marks[:slow_done] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
437
+ end
438
+
439
+ timer.wait
440
+ failing.wait
441
+ successful.wait
442
+ end
443
+
444
+ assert_kind_of Curl::Err::AbortedByCallbackError, marks[:failing]
445
+ assert_operator marks[:timer], :<, marks[:slow_done] - 0.02,
446
+ "scheduler timer should fire before the slow sibling finishes draining"
447
+ assert_equal 1, hits[:fast]
448
+ assert_equal 1, hits[:slow]
449
+ end
450
+ end
451
+
452
+ def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
453
+ state = Curl.scheduler_state
454
+ easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
455
+ waiter = {completed: false, done: false, error: nil}
456
+
457
+ state[:waiters][easy.object_id] = waiter
458
+ state[:pending] << easy
459
+ state[:multi].instance_variable_set(:@__curb_deferred_exception, RuntimeError.new("boom"))
460
+
461
+ Curl.drain_scheduler_pending(state)
462
+
463
+ assert(state[:pending].include?(easy) || waiter[:error],
464
+ "scheduler enqueue rejected during deferred abort should remain pending or fail its waiter instead of disappearing")
465
+ assert_equal 0, state[:multi].requests.length
466
+ ensure
467
+ cleanup_scheduler_state
468
+ end
469
+
470
+ private
471
+ def skip_no_async
472
+ if WINDOWS
473
+ warn 'Skipping fiber scheduler tests on Windows'
474
+ return true
475
+ end
476
+ unless fiber_scheduler_supported?
477
+ warn 'Skipping fiber scheduler tests (Fiber scheduler API unavailable)'
478
+ return true
479
+ end
480
+ unless HAS_ASYNC
481
+ warn 'Skipping fiber scheduler test (Async gem not available)'
482
+ return true
483
+ end
484
+ false
485
+ end
486
+
487
+ def async_run(&block)
488
+ # Prefer newer Async.run to avoid deprecated scheduler.async path.
489
+ if defined?(Async) && Async.respond_to?(:run)
490
+ Async.run(&block)
491
+ else
492
+ Async(&block)
493
+ end
494
+ end
495
+
496
+ def fiber_scheduler_supported?
497
+ Fiber.respond_to?(:set_scheduler) && Fiber.respond_to?(:schedule)
498
+ end
499
+
500
+ def with_scheduler(scheduler)
501
+ previous_scheduler = Fiber.scheduler if Fiber.respond_to?(:scheduler)
502
+ Fiber.set_scheduler(scheduler)
503
+ fiber = Fiber.schedule { yield }
504
+ fiber.resume while fiber.alive?
505
+ ensure
506
+ Fiber.set_scheduler(previous_scheduler) if Fiber.respond_to?(:set_scheduler)
507
+ end
508
+
509
+ def cleanup_scheduler_state
510
+ state = Thread.current.thread_variable_get(:curb_scheduler_state)
511
+ state[:multi].close if state && state[:multi]
512
+ Thread.current.thread_variable_set(:curb_scheduler_state, nil)
513
+ end
514
+
515
+ def with_ephemeral_http_server
516
+ port_socket = TCPServer.new('127.0.0.1', 0)
517
+ port = port_socket.addr[1]
518
+ port_socket.close
519
+
520
+ hits = Hash.new(0)
521
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
522
+ server.mount_proc('/fast') do |_req, res|
523
+ hits[:fast] += 1
524
+ res['Content-Type'] = 'text/plain'
525
+ res.body = 'fast'
526
+ end
527
+ server.mount_proc('/slow') do |_req, res|
528
+ hits[:slow] += 1
529
+ res['Content-Type'] = 'text/plain'
530
+ sleep 0.2
531
+ res.body = 'slow'
532
+ end
533
+
534
+ server_thread = Thread.new(server) { |srv| srv.start }
535
+ wait_for_server_ready(port, thread: server_thread)
536
+
537
+ yield port, hits
538
+ ensure
539
+ server.shutdown if defined?(server) && server
540
+ server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
541
+ server_thread.kill if defined?(server_thread) && server_thread&.alive?
542
+ end
543
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class TestCurbFtpOptions < Test::Unit::TestCase
4
+ # Ensure FTP-related set(:option, ...) mappings are accepted and do not raise
5
+ # a TypeError (they used to be unsupported in setopt dispatch).
6
+ def test_can_set_ftp_listing_related_flags
7
+ c = Curl::Easy.new('ftp://example.com/')
8
+
9
+ assert_nothing_raised do
10
+ c.set(:dirlistonly, true) if Curl.const_defined?(:CURLOPT_DIRLISTONLY)
11
+ c.set(:ftp_use_epsv, 0) if Curl.const_defined?(:CURLOPT_FTP_USE_EPSV)
12
+ # These may not be present on all libcurl builds; guard by constant
13
+ c.set(:ftp_use_eprt, 0) if Curl.const_defined?(:CURLOPT_FTP_USE_EPRT)
14
+ c.set(:ftp_skip_pasv_ip, 1) if Curl.const_defined?(:CURLOPT_FTP_SKIP_PASV_IP)
15
+ end
16
+ end
17
+
18
+ # Setting ftp_commands remains supported for control-connection commands.
19
+ def test_can_assign_ftp_commands
20
+ c = Curl::Easy.new('ftp://example.com/')
21
+ c.ftp_commands = ["PWD", "CWD /"]
22
+ assert_kind_of(Array, c.ftp_commands)
23
+ assert_equal ["PWD", "CWD /"], c.ftp_commands
24
+ end
25
+
26
+ def test_ftp_command_entries_can_be_objects_that_convert_to_string
27
+ command = Object.new
28
+ command.define_singleton_method(:to_s) { "PWD" }
29
+
30
+ c = Curl::Easy.new($TEST_URL)
31
+ c.ftp_commands = [command]
32
+ m = Curl::Multi.new
33
+
34
+ assert_nothing_raised { m.add(c) }
35
+ ensure
36
+ m.remove(c) if defined?(m) && m && m.requests[c.object_id]
37
+ m.close if defined?(m) && m
38
+ end
39
+ end