diff --git a/crates/e2e_test/src/archive_download_integrity_test.rs b/crates/e2e_test/src/archive_download_integrity_test.rs index aad253cb8..ac23f5d70 100644 --- a/crates/e2e_test/src/archive_download_integrity_test.rs +++ b/crates/e2e_test/src/archive_download_integrity_test.rs @@ -62,6 +62,9 @@ mod tests { let binary_path = rustfs_binary_path(); let mut command = Command::new(&binary_path); command.env("RUST_LOG", "rustfs=info,rustfs_notify=debug"); + // Keep the embedded console off its fixed default port :9001, which may + // already be taken by unrelated local services (e.g. Docker Desktop). + command.env("RUSTFS_CONSOLE_ENABLE", "false"); for (key, value) in extra_env { command.env(key, value); } diff --git a/crates/e2e_test/src/common.rs b/crates/e2e_test/src/common.rs index e5a83c387..fbeedabc4 100644 --- a/crates/e2e_test/src/common.rs +++ b/crates/e2e_test/src/common.rs @@ -391,6 +391,10 @@ impl RustFSTestEnvironment { let binary_path = rustfs_binary_path(); let mut command = Command::new(&binary_path); command.env("RUST_LOG", "rustfs=info,rustfs_notify=debug"); + // The embedded console would bind the fixed default port :9001, which + // collides with unrelated local services (e.g. Docker Desktop). Tests + // that need the console can still override this via extra_env. + command.env("RUSTFS_CONSOLE_ENABLE", "false"); for (key, value) in extra_env { command.env(key, value); } @@ -435,11 +439,20 @@ impl RustFSTestEnvironment { /// connections before the S3 stack is fully initialized, which causes /// early requests to fail intermittently. Treat readiness as "S3 API /// responds successfully" instead. - pub async fn wait_for_server_ready(&self) -> Result<(), Box> { + /// + /// Fails immediately if the spawned server process has already exited + /// (e.g. a port bind failure) instead of polling out the full timeout. + pub async fn wait_for_server_ready(&mut self) -> Result<(), Box> { info!("Waiting for RustFS server to be ready on {}", self.address); let client = self.create_s3_client(); for i in 0..60 { + if let Some(process) = self.process.as_mut() + && let Ok(Some(status)) = process.try_wait() + { + return Err(format!("RustFS server process exited before becoming ready: {status}").into()); + } + if TcpStream::connect(&self.address).await.is_ok() && client.list_buckets().send().await.is_ok() { info!("✅ RustFS server is ready after {} attempts", i + 1); return Ok(()); diff --git a/crates/e2e_test/src/lib.rs b/crates/e2e_test/src/lib.rs index 0f1be0e99..272a8a55a 100644 --- a/crates/e2e_test/src/lib.rs +++ b/crates/e2e_test/src/lib.rs @@ -46,6 +46,10 @@ mod list_objects_duplicates_test; #[cfg(test)] mod quota_test; +// Harness regression tests: console port isolation + fail-fast startup +#[cfg(test)] +mod server_startup_failfast_test; + #[cfg(test)] mod bucket_policy_check_test; diff --git a/crates/e2e_test/src/server_startup_failfast_test.rs b/crates/e2e_test/src/server_startup_failfast_test.rs new file mode 100644 index 000000000..784a08e56 --- /dev/null +++ b/crates/e2e_test/src/server_startup_failfast_test.rs @@ -0,0 +1,60 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Regression tests for e2e harness server-startup behavior. +//! +//! The harness spawns servers with the embedded console disabled so they never +//! contend for its fixed default port :9001 (often held by unrelated local +//! services such as Docker Desktop), and `wait_for_server_ready` must report a +//! server that dies during startup immediately instead of polling out the full +//! readiness timeout. + +#[cfg(test)] +mod tests { + use crate::common::{RustFSTestEnvironment, init_logging, rustfs_binary_path}; + use serial_test::serial; + use std::net::TcpListener; + use std::time::{Duration, Instant}; + + /// Force the console back on (extra_env overrides the harness default) + /// while :9001 is occupied: the server exits at startup, and the harness + /// must surface that promptly rather than waiting out the 60s timeout. + #[tokio::test] + #[serial] + async fn test_start_fails_fast_when_server_exits_during_startup() { + init_logging(); + + // Occupy :9001 on both stacks; a failed bind means another local + // process already holds the port, which serves equally well. + let _guard_v6 = TcpListener::bind(("::", 9001)).ok(); + let _guard_v4 = TcpListener::bind(("0.0.0.0", 9001)).ok(); + + // Resolve (and possibly build) the binary before starting the timer. + let _ = rustfs_binary_path(); + + let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment"); + let started = Instant::now(); + let result = env + .start_rustfs_server_with_env(vec![], &[("RUSTFS_CONSOLE_ENABLE", "true")]) + .await; + let elapsed = started.elapsed(); + + let err = result.expect_err("server start should fail while :9001 is occupied"); + assert!(err.to_string().contains("exited before becoming ready"), "unexpected start error: {err}"); + assert!( + elapsed < Duration::from_secs(30), + "startup failure should be detected fast, took {elapsed:?}" + ); + } +}