curb 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +22 -0
- data/ext/curb.c +282 -231
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +608 -215
- data/ext/curb_easy.h +5 -0
- data/ext/curb_errors.c +5 -5
- data/ext/curb_errors.h +1 -1
- data/ext/curb_macros.h +14 -14
- data/ext/curb_multi.c +611 -141
- data/ext/curb_multi.h +3 -1
- data/ext/curb_postfield.c +47 -21
- data/ext/curb_postfield.h +1 -0
- data/ext/curb_upload.c +32 -9
- data/ext/curb_upload.h +2 -0
- data/ext/extconf.rb +40 -0
- data/lib/curl/easy.rb +154 -13
- data/lib/curl/multi.rb +69 -9
- data/lib/curl.rb +193 -0
- data/tests/helper.rb +222 -36
- data/tests/leak_trace.rb +237 -0
- data/tests/tc_curl_download.rb +6 -2
- data/tests/tc_curl_easy.rb +450 -1
- data/tests/tc_curl_multi.rb +573 -59
- data/tests/tc_curl_native_coverage.rb +130 -0
- data/tests/tc_curl_postfield.rb +161 -0
- data/tests/tc_fiber_scheduler.rb +342 -7
- data/tests/tc_gc_compact.rb +178 -16
- data/tests/tc_test_server_methods.rb +110 -0
- metadata +10 -14
- data/tests/test_basic.rb +0 -29
- data/tests/test_fiber_debug.rb +0 -69
- data/tests/test_fiber_simple.rb +0 -65
- data/tests/test_real_url.rb +0 -65
- data/tests/test_simple_fiber.rb +0 -34
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
|
2
|
+
|
|
3
|
+
class TestCurbCurlErrorMappings < Test::Unit::TestCase
|
|
4
|
+
def assert_easy_error_mapping(code, expected_class)
|
|
5
|
+
actual_class, message = Curl::Easy.error(code)
|
|
6
|
+
|
|
7
|
+
assert_equal expected_class, actual_class
|
|
8
|
+
assert_kind_of String, message
|
|
9
|
+
assert_not_empty message
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_easy_error_known_mappings
|
|
13
|
+
assert_easy_error_mapping(0, Curl::Err::CurlOK)
|
|
14
|
+
assert_easy_error_mapping(1, Curl::Err::UnsupportedProtocolError)
|
|
15
|
+
assert_easy_error_mapping(2, Curl::Err::FailedInitError)
|
|
16
|
+
assert_easy_error_mapping(3, Curl::Err::MalformedURLError)
|
|
17
|
+
assert_easy_error_mapping(5, Curl::Err::ProxyResolutionError)
|
|
18
|
+
assert_easy_error_mapping(6, Curl::Err::HostResolutionError)
|
|
19
|
+
assert_easy_error_mapping(7, Curl::Err::ConnectionFailedError)
|
|
20
|
+
assert_easy_error_mapping(23, Curl::Err::WriteError)
|
|
21
|
+
assert_easy_error_mapping(26, Curl::Err::ReadError)
|
|
22
|
+
assert_easy_error_mapping(28, Curl::Err::TimeoutError)
|
|
23
|
+
assert_easy_error_mapping(42, Curl::Err::AbortedByCallbackError)
|
|
24
|
+
assert_easy_error_mapping(47, Curl::Err::TooManyRedirectsError)
|
|
25
|
+
assert_easy_error_mapping(52, Curl::Err::GotNothingError)
|
|
26
|
+
assert_easy_error_mapping(55, Curl::Err::SendError)
|
|
27
|
+
assert_easy_error_mapping(56, Curl::Err::RecvError)
|
|
28
|
+
assert_easy_error_mapping(57, Curl::Err::ShareInUseError)
|
|
29
|
+
assert_easy_error_mapping(58, Curl::Err::SSLCertificateError)
|
|
30
|
+
assert_easy_error_mapping(59, Curl::Err::SSLCypherError)
|
|
31
|
+
assert_easy_error_mapping(61, Curl::Err::BadContentEncodingError)
|
|
32
|
+
assert_easy_error_mapping(63, Curl::Err::FileSizeExceededError)
|
|
33
|
+
assert_easy_error_mapping(64, Curl::Err::FTPSSLFailed)
|
|
34
|
+
|
|
35
|
+
ssl_peer_or_ca_error =
|
|
36
|
+
if Gem::Version.new(Curl::CURL_VERSION) >= Gem::Version.new('7.62.0')
|
|
37
|
+
Curl::Err::SSLPeerCertificateError
|
|
38
|
+
else
|
|
39
|
+
Curl::Err::SSLCACertificateError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
assert_easy_error_mapping(60, ssl_peer_or_ca_error)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_easy_error_returns_error_info_for_known_numeric_range
|
|
46
|
+
0.upto(92) do |code|
|
|
47
|
+
error_class, message = Curl::Easy.error(code)
|
|
48
|
+
|
|
49
|
+
assert_kind_of Class, error_class
|
|
50
|
+
assert error_class <= Curl::Err::CurlError
|
|
51
|
+
assert_kind_of String, message
|
|
52
|
+
assert_not_empty message
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_easy_error_uses_generic_mapping_for_unknown_codes
|
|
57
|
+
error_class, message = Curl::Easy.error(9_999)
|
|
58
|
+
|
|
59
|
+
assert_equal Curl::Err::CurlError, error_class
|
|
60
|
+
assert_equal 'Unknown error result from libcurl', message
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class TestCurbCurlNativeCoverage < Test::Unit::TestCase
|
|
65
|
+
include TestServerMethods
|
|
66
|
+
|
|
67
|
+
def setup
|
|
68
|
+
server_setup
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_clone_preserves_native_lists_after_original_handle_closes
|
|
72
|
+
easy = Curl::Easy.new("http://curb.invalid:#{TestServlet.port}#{TestServlet.path}")
|
|
73
|
+
easy.headers['X-Test'] = '1'
|
|
74
|
+
easy.proxy_headers['X-Proxy'] = '2'
|
|
75
|
+
easy.ftp_commands = ['PWD']
|
|
76
|
+
easy.resolve = ["curb.invalid:#{TestServlet.port}:127.0.0.1"]
|
|
77
|
+
|
|
78
|
+
clone = easy.clone
|
|
79
|
+
easy.close
|
|
80
|
+
|
|
81
|
+
clone.http_get
|
|
82
|
+
|
|
83
|
+
assert_equal 'GET', clone.body_str
|
|
84
|
+
assert_equal '1', clone.headers['X-Test']
|
|
85
|
+
assert_equal '2', clone.proxy_headers['X-Proxy']
|
|
86
|
+
assert_equal ['PWD'], clone.ftp_commands
|
|
87
|
+
assert_equal ["curb.invalid:#{TestServlet.port}:127.0.0.1"], clone.resolve
|
|
88
|
+
ensure
|
|
89
|
+
clone.close if defined?(clone) && clone
|
|
90
|
+
easy.close if defined?(easy) && easy
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_native_accessors_round_trip
|
|
94
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
95
|
+
|
|
96
|
+
assert_equal '127.0.0.1', easy.interface = '127.0.0.1'
|
|
97
|
+
assert_equal 'user:pass', easy.userpwd = 'user:pass'
|
|
98
|
+
assert_equal 'proxy:pass', easy.proxypwd = 'proxy:pass'
|
|
99
|
+
assert_equal 'tests/cert.pem', easy.cert_key = 'tests/cert.pem'
|
|
100
|
+
assert_equal 'gzip', easy.encoding = 'gzip'
|
|
101
|
+
|
|
102
|
+
if easy.respond_to?(:max_send_speed_large=)
|
|
103
|
+
assert_equal 123, easy.max_send_speed_large = 123
|
|
104
|
+
assert_equal 123, easy.max_send_speed_large
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if easy.respond_to?(:max_recv_speed_large=)
|
|
108
|
+
assert_equal 456, easy.max_recv_speed_large = 456
|
|
109
|
+
assert_equal 456, easy.max_recv_speed_large
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
assert_equal '127.0.0.1', easy.interface
|
|
113
|
+
assert_equal 'user:pass', easy.userpwd
|
|
114
|
+
assert_equal 'proxy:pass', easy.proxypwd
|
|
115
|
+
assert_equal 'tests/cert.pem', easy.cert_key
|
|
116
|
+
assert_equal 'gzip', easy.encoding
|
|
117
|
+
ensure
|
|
118
|
+
easy.close if defined?(easy) && easy
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_upload_round_trips_stream_and_offset
|
|
122
|
+
upload = Curl::Upload.new
|
|
123
|
+
stream = StringIO.new('payload')
|
|
124
|
+
|
|
125
|
+
assert_same stream, upload.stream = stream
|
|
126
|
+
assert_same stream, upload.stream
|
|
127
|
+
assert_equal 7, upload.offset = 7
|
|
128
|
+
assert_equal 7, upload.offset
|
|
129
|
+
end
|
|
130
|
+
end
|
data/tests/tc_curl_postfield.rb
CHANGED
|
@@ -142,3 +142,164 @@ class TestCurbCurlPostfield < Test::Unit::TestCase
|
|
|
142
142
|
#assert_raise(Curl::Err::InvalidPostFieldError) { pf.to_s }
|
|
143
143
|
end
|
|
144
144
|
end
|
|
145
|
+
|
|
146
|
+
class TestCurbCurlPostfieldNativeCoverage < Test::Unit::TestCase
|
|
147
|
+
def test_attribute_writers_round_trip
|
|
148
|
+
pf = Curl::PostField.content('foo', 'bar')
|
|
149
|
+
|
|
150
|
+
assert_equal 'renamed', pf.name = 'renamed'
|
|
151
|
+
assert_equal 'payload', pf.content = 'payload'
|
|
152
|
+
assert_equal 'text/plain', pf.content_type = 'text/plain'
|
|
153
|
+
assert_equal 'local.txt', pf.local_file = 'local.txt'
|
|
154
|
+
assert_equal 'remote.txt', pf.remote_file = 'remote.txt'
|
|
155
|
+
|
|
156
|
+
assert_equal 'renamed', pf.name
|
|
157
|
+
assert_equal 'payload', pf.content
|
|
158
|
+
assert_equal 'text/plain', pf.content_type
|
|
159
|
+
assert_equal 'local.txt', pf.local_file
|
|
160
|
+
assert_equal 'remote.txt', pf.remote_file
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_to_s_accepts_non_string_name_via_to_s
|
|
164
|
+
name_like = Object.new
|
|
165
|
+
def name_like.to_s
|
|
166
|
+
'fancy name'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
pf = Curl::PostField.content(name_like, 'value')
|
|
170
|
+
assert_equal 'fancy%20name=value', pf.to_s
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def test_to_s_uses_remote_file_when_local_file_is_missing
|
|
174
|
+
pf = Curl::PostField.file('upload', 'local.txt', 'remote.txt')
|
|
175
|
+
pf.local_file = nil
|
|
176
|
+
|
|
177
|
+
assert_equal 'upload=remote.txt', pf.to_s
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def test_to_s_rejects_name_without_to_s
|
|
181
|
+
name_like = Class.new do
|
|
182
|
+
undef to_s
|
|
183
|
+
end.new
|
|
184
|
+
|
|
185
|
+
pf = Curl::PostField.content(name_like, 'value')
|
|
186
|
+
|
|
187
|
+
error = assert_raise(Curl::Err::InvalidPostFieldError) { pf.to_s }
|
|
188
|
+
assert_match(/Cannot convert unnamed field to string/, error.message)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_to_s_rejects_content_without_to_s
|
|
192
|
+
content_like = Class.new do
|
|
193
|
+
undef to_s
|
|
194
|
+
end.new
|
|
195
|
+
|
|
196
|
+
pf = Curl::PostField.content('name', content_like)
|
|
197
|
+
|
|
198
|
+
error = assert_raise(RuntimeError) { pf.to_s }
|
|
199
|
+
assert_match(/does not respond_to to_s/, error.message)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def test_multipart_rejects_unnamed_field
|
|
203
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
204
|
+
curl.multipart_form_post = true
|
|
205
|
+
pf = Curl::PostField.content('name', 'value')
|
|
206
|
+
pf.name = nil
|
|
207
|
+
|
|
208
|
+
error = assert_raise(Curl::Err::InvalidPostFieldError) { curl.http_post(pf) }
|
|
209
|
+
assert_match(/Cannot post unnamed field/, error.message)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def test_multipart_rejects_content_field_without_data
|
|
213
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
214
|
+
curl.multipart_form_post = true
|
|
215
|
+
pf = Curl::PostField.content('name', 'value')
|
|
216
|
+
pf.content = nil
|
|
217
|
+
|
|
218
|
+
error = assert_raise(Curl::Err::InvalidPostFieldError) { curl.http_post(pf) }
|
|
219
|
+
assert_match(/Cannot post content field with no data/, error.message)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_multipart_rejects_file_field_without_filename
|
|
223
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
224
|
+
curl.multipart_form_post = true
|
|
225
|
+
pf = Curl::PostField.file('upload', 'remote.txt') { 'payload' }
|
|
226
|
+
pf.local_file = 'dummy.txt'
|
|
227
|
+
pf.remote_file = nil
|
|
228
|
+
|
|
229
|
+
error = assert_raise(Curl::Err::InvalidPostFieldError) { curl.http_post(pf) }
|
|
230
|
+
assert_match(/Cannot post file upload field with no filename/, error.message)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
class TestCurbCurlPostfieldMultipartCoverage < Test::Unit::TestCase
|
|
235
|
+
include TestServerMethods
|
|
236
|
+
|
|
237
|
+
def setup
|
|
238
|
+
server_setup
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_multipart_content_variants_include_dynamic_and_typed_fields
|
|
242
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
243
|
+
curl.multipart_form_post = true
|
|
244
|
+
|
|
245
|
+
proc_without_type = Curl::PostField.content('proc_without_type') { 'alpha' }
|
|
246
|
+
proc_with_type = Curl::PostField.content('proc_with_type', 'text/plain') { 'beta' }
|
|
247
|
+
direct_with_type = Curl::PostField.content('direct_with_type', 'gamma', 'text/plain')
|
|
248
|
+
|
|
249
|
+
curl.http_post([proc_without_type, proc_with_type, direct_with_type])
|
|
250
|
+
body = curl.body_str
|
|
251
|
+
|
|
252
|
+
assert_match(/name="proc_without_type"/, body)
|
|
253
|
+
assert_match(/alpha/, body)
|
|
254
|
+
assert_match(/name="proc_with_type"/, body)
|
|
255
|
+
assert_match(/beta/, body)
|
|
256
|
+
assert_match(/name="direct_with_type"/, body)
|
|
257
|
+
assert_match(/gamma/, body)
|
|
258
|
+
assert_match(/Content-Type: text\/plain/, body)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def test_multipart_file_variants_include_buffered_and_local_uploads
|
|
262
|
+
readme = File.expand_path(File.join(File.dirname(__FILE__), '..', 'README.md'))
|
|
263
|
+
|
|
264
|
+
proc_without_type = Curl::PostField.file('proc_without_type', 'proc_without_type.txt') { 'alpha' }
|
|
265
|
+
proc_with_type = Curl::PostField.file('proc_with_type', 'proc_with_type.txt') { 'beta' }
|
|
266
|
+
proc_with_type.content_type = 'text/plain'
|
|
267
|
+
|
|
268
|
+
direct_without_type = Curl::PostField.file('direct_without_type', 'ignored.txt', 'direct_without_type.txt')
|
|
269
|
+
direct_without_type.content = 'gamma'
|
|
270
|
+
direct_without_type.local_file = nil
|
|
271
|
+
|
|
272
|
+
direct_with_type = Curl::PostField.file('direct_with_type', 'ignored.txt', 'direct_with_type.txt')
|
|
273
|
+
direct_with_type.content = 'delta'
|
|
274
|
+
direct_with_type.local_file = nil
|
|
275
|
+
direct_with_type.content_type = 'text/plain'
|
|
276
|
+
|
|
277
|
+
local_with_type = Curl::PostField.file('local_with_type', readme)
|
|
278
|
+
local_with_type.content_type = 'text/plain'
|
|
279
|
+
|
|
280
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
281
|
+
curl.multipart_form_post = true
|
|
282
|
+
curl.http_post([proc_without_type, proc_with_type, direct_without_type, direct_with_type, local_with_type])
|
|
283
|
+
body = curl.body_str
|
|
284
|
+
|
|
285
|
+
assert_match(/name="proc_without_type"/, body)
|
|
286
|
+
assert_match(/filename="proc_without_type.txt"/, body)
|
|
287
|
+
assert_match(/alpha/, body)
|
|
288
|
+
|
|
289
|
+
assert_match(/name="proc_with_type"/, body)
|
|
290
|
+
assert_match(/filename="proc_with_type.txt"/, body)
|
|
291
|
+
assert_match(/beta/, body)
|
|
292
|
+
|
|
293
|
+
assert_match(/name="direct_without_type"/, body)
|
|
294
|
+
assert_match(/filename="direct_without_type.txt"/, body)
|
|
295
|
+
assert_match(/gamma/, body)
|
|
296
|
+
|
|
297
|
+
assert_match(/name="direct_with_type"/, body)
|
|
298
|
+
assert_match(/filename="direct_with_type.txt"/, body)
|
|
299
|
+
assert_match(/delta/, body)
|
|
300
|
+
|
|
301
|
+
assert_match(/name="local_with_type"/, body)
|
|
302
|
+
assert_match(/Curb - Libcurl bindings for Ruby/, body)
|
|
303
|
+
assert_match(/Content-Type: text\/plain/, body)
|
|
304
|
+
end
|
|
305
|
+
end
|
data/tests/tc_fiber_scheduler.rb
CHANGED
|
@@ -12,6 +12,51 @@ end
|
|
|
12
12
|
# by running multiple requests concurrently in a single thread using the Async gem.
|
|
13
13
|
class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
14
14
|
include BugTestServerSetupTeardown
|
|
15
|
+
include TestServerMethods
|
|
16
|
+
|
|
17
|
+
class RecordingScheduler
|
|
18
|
+
attr_reader :io_wait_events
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@io_wait_events = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fiber(&block)
|
|
25
|
+
Fiber.new(blocking: false, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def io_wait(io, events, timeout = nil)
|
|
29
|
+
@io_wait_events << events
|
|
30
|
+
raise TypeError, "expected Integer events, got #{events.class}" unless events.is_a?(Integer)
|
|
31
|
+
|
|
32
|
+
readers = (events & IO::READABLE) != 0 ? [io] : nil
|
|
33
|
+
writers = (events & IO::WRITABLE) != 0 ? [io] : nil
|
|
34
|
+
readable, writable = IO.select(readers, writers, nil, timeout)
|
|
35
|
+
|
|
36
|
+
ready = 0
|
|
37
|
+
ready |= IO::READABLE if readable && !readable.empty?
|
|
38
|
+
ready |= IO::WRITABLE if writable && !writable.empty?
|
|
39
|
+
ready.zero? ? false : ready
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def kernel_sleep(duration = nil)
|
|
43
|
+
sleep(duration || 0)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def block(_blocker, timeout = nil)
|
|
47
|
+
sleep(timeout || 0)
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unblock(*)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def close
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fiber_interrupt(*)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
15
60
|
|
|
16
61
|
ITERS = 4
|
|
17
62
|
MIN_S = 0.25
|
|
@@ -50,6 +95,8 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
50
95
|
m.add(c)
|
|
51
96
|
end
|
|
52
97
|
m.perform
|
|
98
|
+
ensure
|
|
99
|
+
m.close if m
|
|
53
100
|
end
|
|
54
101
|
|
|
55
102
|
duration = Time.now - started
|
|
@@ -109,6 +156,8 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
109
156
|
m.perform do
|
|
110
157
|
yielded += 1
|
|
111
158
|
end
|
|
159
|
+
ensure
|
|
160
|
+
m.close if m
|
|
112
161
|
end
|
|
113
162
|
|
|
114
163
|
assert_operator yielded, :>=, 1, 'perform did not yield block while waiting under scheduler'
|
|
@@ -130,20 +179,49 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
130
179
|
c.on_complete { result = c.code }
|
|
131
180
|
m.add(c)
|
|
132
181
|
m.perform
|
|
182
|
+
ensure
|
|
183
|
+
m.close if m
|
|
133
184
|
end
|
|
134
185
|
|
|
135
186
|
assert_equal 200, result
|
|
136
187
|
end
|
|
137
188
|
|
|
189
|
+
def test_multi_single_request_socket_perform_passes_integer_events_to_io_wait
|
|
190
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
191
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
192
|
+
|
|
193
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
194
|
+
scheduler = RecordingScheduler.new
|
|
195
|
+
result = nil
|
|
196
|
+
|
|
197
|
+
with_scheduler(scheduler) do
|
|
198
|
+
m = Curl::Multi.new
|
|
199
|
+
c = Curl::Easy.new(url)
|
|
200
|
+
c.on_complete { result = c.code }
|
|
201
|
+
m.add(c)
|
|
202
|
+
unless m.respond_to?(:_socket_perform, true)
|
|
203
|
+
omit('socket-action perform path is not available in this build')
|
|
204
|
+
end
|
|
205
|
+
m.send(:_socket_perform)
|
|
206
|
+
ensure
|
|
207
|
+
m.close if m
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
assert_equal 200, result
|
|
211
|
+
assert_operator scheduler.io_wait_events.length, :>=, 1
|
|
212
|
+
assert scheduler.io_wait_events.all? { |events| events.is_a?(Integer) }
|
|
213
|
+
assert scheduler.io_wait_events.any? { |events| (events & (IO::READABLE | IO::WRITABLE)) != 0 }
|
|
214
|
+
end
|
|
215
|
+
|
|
138
216
|
def test_multi_reuse_after_scheduler_perform
|
|
139
217
|
unless HAS_ASYNC
|
|
140
218
|
warn 'Skipping fiber scheduler test (Async gem not available)'
|
|
141
219
|
return
|
|
142
220
|
end
|
|
143
|
-
|
|
221
|
+
|
|
144
222
|
url = "http://127.0.0.1:#{@port}/test"
|
|
145
223
|
results = []
|
|
146
|
-
|
|
224
|
+
|
|
147
225
|
async_run do
|
|
148
226
|
m = Curl::Multi.new
|
|
149
227
|
# First round
|
|
@@ -151,15 +229,224 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
151
229
|
c1.on_complete { results << c1.code }
|
|
152
230
|
m.add(c1)
|
|
153
231
|
m.perform
|
|
154
|
-
|
|
232
|
+
|
|
155
233
|
# Second round on same multi
|
|
156
234
|
c2 = Curl::Easy.new(url)
|
|
157
235
|
c2.on_complete { results << c2.code }
|
|
158
236
|
m.add(c2)
|
|
159
237
|
m.perform
|
|
238
|
+
ensure
|
|
239
|
+
m.close if m
|
|
160
240
|
end
|
|
241
|
+
|
|
242
|
+
assert_equal [200, 200], results
|
|
243
|
+
end
|
|
161
244
|
|
|
245
|
+
def test_easy_perform_reuses_scheduler_multi_when_autoclose_is_enabled
|
|
246
|
+
if skip_no_async
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
251
|
+
results = []
|
|
252
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
253
|
+
Curl::Multi.autoclose = true
|
|
254
|
+
|
|
255
|
+
async_run do
|
|
256
|
+
2.times do
|
|
257
|
+
easy = Curl::Easy.new(url)
|
|
258
|
+
easy.perform
|
|
259
|
+
results << easy.code
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
162
263
|
assert_equal [200, 200], results
|
|
264
|
+
ensure
|
|
265
|
+
Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
|
|
266
|
+
cleanup_scheduler_state
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def test_easy_perform_reraises_on_body_exception_under_scheduler
|
|
270
|
+
if skip_no_async
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
275
|
+
|
|
276
|
+
async_run do |top|
|
|
277
|
+
task = top.async do
|
|
278
|
+
c = Curl::Easy.new(url)
|
|
279
|
+
c.on_body { raise "body blew up" }
|
|
280
|
+
c.on_complete { sleep 0 }
|
|
281
|
+
c.perform
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
error = assert_raise(RuntimeError) do
|
|
285
|
+
task.wait
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
assert_equal "body blew up", error.message
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_easy_perform_reraises_on_header_exception_under_scheduler
|
|
293
|
+
if skip_no_async
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
298
|
+
|
|
299
|
+
async_run do |top|
|
|
300
|
+
task = top.async do
|
|
301
|
+
c = Curl::Easy.new(url)
|
|
302
|
+
c.on_header { raise "header blew up" }
|
|
303
|
+
c.on_complete { sleep 0 }
|
|
304
|
+
c.perform
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
error = assert_raise(RuntimeError) do
|
|
308
|
+
task.wait
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
assert_equal "header blew up", error.message
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def test_easy_perform_does_not_reraise_sibling_on_complete_exception_under_scheduler
|
|
316
|
+
if skip_no_async
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
with_ephemeral_http_server do |port, hits|
|
|
321
|
+
results = {}
|
|
322
|
+
|
|
323
|
+
async_run do |top|
|
|
324
|
+
successful = top.async do
|
|
325
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
|
|
326
|
+
easy.perform
|
|
327
|
+
results[:successful] = easy.response_code
|
|
328
|
+
rescue => e
|
|
329
|
+
results[:successful] = e
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
failing = top.async do
|
|
333
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
|
|
334
|
+
easy.on_complete { raise "boom" }
|
|
335
|
+
easy.perform
|
|
336
|
+
results[:failing] = :returned
|
|
337
|
+
rescue => e
|
|
338
|
+
results[:failing] = e
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
successful.wait
|
|
342
|
+
failing.wait
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
assert_equal 200, results[:successful], "successful scheduler peer should return normally"
|
|
346
|
+
assert_kind_of Curl::Err::AbortedByCallbackError, results[:failing]
|
|
347
|
+
assert_equal "boom", results[:failing].message
|
|
348
|
+
assert_equal 1, hits[:fast]
|
|
349
|
+
assert_equal 1, hits[:slow]
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def test_easy_perform_does_not_reraise_fast_on_complete_exception_into_slow_successful_peer_under_scheduler
|
|
354
|
+
if skip_no_async
|
|
355
|
+
return
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
with_ephemeral_http_server do |port, hits|
|
|
359
|
+
results = {}
|
|
360
|
+
|
|
361
|
+
async_run do |top|
|
|
362
|
+
failing = top.async do
|
|
363
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
|
|
364
|
+
easy.on_complete { raise "boom" }
|
|
365
|
+
easy.perform
|
|
366
|
+
results[:failing] = :returned
|
|
367
|
+
rescue => e
|
|
368
|
+
results[:failing] = e
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
successful = top.async do
|
|
372
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
|
|
373
|
+
easy.perform
|
|
374
|
+
results[:successful] = easy.response_code
|
|
375
|
+
rescue => e
|
|
376
|
+
results[:successful] = e
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
failing.wait
|
|
380
|
+
successful.wait
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
assert_equal 200, results[:successful], "slow successful scheduler peer should not inherit a fast sibling callback error"
|
|
384
|
+
assert_kind_of Curl::Err::AbortedByCallbackError, results[:failing]
|
|
385
|
+
assert_equal "boom", results[:failing].message
|
|
386
|
+
assert_equal 1, hits[:fast]
|
|
387
|
+
assert_equal 1, hits[:slow]
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def test_easy_perform_keeps_scheduler_timers_running_while_draining_deferred_on_complete_exception
|
|
392
|
+
if skip_no_async
|
|
393
|
+
return
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
with_ephemeral_http_server do |port, hits|
|
|
397
|
+
marks = {}
|
|
398
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
399
|
+
|
|
400
|
+
async_run do |top|
|
|
401
|
+
timer = top.async do
|
|
402
|
+
sleep 0.05
|
|
403
|
+
marks[:timer] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
failing = top.async do
|
|
407
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
|
|
408
|
+
easy.on_complete { raise "boom" }
|
|
409
|
+
easy.perform
|
|
410
|
+
marks[:failing] = :returned
|
|
411
|
+
rescue => e
|
|
412
|
+
marks[:failing] = e
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
successful = top.async do
|
|
416
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
|
|
417
|
+
easy.perform
|
|
418
|
+
marks[:slow_done] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
timer.wait
|
|
422
|
+
failing.wait
|
|
423
|
+
successful.wait
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
assert_kind_of Curl::Err::AbortedByCallbackError, marks[:failing]
|
|
427
|
+
assert_operator marks[:timer], :<, marks[:slow_done] - 0.02,
|
|
428
|
+
"scheduler timer should fire before the slow sibling finishes draining"
|
|
429
|
+
assert_equal 1, hits[:fast]
|
|
430
|
+
assert_equal 1, hits[:slow]
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
|
|
435
|
+
state = Curl.scheduler_state
|
|
436
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
|
|
437
|
+
waiter = {completed: false, done: false, error: nil}
|
|
438
|
+
|
|
439
|
+
state[:waiters][easy.object_id] = waiter
|
|
440
|
+
state[:pending] << easy
|
|
441
|
+
state[:multi].instance_variable_set(:@__curb_deferred_exception, RuntimeError.new("boom"))
|
|
442
|
+
|
|
443
|
+
Curl.drain_scheduler_pending(state)
|
|
444
|
+
|
|
445
|
+
assert(state[:pending].include?(easy) || waiter[:error],
|
|
446
|
+
"scheduler enqueue rejected during deferred abort should remain pending or fail its waiter instead of disappearing")
|
|
447
|
+
assert_equal 0, state[:multi].requests.length
|
|
448
|
+
ensure
|
|
449
|
+
cleanup_scheduler_state
|
|
163
450
|
end
|
|
164
451
|
|
|
165
452
|
private
|
|
@@ -168,6 +455,10 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
168
455
|
warn 'Skipping fiber scheduler tests on Windows'
|
|
169
456
|
return true
|
|
170
457
|
end
|
|
458
|
+
unless fiber_scheduler_supported?
|
|
459
|
+
warn 'Skipping fiber scheduler tests (Fiber scheduler API unavailable)'
|
|
460
|
+
return true
|
|
461
|
+
end
|
|
171
462
|
unless HAS_ASYNC
|
|
172
463
|
warn 'Skipping fiber scheduler test (Async gem not available)'
|
|
173
464
|
return true
|
|
@@ -183,8 +474,52 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
183
474
|
Async(&block)
|
|
184
475
|
end
|
|
185
476
|
end
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
477
|
+
|
|
478
|
+
def fiber_scheduler_supported?
|
|
479
|
+
Fiber.respond_to?(:set_scheduler) && Fiber.respond_to?(:schedule)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def with_scheduler(scheduler)
|
|
483
|
+
previous_scheduler = Fiber.scheduler if Fiber.respond_to?(:scheduler)
|
|
484
|
+
Fiber.set_scheduler(scheduler)
|
|
485
|
+
fiber = Fiber.schedule { yield }
|
|
486
|
+
fiber.resume while fiber.alive?
|
|
487
|
+
ensure
|
|
488
|
+
Fiber.set_scheduler(previous_scheduler) if Fiber.respond_to?(:set_scheduler)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def cleanup_scheduler_state
|
|
492
|
+
state = Thread.current.thread_variable_get(:curb_scheduler_state)
|
|
493
|
+
state[:multi].close if state && state[:multi]
|
|
494
|
+
Thread.current.thread_variable_set(:curb_scheduler_state, nil)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def with_ephemeral_http_server
|
|
498
|
+
port_socket = TCPServer.new('127.0.0.1', 0)
|
|
499
|
+
port = port_socket.addr[1]
|
|
500
|
+
port_socket.close
|
|
501
|
+
|
|
502
|
+
hits = Hash.new(0)
|
|
503
|
+
server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
|
504
|
+
server.mount_proc('/fast') do |_req, res|
|
|
505
|
+
hits[:fast] += 1
|
|
506
|
+
res['Content-Type'] = 'text/plain'
|
|
507
|
+
res.body = 'fast'
|
|
508
|
+
end
|
|
509
|
+
server.mount_proc('/slow') do |_req, res|
|
|
510
|
+
hits[:slow] += 1
|
|
511
|
+
res['Content-Type'] = 'text/plain'
|
|
512
|
+
sleep 0.2
|
|
513
|
+
res.body = 'slow'
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
server_thread = Thread.new(server) { |srv| srv.start }
|
|
517
|
+
wait_for_server_ready(port, thread: server_thread)
|
|
518
|
+
|
|
519
|
+
yield port, hits
|
|
520
|
+
ensure
|
|
521
|
+
server.shutdown if defined?(server) && server
|
|
522
|
+
server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
|
|
523
|
+
server_thread.kill if defined?(server_thread) && server_thread&.alive?
|
|
524
|
+
end
|
|
190
525
|
end
|