curb 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,20 +48,62 @@ class TestCurbCurlEasy < Test::Unit::TestCase
48
48
  end
49
49
 
50
50
  def test_curlopt_stderr_with_io
51
- path = Tempfile.new('curb_test_curlopt_stderr').path
51
+ tmp = Tempfile.new('curb_test_curlopt_stderr')
52
+ path = tmp.path
52
53
  fd = IO.sysopen(path, 'w')
53
- io = IO.for_fd(fd)
54
+ io = IO.new(fd, 'w')
55
+ io.sync = true
54
56
 
55
57
  easy = Curl::Easy.new(TestServlet.url)
56
58
  easy.verbose = true
57
59
  easy.setopt(Curl::CURLOPT_STDERR, io)
58
60
  easy.perform
59
61
 
62
+ io.flush
63
+ io.close
64
+ output = File.read(path)
65
+
66
+ assert_match(/HTTP\/1\.1\ 200\ OK(?:\ )?/, output)
67
+ assert_match('Host: 127.0.0.1:9129', output)
68
+ ensure
69
+ tmp.close! if defined?(tmp) && tmp
70
+ end
71
+
72
+ def test_curlopt_stderr_gc_safe
73
+ tmp = Tempfile.new('curb_test_curlopt_stderr_gc')
74
+ path = tmp.path
75
+ fd = IO.sysopen(path, 'w')
76
+ io = IO.new(fd, 'w')
77
+
78
+ easy = Curl::Easy.new(TestServlet.url)
79
+ easy.verbose = true
80
+ easy.setopt(Curl::CURLOPT_STDERR, io)
81
+
82
+ # Drop our Ruby reference and force GC; curb should retain it internally
83
+ io = nil
84
+ GC.start
85
+
86
+ easy.perform
60
87
 
61
88
  output = File.read(path)
89
+ assert_match(/HTTP\/1\.1\ 200\ OK(?:\ )?/, output)
90
+ assert_match('Host: 127.0.0.1:9129', output)
91
+ ensure
92
+ tmp.close! if defined?(tmp) && tmp
93
+ end
62
94
 
63
- assert_match(output, 'HTTP/1.1 200 OK')
64
- assert_match(output, 'Host: 127.0.0.1:9129')
95
+ def test_head_request_no_body_and_no_timeout
96
+ # Ensure a HEAD request completes and does not attempt to read a body
97
+ # even when the server advertises a Content-Length.
98
+ easy = nil
99
+ assert_nothing_raised do
100
+ easy = Curl.http(:HEAD, TestServlet.url)
101
+ end
102
+ assert_not_nil easy
103
+ # Header string should contain the HTTP status line.
104
+ assert_match(/HTTP\/1\.1\s\d+\s/, easy.header_str.to_s)
105
+ # Body should be empty for HEAD requests (libcurl won't call the write callback).
106
+ assert_equal "", easy.body_str.to_s
65
107
  end
66
108
 
67
109
  def test_curlopt_stderr_fails_with_tempdir
@@ -245,6 +287,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
245
287
 
246
288
 
247
289
  def test_last_effective_url_01
290
+ omit('Windows file URL semantics differ') if WINDOWS
248
291
  c = Curl::Easy.new($TEST_URL)
249
292
 
250
293
  assert_equal $TEST_URL, c.url
@@ -1041,7 +1084,11 @@ class TestCurbCurlEasy < Test::Unit::TestCase
1041
1084
  $auth_header = nil
1042
1085
  # curl checks the auth type supported by the server, so we have to create a
1043
1086
  # new easy handle if we're going to change the auth type...
1044
-
1087
+ if WINDOWS
1088
+ # On Windows, libcurl often uses SSPI for NTLM which yields a different
1089
+ # header value and encoding; skip the NTLM-specific assertion.
1090
+ return
1091
+ end
1045
1092
  curl = Curl::Easy.new(TestServlet.url)
1046
1093
  curl.username = "foo"
1047
1094
  curl.password = "bar"
@@ -1073,7 +1120,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
1073
1120
  easy.http_post(pf)
1074
1121
 
1075
1122
  assert_not_equal(0,easy.body.size)
1076
- assert_equal(Digest::MD5.hexdigest(easy.body), Digest::MD5.hexdigest(File.read(readme)))
1123
+ assert_equal(Digest::MD5.hexdigest(easy.body), Digest::MD5.hexdigest(File.binread(readme)))
1077
1124
  end
1078
1125
 
1079
1126
  def test_easy_close
@@ -1095,6 +1142,18 @@ class TestCurbCurlEasy < Test::Unit::TestCase
1095
1142
  easy.http_get
1096
1143
  end
1097
1144
 
1145
+ def test_last_result_initialization
1146
+ # Test for issue #463 - ensure last_result is properly initialized to 0
1147
+ easy = Curl::Easy.new
1148
+ assert_equal 0, easy.last_result, "last_result should be initialized to 0 for new instance"
1149
+
1150
+ # Test that reset also sets last_result to 0
1151
+ easy.url = TestServlet.url
1152
+ easy.http_get
1153
+ easy.reset
1154
+ assert_equal 0, easy.last_result, "last_result should be reset to 0 after calling reset"
1155
+ end
1156
+
1098
1157
  def test_easy_use_http_versions
1099
1158
  easy = Curl::Easy.new
1100
1159
  easy.url = TestServlet.url + "?query=foo"
@@ -10,6 +10,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
10
10
  # for https://github.com/taf2/curb/issues/277
11
11
  # must connect to an external
12
12
  def test_connection_keepalive
13
+ omit('Not supported on Windows runners') if WINDOWS
13
14
  # this test fails with libcurl 7.22.0. I didn't investigate, but it may be related
14
15
  # to CURLOPT_MAXCONNECTS bug fixed in 7.30.0:
15
16
  # https://github.com/curl/curl/commit/e87e76e2dc108efb1cae87df496416f49c55fca0
@@ -171,6 +172,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
171
172
  end
172
173
 
173
174
  def test_multi_easy_get_with_error
175
+ omit('Path/line parsing differs on Windows') if WINDOWS
174
176
  begin
175
177
  did_raise = false
176
178
  n = 3
@@ -571,6 +573,35 @@ class TestCurbCurlMulti < Test::Unit::TestCase
571
573
  end
572
574
  end
573
575
 
576
+ # Regression test for issue #267 (2015): ensure that when reusing
577
+ # easy handles with constrained concurrency, the callback receives
578
+ # the correct URL for each completed request rather than repeating
579
+ # the first URL.
580
+ def test_multi_easy_http_urls_unique_across_max_connects
581
+ urls = [
582
+ { :url => TestServlet.url + '?q=1', :method => :get },
583
+ { :url => TestServlet.url + '?q=2', :method => :get },
584
+ { :url => TestServlet.url + '?q=3', :method => :get }
585
+ ]
586
+
587
+ [1, 2, 3].each do |max|
588
+ results = []
589
+ Curl::Multi.http(urls.dup, {:pipeline => true, :max_connects => max}) do |easy, code, method|
590
+ assert_equal 200, code
591
+ assert_equal :get, method
592
+ results << easy.last_effective_url
593
+ end
594
+
595
+ # Ensure we saw one completion per input URL
596
+ assert_equal urls.size, results.size, "expected #{urls.size} results with max_connects=#{max}"
597
+
598
+ # And that each URL completed exactly once (no accidental reuse/mis-reporting)
599
+ expected_urls = urls.map { |u| u[:url] }
600
+ assert_equal expected_urls.to_set, results.to_set, "unexpected URLs for max_connects=#{max}: #{results.inspect}"
601
+ assert_equal expected_urls.size, results.uniq.size, "duplicate URLs seen for max_connects=#{max}: #{results.inspect}"
602
+ end
603
+ end
604
+
574
605
  def test_multi_recieves_500
575
606
  m = Curl::Multi.new
576
607
  e = Curl::Easy.new("http://127.0.0.1:9129/methods")
@@ -0,0 +1,190 @@
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
+
16
+ ITERS = 4
17
+ MIN_S = 0.25
18
+ # Each request sleeps 0.25s; two concurrent requests should be ~0.25–0.5s.
19
+ # Allow some jitter in CI environments.
20
+ THRESHOLD = ((MIN_S*(ITERS/2.0)) + (MIN_S/2.0)) # add more jitter for slower CI environments
21
+ SERIAL_TIME_WOULD_BE_ABOUT = MIN_S * ITERS
22
+
23
+ def setup
24
+ @port = 9993
25
+
26
+ @response_proc = lambda do |res|
27
+ res['Content-Type'] = 'text/plain'
28
+ sleep MIN_S
29
+ res.body = '200'
30
+ end
31
+ super
32
+ end
33
+
34
+ def test_multi_is_scheduler_friendly
35
+ if skip_no_async
36
+ return
37
+ end
38
+
39
+ url = "http://127.0.0.1:#{@port}/test"
40
+
41
+ started = Time.now
42
+ results = []
43
+
44
+
45
+ async_run do
46
+ m = Curl::Multi.new
47
+ ITERS.times.each do
48
+ c = Curl::Easy.new(url)
49
+ c.on_complete { results << c.code }
50
+ m.add(c)
51
+ end
52
+ m.perform
53
+ end
54
+
55
+ duration = Time.now - started
56
+
57
+ 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}"
58
+ assert_equal ITERS, results.size
59
+ assert_equal ITERS.times.map {200}, results
60
+ end
61
+
62
+ def test_easy_perform_is_scheduler_friendly
63
+ if skip_no_async
64
+ return
65
+ end
66
+
67
+ url = "http://127.0.0.1:#{@port}/test"
68
+
69
+ started = Time.now
70
+ results = []
71
+
72
+ async_run do |top|
73
+ tasks = ITERS.times.map do
74
+ top.async do
75
+ #t = Time.now.to_i
76
+ #puts "starting fiber [#{results.size}] -> #{t}"
77
+ c = Curl.get(url)
78
+ #puts "received result: #{results.size} -> #{Time.now.to_f - t.to_f}"
79
+ results << c.code
80
+
81
+ end
82
+ end
83
+ tasks.each(&:wait)
84
+ end
85
+
86
+ duration = Time.now - started
87
+
88
+ 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}"
89
+ assert_equal ITERS, results.size
90
+ assert_equal ITERS.times.map {200}, results
91
+ end
92
+
93
+ def test_multi_perform_yields_block_under_scheduler
94
+ if skip_no_async
95
+ return
96
+ end
97
+
98
+ url = "http://127.0.0.1:#{@port}/test"
99
+ yielded = 0
100
+ results = []
101
+
102
+ async_run do
103
+ m = Curl::Multi.new
104
+ ITERS.times do
105
+ c = Curl::Easy.new(url)
106
+ c.on_complete { results << c.code }
107
+ m.add(c)
108
+ end
109
+ m.perform do
110
+ yielded += 1
111
+ end
112
+ end
113
+
114
+ assert_operator yielded, :>=, 1, 'perform did not yield block while waiting under scheduler'
115
+ assert_equal ITERS, results.size
116
+ assert_equal ITERS.times.map {200}, results
117
+ end
118
+
119
+ def test_multi_single_request_scheduler_path
120
+ if skip_no_async
121
+ return
122
+ end
123
+
124
+ url = "http://127.0.0.1:#{@port}/test"
125
+ result = nil
126
+
127
+ async_run do
128
+ m = Curl::Multi.new
129
+ c = Curl::Easy.new(url)
130
+ c.on_complete { result = c.code }
131
+ m.add(c)
132
+ m.perform
133
+ end
134
+
135
+ assert_equal 200, result
136
+ end
137
+
138
+ def test_multi_reuse_after_scheduler_perform
139
+ unless HAS_ASYNC
140
+ warn 'Skipping fiber scheduler test (Async gem not available)'
141
+ return
142
+ end
143
+
144
+ url = "http://127.0.0.1:#{@port}/test"
145
+ results = []
146
+
147
+ async_run do
148
+ m = Curl::Multi.new
149
+ # First round
150
+ c1 = Curl::Easy.new(url)
151
+ c1.on_complete { results << c1.code }
152
+ m.add(c1)
153
+ m.perform
154
+
155
+ # Second round on same multi
156
+ c2 = Curl::Easy.new(url)
157
+ c2.on_complete { results << c2.code }
158
+ m.add(c2)
159
+ m.perform
160
+ end
161
+
162
+ assert_equal [200, 200], results
163
+ end
164
+
165
+ private
166
+ def skip_no_async
167
+ if WINDOWS
168
+ warn 'Skipping fiber scheduler tests on Windows'
169
+ return true
170
+ end
171
+ unless HAS_ASYNC
172
+ warn 'Skipping fiber scheduler test (Async gem not available)'
173
+ return true
174
+ end
175
+ false
176
+ end
177
+
178
+ def async_run(&block)
179
+ # Prefer newer Async.run to avoid deprecated scheduler.async path.
180
+ if defined?(Async) && Async.respond_to?(:run)
181
+ Async.run(&block)
182
+ else
183
+ Async(&block)
184
+ end
185
+ end
186
+ end
187
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1')
188
+ warn 'Skipping fiber scheduler tests on Ruby < 3.1'
189
+ return
190
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ross Bamford
8
8
  - Todd A. Fisher
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-24 00:00:00.000000000 Z
11
+ date: 2025-09-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
14
14
  for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
@@ -52,13 +52,14 @@ files:
52
52
  - tests/bug_follow_redirect_288.rb
53
53
  - tests/bug_instance_post_differs_from_class_post.rb
54
54
  - tests/bug_issue102.rb
55
- - tests/bug_issue277.rb
55
+ - tests/bug_issue_noproxy.rb
56
+ - tests/bug_issue_post_redirect.rb
57
+ - tests/bug_issue_spnego.rb
56
58
  - tests/bug_multi_segfault.rb
57
59
  - tests/bug_postfields_crash.rb
58
60
  - tests/bug_postfields_crash2.rb
59
61
  - tests/bug_raise_on_callback.rb
60
62
  - tests/bug_require_last_or_segfault.rb
61
- - tests/bug_resolve.rb
62
63
  - tests/bugtests.rb
63
64
  - tests/helper.rb
64
65
  - tests/mem_check.rb
@@ -74,6 +75,7 @@ files:
74
75
  - tests/tc_curl_multi.rb
75
76
  - tests/tc_curl_postfield.rb
76
77
  - tests/tc_curl_protocols.rb
78
+ - tests/tc_fiber_scheduler.rb
77
79
  - tests/timeout.rb
78
80
  - tests/timeout_server.rb
79
81
  - tests/unittests.rb
@@ -111,13 +113,14 @@ test_files:
111
113
  - tests/bug_follow_redirect_288.rb
112
114
  - tests/bug_instance_post_differs_from_class_post.rb
113
115
  - tests/bug_issue102.rb
114
- - tests/bug_issue277.rb
116
+ - tests/bug_issue_noproxy.rb
117
+ - tests/bug_issue_post_redirect.rb
118
+ - tests/bug_issue_spnego.rb
115
119
  - tests/bug_multi_segfault.rb
116
120
  - tests/bug_postfields_crash.rb
117
121
  - tests/bug_postfields_crash2.rb
118
122
  - tests/bug_raise_on_callback.rb
119
123
  - tests/bug_require_last_or_segfault.rb
120
- - tests/bug_resolve.rb
121
124
  - tests/bugtests.rb
122
125
  - tests/helper.rb
123
126
  - tests/mem_check.rb
@@ -133,6 +136,7 @@ test_files:
133
136
  - tests/tc_curl_multi.rb
134
137
  - tests/tc_curl_postfield.rb
135
138
  - tests/tc_curl_protocols.rb
139
+ - tests/tc_fiber_scheduler.rb
136
140
  - tests/timeout.rb
137
141
  - tests/timeout_server.rb
138
142
  - tests/unittests.rb
@@ -1,32 +0,0 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
-
3
-
4
- require 'curb'
5
-
6
- class BugIssue102 < Test::Unit::TestCase
7
-
8
- def test_gc_closewait
9
- 100.times do
10
- responses = {}
11
- requests = ["http://www.google.co.uk/", "http://www.ruby-lang.org/"]
12
- m = Curl::Multi.new
13
- # add a few easy handles
14
- requests.each do |url|
15
- responses[url] = ""
16
- c = Curl::Easy.new(url) do|curl|
17
- curl.follow_location = true
18
- curl.on_body{|data| responses[url] << data; data.size }
19
- curl.on_success {|easy| #puts "success, add more easy handles"
20
- }
21
- end
22
- m.add(c)
23
- end
24
-
25
- m.perform do
26
- #puts "idling... can do some work here"
27
- end
28
- GC.start
29
- end
30
- end
31
-
32
- end
data/tests/bug_resolve.rb DELETED
@@ -1,15 +0,0 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
-
3
- class BugResolve < Test::Unit::TestCase
4
- include BugTestServerSetupTeardown
5
-
6
- def test_abuse_resolve
7
- c = Curl::Easy.new("http://localhost:#{@port}/test")
8
- while true
9
- c.get
10
- assert_equal 200, c.response_code
11
- assert_match(/hi/, c.body)
12
- end
13
- end
14
-
15
- end