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.
@@ -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 support is enabled
220
- # @param headers [Boolean] Whether default emulation headers are enabled
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 method_defined?(:new)
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 support is enabled
228
- # @param headers [Boolean] Whether default emulation headers are enabled
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
- def self.new(profile: nil, platform: nil, http2: true, headers: true)
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
@@ -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
- # Client configuration is invalid.
142
+ # A native client or request configuration could not be built.
143
143
  #
144
- # Raised when the client is configured with invalid options.
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
@@ -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
@@ -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
@@ -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