chrome_for_testing_manager/
chromedriver.rs1use crate::ChromeForTestingManagerError;
2use crate::mgr::{ChromeForTestingManager, LoadedChromePackage};
3use crate::output::{DriverOutputInspectors, DriverOutputListener};
4use crate::port::{Port, PortRequest};
5use crate::version::VersionRequest;
6use chrome_for_testing::Channel;
7use rootcause::prelude::ResultExt;
8#[cfg(feature = "thirtyfour")]
9use rootcause::{IntoReportCollection, markers::SendSync};
10use rootcause::{Report, report};
11use std::fmt::{Debug, Formatter};
12use std::path::PathBuf;
13use std::process::ExitStatus;
14use std::time::Duration;
15use tokio::runtime::RuntimeFlavor;
16use tokio_process_tools::{
17 BroadcastOutputStream, GracefulShutdown, ReliableWithBackpressure, ReplayEnabled,
18 TerminateOnDrop,
19};
20use typed_builder::TypedBuilder;
21
22#[must_use]
26pub(crate) fn default_graceful_shutdown() -> GracefulShutdown {
27 let timeout = Duration::from_secs(3);
28 GracefulShutdown::builder()
29 .unix_sigterm(timeout)
30 .windows_ctrl_break(timeout)
31 .build()
32}
33
34#[derive(Debug, Clone, TypedBuilder)]
56pub struct ChromedriverRunConfig {
57 #[builder(default = VersionRequest::LatestIn(Channel::Stable), setter(into))]
62 pub version: VersionRequest,
63
64 #[builder(default = PortRequest::Any, setter(into))]
68 pub port: PortRequest,
69
70 #[builder(default, setter(strip_option(fallback = output_listener_opt)))]
72 pub output_listener: Option<DriverOutputListener>,
73
74 #[builder(default, setter(strip_option(fallback = cache_dir_opt)))]
77 pub cache_dir: Option<PathBuf>,
78
79 #[builder(default = default_graceful_shutdown())]
82 pub graceful_shutdown: GracefulShutdown,
83}
84
85impl Default for ChromedriverRunConfig {
86 fn default() -> Self {
87 Self::builder().build()
88 }
89}
90
91pub struct Chromedriver {
102 mgr: ChromeForTestingManager,
104
105 loaded: LoadedChromePackage,
107
108 process:
113 Option<TerminateOnDrop<BroadcastOutputStream<ReliableWithBackpressure, ReplayEnabled>>>,
114
115 output_inspectors: Option<DriverOutputInspectors>,
117
118 port: Port,
120
121 graceful_shutdown: GracefulShutdown,
123}
124
125impl Debug for Chromedriver {
126 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127 f.debug_struct("Chromedriver")
128 .field("mgr", &self.mgr)
129 .field("loaded", &self.loaded)
130 .field("process", &self.process)
131 .field("output_inspectors", &self.output_inspectors)
132 .field("port", &self.port)
133 .field("graceful_shutdown", &self.graceful_shutdown)
134 .finish()
135 }
136}
137
138impl Chromedriver {
139 pub async fn run_default() -> Result<Chromedriver, Report<ChromeForTestingManagerError>> {
148 Self::run(ChromedriverRunConfig::default()).await
149 }
150
151 pub async fn run(
158 config: ChromedriverRunConfig,
159 ) -> Result<Chromedriver, Report<ChromeForTestingManagerError>> {
160 match tokio::runtime::Handle::current().runtime_flavor() {
164 RuntimeFlavor::MultiThread => { }
165 unsupported_flavor => {
166 return Err(report!(ChromeForTestingManagerError::UnsupportedRuntime {
167 runtime_flavor: unsupported_flavor,
168 }));
169 }
170 }
171
172 let mgr = match config.cache_dir {
173 Some(cache_dir) => ChromeForTestingManager::new_with_cache_dir(cache_dir)?,
174 None => ChromeForTestingManager::new()?,
175 };
176 let selected = mgr.resolve_version(config.version).await?;
177 let loaded = mgr.download(selected).await?;
178 let graceful_shutdown = config.graceful_shutdown;
179 let (process_handle, actual_port, output_inspectors) = mgr
180 .launch_chromedriver(
181 &loaded,
182 config.port,
183 config.output_listener,
184 graceful_shutdown.clone(),
185 )
186 .await?;
187 Ok(Chromedriver {
188 process: Some(process_handle.terminate_on_drop(graceful_shutdown.clone())),
189 output_inspectors: Some(output_inspectors),
190 port: actual_port,
191 loaded,
192 mgr,
193 graceful_shutdown,
194 })
195 }
196
197 #[must_use]
201 pub fn port(&self) -> Port {
202 self.port
203 }
204
205 #[expect(clippy::missing_panics_doc)] pub async fn terminate(mut self) -> Result<ExitStatus, Report<ChromeForTestingManagerError>> {
214 let _output_inspectors = self.output_inspectors.take();
215 self.process
216 .take()
217 .expect("present")
218 .terminate(self.graceful_shutdown)
219 .await
220 .context(ChromeForTestingManagerError::TerminateChromedriver { port: self.port })
221 }
222
223 #[cfg(feature = "thirtyfour")]
230 pub async fn with_session<T, E, F>(
231 &self,
232 f: F,
233 ) -> Result<T, Report<ChromeForTestingManagerError>>
234 where
235 F: for<'a> AsyncFnOnce(&'a crate::session::Session) -> Result<T, E>,
236 E: IntoReportCollection<SendSync>,
237 {
238 self.with_custom_session(|_caps| Ok(()), f).await
239 }
240
241 #[cfg(feature = "thirtyfour")]
248 pub async fn with_custom_session<T, E, F>(
249 &self,
250 setup: impl FnOnce(
251 &mut thirtyfour::ChromeCapabilities,
252 ) -> Result<(), thirtyfour::prelude::WebDriverError>,
253 f: F,
254 ) -> Result<T, Report<ChromeForTestingManagerError>>
255 where
256 F: for<'a> AsyncFnOnce(&'a crate::session::Session) -> Result<T, E>,
257 E: IntoReportCollection<SendSync>,
258 {
259 use crate::session::Session;
260 use futures::FutureExt;
261
262 let mut caps = self.mgr.prepare_caps(&self.loaded)?;
263 setup(&mut caps).context(ChromeForTestingManagerError::ConfigureSessionCapabilities)?;
264 let driver = thirtyfour::WebDriver::new(format!("http://localhost:{}", self.port), caps)
265 .await
266 .context(ChromeForTestingManagerError::StartWebDriverSession { port: self.port })?;
267
268 let session = Session { driver };
269
270 let maybe_panicked = core::panic::AssertUnwindSafe(f(&session))
272 .catch_unwind()
273 .await;
274
275 let user_result = match maybe_panicked {
276 Ok(result) => result.context(ChromeForTestingManagerError::RunSessionCallback),
277 Err(payload) => {
278 if let Err(quit_err) = session.quit().await {
279 tracing::error!(
280 "Failed to quit WebDriver session after user callback panic: {quit_err:?}"
281 );
282 }
283 std::panic::resume_unwind(payload);
284 }
285 };
286
287 let quit_result = session.quit().await;
289
290 match (user_result, quit_result) {
291 (Ok(value), Ok(())) => Ok(value),
292 (Ok(_), Err(quit_err)) => Err(quit_err),
293 (Err(user_err), Ok(())) => Err(user_err),
294 (Err(mut user_err), Err(quit_err)) => {
295 tracing::error!(
296 "Failed to quit WebDriver session after user failure: {quit_err:?}"
297 );
298 user_err
299 .children_mut()
300 .push(quit_err.into_dynamic().into_cloneable());
301 Err(user_err)
302 }
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use assertr::prelude::*;
311
312 #[test]
313 fn run_config_defaults_to_latest_stable_on_any_port() {
314 let config = ChromedriverRunConfig::builder().build();
315
316 assert_that!(config.version).is_equal_to(VersionRequest::LatestIn(Channel::Stable));
317 assert_that!(config.port).is_equal_to(PortRequest::Any);
318 assert_that!(config.output_listener).is_none();
319 }
320
321 #[test]
322 fn run_config_accepts_bare_output_listener() {
323 let listener = DriverOutputListener::new(|_line| {});
324
325 let config = ChromedriverRunConfig::builder()
326 .output_listener(listener)
327 .build();
328
329 assert_that!(config.output_listener).is_some();
330 }
331
332 #[test]
333 fn run_config_accepts_optional_output_listener() {
334 let listener = DriverOutputListener::new(|_line| {});
335
336 let config = ChromedriverRunConfig::builder()
337 .output_listener_opt(Some(listener))
338 .build();
339
340 assert_that!(config.output_listener).is_some();
341
342 let config = ChromedriverRunConfig::builder()
343 .output_listener_opt(None)
344 .build();
345
346 assert_that!(config.output_listener).is_none();
347 }
348
349 #[test]
350 fn builder_port_accepts_u16_via_setter_into() {
351 let config = ChromedriverRunConfig::builder().port(8080u16).build();
352 assert_that!(config.port).is_equal_to(PortRequest::Specific(Port(8080)));
353 }
354
355 #[test]
356 fn builder_version_accepts_channel_via_setter_into() {
357 let config = ChromedriverRunConfig::builder()
358 .version(Channel::Beta)
359 .build();
360 assert_that!(config.version).is_equal_to(VersionRequest::LatestIn(Channel::Beta));
361 }
362
363 #[test]
364 fn builder_accepts_cache_dir_and_graceful_shutdown() {
365 let shutdown = GracefulShutdown::builder()
366 .unix_sigterm(Duration::from_secs(1))
367 .windows_ctrl_break(Duration::from_secs(2))
368 .build();
369 let config = ChromedriverRunConfig::builder()
370 .cache_dir(PathBuf::from("/tmp/cft-cache"))
371 .graceful_shutdown(shutdown.clone())
372 .build();
373
374 assert_that!(config.cache_dir).is_equal_to(Some(PathBuf::from("/tmp/cft-cache")));
375 assert_that!(config.graceful_shutdown).is_equal_to(shutdown);
376 }
377
378 #[test]
379 fn default_graceful_shutdown_uses_three_second_sigterm_and_ctrl_break() {
380 let expected = GracefulShutdown::builder()
381 .unix_sigterm(Duration::from_secs(3))
382 .windows_ctrl_break(Duration::from_secs(3))
383 .build();
384 assert_that!(default_graceful_shutdown()).is_equal_to(expected);
385 }
386}