diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 7ec3fd20f288..5f28c91cd04b 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -103,3 +103,7 @@ features = [ [build-dependencies] tonic-build = { workspace = true, default-features = false, features = ["transport", "prost"] } + + +[dev-dependencies] +tokio = { workspace = true, features = [ "io-util" ] } diff --git a/talpid-core/src/split_tunnel/macos/process.rs b/talpid-core/src/split_tunnel/macos/process.rs index d5d00451449b..278782fd09b0 100644 --- a/talpid-core/src/split_tunnel/macos/process.rs +++ b/talpid-core/src/split_tunnel/macos/process.rs @@ -564,12 +564,51 @@ fn check_os_version_support_inner(version: MacosVersion) -> Result<(), Error> { #[cfg(test)] mod test { - use super::{ - check_os_version_support_inner, parse_logger_status, NeedFda, EARLY_FAIL_TIMEOUT, - MIN_OS_VERSION, - }; - use std::{process::ExitStatus, time::Duration}; + use super::*; + + use std::{pin::Pin, process::ExitStatus, time::Duration}; + use talpid_platform_metadata::MacosVersion; + use tokio::io::{AsyncWriteExt, ReadHalf, SimplexStream}; + // use tokio::io::SimplexStream; + + /// A mock-version of stdout. [SimplexStream] implements [AsyncRead], so it can be used to test + /// [parse_logger_status]. + struct MockStdout { + stdout: Pin>>, + lag: Pin>, + } + + impl MockStdout { + /// "print" to "stdout" after `duration`. + async fn delay(until: Duration) -> Self { + const MSG: &[u8] = b"this will arrive.. Eventually"; + let stdout = { + let (read_half, mut write_half) = tokio::io::simplex(MSG.len()); + write_half.write_all(MSG).await.unwrap(); + Box::pin(read_half) + }; + let lag = Box::pin(tokio::time::sleep(until)); + + Self { stdout, lag } + } + } + + impl AsyncRead for MockStdout { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + use std::task::Poll; + // Wait for the deadline to be exceeded + match self.lag.as_mut().poll(cx) { + Poll::Pending => Poll::Pending, + // When the deadline has passed, we can start "reading" from "stdout" + Poll::Ready(_) => self.stdout.as_mut().poll_read(cx, buf), + } + } + } #[test] fn test_min_os_version() { @@ -643,4 +682,23 @@ mod test { "expected 'NeedFda::No' on immediate exit", ); } + + /// Check that [parse_logger_status] doesn't get stuck because nothing is ever output + /// to stdout. + #[tokio::test] + async fn test_parse_logger_status_hogged() { + let stdout = MockStdout::delay(Duration::from_secs(999)).await; + let need_fda = parse_logger_status( + async { Ok(ExitStatus::default()) }, + stdout, + b"ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED\n".as_slice(), + ) + .await; + + assert_eq!( + need_fda, + NeedFda::Yes, + "expected 'NeedFda::Yes' when ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED was present" + ); + } }