chrome_for_testing_manager/
chromedriver.rs1use crate::ChromeForTestingManagerError;
2use crate::mgr::{ChromeForTestingManager, LoadedChromePackage, VersionRequest};
3use crate::output::{DriverOutputInspectors, DriverOutputListener};
4use crate::port::{Port, PortRequest};
5use chrome_for_testing::Channel;
6use rootcause::prelude::ResultExt;
7#[cfg(feature = "thirtyfour")]
8use rootcause::{IntoReportCollection, markers::SendSync};
9use rootcause::{Report, report};
10use std::fmt::{Debug, Formatter};
11use std::process::ExitStatus;
12use std::time::Duration;
13use tokio::runtime::RuntimeFlavor;
14use tokio_process_tools::TerminateOnDrop;
15use tokio_process_tools::broadcast::BroadcastOutputStream;
16use typed_builder::TypedBuilder;
17
18#[derive(Debug, Clone, TypedBuilder)]
20pub struct ChromedriverRunConfig {
21 #[builder(default = VersionRequest::LatestIn(Channel::Stable))]
23 pub version: VersionRequest,
24
25 #[builder(default = PortRequest::Any)]
27 pub port: PortRequest,
28
29 #[builder(default, setter(strip_option(fallback = output_listener_opt)))]
31 pub output_listener: Option<DriverOutputListener>,
32}
33
34impl Default for ChromedriverRunConfig {
35 fn default() -> Self {
36 Self::builder().build()
37 }
38}
39
40pub struct Chromedriver {
48 mgr: ChromeForTestingManager,
50
51 loaded: LoadedChromePackage,
53
54 process: Option<TerminateOnDrop<BroadcastOutputStream>>,
59
60 output_inspectors: Option<DriverOutputInspectors>,
62
63 port: Port,
65}
66
67impl Debug for Chromedriver {
68 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("Chromedriver")
70 .field("mgr", &self.mgr)
71 .field("loaded", &self.loaded)
72 .field("process", &self.process)
73 .field("output_inspectors", &self.output_inspectors)
74 .field("port", &self.port)
75 .finish()
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use assertr::prelude::*;
83
84 #[test]
85 fn run_config_defaults_to_latest_stable_on_any_port() {
86 let config = ChromedriverRunConfig::builder().build();
87
88 assert_that!(config.version).is_equal_to(VersionRequest::LatestIn(Channel::Stable));
89 assert_that!(config.port).is_equal_to(PortRequest::Any);
90 assert_that!(config.output_listener).is_none();
91 }
92
93 #[test]
94 fn run_config_accepts_bare_output_listener() {
95 let listener = DriverOutputListener::new(|_line| {});
96
97 let config = ChromedriverRunConfig::builder()
98 .output_listener(listener)
99 .build();
100
101 assert_that!(config.output_listener).is_some();
102 }
103
104 #[test]
105 fn run_config_accepts_optional_output_listener() {
106 let listener = DriverOutputListener::new(|_line| {});
107
108 let config = ChromedriverRunConfig::builder()
109 .output_listener_opt(Some(listener))
110 .build();
111
112 assert_that!(config.output_listener).is_some();
113
114 let config = ChromedriverRunConfig::builder()
115 .output_listener_opt(None)
116 .build();
117
118 assert_that!(config.output_listener).is_none();
119 }
120}
121
122impl Chromedriver {
123 pub async fn run(
130 config: ChromedriverRunConfig,
131 ) -> Result<Chromedriver, Report<ChromeForTestingManagerError>> {
132 match tokio::runtime::Handle::current().runtime_flavor() {
136 RuntimeFlavor::MultiThread => { }
137 unsupported_flavor => {
138 return Err(report!(ChromeForTestingManagerError::UnsupportedRuntime {
139 runtime_flavor: unsupported_flavor,
140 }));
141 }
142 }
143
144 let mgr = ChromeForTestingManager::new()?;
145 let selected = mgr.resolve_version(config.version).await?;
146 let loaded = mgr.download(selected).await?;
147 let (process_handle, actual_port, output_inspectors) = mgr
148 .launch_chromedriver(&loaded, config.port, config.output_listener)
149 .await?;
150 Ok(Chromedriver {
151 process: Some(
152 process_handle.terminate_on_drop(Duration::from_secs(3), Duration::from_secs(3)),
153 ),
154 output_inspectors: Some(output_inspectors),
155 port: actual_port,
156 loaded,
157 mgr,
158 })
159 }
160
161 pub async fn terminate(self) -> Result<ExitStatus, Report<ChromeForTestingManagerError>> {
167 self.terminate_with_timeouts(Duration::from_secs(3), Duration::from_secs(3))
168 .await
169 }
170
171 #[expect(clippy::missing_panics_doc)] pub async fn terminate_with_timeouts(
178 mut self,
179 interrupt_timeout: Duration,
180 terminate_timeout: Duration,
181 ) -> Result<ExitStatus, Report<ChromeForTestingManagerError>> {
182 let _output_inspectors = self.output_inspectors.take();
183 self.process
184 .take()
185 .expect("present")
186 .terminate(interrupt_timeout, terminate_timeout)
187 .await
188 .context(ChromeForTestingManagerError::TerminateChromedriver { port: self.port })
189 }
190
191 #[cfg(feature = "thirtyfour")]
198 pub async fn with_session<T, E, F>(
199 &self,
200 f: F,
201 ) -> Result<T, Report<ChromeForTestingManagerError>>
202 where
203 F: for<'a> AsyncFnOnce(&'a crate::session::Session) -> Result<T, E>,
204 E: IntoReportCollection<SendSync>,
205 {
206 self.with_custom_session(|_caps| Ok(()), f).await
207 }
208
209 #[cfg(feature = "thirtyfour")]
216 pub async fn with_custom_session<T, E, F>(
217 &self,
218 setup: impl Fn(
219 &mut thirtyfour::ChromeCapabilities,
220 ) -> Result<(), thirtyfour::prelude::WebDriverError>,
221 f: F,
222 ) -> Result<T, Report<ChromeForTestingManagerError>>
223 where
224 F: for<'a> AsyncFnOnce(&'a crate::session::Session) -> Result<T, E>,
225 E: IntoReportCollection<SendSync>,
226 {
227 use crate::session::Session;
228 use futures::FutureExt;
229
230 let mut caps = self.mgr.prepare_caps(&self.loaded)?;
231 setup(&mut caps).context(ChromeForTestingManagerError::ConfigureSessionCapabilities)?;
232 let driver = thirtyfour::WebDriver::new(format!("http://localhost:{}", self.port), caps)
233 .await
234 .context(ChromeForTestingManagerError::StartWebDriverSession { port: self.port })?;
235
236 let session = Session { driver };
237
238 let maybe_panicked = core::panic::AssertUnwindSafe(f(&session))
240 .catch_unwind()
241 .await;
242
243 let user_result = match maybe_panicked {
244 Ok(result) => result.context(ChromeForTestingManagerError::RunSessionCallback),
245 Err(payload) => {
246 if let Err(quit_err) = session.quit().await {
247 tracing::error!(
248 "Failed to quit WebDriver session after user callback panic: {quit_err:?}"
249 );
250 }
251 std::panic::resume_unwind(payload);
252 }
253 };
254
255 let quit_result = session.quit().await;
257
258 match (user_result, quit_result) {
259 (Ok(value), Ok(())) => Ok(value),
260 (Ok(_), Err(quit_err)) => Err(quit_err),
261 (Err(user_err), Ok(())) => Err(user_err),
262 (Err(mut user_err), Err(quit_err)) => {
263 tracing::error!(
264 "Failed to quit WebDriver session after user failure: {quit_err:?}"
265 );
266 user_err
267 .children_mut()
268 .push(quit_err.into_dynamic().into_cloneable());
269 Err(user_err)
270 }
271 }
272 }
273}