// file: crates/apps/game-realtime-websocket-smoke/src/main.rs // version: 1 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] //! Executable localhost smoke for the public games.sasedev WebSocket realtime transport path. use game_realtime_transport_lib::RealtimeConnection; // rust-rules: trait-import use game_realtime_transport_lib::RealtimeReceiver; // rust-rules: trait-import use game_realtime_transport_lib::RealtimeSender; // rust-rules: trait-import const CLIENT_PAYLOAD: &[u8] = b"games.sasedev-client-smoke"; const SERVER_PAYLOAD: &[u8] = b"games.sasedev-server-smoke"; const SMOKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); const TRACING_TARGET: &str = "games::realtime::websocket::smoke"; #[tokio::main(flavor = "current_thread")] async fn main() -> std::process::ExitCode { let _logging_guard = match game_logging_lib::init_console_tracing() { std::result::Result::Ok(guard) => guard, std::result::Result::Err(error) => { eprintln!("failed to initialize realtime WebSocket smoke tracing: {error}"); return std::process::ExitCode::FAILURE; }, }; tracing::info!(target: TRACING_TARGET, "realtime WebSocket smoke started"); let result = tokio::time::timeout(SMOKE_TIMEOUT, run_smoke()).await; return match result { std::result::Result::Ok(std::result::Result::Ok(())) => { tracing::info!(target: TRACING_TARGET, "realtime WebSocket smoke passed"); println!("game-realtime-websocket-smoke: PASS"); std::process::ExitCode::SUCCESS }, std::result::Result::Ok(std::result::Result::Err(error)) => { tracing::error!(target: TRACING_TARGET, detail = error.as_str(), "realtime WebSocket smoke failed"); eprintln!("game-realtime-websocket-smoke: FAIL: {error}"); std::process::ExitCode::FAILURE }, std::result::Result::Err(_) => { tracing::error!(target: TRACING_TARGET, timeout_ms = SMOKE_TIMEOUT.as_millis(), "realtime WebSocket smoke timed out"); eprintln!("game-realtime-websocket-smoke: FAIL: smoke timed out"); std::process::ExitCode::FAILURE }, }; } async fn run_smoke() -> std::result::Result<(), String> { let bind_address = std::net::SocketAddr::from(([127, 0, 0, 1], 0)); let listener = match game_realtime_websocket_lib::WebSocketListener::bind(bind_address).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(format!("listener bind failed: {error}")), }; let endpoint = format!("ws://{}/", listener.local_addr()); tracing::info!(target: TRACING_TARGET, endpoint = endpoint.as_str(), "loopback endpoint bound"); let (server_result, client_result) = tokio::join!(listener.accept(), game_realtime_websocket_lib::connect(endpoint.as_str())); let server_connection = match server_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(format!("server accept failed: {error}")), }; let client_connection = match client_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(format!("client connect failed: {error}")), }; let (mut server_sender, mut server_receiver) = server_connection.split(); let (mut client_sender, mut client_receiver) = client_connection.split(); let client_message = game_realtime_transport_lib::TransportMessage::new(CLIENT_PAYLOAD.to_vec()); if let std::result::Result::Err(error) = client_sender.send(client_message).await { return std::result::Result::Err(format!("client send failed: {error}")); } let server_received = match server_receiver.receive().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(format!("server receive failed: {error}")), }; if !receive_matches(server_received, CLIENT_PAYLOAD) { return std::result::Result::Err(String::from("server did not receive the expected client payload")); } let server_message = game_realtime_transport_lib::TransportMessage::new(SERVER_PAYLOAD.to_vec()); if let std::result::Result::Err(error) = server_sender.send(server_message).await { return std::result::Result::Err(format!("server send failed: {error}")); } let client_received = match client_receiver.receive().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(format!("client receive failed: {error}")), }; if !receive_matches(client_received, SERVER_PAYLOAD) { return std::result::Result::Err(String::from("client did not receive the expected server payload")); } if let std::result::Result::Err(error) = client_sender.close().await { return std::result::Result::Err(format!("client close failed: {error}")); } let server_close = match server_receiver.receive().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(format!("server close observation failed: {error}")), }; if server_close != game_realtime_transport_lib::TransportReceive::Closed { return std::result::Result::Err(String::from("server did not observe the client close handshake")); } return std::result::Result::Ok(()); } fn receive_matches(receive: game_realtime_transport_lib::TransportReceive, expected: &[u8]) -> bool { return match receive { game_realtime_transport_lib::TransportReceive::Message(message) => message.as_bytes() == expected, game_realtime_transport_lib::TransportReceive::Closed => false, }; }