diff --git a/crates/fake-fxserver/tests/archive.rs b/crates/fake-fxserver/tests/archive.rs index 19521e03..b81de10d 100644 --- a/crates/fake-fxserver/tests/archive.rs +++ b/crates/fake-fxserver/tests/archive.rs @@ -3,6 +3,8 @@ // The helpers below are test code too, though not inside a `#[test]` function. #![allow(clippy::unwrap_used)] +mod common; + use std::io::{BufRead, BufReader}; use std::net::TcpListener; use std::process::{ChildStdout, Command, Stdio}; @@ -10,8 +12,7 @@ use std::sync::mpsc; use std::thread; use std::time::Duration; -/// The build tests' port: Cargo runs one test binary at a time. -const PORT: u16 = 41956; +use common::Port; /// The first line on `stdout`, unless none comes within 30 seconds. fn first_line(stdout: ChildStdout) -> Option { @@ -28,14 +29,15 @@ fn first_line(stdout: ChildStdout) -> Option { /// tells that it is the one listening. #[test] fn fake_archive_says_it_listens_only_on_a_port_of_its_own() { - let other = TcpListener::bind(("127.0.0.1", PORT)).unwrap(); + let port = Port::free(); + let other = TcpListener::bind(("127.0.0.1", port.number)).unwrap(); let refused = Command::new(env!("CARGO_BIN_EXE_fake-archive")) - .arg(PORT.to_string()) + .arg(port.number.to_string()) .output() .unwrap(); drop(other); let mut serving = Command::new(env!("CARGO_BIN_EXE_fake-archive")) - .arg(PORT.to_string()) + .arg(port.number.to_string()) .stdout(Stdio::piped()) .spawn() .unwrap(); @@ -48,5 +50,5 @@ fn fake_archive_says_it_listens_only_on_a_port_of_its_own() { refused.stdout.is_empty(), "a port it could not take is never announced" ); - assert_eq!(said.as_deref(), Some("listening on 127.0.0.1:41956\n")); + assert_eq!(said, Some(format!("listening on 127.0.0.1:{}\n", port.number))); } diff --git a/crates/fake-fxserver/tests/artifacts.rs b/crates/fake-fxserver/tests/artifacts.rs index 978bce7c..44ff2412 100644 --- a/crates/fake-fxserver/tests/artifacts.rs +++ b/crates/fake-fxserver/tests/artifacts.rs @@ -7,14 +7,17 @@ // The helpers below are test code too, though not inside a `#[test]` function. #![allow(clippy::unwrap_used)] +mod common; + use std::fs; use std::io::{BufRead, BufReader}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; -use std::sync::{LazyLock, Mutex, PoisonError}; +use std::sync::LazyLock; use std::time::Duration; +use common::{Port, Ports}; use jiff::{SignedDuration, Timestamp, Unit}; use observer_app::{ Accounts, ArtifactTimings, Artifacts, ArtifactsSetup, ArtifactsTask, AuditLog, Caller, Client, Clock, @@ -46,8 +49,8 @@ const NEW: u32 = 37900; const OTHER: u32 = 37950; /// The build the fake's own archive holds. const ARCHIVED: u32 = 99001; -/// Where the fake serves its archive, apart from the browser tests' 41955. -const ARCHIVE_PORT: u16 = 41956; +/// Where the fake serves its archive. +static ARCHIVE: LazyLock = LazyLock::new(Port::free); const COMMIT: &str = "0cfcdb5ead57a4a26fcb6ebad98adbcd0273e607"; const SELECTION_ROUTE: (&str, &str) = ("PUT", "/api/servers/default/artifacts/selection"); /// The build on probation crashes late enough that Observer has seen it online and read its @@ -56,27 +59,8 @@ const CRASH_ON_PROBATION: &str = "set fake_crash_after_ms 8000"; /// The main loop of the build on probation freezes once it is ready, well before its window ends. const HANG_ON_PROBATION: &str = "set fake_hang_after_ms 5000"; -/// Ports of their own, apart from the other test binaries'. -static PORTS: LazyLock>> = LazyLock::new(|| Mutex::new((41960..=41962).collect())); - -struct Port(u16); - -impl Port { - async fn lease() -> Self { - loop { - if let Some(port) = PORTS.lock().unwrap_or_else(PoisonError::into_inner).pop() { - return Self(port); - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - } -} - -impl Drop for Port { - fn drop(&mut self) { - PORTS.lock().unwrap_or_else(PoisonError::into_inner).push(self.0); - } -} +/// The labs' ports, lent to three at a time. +static PORTS: Ports = Ports::new(3); struct Lab { dir: TempDir, @@ -99,7 +83,7 @@ impl Lab { /// healthy, and 37900 installed beside it doing what `new_build` says. Its catalog lists /// 99001, downloaded from the fake's own archive. async fn new(stable_for: Duration, new_build: &str) -> Self { - let port = Port::lease().await; + let port = PORTS.lease().await; let dir = tempfile::tempdir().unwrap(); let root = dir.path().join("observer-data/artifacts"); install_fake(&root, HEALTHY, ""); @@ -110,7 +94,7 @@ impl Lab { data.join("server.cfg"), format!( "endpoint_add_tcp \"127.0.0.1:{0}\"\nendpoint_add_udp \"127.0.0.1:{0}\"\nsv_maxclients 8\n", - port.0 + port.number ), ) .unwrap(); @@ -153,7 +137,7 @@ impl Lab { supervision: supervision.clone(), schedule: schedule.status(), catalog: Catalog::fixed(Ok(listing)), - http: Http::local(ARCHIVE_PORT), + http: Http::local(ARCHIVE.number), }, |timings: &mut ArtifactTimings| timings.stable_for = stable_for, ) @@ -292,7 +276,7 @@ async fn supervise( accounts: accounts.clone(), servers: servers.clone(), data_dir: dir.join("observer-data"), - panel: SocketAddr::from(([127, 0, 0, 1], 41975)), + panel: common::panel(), artifacts: dir.join("observer-data/artifacts"), console_logs: dir.join("observer-data/logs/console"), log: None, @@ -395,7 +379,8 @@ fn archived() -> CatalogBuild { latest: false, availability: Availability::Downloadable { url: format!( - "http://127.0.0.1:{ARCHIVE_PORT}/{ARCHIVED}-{COMMIT}/{}", + "http://127.0.0.1:{}/{ARCHIVED}-{COMMIT}/{}", + ARCHIVE.number, kind.file_name() ) .parse() @@ -411,7 +396,7 @@ fn archived() -> CatalogBuild { } } -/// `fake-archive` serving the fake on `ARCHIVE_PORT`, until dropped. +/// `fake-archive` serving the fake on [`ARCHIVE`]'s port, until dropped. struct ArchiveServer(Child); impl ArchiveServer { @@ -419,7 +404,7 @@ impl ArchiveServer { /// port would answer too, so the line is waited for rather than a connection. fn start() -> Self { let mut server = Command::new(env!("CARGO_BIN_EXE_fake-archive")) - .arg(ARCHIVE_PORT.to_string()) + .arg(ARCHIVE.number.to_string()) .stdout(Stdio::piped()) .spawn() .unwrap(); @@ -429,7 +414,8 @@ impl ArchiveServer { .unwrap(); assert!( !said.is_empty(), - "fake-archive could not serve on port {ARCHIVE_PORT}, see its error above" + "fake-archive could not serve on port {}, see its error above", + ARCHIVE.number ); Self(server) } diff --git a/crates/fake-fxserver/tests/changes.rs b/crates/fake-fxserver/tests/changes.rs index 60d488a0..1a80733b 100644 --- a/crates/fake-fxserver/tests/changes.rs +++ b/crates/fake-fxserver/tests/changes.rs @@ -3,12 +3,14 @@ // The helpers below are test code too, though not inside a `#[test]` function. #![allow(clippy::unwrap_used)] +mod common; + use std::fs; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr}; use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, Mutex, PoisonError}; use std::time::{Duration, Instant}; +use common::{Port, Ports}; use jiff::Timestamp; use observer_app::{ Accounts, Caller, Changes, ChangesSetup, ChangesTask, Client, Clock, ComparedTo, DiffOptions, @@ -39,26 +41,8 @@ const CONSOLE_ROUTE: (&str, &str) = ("POST", "/api/servers/default/console"); /// A webhook planted in a resource, which no stored text may hold. const WEBHOOK: &str = "https://discord.com/api/webhooks/123456/PlantedTokenThatMustNeverBeStored"; -static PORTS: LazyLock>> = LazyLock::new(|| Mutex::new((41940..=41942).collect())); - -struct Port(u16); - -impl Port { - async fn lease() -> Self { - loop { - if let Some(port) = PORTS.lock().unwrap_or_else(PoisonError::into_inner).pop() { - return Self(port); - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - } -} - -impl Drop for Port { - fn drop(&mut self) { - PORTS.lock().unwrap_or_else(PoisonError::into_inner).push(self.0); - } -} +/// The labs' ports, lent to three at a time. +static PORTS: Ports = Ports::new(3); struct Lab { dir: TempDir, @@ -74,7 +58,7 @@ struct Lab { impl Lab { async fn new() -> Self { - let port = Port::lease().await; + let port = PORTS.lease().await; let dir = tempfile::tempdir().unwrap(); let observer_data = dir.path().join("observer-data"); let build = observer_data.join("artifacts").join(BUILD.to_string()); @@ -82,7 +66,7 @@ impl Lab { fs::create_dir_all(program.parent().unwrap()).unwrap(); fs::copy(env!("CARGO_BIN_EXE_fake-fxserver"), &program).unwrap(); let data = dir.path().join("server-data"); - plant(&data, port.0); + plant(&data, port.number); let db = Database::open(dir.path().join("observer.sqlite3"), dir.path().join("backups")) .await .unwrap(); @@ -95,7 +79,7 @@ impl Lab { accounts, servers, data_dir: observer_data.clone(), - panel: SocketAddr::from(([127, 0, 0, 1], 41975)), + panel: common::panel(), artifacts: observer_data.join("artifacts"), console_logs: observer_data.join("logs/console"), log: None, diff --git a/crates/fake-fxserver/tests/common/mod.rs b/crates/fake-fxserver/tests/common/mod.rs new file mode 100644 index 00000000..b9fc3d0d --- /dev/null +++ b/crates/fake-fxserver/tests/common/mod.rs @@ -0,0 +1,76 @@ +//! Loopback ports for the servers these tests start, each given by the system: another run of the +//! tests on the same host, from another checkout, gets other ports. + +// Each test file uses some of these helpers; all are test code, though not in `#[test]` functions. +#![allow(dead_code, clippy::unwrap_used)] + +use std::io::ErrorKind; +use std::net::{Ipv4Addr, SocketAddr, TcpListener}; +use std::ops::RangeInclusive; +use std::sync::LazyLock; + +use tokio::sync::{Semaphore, SemaphorePermit}; + +/// The loopback address a test holds its port on, which leaves 127.0.0.1 to FXServer. +const CLAIMS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 2); +/// txAdmin's ports, among those Linux gives: the cfg scan warns of an endpoint on one of them. +const TXADMIN: RangeInclusive = 40120..=40150; + +/// A port of 127.0.0.1 the system gave, this test's own until dropped. +pub struct Port { + pub number: u16, + /// The system may give a port again as soon as nothing listens on it, and FXServer only + /// listens once it has started. Whoever is given this one meanwhile, in this run or another, + /// cannot claim it and asks for another. + _claim: TcpListener, + _turn: Option>, +} + +impl Port { + pub fn free() -> Self { + loop { + let free = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let number = free.local_addr().unwrap().port(); + if TXADMIN.contains(&number) { + continue; + } + match TcpListener::bind((CLAIMS, number)) { + Ok(claim) => { + return Self { + number, + _claim: claim, + _turn: None, + }; + } + Err(error) if error.kind() == ErrorKind::AddrInUse => {} + Err(error) => panic!("cannot claim port {number} on {CLAIMS}: {error}"), + } + } + } +} + +/// Lends ports to the labs of a test file, to so many of them at a time, which bounds how many +/// servers the file runs at once. +pub struct Ports(Semaphore); + +impl Ports { + pub const fn new(at_a_time: usize) -> Self { + Self(Semaphore::const_new(at_a_time)) + } + + pub async fn lease(&'static self) -> Port { + let turn = self.0.acquire().await.unwrap(); + Port { + _turn: Some(turn), + ..Port::free() + } + } +} + +/// The panel's address the labs give Observer, which the cfg checks only compare with: a port held +/// for the whole run, so that no server is given it. +pub fn panel() -> SocketAddr { + static PANEL: LazyLock = + LazyLock::new(|| TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap()); + PANEL.local_addr().unwrap() +} diff --git a/crates/fake-fxserver/tests/pre_launch.rs b/crates/fake-fxserver/tests/pre_launch.rs index f5a3bb22..f864aaaa 100644 --- a/crates/fake-fxserver/tests/pre_launch.rs +++ b/crates/fake-fxserver/tests/pre_launch.rs @@ -3,14 +3,17 @@ // The helpers below are test code too, though not inside a `#[test]` function. #![allow(clippy::unwrap_used)] +mod common; + use std::collections::VecDeque; use std::fs; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, LazyLock, Mutex, PoisonError}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use common::{Port, Ports}; use observer_domain::launch::{ExtraArgument, LaunchSettings, OneSync}; use observer_fxserver::console::{ConsoleHub, plain}; use observer_fxserver::health::HealthLimits; @@ -31,28 +34,8 @@ const HEARTBEAT_MS: &str = "200"; /// How often the fake work looks at its flags. const POLL: Duration = Duration::from_millis(10); -/// The ports tests may use, lent to one test at a time. 41929 and up belong to other Observers -/// of the development host. -static PORTS: LazyLock>> = LazyLock::new(|| Mutex::new((41920..=41928).collect())); - -struct Port(u16); - -impl Port { - async fn lease() -> Self { - loop { - if let Some(port) = PORTS.lock().unwrap_or_else(PoisonError::into_inner).pop() { - return Self(port); - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - } -} - -impl Drop for Port { - fn drop(&mut self) { - PORTS.lock().unwrap_or_else(PoisonError::into_inner).push(self.0); - } -} +/// The labs' ports, lent to nine at a time. +static PORTS: Ports = Ports::new(9); /// What one run of the fake work does. #[derive(Debug, Clone)] @@ -170,7 +153,7 @@ impl Lab { /// A lab whose supervisor runs `work` before each spawn, with settings `tune` adjusts. async fn tuned(work: &Arc, tune: impl FnOnce(&mut Settings)) -> Self { - let port = Port::lease().await; + let port = PORTS.lease().await; let dir = tempfile::tempdir().unwrap(); let program = fake_program(&dir.path().join("build")); fs::create_dir_all(program.parent().unwrap()).unwrap(); @@ -181,7 +164,7 @@ impl Lab { data.join("server.cfg"), format!( "endpoint_add_tcp \"127.0.0.1:{0}\"\nendpoint_add_udp \"127.0.0.1:{0}\"\nsv_hostname \"Observer test\"\nsv_maxclients 8\n", - port.0 + port.number ), ) .unwrap(); @@ -385,7 +368,7 @@ async fn the_work_runs_before_the_spawn_and_its_lines_carry_the_coming_generatio ); assert_eq!( seen[0].endpoint.map(|endpoint| endpoint.port()), - Some(lab.port.0), + Some(lab.port.number), "and the scan of its cfg" ); assert_eq!(seen[0].cause, Cause::Requested { by: by("test") }); diff --git a/crates/fake-fxserver/tests/runtime.rs b/crates/fake-fxserver/tests/runtime.rs index 874cd277..aba6fa41 100644 --- a/crates/fake-fxserver/tests/runtime.rs +++ b/crates/fake-fxserver/tests/runtime.rs @@ -3,12 +3,14 @@ // The helpers below are test code too, though not inside a `#[test]` function. #![allow(clippy::unwrap_used)] +mod common; + use std::fs; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr}; use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, Mutex, PoisonError}; use std::time::{Duration, Instant}; +use common::{Port, Ports}; use jiff::{SignedDuration, Timestamp, Unit}; use observer_app::{ Accounts, AuditLog, Bans, BansSetup, Caller, Client, Clock, DatabaseFiles, Diagnostics, DiagnosticsSetup, @@ -58,27 +60,8 @@ const FAKE: [(&str, &str); 3] = [ ("fake_tick_ms", "12"), ]; -/// Ports of their own: test binaries run one after the other, but this one's tests run together. -static PORTS: LazyLock>> = LazyLock::new(|| Mutex::new((41926..=41928).collect())); - -struct Port(u16); - -impl Port { - async fn lease() -> Self { - loop { - if let Some(port) = PORTS.lock().unwrap_or_else(PoisonError::into_inner).pop() { - return Self(port); - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - } -} - -impl Drop for Port { - fn drop(&mut self) { - PORTS.lock().unwrap_or_else(PoisonError::into_inner).push(self.0); - } -} +/// The Observers' ports, lent to three at a time. +static PORTS: Ports = Ports::new(3); struct Observer { dir: TempDir, @@ -144,7 +127,7 @@ impl Services { accounts: self.accounts.clone(), servers: self.servers.clone(), data_dir: dir.join("observer-data"), - panel: SocketAddr::from(([127, 0, 0, 1], 41975)), + panel: common::panel(), artifacts: dir.join("observer-data/artifacts"), console_logs: dir.join("observer-data/logs/console"), log: None, @@ -212,7 +195,7 @@ impl Observer { /// The fake takes `arguments` instead of [`FAKE`]'s. async fn launched(auto_start: bool, longest_sleep: Duration, arguments: &[(&str, &str)]) -> Self { - let port = Port::lease().await; + let port = PORTS.lease().await; let dir = tempfile::tempdir().unwrap(); let build = dir.path().join("observer-data/artifacts").join(BUILD.to_string()); let program = fake_program(&build); @@ -224,7 +207,7 @@ impl Observer { data.join("server.cfg"), format!( "endpoint_add_tcp \"127.0.0.1:{0}\"\nendpoint_add_udp \"127.0.0.1:{0}\"\nsv_maxclients 8\nsets sv_projectName \"^2Observer ^7lab\"\n", - port.0 + port.number ), ) .unwrap(); @@ -511,7 +494,7 @@ async fn a_server_started_from_the_panel_runs_a_command_then_stops_gracefully() assert_eq!(online.cause, Some(StateCause::Ready)); assert_eq!(online.build, Some(35245)); assert_eq!(online.name.as_deref(), Some("Observer lab")); - assert_eq!(online.port, Some(observer.port.0)); + assert_eq!(online.port, Some(observer.port.number)); assert!(online.started_at.is_some()); assert_eq!( delivery, diff --git a/crates/fake-fxserver/tests/spawn.rs b/crates/fake-fxserver/tests/spawn.rs index d4985f2b..a06a858f 100644 --- a/crates/fake-fxserver/tests/spawn.rs +++ b/crates/fake-fxserver/tests/spawn.rs @@ -3,6 +3,8 @@ // The helpers below are test code too, though not inside a `#[test]` function. #![allow(clippy::unwrap_used)] +mod common; + use std::collections::VecDeque; use std::ffi::OsString; use std::fs; @@ -10,6 +12,7 @@ use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::time::{Duration, Instant}; +use common::Port; use observer_fxserver::bridge; use observer_fxserver::layout::{Flavour, Layout}; use observer_fxserver::trace::{Trace, TraceDecoder, TraceKind}; @@ -196,7 +199,8 @@ fn main_ticks(port: u16, authorization: Option<&str>) -> u64 { #[tokio::test] async fn a_started_server_speaks_on_every_channel_then_quits() { - let mut fake = Fake::start(Some(41920), &["+set", "fake_heartbeat_ms", "100"]); + let port = Port::free(); + let mut fake = Fake::start(Some(port.number), &["+set", "fake_heartbeat_ms", "100"]); fake.bridge_started().await; assert_eq!(fake.trace(|_| true).await, TraceKind::Probe); @@ -208,12 +212,12 @@ async fn a_started_server_speaks_on_every_channel_then_quits() { fake.trace(|kind| matches!(kind, TraceKind::Script { resource, payload } if *resource == bridge && payload["kind"] == "hb")) .await; } - let (status, body) = get(41920, "/dynamic.json", None); + let (status, body) = get(port.number, "/dynamic.json", None); assert_eq!(status, 200); let dynamic: serde_json::Value = serde_json::from_str(&body).unwrap(); assert_eq!(dynamic["hostname"], "Observer test"); assert_eq!(dynamic["sv_maxclients"], "8"); - assert_eq!(get(41920, "/players.json", None), (200, "[]".to_owned())); + assert_eq!(get(port.number, "/players.json", None), (200, "[]".to_owned())); fake.send("quit").await; @@ -234,38 +238,43 @@ async fn unknown_commands_are_answered_like_fxserver_does() { #[tokio::test] async fn perf_requires_the_credentials_set_on_stdin() { - let mut fake = Fake::start(Some(41922), &[]); + let port = Port::free(); + let mut fake = Fake::start(Some(port.number), &[]); fake.serving().await; - assert_eq!(get(41922, "/perf/", None).0, 200); + assert_eq!(get(port.number, "/perf/", None).0, 200); fake.send("set sv_prometheusBasicAuthUser observer").await; fake.send("set sv_prometheusBasicAuthPassword secret").await; let deadline = Instant::now() + WAIT; - while get(41922, "/perf/", None).0 != 401 { + while get(port.number, "/perf/", None).0 != 401 { assert!( Instant::now() < deadline, "/perf/ still answers without credentials" ); tokio::time::sleep(Duration::from_millis(20)).await; } - assert_eq!(get(41922, "/perf/", Some("Basic b2JzZXJ2ZXI6d3Jvbmc=")).0, 401); - assert!(main_ticks(41922, Some("Basic b2JzZXJ2ZXI6c2VjcmV0")) > 0); + assert_eq!( + get(port.number, "/perf/", Some("Basic b2JzZXJ2ZXI6d3Jvbmc=")).0, + 401 + ); + assert!(main_ticks(port.number, Some("Basic b2JzZXJ2ZXI6c2VjcmV0")) > 0); fake.kill().await; } #[tokio::test] async fn a_hung_main_loop_freezes_the_tick_count_while_http_still_answers() { - let mut fake = Fake::start(Some(41921), &[]); + let port = Port::free(); + let mut fake = Fake::start(Some(port.number), &[]); fake.serving().await; fake.send("fake_hang").await; tokio::time::sleep(Duration::from_millis(300)).await; - let frozen = main_ticks(41921, None); + let frozen = main_ticks(port.number, None); tokio::time::sleep(Duration::from_millis(500)).await; - assert_eq!(main_ticks(41921, None), frozen); - assert_eq!(get(41921, "/dynamic.json", None).0, 200); + assert_eq!(main_ticks(port.number, None), frozen); + assert_eq!(get(port.number, "/dynamic.json", None).0, 200); fake.send("quit").await; tokio::time::sleep(Duration::from_millis(800)).await; assert_eq!(fake.spawned.exit.get(), None, "a hung main loop cannot run quit"); @@ -275,8 +284,9 @@ async fn a_hung_main_loop_freezes_the_tick_count_while_http_still_answers() { #[tokio::test] async fn a_taken_port_is_a_bind_error_then_a_fatal_exit() { - let _taken = TcpListener::bind("127.0.0.1:41923").unwrap(); - let mut fake = Fake::start(Some(41923), &[]); + let taken = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = taken.local_addr().unwrap(); + let mut fake = Fake::start(Some(address.port()), &[]); let error = fake .trace(|kind| matches!(kind, TraceKind::BindError { .. })) @@ -285,7 +295,7 @@ async fn a_taken_port_is_a_bind_error_then_a_fatal_exit() { assert_eq!( error, TraceKind::BindError { - address: "127.0.0.1:41923".to_owned() + address: address.to_string() } ); assert_eq!(fake.exit().await, Exit::Code(1)); @@ -293,13 +303,16 @@ async fn a_taken_port_is_a_bind_error_then_a_fatal_exit() { while let Some(chunk) = fake.spawned.stderr.next().await { stderr.extend(chunk); } - assert!(String::from_utf8_lossy(&stderr).starts_with("Fatal Error: Could not bind on 127.0.0.1:41923")); + assert!( + String::from_utf8_lossy(&stderr).starts_with(&format!("Fatal Error: Could not bind on {address}")) + ); } #[tokio::test] async fn a_fatal_error_can_leave_the_process_up_until_the_tree_is_killed() { + let port = Port::free(); let mut fake = Fake::start( - Some(41924), + Some(port.number), &["+set", "fake_bind_error", "1", "+set", "fake_fatal_dialog", "1"], ); diff --git a/crates/fake-fxserver/tests/supervisor.rs b/crates/fake-fxserver/tests/supervisor.rs index fc6155ca..6ba0b2fc 100644 --- a/crates/fake-fxserver/tests/supervisor.rs +++ b/crates/fake-fxserver/tests/supervisor.rs @@ -3,13 +3,16 @@ // The helpers below are test code too, though not inside a `#[test]` function. #![allow(clippy::unwrap_used)] +mod common; + use std::fs; use std::net::{SocketAddr, TcpListener, UdpSocket}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, LazyLock, Mutex, PoisonError}; use std::time::{Duration, Instant}; +use common::{Port, Ports}; use observer_domain::diagnostics::ProcessKind; #[cfg(unix)] use observer_domain::diagnostics::ThreadRun; @@ -45,28 +48,8 @@ const KILLED: Option = Some(1); #[cfg(unix)] const KILLED: Option = None; -/// The ports tests may use, lent to one test at a time. 41929 and up belong to other Observers -/// of the development host. -static PORTS: LazyLock>> = LazyLock::new(|| Mutex::new((41920..=41928).collect())); - -struct Port(u16); - -impl Port { - async fn lease() -> Self { - loop { - if let Some(port) = PORTS.lock().unwrap_or_else(PoisonError::into_inner).pop() { - return Self(port); - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - } -} - -impl Drop for Port { - fn drop(&mut self) { - PORTS.lock().unwrap_or_else(PoisonError::into_inner).push(self.0); - } -} +/// The labs' ports, lent to nine at a time. +static PORTS: Ports = Ports::new(9); struct Lab { dir: TempDir, @@ -87,7 +70,7 @@ impl Lab { /// A lab whose supervisor settings `tune` adjusts. A hung server is killed without a snapshot /// unless `tune` asks for one. async fn tuned(extra: &[(&str, &str)], tune: impl FnOnce(&mut Settings)) -> Self { - let port = Port::lease().await; + let port = PORTS.lease().await; let dir = tempfile::tempdir().unwrap(); let program = fake_program(&dir.path().join("build")); fs::create_dir_all(program.parent().unwrap()).unwrap(); @@ -98,7 +81,7 @@ impl Lab { data.join("server.cfg"), format!( "endpoint_add_tcp \"127.0.0.1:{0}\"\nendpoint_add_udp \"127.0.0.1:{0}\"\nsv_hostname \"Observer test\"\nsv_maxclients 8\n", - port.0 + port.number ), ) .unwrap(); @@ -455,7 +438,7 @@ async fn a_started_server_goes_online_then_stops_gracefully() { } ); let spawned = lab.event(|kind| matches!(kind, EventKind::Spawned { .. })).await; - assert!(matches!(spawned, EventKind::Spawned { probe, .. } if probe.port() == lab.port.0)); + assert!(matches!(spawned, EventKind::Spawned { probe, .. } if probe.port() == lab.port.number)); assert_eq!(lab.state(State::Online).await, Cause::Ready); assert!(lab.supervisor.status().borrow().pid.is_some()); let bridge = lab.bridge(); @@ -1207,8 +1190,8 @@ async fn hang_detection_turned_back_on_reaches_the_running_server_and_counts_fro #[tokio::test] async fn a_taken_port_backs_off_more_each_time_without_tripping_the_breaker() { - let lab_port = Port::lease().await; - let taken = TcpListener::bind(("127.0.0.1", lab_port.0)).unwrap(); + let lab_port = PORTS.lease().await; + let taken = TcpListener::bind(("127.0.0.1", lab_port.number)).unwrap(); let mut lab = Lab::new(&[]).await; // Point the cfg at the taken port. let cfg = lab.dir.path().join("server-data/server.cfg"); @@ -1216,7 +1199,7 @@ async fn a_taken_port_backs_off_more_each_time_without_tripping_the_breaker() { &cfg, fs::read_to_string(&cfg) .unwrap() - .replace(&lab.port.0.to_string(), &lab_port.0.to_string()), + .replace(&lab.port.number.to_string(), &lab_port.number.to_string()), ) .unwrap(); lab.supervisor.start("test").await.unwrap(); @@ -1227,7 +1210,7 @@ async fn a_taken_port_backs_off_more_each_time_without_tripping_the_breaker() { assert_eq!( cause, Cause::BindError { - address: format!("127.0.0.1:{}", lab_port.0) + address: format!("127.0.0.1:{}", lab_port.number) } ); match lab @@ -1846,7 +1829,7 @@ async fn the_connections_the_server_opens_are_listed_with_its_listener() { while !(held(&sockets) && sockets .iter() - .any(|socket| socket.local.port() != lab.port.0 && socket.protocol == Protocol::Udp)) + .any(|socket| socket.local.port() != lab.port.number && socket.protocol == Protocol::Udp)) && Instant::now() < deadline { tokio::time::sleep(Duration::from_millis(50)).await; @@ -1854,7 +1837,7 @@ async fn the_connections_the_server_opens_are_listed_with_its_listener() { } assert!(held(&sockets), "{sockets:#?}"); - let listener: SocketAddr = format!("127.0.0.1:{}", lab.port.0).parse().unwrap(); + let listener: SocketAddr = format!("127.0.0.1:{}", lab.port.number).parse().unwrap(); assert!( sockets .iter() @@ -1863,7 +1846,7 @@ async fn the_connections_the_server_opens_are_listed_with_its_listener() { ); let udp = sockets .iter() - .find(|socket| socket.protocol == Protocol::Udp && socket.local.port() != lab.port.0) + .find(|socket| socket.protocol == Protocol::Udp && socket.local.port() != lab.port.number) .unwrap_or_else(|| panic!("no UDP socket in {sockets:#?}")); // Windows' table holds no remote end for UDP. let remote = if cfg!(windows) { None } else { Some(udp_address) };