diff --git a/testsuite/Cargo.toml b/testsuite/Cargo.toml index aa8fa4fc76..680f08a581 100644 --- a/testsuite/Cargo.toml +++ b/testsuite/Cargo.toml @@ -7,7 +7,6 @@ publish = false autotests = false [lib] -doctest = false test = false [[test]] diff --git a/testsuite/src/cli.rs b/testsuite/src/cli.rs index 57a91637b1..4279c32dc7 100644 --- a/testsuite/src/cli.rs +++ b/testsuite/src/cli.rs @@ -2,8 +2,6 @@ use std::sync::LazyLock; -use anyhow::Context as _; - static JETSOCAT_BIN_PATH: LazyLock = LazyLock::new(|| { let mut build = escargot::CargoBuild::new() .manifest_path("../jetsocat/Cargo.toml") @@ -67,28 +65,6 @@ pub fn dgw_tokio_cmd() -> tokio::process::Command { cmd } -/// Starts Devolutions Gateway and waits for its HTTP listener. -/// -/// The caller must keep `config_handle` alive until the process exits because it owns the -/// temporary configuration directory. -pub async fn start_dgw(config_handle: &crate::dgw_config::DgwConfigHandle) -> anyhow::Result { - let mut process = dgw_tokio_cmd() - .env("DGATEWAY_CONFIG_PATH", config_handle.config_dir()) - .kill_on_drop(true) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - .context("start Devolutions Gateway")?; - - if let Err(error) = wait_for_tcp_port(config_handle.http_port()).await { - let _ = process.kill().await; - let _ = process.wait().await; - return Err(error).context("wait for Devolutions Gateway HTTP listener"); - } - - Ok(process) -} - static AGENT_BIN_PATH: LazyLock = LazyLock::new(|| { escargot::CargoBuild::new() .manifest_path("../devolutions-agent/Cargo.toml") diff --git a/testsuite/src/dgw_config.rs b/testsuite/src/dgw_config.rs index f55f6f7c1f..28113618fa 100644 --- a/testsuite/src/dgw_config.rs +++ b/testsuite/src/dgw_config.rs @@ -122,7 +122,63 @@ pub struct DgwConfigHandle { http_port: u16, } +/// A Gateway process that borrows its temporary configuration until dropped. +/// +/// Dropping the guard requests process termination without waiting for exit. +/// Use [`Self::start_kill`] and [`Self::wait`] to terminate and reap it explicitly. +/// +/// The configuration cannot be dropped while the guard is alive: +/// +/// ```compile_fail,E0505 +/// use testsuite::dgw_config::DgwConfig; +/// +/// async fn drop_config_before_process() -> anyhow::Result<()> { +/// let config = DgwConfig::builder().build().init()?; +/// let _process = config.start().await?; +/// drop(config); +/// Ok(()) +/// } +/// ``` +pub struct DgwProcess<'a> { + child: tokio::process::Child, + _config: &'a DgwConfigHandle, +} + +impl DgwProcess<'_> { + pub fn start_kill(&mut self) -> std::io::Result<()> { + self.child.start_kill() + } + + pub async fn wait(&mut self) -> std::io::Result { + self.child.wait().await + } +} + +#[expect(clippy::empty_drop, reason = "keep the config borrowed until the child is dropped")] +impl Drop for DgwProcess<'_> { + fn drop(&mut self) {} +} + impl DgwConfigHandle { + /// Starts Devolutions Gateway and waits for its HTTP listener. + pub async fn start(&self) -> anyhow::Result> { + let mut child = crate::cli::dgw_tokio_cmd() + .env("DGATEWAY_CONFIG_PATH", self.config_dir()) + .kill_on_drop(true) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + .context("start Devolutions Gateway")?; + + if let Err(error) = crate::cli::wait_for_tcp_port(self.http_port()).await { + let _ = child.kill().await; + let _ = child.wait().await; + return Err(error).context("wait for Devolutions Gateway HTTP listener"); + } + + Ok(DgwProcess { child, _config: self }) + } + pub fn init(config: DgwConfig) -> anyhow::Result { let DgwConfig { hostname, diff --git a/testsuite/tests/cli/dgw/heartbeat.rs b/testsuite/tests/cli/dgw/heartbeat.rs index 5a778ec3a6..3fe8c24056 100644 --- a/testsuite/tests/cli/dgw/heartbeat.rs +++ b/testsuite/tests/cli/dgw/heartbeat.rs @@ -1,7 +1,6 @@ //! Heartbeat API tests. use anyhow::Context as _; -use testsuite::cli::start_dgw; use testsuite::dgw_config::DgwConfig; /// Scope token with `gateway.heartbeat.read` scope. @@ -73,7 +72,7 @@ async fn heartbeat_reports_disk_space_when_recording_dir_not_yet_created() -> an .init() .context("init config")?; - let mut process = start_dgw(&config_handle).await?; + let mut process = config_handle.start().await?; let heartbeat = get_heartbeat(config_handle.http_port()) .await diff --git a/testsuite/tests/cli/dgw/kdc_proxy.rs b/testsuite/tests/cli/dgw/kdc_proxy.rs index a4cfa0251a..9ed424f5c9 100644 --- a/testsuite/tests/cli/dgw/kdc_proxy.rs +++ b/testsuite/tests/cli/dgw/kdc_proxy.rs @@ -3,7 +3,6 @@ use std::time::Duration; use base64::Engine as _; -use testsuite::cli::start_dgw; use testsuite::dgw_config::{AgentTunnelConfig, DgwConfig}; use tokio::net::TcpListener; use uuid::Uuid; @@ -41,7 +40,7 @@ async fn kdc_proxy_never_dials_the_kdc_directly_for_a_missing_explicit_agent() { .build() .init() .expect("init gateway config"); - let mut gateway = start_dgw(&config).await.expect("start gateway"); + let mut gateway = config.start().await.expect("start gateway"); let kdc = TcpListener::bind("127.0.0.1:0").await.expect("bind fake kdc"); let kdc_addr = kdc.local_addr().expect("read fake kdc address"); diff --git a/testsuite/tests/cli/dgw/subscriber.rs b/testsuite/tests/cli/dgw/subscriber.rs index 92508c12ef..d987d37103 100644 --- a/testsuite/tests/cli/dgw/subscriber.rs +++ b/testsuite/tests/cli/dgw/subscriber.rs @@ -1,5 +1,4 @@ use anyhow::Context as _; -use testsuite::cli::start_dgw; use testsuite::dgw_config::{DgwConfig, ProxyMode, SubscriberConfig}; use testsuite::http::HttpRequestCapture; @@ -16,7 +15,7 @@ async fn subscriber_requests_identify_gateway() -> anyhow::Result<()> { .build() .init() .context("initialize Gateway configuration")?; - let mut process = start_dgw(&config_handle).await?; + let mut process = config_handle.start().await?; let request = capture.receive().await;