diff --git a/Cargo.lock b/Cargo.lock index 83c80f758..bcc97fb76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,6 +1137,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_yaml", + "socket2 0.5.6", "tempdir", "tokio", "tokio-util", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f157bca91..472a8ce60 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -78,6 +78,7 @@ arc-swap = "1.6.0" [dev-dependencies] tempdir = "0.3" serde_json = "1.0" +socket2 = "0.5.6" # Include console-logging when building tests opcua = { path = ".", features = ["console-logging"] } diff --git a/lib/tests/simple_server_test.rs b/lib/tests/simple_server_test.rs new file mode 100644 index 000000000..cfcb3900f --- /dev/null +++ b/lib/tests/simple_server_test.rs @@ -0,0 +1,104 @@ +use socket2::{Domain, Socket, Type}; +use std::io; +use std::net::SocketAddr; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +async fn build_sample(sample_dir: &str) { + let mut cargo_command = Command::new("cargo"); + cargo_command.arg("build"); + + if cfg!(feature = "test-vendored-openssl") { + cargo_command.args(&["--features", "test-vendored-openssl"]); + } + + let status = cargo_command + .current_dir(sample_dir) + .status() + .expect("Failed to build sample"); + + assert!( + status.success(), + "Failed to build sample in: {}", + sample_dir + ); +} + +fn is_port_bound(port: u16) -> bool { + let socket = match Socket::new(Domain::IPV4, Type::STREAM, None) { + Ok(sock) => sock, + Err(_) => return false, + }; + + // Set SO_REUSEADDR to allow for the server to bind while this function is testing + if socket.set_reuse_address(true).is_err() { + return false; + } + + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + + // Attempt to bind the socket + match socket.bind(&addr.into()) { + Ok(_) => false, // Successfully bound, so port was not bound + Err(ref e) if e.kind() == io::ErrorKind::AddrInUse => true, // Port is already bound + Err(_) => false, // Other errors are treated as port not bound + } +} + +fn wait_for_function_to_pass(function: F, timeout_ms: u64) -> Result<(), io::Error> +where + F: Fn() -> bool, +{ + let start_time = Instant::now(); + let timeout_duration = Duration::from_millis(timeout_ms); + let check_interval = Duration::from_millis(100); + + while Instant::now().duration_since(start_time) < timeout_duration { + if function() { + return Ok(()); + } + thread::sleep(check_interval); + } + + Err(io::Error::new( + io::ErrorKind::TimedOut, + "Expectation fn()==True timed out", + )) +} + +#[cfg(target_os = "linux")] +#[tokio::test] +async fn test_simple_server_binds_port() { + build_sample("../samples/simple-server").await; + + // Start the server in the background + let mut cargo_command = Command::new("cargo"); + cargo_command.arg("run"); + + if cfg!(feature = "test-vendored-openssl") { + cargo_command.args(&["--features", "test-vendored-openssl"]); + } + + let mut server_process = cargo_command + .current_dir("../samples/simple-server/") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to start server"); + + let expected_server_port = 4855; + let res = wait_for_function_to_pass(|| is_port_bound(expected_server_port), 5000); + + server_process.kill().expect("Failed to kill server"); + + match res { + Ok(_) => println!("Port was bound within 5 seconds."), + Err(e) => { + panic!( + "Failed to assert port {} binding within the timeout period: {}", + expected_server_port, e + ); + } + } +} diff --git a/samples/simple-server/Cargo.toml b/samples/simple-server/Cargo.toml index 1c176db00..8df14b11c 100644 --- a/samples/simple-server/Cargo.toml +++ b/samples/simple-server/Cargo.toml @@ -12,3 +12,7 @@ log = "0.4" path = "../../lib" version = "0.13.0" # OPCUARustVersion features = ["server", "console-logging"] + +[features] +default = [] +test-vendored-openssl = ["opcua/test-vendored-openssl"]