wreq 1.2.5 → 1.2.7

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +4 -0
  3. data/Cargo.lock +135 -119
  4. data/Cargo.toml +11 -3
  5. data/Rakefile +6 -0
  6. data/crates/wreq-util/README.md +1 -1
  7. data/crates/wreq-util/examples/emulate.rs +3 -3
  8. data/crates/wreq-util/src/emulate/macros.rs +13 -11
  9. data/crates/wreq-util/src/emulate/profile/chrome/header.rs +18 -3
  10. data/crates/wreq-util/src/emulate/profile/firefox/header.rs +16 -0
  11. data/crates/wreq-util/src/emulate/profile/opera/header.rs +6 -7
  12. data/crates/wreq-util/tests/client.rs +103 -1
  13. data/docs/windows-gnu-tokio-crash.md +49 -3
  14. data/extconf.rb +4 -0
  15. data/lib/wreq.rb +70 -45
  16. data/lib/wreq_ruby/body.rb +29 -11
  17. data/lib/wreq_ruby/client.rb +88 -55
  18. data/lib/wreq_ruby/cookie.rb +79 -19
  19. data/lib/wreq_ruby/emulate.rb +74 -6
  20. data/lib/wreq_ruby/error.rb +5 -7
  21. data/lib/wreq_ruby/http.rb +74 -0
  22. data/lib/wreq_ruby/response.rb +3 -0
  23. data/script/rust_env.rb +85 -0
  24. data/src/arch.rs +22 -0
  25. data/src/client/body/form.rs +2 -0
  26. data/src/client/body/json.rs +47 -14
  27. data/src/client/body/stream.rs +147 -43
  28. data/src/client/body.rs +11 -15
  29. data/src/client/param.rs +7 -7
  30. data/src/client/req.rs +87 -47
  31. data/src/client/resp.rs +23 -24
  32. data/src/client.rs +310 -230
  33. data/src/cookie.rs +280 -87
  34. data/src/emulate.rs +85 -50
  35. data/src/error.rs +198 -41
  36. data/src/extractor.rs +16 -76
  37. data/src/header.rs +146 -23
  38. data/src/http.rs +62 -33
  39. data/src/lib.rs +26 -20
  40. data/src/macros.rs +71 -46
  41. data/src/options.rs +284 -0
  42. data/src/rt.rs +22 -11
  43. data/src/serde/de/array_deserializer.rs +48 -0
  44. data/src/serde/de/array_enumerator.rs +59 -0
  45. data/src/serde/de/deserializer.rs +307 -0
  46. data/src/serde/de/enum_deserializer.rs +47 -0
  47. data/src/serde/de/hash_deserializer.rs +89 -0
  48. data/src/serde/de/number_deserializer.rs +51 -0
  49. data/src/serde/de/variant_deserializer.rs +101 -0
  50. data/src/serde/de.rs +33 -0
  51. data/src/serde/error.rs +146 -0
  52. data/src/serde/ser/enums.rs +14 -0
  53. data/src/serde/ser/map_serializer.rs +56 -0
  54. data/src/serde/ser/seq_serializer.rs +71 -0
  55. data/src/serde/ser/serializer.rs +194 -0
  56. data/src/serde/ser/struct_serializer.rs +106 -0
  57. data/src/serde/ser/struct_variant_serializer.rs +44 -0
  58. data/src/serde/ser/tuple_variant_serializer.rs +41 -0
  59. data/src/serde/ser.rs +18 -0
  60. data/src/serde/tests.rs +237 -0
  61. data/src/serde.rs +202 -0
  62. data/test/body_sender_test.rb +246 -0
  63. data/test/cookie_test.rb +211 -10
  64. data/test/json_precision_test.rb +209 -0
  65. data/test/option_validation_test.rb +311 -0
  66. data/test/value_semantics_test.rb +238 -0
  67. data/wreq.gemspec +1 -1
  68. metadata +28 -4
  69. data/script/build_windows_gnu.ps1 +0 -264
  70. data/src/header/helper.rs +0 -112
@@ -0,0 +1,237 @@
1
+ use std::{collections::BTreeMap, fmt};
2
+
3
+ use ::serde::{Deserialize, Serialize, de::Visitor};
4
+ use magnus::{RArray, RHash, RString, Ruby, Value, encoding::EncodingCapable, value::ReprValue};
5
+
6
+ use super::{deserialize_json, deserialize_ruby, serialize};
7
+
8
+ #[derive(Debug, Deserialize, PartialEq, Serialize)]
9
+ struct Record {
10
+ count: u64,
11
+ enabled: bool,
12
+ tags: Vec<String>,
13
+ note: Option<String>,
14
+ }
15
+
16
+ #[derive(Debug, Deserialize, PartialEq, Serialize)]
17
+ struct UnitRecord;
18
+
19
+ #[derive(Debug, Deserialize, PartialEq, Serialize)]
20
+ struct NewtypeRecord(u64);
21
+
22
+ #[derive(Debug, Deserialize, PartialEq, Serialize)]
23
+ struct TupleRecord(u64, bool, String);
24
+
25
+ #[derive(Debug, Deserialize, PartialEq, Serialize)]
26
+ enum State {
27
+ Ready,
28
+ Count(u64),
29
+ Progress(u64, bool),
30
+ Failed { message: String },
31
+ }
32
+
33
+ /// Byte sequence that exercises Serde's owned byte-buffer entry points.
34
+ #[derive(Debug, PartialEq)]
35
+ struct ByteBuffer(Vec<u8>);
36
+
37
+ impl Serialize for ByteBuffer {
38
+ fn serialize<Serializer>(
39
+ &self,
40
+ serializer: Serializer,
41
+ ) -> Result<Serializer::Ok, Serializer::Error>
42
+ where
43
+ Serializer: ::serde::Serializer,
44
+ {
45
+ serializer.serialize_bytes(&self.0)
46
+ }
47
+ }
48
+
49
+ impl<'de> Deserialize<'de> for ByteBuffer {
50
+ fn deserialize<Deserializer>(deserializer: Deserializer) -> Result<Self, Deserializer::Error>
51
+ where
52
+ Deserializer: ::serde::Deserializer<'de>,
53
+ {
54
+ struct ByteBufferVisitor;
55
+
56
+ impl<'de> Visitor<'de> for ByteBufferVisitor {
57
+ type Value = ByteBuffer;
58
+
59
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60
+ formatter.write_str("an owned byte buffer")
61
+ }
62
+
63
+ fn visit_bytes<Error>(self, value: &[u8]) -> Result<Self::Value, Error>
64
+ where
65
+ Error: ::serde::de::Error,
66
+ {
67
+ Ok(ByteBuffer(value.to_vec()))
68
+ }
69
+
70
+ fn visit_byte_buf<Error>(self, value: Vec<u8>) -> Result<Self::Value, Error>
71
+ where
72
+ Error: ::serde::de::Error,
73
+ {
74
+ Ok(ByteBuffer(value))
75
+ }
76
+ }
77
+
78
+ deserializer.deserialize_byte_buf(ByteBufferVisitor)
79
+ }
80
+ }
81
+
82
+ /// Assert that a supported Serde value survives conversion through Ruby.
83
+ fn assert_ruby_round_trip<Input>(ruby: &Ruby, input: Input) -> Result<(), magnus::Error>
84
+ where
85
+ Input: fmt::Debug + PartialEq + Serialize + for<'de> Deserialize<'de>,
86
+ {
87
+ let value: Value = serialize(ruby, &input)?;
88
+ let output: Input = deserialize_ruby(ruby, value)?;
89
+ assert_eq!(input, output);
90
+ Ok(())
91
+ }
92
+
93
+ /// Assert that a failed conversion preserves Ruby's `TypeError` classification.
94
+ fn assert_type_error(error: magnus::Error, message: &str) {
95
+ let error = error.to_string();
96
+ assert!(error.starts_with("TypeError: "), "{error}");
97
+ assert!(error.contains(message), "{error}");
98
+ }
99
+
100
+ /// Verify scalar, option, result, and byte-string conversion behavior.
101
+ fn assert_scalar_conversions(ruby: &Ruby) -> Result<(), magnus::Error> {
102
+ assert_ruby_round_trip(ruby, true)?;
103
+ assert_ruby_round_trip(ruby, 1.25_f32)?;
104
+ assert_ruby_round_trip(ruby, 1.25_f64)?;
105
+ assert_ruby_round_trip(ruby, Option::<u64>::None)?;
106
+ assert_ruby_round_trip(ruby, Some(123_u64))?;
107
+ assert_ruby_round_trip(ruby, Result::<u64, String>::Ok(1234))?;
108
+ assert_ruby_round_trip(ruby, Result::<u64, String>::Err("failed".to_owned()))?;
109
+
110
+ let character: RString = serialize(ruby, &'\u{2603}')?;
111
+ assert_eq!("\u{2603}", character.to_string()?);
112
+ assert!(character.enc_get() == ruby.utf8_encindex());
113
+ let output: char = deserialize_ruby(ruby, character)?;
114
+ assert_eq!('\u{2603}', output);
115
+
116
+ let string: RString = serialize(ruby, &"Hello, world!")?;
117
+ assert_eq!("Hello, world!", string.to_string()?);
118
+ assert!(string.enc_get() == ruby.utf8_encindex());
119
+ assert_eq!(
120
+ "Hello, world!",
121
+ deserialize_ruby::<_, String>(ruby, string)?
122
+ );
123
+
124
+ let bytes = ByteBuffer(b"\0binary\xff".to_vec());
125
+ let value: RString = serialize(ruby, &bytes)?;
126
+ assert_eq!(bytes.0.as_slice(), value.to_bytes().as_ref());
127
+ assert!(value.enc_get() == ruby.ascii8bit_encindex());
128
+ assert_eq!(bytes, deserialize_ruby(ruby, value)?);
129
+
130
+ let none: Value = serialize(ruby, &Option::<u64>::None)?;
131
+ assert!(none.is_nil());
132
+
133
+ let ok: RHash = serialize(ruby, &Result::<u64, String>::Ok(1234))?;
134
+ let value: u64 = ok.aref("Ok")?;
135
+ assert_eq!(1234, value);
136
+
137
+ let error: RHash = serialize(ruby, &Result::<u64, String>::Err("failed".to_owned()))?;
138
+ assert_eq!("failed", error.aref::<_, String>("Err")?);
139
+ Ok(())
140
+ }
141
+
142
+ /// Verify collection, tuple, struct, and enum conversion behavior.
143
+ fn assert_composite_conversions(ruby: &Ruby) -> Result<(), magnus::Error> {
144
+ assert_ruby_round_trip(ruby, ())?;
145
+ assert_ruby_round_trip(ruby, [1_i64, 2, 3])?;
146
+ assert_ruby_round_trip(ruby, (123_i64, true, "tuple".to_owned()))?;
147
+ assert_ruby_round_trip(ruby, UnitRecord)?;
148
+ assert_ruby_round_trip(ruby, NewtypeRecord(123))?;
149
+ assert_ruby_round_trip(ruby, TupleRecord(123, true, "tuple struct".to_owned()))?;
150
+
151
+ let record = Record {
152
+ count: 42,
153
+ enabled: true,
154
+ tags: vec!["ruby".into(), "rust".into()],
155
+ note: Some("present".into()),
156
+ };
157
+ let value: RHash = serialize(ruby, &record)?;
158
+ let count: u64 = value.aref(ruby.to_symbol("count"))?;
159
+ assert_eq!(record.count, count);
160
+ assert_eq!(record, deserialize_ruby(ruby, value)?);
161
+
162
+ let map = BTreeMap::from([("first".to_owned(), 1_u64), ("second".to_owned(), 2)]);
163
+ assert_ruby_round_trip(ruby, map)?;
164
+
165
+ let tuple = (123_i64, true, "tuple".to_owned());
166
+ let value: RArray = serialize(ruby, &tuple)?;
167
+ assert_eq!(3, value.len());
168
+ let first: i64 = value.entry(0)?;
169
+ assert_eq!(123, first);
170
+
171
+ for state in [
172
+ State::Ready,
173
+ State::Count(2),
174
+ State::Progress(3, false),
175
+ State::Failed {
176
+ message: "failed".into(),
177
+ },
178
+ ] {
179
+ assert_ruby_round_trip(ruby, state)?;
180
+ }
181
+
182
+ let count: RHash = serialize(ruby, &State::Count(7))?;
183
+ let value: u64 = count.aref("Count")?;
184
+ assert_eq!(7, value);
185
+ Ok(())
186
+ }
187
+
188
+ /// Verify exact typed integer conversion in both deserialization modes.
189
+ fn assert_integer_conversions(ruby: &Ruby) -> Result<(), magnus::Error> {
190
+ let value: Value = serialize(ruby, &i128::MIN)?;
191
+ let decimal: String = value.funcall("to_s", ())?;
192
+ assert_eq!(i128::MIN.to_string(), decimal);
193
+ assert_eq!(i128::MIN, deserialize_ruby::<_, i128>(ruby, value)?);
194
+ assert_eq!(i128::MIN, deserialize_json::<_, i128>(ruby, value)?);
195
+
196
+ let value: Value = serialize(ruby, &u128::MAX)?;
197
+ let decimal: String = value.funcall("to_s", ())?;
198
+ assert_eq!(u128::MAX.to_string(), decimal);
199
+ assert_eq!(u128::MAX, deserialize_ruby::<_, u128>(ruby, value)?);
200
+ assert_eq!(u128::MAX, deserialize_json::<_, u128>(ruby, value)?);
201
+
202
+ let value: Value = serialize(ruby, &u64::MAX)?;
203
+ assert_eq!(u64::MAX, deserialize_ruby::<_, u64>(ruby, value)?);
204
+ assert_eq!(u64::MAX, deserialize_json::<_, u64>(ruby, value)?);
205
+ Ok(())
206
+ }
207
+
208
+ /// Verify unsupported borrowed values and malformed enum shapes return errors.
209
+ fn assert_conversion_errors(ruby: &Ruby) -> Result<(), magnus::Error> {
210
+ let error = deserialize_ruby::<_, &str>(ruby, ruby.str_new("borrowed"))
211
+ .expect_err("borrowed strings must not outlive their Ruby value");
212
+ assert_type_error(error, "expected a borrowed string");
213
+
214
+ let error = deserialize_ruby::<_, &[u8]>(ruby, ruby.str_new("borrowed"))
215
+ .expect_err("borrowed byte slices must not outlive their Ruby value");
216
+ assert_type_error(error, "can't deserialize into byte slice");
217
+
218
+ let variants = ruby.hash_new();
219
+ variants.aset("Ready", ruby.qnil())?;
220
+ variants.aset("Count", 1)?;
221
+ let error = deserialize_ruby::<_, State>(ruby, variants)
222
+ .expect_err("an enum hash must contain exactly one variant");
223
+ assert_type_error(error, "Hash of length 2");
224
+ Ok(())
225
+ }
226
+
227
+ #[test]
228
+ fn retains_ruby_serde_conversion_surface() -> Result<(), magnus::Error> {
229
+ // SAFETY: this is the only test that initializes the embedded Ruby VM.
230
+ let ruby = unsafe { magnus::embed::init() };
231
+
232
+ assert_scalar_conversions(&ruby)?;
233
+ assert_composite_conversions(&ruby)?;
234
+ assert_integer_conversions(&ruby)?;
235
+ assert_conversion_errors(&ruby)?;
236
+ Ok(())
237
+ }
data/src/serde.rs ADDED
@@ -0,0 +1,202 @@
1
+ /*
2
+ Copyright 2022 George Claghorn
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8
+ of the Software, and to permit persons to whom the Software is furnished to do
9
+ so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ //! Serde integration for Magnus with JSON-specific conversion behavior.
24
+ //!
25
+ //! This module is adapted from `serde_magnus` 0.11.0 and retains its generic
26
+ //! Ruby serialization and deserialization surface. Local changes add a JSON
27
+ //! mode with arbitrary-size integer support, finite-float and object-key
28
+ //! validation, insertion-order preservation, and bounded container nesting.
29
+ //! The bridge also avoids the upstream `Ruby::get().unwrap()` error path,
30
+ //! checks iterator and map state explicitly, reports unknown option fields and
31
+ //! conversion paths, and supports `i128` and `u128` in both directions for
32
+ //! typed Rust values.
33
+ #![allow(unsafe_code)]
34
+
35
+ mod de;
36
+ mod error;
37
+ mod ser;
38
+ #[cfg(test)]
39
+ mod tests;
40
+
41
+ use ::serde::{Deserialize, Serialize, de::DeserializeOwned};
42
+ use indexmap::IndexSet;
43
+ use magnus::{IntoValue, Ruby, TryConvert};
44
+ use serde_ignored::Path;
45
+ use serde_path_to_error::{Deserializer as PathDeserializer, Track};
46
+
47
+ use crate::error::argument_error;
48
+
49
+ pub(super) use error::Error;
50
+
51
+ /// Private map key used to carry arbitrary-precision numbers through Serde.
52
+ ///
53
+ /// This mirrors the representation used by `serde_json` when its
54
+ /// `arbitrary_precision` feature is enabled. The precision tests protect this
55
+ /// integration point when `serde_json` is updated.
56
+ pub(super) const JSON_NUMBER_TOKEN: &str = "$serde_json::private::Number";
57
+
58
+ /// Maximum nesting accepted while converting a Ruby request value.
59
+ pub(super) const MAX_JSON_NESTING: usize = 100;
60
+
61
+ /// Deserialize a Ruby value using native Ruby data model semantics.
62
+ ///
63
+ /// This preserves the public conversion behavior provided by the upstream
64
+ /// `serde_magnus::deserialize` function. JSON callers should use
65
+ /// [`deserialize_json`] when arbitrary-size integer precision is required.
66
+ ///
67
+ /// # Errors
68
+ ///
69
+ /// Returns the Ruby exception produced when the input cannot be represented by
70
+ /// `Output`.
71
+ #[allow(dead_code)] // Retained as part of the upstream-compatible Serde surface.
72
+ pub(crate) fn deserialize_ruby<'de, Input, Output>(
73
+ ruby: &Ruby,
74
+ input: Input,
75
+ ) -> Result<Output, magnus::Error>
76
+ where
77
+ Input: IntoValue,
78
+ Output: Deserialize<'de>,
79
+ {
80
+ de::deserialize_ruby(ruby, input.into_value_with(ruby)).map_err(|error| error.into_magnus(ruby))
81
+ }
82
+
83
+ /// Serialize any Serde value into a Ruby value.
84
+ ///
85
+ /// This preserves the public conversion behavior provided by the upstream
86
+ /// `serde_magnus::serialize` function.
87
+ ///
88
+ /// # Errors
89
+ ///
90
+ /// Returns the Ruby exception produced while creating or converting the output
91
+ /// value.
92
+ pub(crate) fn serialize<Input, Output>(ruby: &Ruby, input: &Input) -> Result<Output, magnus::Error>
93
+ where
94
+ Input: Serialize + ?Sized,
95
+ Output: TryConvert,
96
+ {
97
+ let value = ser::serialize(ruby, input).map_err(|error| error.into_magnus(ruby))?;
98
+ Output::try_convert(value)
99
+ }
100
+
101
+ /// Deserialize a Ruby value using JSON-specific conversion rules.
102
+ ///
103
+ /// Unlike [`deserialize_ruby`], this preserves arbitrary-size integers and
104
+ /// rejects non-finite floats, unsupported object keys, and excessive nesting.
105
+ ///
106
+ /// # Errors
107
+ ///
108
+ /// Returns the Ruby exception produced when the input cannot be represented by
109
+ /// `Output`.
110
+ pub(crate) fn deserialize_json<'de, Input, Output>(
111
+ ruby: &Ruby,
112
+ input: Input,
113
+ ) -> Result<Output, magnus::Error>
114
+ where
115
+ Input: IntoValue,
116
+ Output: Deserialize<'de>,
117
+ {
118
+ de::deserialize_json(ruby, input.into_value_with(ruby)).map_err(|error| error.into_magnus(ruby))
119
+ }
120
+
121
+ /// Validate Ruby option keys without inspecting their values.
122
+ ///
123
+ /// The derived struct is the only list of accepted keys. Every option field is
124
+ /// optional, so this pass can collect unknown keys and duplicates without
125
+ /// converting or retaining any Ruby value.
126
+ ///
127
+ /// # Errors
128
+ ///
129
+ /// Returns `ArgumentError` for unknown or duplicate keys and `TypeError` for
130
+ /// keys that are neither Ruby Symbols nor Strings.
131
+ pub(crate) fn validate_option_keys<Input, Output>(
132
+ ruby: &Ruby,
133
+ input: Input,
134
+ ) -> Result<(), magnus::Error>
135
+ where
136
+ Input: IntoValue,
137
+ Output: DeserializeOwned,
138
+ {
139
+ let value: magnus::Value = input.into_value_with(ruby);
140
+ let mut unknown = IndexSet::new();
141
+ {
142
+ let mut callback = |path: Path<'_>| {
143
+ if let Path::Map { key, .. } = path {
144
+ unknown.insert(key);
145
+ }
146
+ };
147
+ serde_ignored::deserialize::<_, _, Output>(
148
+ de::Deserializer::new_option_keys(ruby, value),
149
+ &mut callback,
150
+ )
151
+ .map_err(|error| error.into_option_magnus(ruby, None))?;
152
+ }
153
+
154
+ if unknown.is_empty() {
155
+ return Ok(());
156
+ }
157
+
158
+ let label = if unknown.len() == 1 {
159
+ "unknown option"
160
+ } else {
161
+ "unknown options"
162
+ };
163
+ let mut message = String::from(label);
164
+ message.push(':');
165
+ for (index, name) in unknown.into_iter().enumerate() {
166
+ if index > 0 {
167
+ message.push(',');
168
+ }
169
+ message.push(' ');
170
+ message.push(':');
171
+ message.push_str(&name);
172
+ }
173
+
174
+ Err(argument_error(ruby, message))
175
+ }
176
+
177
+ /// Deserialize validated Ruby options and retain the failing field path.
178
+ ///
179
+ /// # Errors
180
+ ///
181
+ /// Returns the Ruby conversion error with the failing option path attached.
182
+ pub(crate) fn deserialize_options<Input, Output>(
183
+ ruby: &Ruby,
184
+ input: Input,
185
+ ) -> Result<Output, magnus::Error>
186
+ where
187
+ Input: IntoValue,
188
+ Output: DeserializeOwned,
189
+ {
190
+ let value = input.into_value_with(ruby);
191
+ let mut track = Track::new();
192
+ let result = Output::deserialize(PathDeserializer::new(
193
+ de::Deserializer::new_ruby(ruby, value),
194
+ &mut track,
195
+ ));
196
+
197
+ result.map_err(|error| {
198
+ let path = track.path();
199
+ let path = path.iter().next().is_some().then_some(&path);
200
+ error.into_option_magnus(ruby, path)
201
+ })
202
+ }
@@ -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