|
12 | 12 | //! two, and [`Proxy`] rather than [`capture`]. |
13 | 13 |
|
14 | 14 | use std::io::{BufRead, BufReader, Read, Write}; |
15 | | -use std::net::TcpListener; |
| 15 | +use std::net::{TcpListener, TcpStream}; |
16 | 16 |
|
17 | 17 | use ytsaurus_client::{ |
18 | 18 | Client, ClientError, DataFormat, Method, OperationFilter, OperationParameters, RetryPolicy, |
@@ -1022,11 +1022,40 @@ fn refusal() -> Vec<u8> { |
1022 | 1022 | } |
1023 | 1023 |
|
1024 | 1024 | /// An address nothing is listening on, for a proxy that has gone away. |
| 1025 | +/// |
| 1026 | +/// It has one job — a connection to it must be *refused*, at once — and one |
| 1027 | +/// hazard behind that job: the port must be one no other stub in the suite can |
| 1028 | +/// end up bound to, or "nowhere" becomes somewhere. |
| 1029 | +/// |
| 1030 | +/// The obvious way is to bind `127.0.0.1:0`, read the port the OS chose, and |
| 1031 | +/// drop the listener so nothing is left answering. That refuses connections — |
| 1032 | +/// until the OS hands the freed port to another test's `TcpListener::bind`. The |
| 1033 | +/// range is cycled through before a port is reused, so within one binary it |
| 1034 | +/// almost never comes round; across a suite that binds thousands of sockets it |
| 1035 | +/// does, and then a request meant to go nowhere reaches a live listener (or the |
| 1036 | +/// stub that took the port receives a stray request). It is the kind of flake |
| 1037 | +/// that passes on a re-run and cannot be reproduced on demand. |
| 1038 | +/// |
| 1039 | +/// A **privileged** port sidesteps the hazard entirely: `bind("127.0.0.1:0")` |
| 1040 | +/// draws from the ephemeral range only — 32768+ on Linux, 49152+ on macOS — so |
| 1041 | +/// nothing in this suite can ever be assigned a port below 1024, and no test |
| 1042 | +/// process binds one deliberately. Nothing is listening there, so a connection |
| 1043 | +/// is refused immediately on both platforms. The refusal is *verified* rather |
| 1044 | +/// than assumed: the first candidate that actually refuses is the one returned, |
| 1045 | +/// so a machine that happens to run something on one of these ports is skipped |
| 1046 | +/// rather than silently turning "nowhere" into a live host. |
1025 | 1047 | fn nowhere() -> String { |
1026 | | - let listener = TcpListener::bind("127.0.0.1:0").expect("binds"); |
1027 | | - let address = listener.local_addr().expect("has an address"); |
1028 | | - drop(listener); |
1029 | | - address.to_string() |
| 1048 | + for port in 1u16..=16 { |
| 1049 | + let address = format!("127.0.0.1:{port}"); |
| 1050 | + let socket = address.parse().expect("a valid loopback address"); |
| 1051 | + match TcpStream::connect_timeout(&socket, std::time::Duration::from_millis(200)) { |
| 1052 | + Err(error) if error.kind() == std::io::ErrorKind::ConnectionRefused => return address, |
| 1053 | + // A listener answered, or the connect timed out: not nowhere. Try |
| 1054 | + // the next reserved port rather than hand back a live one. |
| 1055 | + _ => continue, |
| 1056 | + } |
| 1057 | + } |
| 1058 | + panic!("no reserved loopback port refused a connection; cannot address nowhere"); |
1030 | 1059 | } |
1031 | 1060 |
|
1032 | 1061 | /// A client that discovers, though it is talking to a listener on loopback. |
|
0 commit comments