Skip to main content

hclient_rt/
caps.rs

1use std::error::Error as StdError;
2use std::fmt::Display;
3use std::future::Future;
4use std::net::{IpAddr, SocketAddr};
5use std::time::Duration;
6
7/// The shape is deliberately copied from `hyper::rt::Executor`: generic
8/// over the future, zero bounds in the declaration. `Send` is added by the
9/// `impl`, not the trait, so single-threaded runtimes can implement it
10/// honestly.
11pub trait Spawn<F: Future<Output = ()>> {
12    fn spawn(&self, f: F);
13}
14
15/// Socket options are applied in hclient **once**, on the `socket2::Socket`,
16/// and the runtime only adopts the descriptor (`TcpAdoptStd`). Otherwise
17/// every runtime crate would rewrite this whole rigmarole again.
18///
19/// # `default()` is all-off, and that was re-decided rather than inherited
20///
21/// Nagle's algorithm costs the head of a `Native` TLS exchange **41 ms** on
22/// loopback — measured from the server's side of the wire in
23/// `hclient-native`'s `tests/nagle_cost.rs`, and 0.9 ms with `nodelay` set.
24/// Every field here stays `false`/`None` anyway, for two reasons that are
25/// not caution:
26///
27/// - **This is a socket seam, and it does not know who is writing.** The
28///   41 ms is the write-write-read pattern of a request over TLS meeting a
29///   peer's delayed ACK. A protocol that streams one way is exactly the one
30///   Nagle helps, and a default here would impose one caller's protocol on
31///   every other caller of the trait.
32/// - **A set option is a refusal, not a preference.**
33///   [`TcpOpts::reject_unsupported`] fails the connect on a runtime whose
34///   [`TcpConnect::APPLIES`] does not cover it, and that default is `NONE`.
35///   Turning a field on here would turn every connect on a backend that
36///   forgot to declare `APPLIES` into an `Unsupported` error for an option
37///   its caller never mentioned — a performance fix aimed straight at the
38///   implementors the `NONE` default was written to protect.
39///
40/// So the opinion lives where the protocol is: `hclient_native::Native::new`
41/// asks for `nodelay`, and asks only where the runtime declares it applies
42/// it.
43#[derive(Debug, Clone, Default)]
44pub struct TcpOpts {
45    /// `TCP_NODELAY` — Nagle's algorithm off. See the type's own doc for
46    /// why `default()` leaves it `false` and who turns it on.
47    pub nodelay: bool,
48    /// `TCP_KEEPIDLE` — how long a connection may be idle before the
49    /// first probe.
50    ///
51    /// **One setting in three parts, with
52    /// [`keepalive_interval`](Self::keepalive_interval) and
53    /// [`keepalive_retries`](Self::keepalive_retries).** Setting *any* of
54    /// the three turns `SO_KEEPALIVE` on; each part left `None` keeps the
55    /// operating system's value for it. That is `socket2::TcpKeepalive`'s own shape
56    /// and it is stated here because the field names do not say it: a
57    /// caller who sets only the interval has switched keepalive on, with
58    /// the OS's idle time.
59    pub keepalive: Option<Duration>,
60    /// `TCP_KEEPINTVL` — the gap between probes once they have started.
61    ///
62    /// Worth setting with [`keepalive`](Self::keepalive) rather than
63    /// instead of it: the idle time decides *when a dead peer starts being
64    /// noticed* and this decides *how fast the noticing then goes*, and
65    /// Linux's defaults are 7200 s and 75 s, so an untouched idle time
66    /// makes the interval nearly irrelevant.
67    pub keepalive_interval: Option<Duration>,
68    /// `TCP_KEEPCNT` — how many unanswered probes end the connection.
69    pub keepalive_retries: Option<u32>,
70    /// `SO_BINDTODEVICE` — the interface this socket must use, by name.
71    ///
72    /// Not [`local_address`](Self::local_address) under another name: an
73    /// address binds the *source address*, and the kernel still routes by
74    /// its table, so a request can leave through a different interface
75    /// that happens to hold the same address. This binds the **interface**,
76    /// which is what a caller on a multi-homed host or inside a VRF
77    /// actually means. Linux, Android and Fuchsia only — see
78    /// [`TcpOptsSupport`], which is where a runtime says so per target.
79    ///
80    /// A `String` rather than a `&'static str` because an interface name
81    /// is configuration a caller reads at run time, and rather than bytes
82    /// because every interface name on every platform that has this option
83    /// is ASCII.
84    pub bind_device: Option<String>,
85    /// `TCP_USER_TIMEOUT` — how long transmitted data may stay
86    /// unacknowledged before the connection is dropped.
87    ///
88    /// **The one option here that catches a peer which vanished
89    /// mid-transfer**, where keepalive only catches an *idle* one: probes
90    /// are sent when nothing is in flight, so a connection with unsent
91    /// acknowledgements sits in retransmission for minutes with keepalive
92    /// never firing. Linux, Android, Fuchsia and Cygwin only.
93    ///
94    /// It overlaps `Timeouts::between_bytes` and does not replace it: this
95    /// is the kernel's, applies to a socket rather than to an exchange, and
96    /// is the only one of the two that a build with no `Client` above it
97    /// can reach.
98    pub user_timeout: Option<Duration>,
99    pub local_address: Option<IpAddr>,
100    pub send_buffer_size: Option<usize>,
101    pub recv_buffer_size: Option<usize>,
102    pub reuse_address: bool,
103}
104
105/// Which of [`TcpOpts`]' six fields a runtime can actually apply.
106///
107/// One `bool` per field of `TcpOpts`, not a count and not a bitflags crate:
108/// the error a caller gets has to name the option it asked for, and a
109/// field-per-field mirror is the only shape that can.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct TcpOptsSupport {
112    pub nodelay: bool,
113    pub keepalive: bool,
114    pub keepalive_interval: bool,
115    pub keepalive_retries: bool,
116    /// `SO_BINDTODEVICE`, which exists on Linux, Android and Fuchsia and
117    /// nowhere else — so a runtime that sets this **must** decide it per
118    /// target rather than in one constant. `TcpOptsSupport::ALL` is still
119    /// literally every field, and is therefore no longer a value any real
120    /// runtime can claim on every platform it builds for.
121    pub bind_device: bool,
122    /// `TCP_USER_TIMEOUT`, Linux/Android/Fuchsia/Cygwin — the same
123    /// per-target rule as [`bind_device`](Self::bind_device).
124    pub user_timeout: bool,
125    pub local_address: bool,
126    pub send_buffer_size: bool,
127    pub recv_buffer_size: bool,
128    pub reuse_address: bool,
129}
130
131impl TcpOptsSupport {
132    /// Everything applied — what a runtime that hands the whole set to a
133    /// `socket2::Socket` says. Both shipped runtimes do exactly that.
134    pub const ALL: Self = Self {
135        nodelay: true,
136        keepalive: true,
137        keepalive_interval: true,
138        keepalive_retries: true,
139        bind_device: true,
140        user_timeout: true,
141        local_address: true,
142        send_buffer_size: true,
143        recv_buffer_size: true,
144        reuse_address: true,
145    };
146    /// Nothing applied — the default for [`TcpConnect::APPLIES`], and the
147    /// conservative base a runtime turns individual fields on from.
148    pub const NONE: Self = Self {
149        nodelay: false,
150        keepalive: false,
151        keepalive_interval: false,
152        keepalive_retries: false,
153        bind_device: false,
154        user_timeout: false,
155        local_address: false,
156        send_buffer_size: false,
157        recv_buffer_size: false,
158        reuse_address: false,
159    };
160}
161
162/// The caller set socket options this runtime cannot apply.
163///
164/// Carried inside an [`std::io::Error`] with
165/// [`ErrorKind::Unsupported`](std::io::ErrorKind::Unsupported) by
166/// [`TcpOpts::reject_unsupported`], and reachable again through
167/// `io::Error::get_ref().downcast_ref()`.
168///
169/// `Display` names **every** offending option, not just the first: a caller
170/// who set two unappliable options and fixed the one the message mentioned
171/// would otherwise get a second, identical-looking failure.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct UnsupportedTcpOpts {
174    /// `true` where the caller asked for an option the runtime does not
175    /// apply — i.e. set in [`TcpOpts`] and absent from
176    /// [`TcpConnect::APPLIES`].
177    missing: TcpOptsSupport,
178}
179
180impl UnsupportedTcpOpts {
181    /// The offending option names, in [`TcpOpts`]' own field order.
182    pub fn names(&self) -> impl Iterator<Item = &'static str> {
183        let m = self.missing;
184        [
185            ("nodelay", m.nodelay),
186            ("keepalive", m.keepalive),
187            ("keepalive_interval", m.keepalive_interval),
188            ("keepalive_retries", m.keepalive_retries),
189            ("bind_device", m.bind_device),
190            ("user_timeout", m.user_timeout),
191            ("local_address", m.local_address),
192            ("send_buffer_size", m.send_buffer_size),
193            ("recv_buffer_size", m.recv_buffer_size),
194            ("reuse_address", m.reuse_address),
195        ]
196        .into_iter()
197        .filter_map(|(name, missing)| missing.then_some(name))
198    }
199}
200
201// Hand-written rather than `thiserror`: the message is a computed list, so
202// the derive would buy nothing, and this way the names are written straight
203// into the formatter instead of through an intermediate `String`.
204impl Display for UnsupportedTcpOpts {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        f.write_str(
207            "this runtime cannot apply these TCP socket options, and does not ignore them:",
208        )?;
209        for (i, name) in self.names().enumerate() {
210            f.write_str(if i > 0 { ", " } else { " " })?;
211            f.write_str(name)?;
212        }
213        // Where the claim came from, because half the readers of this
214        // message are on the wrong side of it. `TcpConnect::APPLIES`
215        // defaults to `NONE`, so a runtime that *does* apply an option and
216        // forgot the line refuses it here — which has happened once in
217        // this workspace already (`TokioHandle`, found by measurement).
218        // Naming the option alone sends that author looking at their
219        // `connect` body, where the code is correct and the bug is not.
220        f.write_str(" (a runtime that does apply one declares it in TcpConnect::APPLIES)")
221    }
222}
223
224impl StdError for UnsupportedTcpOpts {}
225
226impl TcpOpts {
227    /// Fail when the caller set an option `can` says this runtime does not
228    /// apply — the one sanctioned answer to an option a runtime cannot
229    /// honour, since silently ignoring it is not one.
230    ///
231    /// Only fields that are actually *set* can offend: [`TcpOpts::default`]
232    /// is all-off, so even a runtime with [`TcpOptsSupport::NONE`] still
233    /// serves every caller that never asked for anything.
234    ///
235    /// A runtime whose [`TcpConnect::APPLIES`] is [`TcpOptsSupport::ALL`]
236    /// need not call this at all — the call is a no-op by construction,
237    /// which `reject_unsupported_is_a_no_op_against_all` pins.
238    pub fn reject_unsupported(&self, can: TcpOptsSupport) -> std::io::Result<()> {
239        let missing = TcpOptsSupport {
240            nodelay: self.nodelay && !can.nodelay,
241            keepalive: self.keepalive.is_some() && !can.keepalive,
242            keepalive_interval: self.keepalive_interval.is_some() && !can.keepalive_interval,
243            keepalive_retries: self.keepalive_retries.is_some() && !can.keepalive_retries,
244            bind_device: self.bind_device.is_some() && !can.bind_device,
245            user_timeout: self.user_timeout.is_some() && !can.user_timeout,
246            local_address: self.local_address.is_some() && !can.local_address,
247            send_buffer_size: self.send_buffer_size.is_some() && !can.send_buffer_size,
248            recv_buffer_size: self.recv_buffer_size.is_some() && !can.recv_buffer_size,
249            reuse_address: self.reuse_address && !can.reuse_address,
250        };
251        if missing == TcpOptsSupport::NONE {
252            return Ok(());
253        }
254        Err(std::io::Error::new(
255            std::io::ErrorKind::Unsupported,
256            UnsupportedTcpOpts { missing },
257        ))
258    }
259}
260
261pub trait TcpConnect {
262    type Stream: hyper::rt::Read + hyper::rt::Write + Unpin;
263
264    /// Which [`TcpOpts`] fields this runtime actually applies.
265    ///
266    /// # Why the default is `NONE` and not `ALL`
267    ///
268    /// A default is a claim made by silence, and it must never be stronger
269    /// than the truth — the rule written down on
270    /// [`CancelSupport::None`](hclient_core::CancelSupport::None) and
271    /// learned from `RedirectSupport::Transparent`. `ALL` would make a
272    /// backend that forgot the line claim it applies every option; `NONE`
273    /// makes it understate itself, so the worst case is one refused connect
274    /// too many rather than an option dropped on the floor without a trace.
275    const APPLIES: TcpOptsSupport = TcpOptsSupport::NONE;
276
277    /// # The options are not optional
278    ///
279    /// A runtime that cannot apply an option the caller set **must fail
280    /// this call** — [`TcpOpts::reject_unsupported`] is the shared way to
281    /// do it, and the error it builds names the option. Ignoring it is not
282    /// an available answer: `connect` returns `io::Result<Self::Stream>`
283    /// and nothing else, so an option quietly dropped here is dropped
284    /// without a trace anywhere in the stack.
285    ///
286    /// On platforms with file descriptors the whole set is applied outside
287    /// the runtime, on a `socket2::Socket`, and the runtime only adopts the
288    /// finished socket ([`TcpAdoptStd`]) — which is why both shipped
289    /// runtimes declare [`TcpOptsSupport::ALL`] and never have to refuse
290    /// anything.
291    fn connect(
292        &self,
293        addr: SocketAddr,
294        opts: &TcpOpts,
295    ) -> impl Future<Output = std::io::Result<Self::Stream>>;
296
297    /// Whether [`connect_unix`](Self::connect_unix) does anything.
298    ///
299    /// [`APPLIES`](Self::APPLIES)' shape, and defaulted the same way and
300    /// for the same reason: a claim made by silence must never be stronger
301    /// than the truth. A runtime that says nothing here refuses the
302    /// setting, where one that over-claimed would fail every connect at
303    /// the socket instead of at the call that asked.
304    ///
305    /// It is a `const` rather than something the connect discovers,
306    /// because the answer is a property of the runtime and the target and
307    /// a caller should learn it at configuration rather than on the wire —
308    /// which is what lets `hclient_native::Native::unix_socket` refuse.
309    const SUPPORTS_UNIX: bool = false;
310
311    /// Connect to a Unix-domain socket at `path`.
312    ///
313    /// # Why it is here rather than on a seam of its own
314    ///
315    /// Because a seam of its own could not be reached. `Native`'s IO type
316    /// **is** [`Self::Stream`], so a second trait would have to produce
317    /// the same associated type — at which point it is this trait with an
318    /// extra method — and putting `R: UnixConnect` on `Native` would tax
319    /// every runtime that has no file descriptors. The `fn`-pointer trick
320    /// that keeps `Spawn` off `Native`'s signature does not work here:
321    /// `spawn` returns `()` where this returns a future, and boxing it
322    /// would drop auto traits (spec amendment C1).
323    ///
324    /// So it is a defaulted method on the seam that already exists —
325    /// `TlsConnect::reports_alpn`'s shape, `applies_ech`'s and
326    /// `TlsIdentity::presents_client_certs`': a constant defaulted to the
327    /// understating value, read by the layer above to decide whether to
328    /// **ask**.
329    ///
330    /// # No `TcpOpts`
331    ///
332    /// Not an omission: every field of [`TcpOpts`] is a TCP or IP socket
333    /// option, and `AF_UNIX` has none of them — no Nagle, no keepalive, no
334    /// source address, no interface. A parameter that could only ever be
335    /// ignored is worse than no parameter.
336    ///
337    /// The default is a refusal rather than a panic, and the error carries
338    /// [`std::io::ErrorKind::Unsupported`] so a caller who reached it
339    /// through some path that skipped
340    /// [`SUPPORTS_UNIX`](Self::SUPPORTS_UNIX) still gets an answer rather
341    /// than an abort.
342    fn connect_unix(
343        &self,
344        path: &std::path::Path,
345    ) -> impl Future<Output = std::io::Result<Self::Stream>> {
346        let _ = path;
347        async {
348            Err(std::io::Error::new(
349                std::io::ErrorKind::Unsupported,
350                UnixSocketsUnsupported,
351            ))
352        }
353    }
354}
355
356/// A runtime that declares no Unix-domain support was asked for a
357/// connection to one.
358///
359/// Reachable only past [`TcpConnect::SUPPORTS_UNIX`], which
360/// `hclient_native::Native::unix_socket` checks at the call that
361/// configures it — so a caller normally meets the refusal where they
362/// wrote the path, not on the wire.
363#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
364#[error("this runtime does not connect to Unix-domain sockets")]
365pub struct UnixSocketsUnsupported;
366
367/// On platforms with file descriptors, the whole set of socket options is
368/// applied outside the runtime, and the runtime only adopts the finished
369/// socket.
370pub trait TcpAdoptStd: TcpConnect {
371    fn adopt(&self, std: std::net::TcpStream) -> std::io::Result<Self::Stream>;
372}
373
374/// A separate trait, not a method: `getaddrinfo` blocks, and wasm and
375/// embedded have no blocking pool at all. The absence of the capability
376/// must be a compile error, not an `unimplemented!()` in the runtime.
377///
378/// **The one place in the whole project where we declare `Send` ourselves**,
379/// and here it's honest: both `tokio::task::spawn_blocking` and
380/// `blocking::unblock` require `Send + 'static`, and the `Blocking`
381/// capability doesn't exist on wasm at all — there's nothing for it to
382/// infect. The justification is `amendment-C5` (`docs/exceptions.md`), an
383/// amendment separate from C1/C2: those two are about erasing auto-traits in `dyn
384/// Trait` on the `Client -> Transport` path, whereas here the bound is
385/// declared directly in the signature of a capability trait that simply
386/// doesn't exist on wasm.
387///
388/// The bounds live in `where`, not in the generic parameter list `fn
389/// run<T: Send + …>`, so each one can carry its own `send-bound-exception`
390/// marker on its own line: the CI `no-declared-send` job matches bound
391/// declarations line by line, and a single shared comment after the
392/// generic list wouldn't cover it.
393///
394/// Two distinct failure modes of `f` are not conflated into one channel:
395///
396/// - A panic in `f` is a bug in the calling code. It must be re-raised as a
397///   panic (`std::panic::resume_unwind`, with the original payload), not
398///   quietly turned into a value that can be `?`-propagated — otherwise the
399///   implementation hides a defect in the caller's code behind a `Result`.
400/// - The background thread pool going away (for example, the runtime
401///   shutting down while a task is still queued and hasn't started
402///   running) is not a bug in the calling code, but an ordinary runtime
403///   lifecycle event. The implementation must return [`Cancelled`], not
404///   panic: a library panicking on a normal (if rare) runtime-shutdown
405///   scenario would contradict the rest of the project ("no silent
406///   no-ops... typed error, never a discarded value" — the same principle
407///   applied here, just to failure instead of success).
408pub trait Blocking {
409    fn run<T, F>(&self, f: F) -> impl Future<Output = Result<T, Cancelled>>
410    where
411        T: Send + 'static, // send-bound-exception: amendment-C5
412        F: FnOnce() -> T + Send + 'static; // send-bound-exception: amendment-C5
413}
414
415/// The background thread pool that `Blocking::run` was supposed to run on
416/// went away before the task got to start — for example, the runtime is
417/// shutting down while the task is still queued. No payload: this is not a
418/// failure of `f` (`f` never ran at all), but a signal from the runtime
419/// that there will be no result.
420///
421/// A panic in `f`, by contrast, does NOT become `Cancelled` — it is
422/// re-raised as a panic by the `Blocking` implementation, see the trait's
423/// doc comment.
424#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
425#[error("blocking task pool went away before the work started")]
426pub struct Cancelled;
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use std::pin::Pin;
432    use std::task::Context;
433    use std::task::Poll;
434
435    #[test]
436    fn tcp_opts_default_is_conservative() {
437        // All SIX fields, not four: a hand-written `Default` that set
438        // `send_buffer_size`/`recv_buffer_size` to `Some(1 << 20)` would
439        // pass this test unnoticed if only the other four were checked
440        // if only the other four were checked. `#[derive(Default)]` gives
441        // `None` by construction, but the test's name promises the
442        // whole struct — so the test must check the whole struct.
443        let o = TcpOpts::default();
444        // "The user turns nodelay on, not us" is how this line read until
445        // the 41 ms was measured, and it was half right: the user, or the
446        // transport that knows what protocol is about to be spoken —
447        // `hclient_native::Native::new`, which asks for it and only where
448        // `TcpConnect::APPLIES` says the runtime applies it. Not this
449        // seam, which cannot know either thing, and where a `true` would
450        // become a refused connect on every backend that left `APPLIES`
451        // at its default. See the type's doc.
452        assert!(!o.nodelay, "the seam has no opinion about who is writing");
453        assert!(o.keepalive.is_none());
454        assert!(o.local_address.is_none());
455        assert!(o.send_buffer_size.is_none());
456        assert!(o.recv_buffer_size.is_none());
457        assert!(!o.reuse_address);
458    }
459
460    /// Every field of `TcpOpts` set to something a runtime would have to
461    /// act on, paired with the `TcpOptsSupport` field that covers it.
462    ///
463    /// Named for a count until the count changed, which is why it is not
464    /// named for one any more: the pairing is what the tests below read,
465    /// and a name carrying a number goes stale the first time the struct
466    /// grows.
467    fn every_field_set() -> TcpOpts {
468        TcpOpts {
469            nodelay: true,
470            keepalive: Some(Duration::from_secs(30)),
471            keepalive_interval: Some(Duration::from_secs(5)),
472            keepalive_retries: Some(3),
473            bind_device: Some("lo".to_owned()),
474            user_timeout: Some(Duration::from_secs(20)),
475            local_address: Some(IpAddr::from([127, 0, 0, 1])),
476            send_buffer_size: Some(4096),
477            recv_buffer_size: Some(4096),
478            reuse_address: true,
479        }
480    }
481
482    /// Every option name, in `TcpOpts`' own field order — which is the
483    /// order `UnsupportedTcpOpts::names` walks, so this list going stale
484    /// is the same failure as that one going stale.
485    ///
486    /// The length is inferred rather than written: it was `[&str; 6]`, and
487    /// a number in a type is one more thing to remember when the struct
488    /// grows. It grew.
489    const NAMES: &[&str] = &[
490        "nodelay",
491        "keepalive",
492        "keepalive_interval",
493        "keepalive_retries",
494        "bind_device",
495        "user_timeout",
496        "local_address",
497        "send_buffer_size",
498        "recv_buffer_size",
499        "reuse_address",
500    ];
501
502    /// `TcpOptsSupport::ALL` with exactly one field turned off, in the same
503    /// order as `NAMES` — so a test can walk both together and check that
504    /// the error names the one option that was withheld.
505    fn all_but(i: usize) -> TcpOptsSupport {
506        let mut can = TcpOptsSupport::ALL;
507        match i {
508            0 => can.nodelay = false,
509            1 => can.keepalive = false,
510            2 => can.keepalive_interval = false,
511            3 => can.keepalive_retries = false,
512            4 => can.bind_device = false,
513            5 => can.user_timeout = false,
514            6 => can.local_address = false,
515            7 => can.send_buffer_size = false,
516            8 => can.recv_buffer_size = false,
517            9 => can.reuse_address = false,
518            _ => unreachable!("one arm per NAMES entry"),
519        }
520        can
521    }
522
523    #[test]
524    fn reject_unsupported_is_a_no_op_against_all() {
525        // The claim `TcpConnect::APPLIES`' doc makes about the two shipped
526        // runtimes: they apply the whole set, so the check they don't call
527        // could not have refused anything anyway.
528        assert!(
529            every_field_set()
530                .reject_unsupported(TcpOptsSupport::ALL)
531                .is_ok()
532        );
533    }
534
535    #[test]
536    fn a_runtime_that_applies_nothing_still_serves_a_caller_that_asked_for_nothing() {
537        // Why `TcpOptsSupport::NONE` is a usable default and not a brick
538        // wall: `TcpOpts::default()` sets nothing, and that is what
539        // `Native` passes unless the caller called `tcp_opts`.
540        assert!(
541            TcpOpts::default()
542                .reject_unsupported(TcpOptsSupport::NONE)
543                .is_ok()
544        );
545    }
546
547    #[test]
548    fn each_unappliable_option_is_named_on_its_own() {
549        // One case per option, not one case: an implementation that named
550        // a fixed option, or the first one it found, would pass a test
551        // that only ever withheld `nodelay`.
552        //
553        // **Compared as data and not as substrings of the message**, which
554        // is what the neighbour above already does and this one did not.
555        // It worked while no two names shared a prefix; `keepalive` and
556        // `keepalive_interval` ended that, and the failure was the test
557        // reporting that a withheld `keepalive_interval` had *also* named
558        // `keepalive` — which the message never did.
559        for (i, name) in NAMES.iter().enumerate() {
560            let err = every_field_set()
561                .reject_unsupported(all_but(i))
562                .expect_err("the one option this runtime cannot apply was set");
563            let named: Vec<&str> = err
564                .get_ref()
565                .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
566                .expect("typed payload")
567                .names()
568                .collect();
569            assert_eq!(
570                named,
571                [*name],
572                "a withheld {name} must be the only option named"
573            );
574            // And the message really does carry it, since that is what a
575            // caller who does not downcast will read.
576            assert!(err.to_string().contains(name), "{err}");
577        }
578    }
579
580    #[test]
581    fn the_message_names_the_constant_an_implementor_would_have_to_change() {
582        // The other audience for this error is the backend author whose
583        // `connect` applies the option perfectly well and whose `APPLIES`
584        // line is missing — `TokioHandle`, in this workspace, found by
585        // measurement rather than by reading. The option's name sends
586        // them to their `connect` body; the constant's name sends them to
587        // the defect.
588        let err = every_field_set()
589            .reject_unsupported(all_but(0))
590            .expect_err("nodelay was withheld");
591        let msg = err.to_string();
592        assert!(msg.contains("TcpConnect::APPLIES"), "{msg}");
593    }
594
595    #[test]
596    fn all_offending_options_are_named_not_only_the_first() {
597        let err = every_field_set()
598            .reject_unsupported(TcpOptsSupport::NONE)
599            .expect_err("nothing can be applied and everything was asked for");
600        let msg = err.to_string();
601        for name in NAMES {
602            assert!(msg.contains(name), "{name} missing from: {msg}");
603        }
604    }
605
606    #[test]
607    fn the_error_is_unsupported_and_carries_a_typed_payload() {
608        // `ErrorKind::Unsupported` rather than `Other`, and the names
609        // reachable as data rather than only by parsing the message —
610        // otherwise a caller wanting to react per-option has to scrape
611        // `Display`.
612        // Indexed through `NAMES` rather than by a literal, so that a
613        // field inserted above this one moves the index and the expected
614        // name together. It was `all_but(2)` against `["local_address"]`
615        // and four fields arrived above it.
616        const I: usize = 6;
617        let err = every_field_set()
618            .reject_unsupported(all_but(I))
619            .expect_err("one option was withheld");
620        assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
621        let payload = err
622            .get_ref()
623            .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
624            .expect("the typed payload survives the trip through io::Error");
625        assert_eq!(payload.names().collect::<Vec<_>>(), [NAMES[I]]);
626        assert_eq!(NAMES[I], "local_address", "the index still names it");
627    }
628
629    #[test]
630    fn an_option_left_unset_is_not_an_offence_even_when_unsupported() {
631        // The check is about what the caller ASKED for, not about what the
632        // runtime lacks: a runtime that applies nothing owes nothing to a
633        // caller who set nothing. Without this distinction
634        // `TcpOptsSupport::NONE` would refuse every connect.
635        let opts = TcpOpts {
636            nodelay: true,
637            ..TcpOpts::default()
638        };
639        let err = opts
640            .reject_unsupported(TcpOptsSupport::NONE)
641            .expect_err("nodelay was set and cannot be applied");
642        let payload = err
643            .get_ref()
644            .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
645            .expect("typed payload");
646        assert_eq!(payload.names().collect::<Vec<_>>(), ["nodelay"], "{err}");
647    }
648
649    #[test]
650    fn a_runtime_that_declares_nothing_applies_nothing() {
651        // The default is a claim made by silence, and this is the only
652        // test that reads it. All three shipped runtimes declare
653        // `APPLIES` explicitly — tokio and smol `ALL`, embassy its own
654        // two-of-six — so flipping the default to `ALL` passes the whole
655        // workspace suite otherwise: 878/878, measured.
656        // The rule it protects is that a backend which forgets the line
657        // must understate itself, so the worst case is one refused
658        // connect too many rather than an option dropped on the floor
659        // without a trace.
660        struct Forgetful;
661        // Never constructed: it exists only so `Forgetful` can satisfy
662        // the associated type without a runtime behind it.
663        struct NeverIo;
664        impl hyper::rt::Read for NeverIo {
665            fn poll_read(
666                self: Pin<&mut Self>,
667                _: &mut Context<'_>,
668                _: hyper::rt::ReadBufCursor<'_>,
669            ) -> Poll<std::io::Result<()>> {
670                unreachable!("this runtime never connects")
671            }
672        }
673        impl hyper::rt::Write for NeverIo {
674            fn poll_write(
675                self: Pin<&mut Self>,
676                _: &mut Context<'_>,
677                _: &[u8],
678            ) -> Poll<std::io::Result<usize>> {
679                unreachable!("this runtime never connects")
680            }
681            fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<std::io::Result<()>> {
682                unreachable!("this runtime never connects")
683            }
684            fn poll_shutdown(
685                self: Pin<&mut Self>,
686                _: &mut Context<'_>,
687            ) -> Poll<std::io::Result<()>> {
688                unreachable!("this runtime never connects")
689            }
690        }
691        impl TcpConnect for Forgetful {
692            type Stream = NeverIo;
693            // No `APPLIES` line, deliberately — that absence is the
694            // subject of this test.
695            async fn connect(&self, _: SocketAddr, _: &TcpOpts) -> std::io::Result<NeverIo> {
696                unreachable!("this runtime never connects")
697            }
698        }
699
700        assert_eq!(
701            <Forgetful as TcpConnect>::APPLIES,
702            TcpOptsSupport::NONE,
703            "a runtime that declares nothing must not claim to apply anything"
704        );
705        // And the consequence, not only the constant: a caller who asks
706        // such a runtime for all six gets all six refused by name, rather
707        // than silently honoured on paper.
708        let err = every_field_set()
709            .reject_unsupported(<Forgetful as TcpConnect>::APPLIES)
710            .expect_err("a runtime that applies nothing must refuse everything asked of it");
711        let payload = err
712            .get_ref()
713            .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
714            .expect("typed payload");
715        assert_eq!(payload.names().collect::<Vec<_>>(), NAMES);
716    }
717
718    #[test]
719    fn spawn_is_generic_over_the_future_not_boxed() {
720        // The shape is copied from hyper::rt::Executor: generic over F,
721        // zero bounds in the declaration. Send is added by the impl, not
722        // the trait.
723        struct Immediate;
724        impl<F: std::future::Future<Output = ()>> Spawn<F> for Immediate {
725            fn spawn(&self, f: F) {
726                futures_executor::block_on(f)
727            }
728        }
729        let done = std::rc::Rc::new(std::cell::Cell::new(false));
730        let d = done.clone();
731        // !Send future — the trait allows this.
732        Immediate.spawn(async move { d.set(true) });
733        assert!(done.get());
734    }
735}