wreq 1.2.5-aarch64-linux → 1.2.7-aarch64-linux
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/.cargo/config.toml +4 -0
- data/Rakefile +6 -0
- data/crates/wreq-util/README.md +1 -1
- data/docs/windows-gnu-tokio-crash.md +49 -3
- data/extconf.rb +4 -0
- data/lib/wreq.rb +70 -45
- data/lib/wreq_ruby/3.3/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/3.4/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/4.0/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/body.rb +29 -11
- data/lib/wreq_ruby/client.rb +88 -55
- data/lib/wreq_ruby/cookie.rb +79 -19
- data/lib/wreq_ruby/emulate.rb +74 -6
- data/lib/wreq_ruby/error.rb +5 -7
- data/lib/wreq_ruby/http.rb +74 -0
- data/lib/wreq_ruby/response.rb +3 -0
- data/script/rust_env.rb +85 -0
- data/test/body_sender_test.rb +246 -0
- data/test/cookie_test.rb +211 -10
- data/test/json_precision_test.rb +209 -0
- data/test/option_validation_test.rb +311 -0
- data/test/value_semantics_test.rb +238 -0
- data/wreq.gemspec +1 -1
- metadata +9 -4
- data/script/build_windows_gnu.ps1 +0 -264
data/lib/wreq_ruby/emulate.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Profile and platform constants mirror the native enum variant names.
|
|
4
|
+
# standard:disable Naming/ConstantName
|
|
5
|
+
|
|
3
6
|
module Wreq
|
|
4
7
|
# Browser and client fingerprint profile enumeration backed by Rust.
|
|
5
8
|
#
|
|
@@ -164,6 +167,29 @@ module Wreq
|
|
|
164
167
|
def to_s
|
|
165
168
|
end
|
|
166
169
|
end
|
|
170
|
+
|
|
171
|
+
unless method_defined?(:==)
|
|
172
|
+
# Value-based equality.
|
|
173
|
+
# @param other [Object]
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
def ==(other)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
unless method_defined?(:eql?)
|
|
180
|
+
# Strict equality for Hash key and Set member semantics.
|
|
181
|
+
# @param other [Object]
|
|
182
|
+
# @return [Boolean]
|
|
183
|
+
def eql?(other)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
unless method_defined?(:hash)
|
|
188
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
189
|
+
# @return [Integer]
|
|
190
|
+
def hash
|
|
191
|
+
end
|
|
192
|
+
end
|
|
167
193
|
end
|
|
168
194
|
|
|
169
195
|
# Operating system platform enumeration backed by Rust.
|
|
@@ -195,6 +221,36 @@ module Wreq
|
|
|
195
221
|
def to_s
|
|
196
222
|
end
|
|
197
223
|
end
|
|
224
|
+
|
|
225
|
+
unless method_defined?(:to_sym)
|
|
226
|
+
# Returns the platform as a lowercase symbol (e.g. :windows, :linux).
|
|
227
|
+
# @return [Symbol]
|
|
228
|
+
def to_sym
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
unless method_defined?(:==)
|
|
233
|
+
# Value-based equality.
|
|
234
|
+
# @param other [Object]
|
|
235
|
+
# @return [Boolean]
|
|
236
|
+
def ==(other)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
unless method_defined?(:eql?)
|
|
241
|
+
# Strict equality for Hash key and Set member semantics.
|
|
242
|
+
# @param other [Object]
|
|
243
|
+
# @return [Boolean]
|
|
244
|
+
def eql?(other)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
unless method_defined?(:hash)
|
|
249
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
250
|
+
# @return [Integer]
|
|
251
|
+
def hash
|
|
252
|
+
end
|
|
253
|
+
end
|
|
198
254
|
end
|
|
199
255
|
|
|
200
256
|
# Emulation option wrapper.
|
|
@@ -216,19 +272,31 @@ module Wreq
|
|
|
216
272
|
#
|
|
217
273
|
# @param profile [Wreq::Profile, nil] Fingerprint profile to emulate
|
|
218
274
|
# @param platform [Wreq::Platform, nil] Operating system platform to emulate
|
|
219
|
-
# @param http2 [Boolean] Whether HTTP/2
|
|
220
|
-
#
|
|
275
|
+
# @param http2 [Boolean, nil] Whether HTTP/2 emulation is enabled; defaults
|
|
276
|
+
# to true when omitted or nil
|
|
277
|
+
# @param headers [Boolean, nil] Whether default emulation headers are enabled;
|
|
278
|
+
# defaults to true when omitted or nil
|
|
279
|
+
# @return [Wreq::Emulation] Configured emulation settings
|
|
280
|
+
# @raise [ArgumentError] if an option is unknown or extra arguments are given
|
|
281
|
+
# @raise [TypeError] if the option argument is not a Hash or a value has the
|
|
282
|
+
# wrong Ruby type
|
|
221
283
|
class Emulation
|
|
222
284
|
# Native fields and methods are set by the extension.
|
|
223
285
|
# This stub is for documentation only.
|
|
224
|
-
unless
|
|
286
|
+
unless singleton_methods(false).include?(:new)
|
|
225
287
|
# @param profile [Wreq::Profile, nil] Fingerprint profile to emulate
|
|
226
288
|
# @param platform [Wreq::Platform, nil] Operating system platform to emulate
|
|
227
|
-
# @param http2 [Boolean] Whether HTTP/2
|
|
228
|
-
#
|
|
289
|
+
# @param http2 [Boolean, nil] Whether HTTP/2 emulation is enabled; defaults
|
|
290
|
+
# to true when omitted or nil
|
|
291
|
+
# @param headers [Boolean, nil] Whether default emulation headers are enabled;
|
|
292
|
+
# defaults to true when omitted or nil
|
|
229
293
|
# @return [Wreq::Emulation] Configured emulation settings
|
|
230
|
-
|
|
294
|
+
# @raise [ArgumentError] if an option is unknown or extra arguments are given
|
|
295
|
+
# @raise [TypeError] if an option or value has the wrong Ruby type
|
|
296
|
+
def self.new(**options)
|
|
231
297
|
end
|
|
232
298
|
end
|
|
233
299
|
end
|
|
234
300
|
end
|
|
301
|
+
|
|
302
|
+
# standard:enable Naming/ConstantName
|
data/lib/wreq_ruby/error.rb
CHANGED
|
@@ -88,7 +88,7 @@ unless defined?(Wreq)
|
|
|
88
88
|
#
|
|
89
89
|
# @example
|
|
90
90
|
# begin
|
|
91
|
-
# client = Wreq::Client.new(max_redirects: 3)
|
|
91
|
+
# client = Wreq::Client.new(allow_redirects: true, max_redirects: 3)
|
|
92
92
|
# client.get("https://httpbin.io/redirect/10")
|
|
93
93
|
# rescue Wreq::RedirectError => e
|
|
94
94
|
# puts "Too many redirects: #{e.message}"
|
|
@@ -139,16 +139,14 @@ unless defined?(Wreq)
|
|
|
139
139
|
|
|
140
140
|
# Configuration and builder errors
|
|
141
141
|
|
|
142
|
-
#
|
|
142
|
+
# A native client or request configuration could not be built.
|
|
143
143
|
#
|
|
144
|
-
# Raised when
|
|
144
|
+
# Raised when validated Ruby options cannot be represented by the native
|
|
145
|
+
# builder or request body.
|
|
145
146
|
#
|
|
146
147
|
# @example
|
|
147
148
|
# begin
|
|
148
|
-
# client = Wreq::Client.new(
|
|
149
|
-
# proxy: "invalid://proxy",
|
|
150
|
-
# timeout: -1
|
|
151
|
-
# )
|
|
149
|
+
# client = Wreq::Client.new(proxy: "invalid://")
|
|
152
150
|
# rescue Wreq::BuilderError => e
|
|
153
151
|
# puts "Invalid configuration: #{e.message}"
|
|
154
152
|
# end
|
data/lib/wreq_ruby/http.rb
CHANGED
|
@@ -25,6 +25,43 @@ module Wreq
|
|
|
25
25
|
TRACE = nil # @return [Wreq::Method] HTTP TRACE method
|
|
26
26
|
PATCH = nil # @return [Wreq::Method] HTTP PATCH method
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
# Returns the HTTP method token (e.g. "GET", "POST").
|
|
30
|
+
# @return [String]
|
|
31
|
+
unless method_defined?(:to_s)
|
|
32
|
+
def to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the HTTP method as a lowercase symbol (e.g. :get, :post).
|
|
37
|
+
# @return [Symbol]
|
|
38
|
+
unless method_defined?(:to_sym)
|
|
39
|
+
def to_sym
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Value-based equality. Returns true when both represent the same HTTP method.
|
|
44
|
+
# @param other [Object]
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
unless method_defined?(:==)
|
|
47
|
+
def ==(other)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Strict equality for Hash key and Set member semantics.
|
|
52
|
+
# @param other [Object]
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
unless method_defined?(:eql?)
|
|
55
|
+
def eql?(other)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
60
|
+
# @return [Integer]
|
|
61
|
+
unless method_defined?(:hash)
|
|
62
|
+
def hash
|
|
63
|
+
end
|
|
64
|
+
end
|
|
28
65
|
end
|
|
29
66
|
|
|
30
67
|
# HTTP version enumeration backed by Rust.
|
|
@@ -63,6 +100,21 @@ module Wreq
|
|
|
63
100
|
def ==(other)
|
|
64
101
|
end
|
|
65
102
|
end
|
|
103
|
+
|
|
104
|
+
# Strict equality for Hash key and Set member semantics.
|
|
105
|
+
# @param other [Object]
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
unless method_defined?(:eql?)
|
|
108
|
+
def eql?(other)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
113
|
+
# @return [Integer]
|
|
114
|
+
unless method_defined?(:hash)
|
|
115
|
+
def hash
|
|
116
|
+
end
|
|
117
|
+
end
|
|
66
118
|
end
|
|
67
119
|
|
|
68
120
|
# HTTP status code wrapper.
|
|
@@ -141,6 +193,28 @@ module Wreq
|
|
|
141
193
|
# @return [String] Status code as string
|
|
142
194
|
def to_s
|
|
143
195
|
end
|
|
196
|
+
|
|
197
|
+
# Returns the status code as an integer.
|
|
198
|
+
# @return [Integer] the numeric HTTP status code
|
|
199
|
+
def to_i
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Value-based equality. Only compares with other StatusCode instances.
|
|
203
|
+
# @param other [Object]
|
|
204
|
+
# @return [Boolean]
|
|
205
|
+
def ==(other)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Strict equality for Hash key and Set member semantics.
|
|
209
|
+
# @param other [Object]
|
|
210
|
+
# @return [Boolean]
|
|
211
|
+
def eql?(other)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
215
|
+
# @return [Integer]
|
|
216
|
+
def hash
|
|
217
|
+
end
|
|
144
218
|
end
|
|
145
219
|
end
|
|
146
220
|
end
|
data/lib/wreq_ruby/response.rb
CHANGED
|
@@ -127,6 +127,9 @@ unless defined?(Wreq)
|
|
|
127
127
|
|
|
128
128
|
# Parse the response body as JSON.
|
|
129
129
|
#
|
|
130
|
+
# Integral JSON numbers are returned as arbitrary-precision Ruby Integer
|
|
131
|
+
# values. Fractional and exponent-form numbers are returned as Float values.
|
|
132
|
+
#
|
|
130
133
|
# @return [Object] Parsed JSON (Hash, Array, String, Integer, Float, Boolean, nil)
|
|
131
134
|
# @raise [Wreq::DecodingError] if body is not valid JSON
|
|
132
135
|
# @example
|
data/script/rust_env.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Wreq
|
|
7
|
+
module RustEnv
|
|
8
|
+
WINDOWS_GNU_TARGET = "x86_64-pc-windows-gnu"
|
|
9
|
+
WINDOWS_RUBY_PLATFORM = "x64-mingw-ucrt"
|
|
10
|
+
|
|
11
|
+
# Uses the UCRT toolchain shipped with the active RubyInstaller installation.
|
|
12
|
+
def self.activate
|
|
13
|
+
unless RbConfig::CONFIG["host_os"].match?(/mingw|mswin/)
|
|
14
|
+
FileUtils.rm_f(cargo_config_path)
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ruby_platform = RbConfig::CONFIG.fetch("arch")
|
|
19
|
+
unless ruby_platform == WINDOWS_RUBY_PLATFORM
|
|
20
|
+
raise "Unsupported Windows Ruby platform: #{ruby_platform}. " \
|
|
21
|
+
"Use RubyInstaller UCRT (#{WINDOWS_RUBY_PLATFORM})."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ruby_root = RbConfig::CONFIG.fetch("prefix")
|
|
25
|
+
msys_root = File.join(ruby_root, "msys64")
|
|
26
|
+
ucrt_bin = File.join(msys_root, "ucrt64", "bin")
|
|
27
|
+
msys_bin = File.join(msys_root, "usr", "bin")
|
|
28
|
+
|
|
29
|
+
validate_tools(ucrt_bin, msys_bin)
|
|
30
|
+
write_cargo_config(ucrt_bin, msys_bin)
|
|
31
|
+
ENV["CARGO_BUILD_TARGET"] = WINDOWS_GNU_TARGET
|
|
32
|
+
ENV["LIBCLANG_PATH"] = ucrt_bin
|
|
33
|
+
ENV.delete("CMAKE_GENERATOR")
|
|
34
|
+
ENV["PATH"] = [ucrt_bin, msys_bin, ENV.fetch("PATH", nil)].compact.join(File::PATH_SEPARATOR)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Reports missing RubyInstaller packages before Cargo starts compiling.
|
|
38
|
+
def self.validate_tools(ucrt_bin, msys_bin)
|
|
39
|
+
required = %w[gcc.exe g++.exe gcc-ar.exe clang.exe libclang.dll cmake.exe]
|
|
40
|
+
.map { |tool| File.join(ucrt_bin, tool) }
|
|
41
|
+
.append(File.join(msys_bin, "make.exe"))
|
|
42
|
+
missing = required.reject { |tool| File.file?(tool) }
|
|
43
|
+
unless %w[pkgconf.exe pkg-config.exe].any? { |tool| File.file?(File.join(ucrt_bin, tool)) }
|
|
44
|
+
missing << File.join(ucrt_bin, "pkgconf.exe")
|
|
45
|
+
end
|
|
46
|
+
return if missing.empty?
|
|
47
|
+
|
|
48
|
+
raise "Missing RubyInstaller UCRT tools: #{missing.join(", ")}. " \
|
|
49
|
+
"Install the Windows build dependencies documented in " \
|
|
50
|
+
"docs/windows-gnu-tokio-crash.md."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Generates machine-local Cargo defaults that work in command lines and IDEs.
|
|
54
|
+
def self.write_cargo_config(ucrt_bin, msys_bin)
|
|
55
|
+
config = <<~TOML
|
|
56
|
+
# Generated by script/rust_env.rb for the active RubyInstaller.
|
|
57
|
+
|
|
58
|
+
[build]
|
|
59
|
+
target = "#{WINDOWS_GNU_TARGET}"
|
|
60
|
+
|
|
61
|
+
[target.#{WINDOWS_GNU_TARGET}]
|
|
62
|
+
linker = "#{File.join(ucrt_bin, "gcc.exe")}"
|
|
63
|
+
|
|
64
|
+
[env]
|
|
65
|
+
CC_x86_64_pc_windows_gnu = { value = "#{File.join(ucrt_bin, "gcc.exe")}", force = true }
|
|
66
|
+
CXX_x86_64_pc_windows_gnu = { value = "#{File.join(ucrt_bin, "g++.exe")}", force = true }
|
|
67
|
+
AR_x86_64_pc_windows_gnu = { value = "#{File.join(ucrt_bin, "gcc-ar.exe")}", force = true }
|
|
68
|
+
CMAKE = { value = "#{File.join(ucrt_bin, "cmake.exe")}", force = true }
|
|
69
|
+
CMAKE_GENERATOR = { value = "MSYS Makefiles", force = true }
|
|
70
|
+
CMAKE_MAKE_PROGRAM = { value = "#{File.join(msys_bin, "make.exe")}", force = true }
|
|
71
|
+
LIBCLANG_PATH = { value = "#{ucrt_bin}", force = true }
|
|
72
|
+
TOML
|
|
73
|
+
|
|
74
|
+
return if File.exist?(cargo_config_path) && File.binread(cargo_config_path) == config
|
|
75
|
+
|
|
76
|
+
FileUtils.mkdir_p(File.dirname(cargo_config_path))
|
|
77
|
+
File.binwrite(cargo_config_path, config)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the ignored Cargo configuration shared by Rake and language servers.
|
|
81
|
+
def self.cargo_config_path
|
|
82
|
+
File.expand_path("../.cargo/windows.toml", __dir__)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
class BodySenderTest < Minitest::Test
|
|
7
|
+
def test_valid_capacity_creates_open_sender
|
|
8
|
+
sender = Wreq::BodySender.new(2)
|
|
9
|
+
|
|
10
|
+
refute_predicate sender, :closed?
|
|
11
|
+
ensure
|
|
12
|
+
sender&.close
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_omitted_capacity_defaults_to_eight
|
|
16
|
+
sender = Wreq::BodySender.new
|
|
17
|
+
progress = Queue.new
|
|
18
|
+
producer = Thread.new do
|
|
19
|
+
9.times do |index|
|
|
20
|
+
sender.push(index.to_s)
|
|
21
|
+
progress << index
|
|
22
|
+
end
|
|
23
|
+
sender.close
|
|
24
|
+
end
|
|
25
|
+
producer.report_on_exception = false
|
|
26
|
+
|
|
27
|
+
assert wait_until { progress.size >= 8 }, "the default channel should buffer eight chunks"
|
|
28
|
+
assert_equal 8, progress.size
|
|
29
|
+
assert_predicate producer, :alive?, "the ninth push should wait for channel capacity"
|
|
30
|
+
|
|
31
|
+
with_request_body_server do |url, request_thread|
|
|
32
|
+
response = Wreq.post(url, body: sender)
|
|
33
|
+
|
|
34
|
+
assert_equal 200, response.code
|
|
35
|
+
assert producer.join(5), "the ninth push should finish once the request drains"
|
|
36
|
+
assert_equal "012345678", request_thread.value
|
|
37
|
+
end
|
|
38
|
+
ensure
|
|
39
|
+
producer&.kill if producer&.alive?
|
|
40
|
+
producer&.join(5)
|
|
41
|
+
sender&.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_non_positive_and_excessive_capacities_raise_argument_error
|
|
45
|
+
[0, -1, 2**256].each do |capacity|
|
|
46
|
+
error = assert_raises(ArgumentError) { Wreq::BodySender.new(capacity) }
|
|
47
|
+
assert_match(/capacity/, error.message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_non_integer_capacities_raise_type_error
|
|
52
|
+
[1.0, "not an integer"].each do |capacity|
|
|
53
|
+
error = assert_raises(TypeError) { Wreq::BodySender.new(capacity) }
|
|
54
|
+
assert_match(/capacity/, error.message)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_too_many_constructor_arguments_raise_argument_error
|
|
59
|
+
assert_raises(ArgumentError) { Wreq::BodySender.new(1, 2) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_close_is_idempotent_and_updates_closed_state
|
|
63
|
+
sender = Wreq::BodySender.new
|
|
64
|
+
|
|
65
|
+
refute_predicate sender, :closed?
|
|
66
|
+
assert_nil sender.close
|
|
67
|
+
assert_predicate sender, :closed?
|
|
68
|
+
assert_nil sender.close
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_push_after_close_raises_io_error
|
|
72
|
+
sender = Wreq::BodySender.new
|
|
73
|
+
sender.close
|
|
74
|
+
|
|
75
|
+
error = assert_raises(IOError) { sender.push("data") }
|
|
76
|
+
assert_equal "closed body sender", error.message
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_queued_chunks_survive_close_before_request_attachment
|
|
80
|
+
sender = Wreq::BodySender.new(2)
|
|
81
|
+
sender.push("queued-")
|
|
82
|
+
sender.push("body")
|
|
83
|
+
sender.close
|
|
84
|
+
|
|
85
|
+
with_request_body_server do |url, request_thread|
|
|
86
|
+
response = Wreq.post(url, body: sender)
|
|
87
|
+
|
|
88
|
+
assert_equal 200, response.code
|
|
89
|
+
assert_equal "queued-body", request_thread.value
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_valid_capacity_keeps_backpressure_until_request_drains
|
|
94
|
+
sender = Wreq::BodySender.new(1)
|
|
95
|
+
sender.push("first-")
|
|
96
|
+
producer = Thread.new do
|
|
97
|
+
sender.push("second")
|
|
98
|
+
sender.close
|
|
99
|
+
end
|
|
100
|
+
producer.report_on_exception = false
|
|
101
|
+
|
|
102
|
+
sleep 0.05
|
|
103
|
+
assert_predicate producer, :alive?, "the second push should wait for channel capacity"
|
|
104
|
+
|
|
105
|
+
with_request_body_server do |url, request_thread|
|
|
106
|
+
response = Wreq.post(url, body: sender)
|
|
107
|
+
|
|
108
|
+
assert_equal 200, response.code
|
|
109
|
+
assert producer.join(5), "the blocked producer should finish once the request drains"
|
|
110
|
+
assert_equal "first-second", request_thread.value
|
|
111
|
+
end
|
|
112
|
+
ensure
|
|
113
|
+
producer&.kill if producer&.alive?
|
|
114
|
+
producer&.join(5)
|
|
115
|
+
sender&.close
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_receiver_termination_closes_sender
|
|
119
|
+
sender = Wreq::BodySender.new(1)
|
|
120
|
+
sender.push("data")
|
|
121
|
+
|
|
122
|
+
with_reset_server do |url|
|
|
123
|
+
assert_raises(StandardError) { Wreq.post(url, body: sender, timeout: 2) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
assert wait_until { sender.closed? }, "sender should close after the request drops its receiver"
|
|
127
|
+
assert_raises(IOError) { sender.push("more") }
|
|
128
|
+
ensure
|
|
129
|
+
sender&.close
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_zero_capacity_regression_exits_subprocess_normally
|
|
133
|
+
lib_dir = File.expand_path("../lib", __dir__)
|
|
134
|
+
script = <<~RUBY
|
|
135
|
+
require "wreq"
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
Wreq::BodySender.new(0)
|
|
139
|
+
rescue ArgumentError => error
|
|
140
|
+
warn "\#{error.class}: \#{error.message}"
|
|
141
|
+
exit 0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
warn "zero capacity did not raise"
|
|
145
|
+
exit 2
|
|
146
|
+
RUBY
|
|
147
|
+
|
|
148
|
+
_stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", lib_dir, "-e", script)
|
|
149
|
+
|
|
150
|
+
assert status.success?, "subprocess failed with #{status.inspect}: #{stderr}"
|
|
151
|
+
assert_match(/ArgumentError:.*capacity/, stderr)
|
|
152
|
+
refute_match(/panicked|mpsc bounded channel/i, stderr)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def wait_until(timeout: 2)
|
|
158
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
159
|
+
loop do
|
|
160
|
+
return true if yield
|
|
161
|
+
return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
162
|
+
|
|
163
|
+
sleep 0.01
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def with_request_body_server
|
|
168
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
169
|
+
port = server.addr[1]
|
|
170
|
+
request_thread = Thread.new do
|
|
171
|
+
socket = server.accept
|
|
172
|
+
begin
|
|
173
|
+
socket.gets
|
|
174
|
+
headers = read_headers(socket)
|
|
175
|
+
body = read_request_body(socket, headers)
|
|
176
|
+
socket.write "HTTP/1.1 200 OK\r\n"
|
|
177
|
+
socket.write "Content-Length: 0\r\n"
|
|
178
|
+
socket.write "Connection: close\r\n\r\n"
|
|
179
|
+
body
|
|
180
|
+
ensure
|
|
181
|
+
socket.close unless socket.closed?
|
|
182
|
+
end
|
|
183
|
+
ensure
|
|
184
|
+
server.close unless server.closed?
|
|
185
|
+
end
|
|
186
|
+
request_thread.report_on_exception = false
|
|
187
|
+
|
|
188
|
+
yield "http://127.0.0.1:#{port}/", request_thread
|
|
189
|
+
ensure
|
|
190
|
+
server&.close unless server&.closed?
|
|
191
|
+
request_thread&.join(5)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def with_reset_server
|
|
195
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
196
|
+
port = server.addr[1]
|
|
197
|
+
thread = Thread.new do
|
|
198
|
+
socket = server.accept
|
|
199
|
+
socket.close
|
|
200
|
+
ensure
|
|
201
|
+
server.close unless server.closed?
|
|
202
|
+
end
|
|
203
|
+
thread.report_on_exception = false
|
|
204
|
+
|
|
205
|
+
yield "http://127.0.0.1:#{port}/"
|
|
206
|
+
ensure
|
|
207
|
+
server&.close unless server&.closed?
|
|
208
|
+
thread&.join(5)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def read_headers(socket)
|
|
212
|
+
headers = {}
|
|
213
|
+
while (line = socket.gets)
|
|
214
|
+
break if line == "\r\n"
|
|
215
|
+
|
|
216
|
+
name, value = line.split(":", 2)
|
|
217
|
+
headers[name.downcase] = value.strip
|
|
218
|
+
end
|
|
219
|
+
headers
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def read_request_body(socket, headers)
|
|
223
|
+
if headers.fetch("transfer-encoding", "").downcase.include?("chunked")
|
|
224
|
+
read_chunked_body(socket)
|
|
225
|
+
else
|
|
226
|
+
socket.read(headers.fetch("content-length", "0").to_i)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def read_chunked_body(socket)
|
|
231
|
+
body = "".b
|
|
232
|
+
loop do
|
|
233
|
+
size_line = socket.gets or raise EOFError, "missing chunk size"
|
|
234
|
+
size = Integer(size_line.split(";", 2).first, 16)
|
|
235
|
+
break if size.zero?
|
|
236
|
+
|
|
237
|
+
body << socket.read(size)
|
|
238
|
+
raise IOError, "missing chunk terminator" unless socket.read(2) == "\r\n"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
while (line = socket.gets)
|
|
242
|
+
break if line == "\r\n"
|
|
243
|
+
end
|
|
244
|
+
body
|
|
245
|
+
end
|
|
246
|
+
end
|