use crate::ChromeForTestingManagerError;
use crate::mgr::{ChromeForTestingManager, LoadedChromePackage, VersionRequest};
use crate::output::{DriverOutputInspectors, DriverOutputListener};
use crate::port::{Port, PortRequest};
use chrome_for_testing::Channel;
use rootcause::prelude::ResultExt;
#[cfg(feature = "thirtyfour")]
use rootcause::{IntoReportCollection, markers::SendSync};
use rootcause::{Report, report};
use std::fmt::{Debug, Formatter};
use std::process::ExitStatus;
use std::time::Duration;
use tokio::runtime::RuntimeFlavor;
use tokio_process_tools::TerminateOnDrop;
use tokio_process_tools::broadcast::BroadcastOutputStream;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, TypedBuilder)]
pub struct ChromedriverRunConfig {
#[builder(default = VersionRequest::LatestIn(Channel::Stable))]
pub version: VersionRequest,
#[builder(default = PortRequest::Any)]
pub port: PortRequest,
#[builder(default, setter(strip_option(fallback = output_listener_opt)))]
pub output_listener: Option<DriverOutputListener>,
}
impl Default for ChromedriverRunConfig {
fn default() -> Self {
Self::builder().build()
}
}
pub struct Chromedriver {
mgr: ChromeForTestingManager,
loaded: LoadedChromePackage,
process: Option<TerminateOnDrop<BroadcastOutputStream>>,
output_inspectors: Option<DriverOutputInspectors>,
port: Port,
}
impl Debug for Chromedriver {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Chromedriver")
.field("mgr", &self.mgr)
.field("loaded", &self.loaded)
.field("process", &self.process)
.field("output_inspectors", &self.output_inspectors)
.field("port", &self.port)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assertr::prelude::*;
#[test]
fn run_config_defaults_to_latest_stable_on_any_port() {
let config = ChromedriverRunConfig::builder().build();
assert_that!(config.version).is_equal_to(VersionRequest::LatestIn(Channel::Stable));
assert_that!(config.port).is_equal_to(PortRequest::Any);
assert_that!(config.output_listener).is_none();
}
#[test]
fn run_config_accepts_bare_output_listener() {
let listener = DriverOutputListener::new(|_line| {});
let config = ChromedriverRunConfig::builder()
.output_listener(listener)
.build();
assert_that!(config.output_listener).is_some();
}
#[test]
fn run_config_accepts_optional_output_listener() {
let listener = DriverOutputListener::new(|_line| {});
let config = ChromedriverRunConfig::builder()
.output_listener_opt(Some(listener))
.build();
assert_that!(config.output_listener).is_some();
let config = ChromedriverRunConfig::builder()
.output_listener_opt(None)
.build();
assert_that!(config.output_listener).is_none();
}
}
impl Chromedriver {
pub async fn run(
config: ChromedriverRunConfig,
) -> Result<Chromedriver, Report<ChromeForTestingManagerError>> {
match tokio::runtime::Handle::current().runtime_flavor() {
RuntimeFlavor::MultiThread => { }
unsupported_flavor => {
return Err(report!(ChromeForTestingManagerError::UnsupportedRuntime {
runtime_flavor: unsupported_flavor,
}));
}
}
let mgr = ChromeForTestingManager::new()?;
let selected = mgr.resolve_version(config.version).await?;
let loaded = mgr.download(selected).await?;
let (process_handle, actual_port, output_inspectors) = mgr
.launch_chromedriver(&loaded, config.port, config.output_listener)
.await?;
Ok(Chromedriver {
process: Some(
process_handle.terminate_on_drop(Duration::from_secs(3), Duration::from_secs(3)),
),
output_inspectors: Some(output_inspectors),
port: actual_port,
loaded,
mgr,
})
}
pub async fn terminate(self) -> Result<ExitStatus, Report<ChromeForTestingManagerError>> {
self.terminate_with_timeouts(Duration::from_secs(3), Duration::from_secs(3))
.await
}
#[expect(clippy::missing_panics_doc)] pub async fn terminate_with_timeouts(
mut self,
interrupt_timeout: Duration,
terminate_timeout: Duration,
) -> Result<ExitStatus, Report<ChromeForTestingManagerError>> {
let _output_inspectors = self.output_inspectors.take();
self.process
.take()
.expect("present")
.terminate(interrupt_timeout, terminate_timeout)
.await
.context(ChromeForTestingManagerError::TerminateChromedriver { port: self.port })
}
#[cfg(feature = "thirtyfour")]
pub async fn with_session<T, E, F>(
&self,
f: F,
) -> Result<T, Report<ChromeForTestingManagerError>>
where
F: for<'a> AsyncFnOnce(&'a crate::session::Session) -> Result<T, E>,
E: IntoReportCollection<SendSync>,
{
self.with_custom_session(|_caps| Ok(()), f).await
}
#[cfg(feature = "thirtyfour")]
pub async fn with_custom_session<T, E, F>(
&self,
setup: impl Fn(
&mut thirtyfour::ChromeCapabilities,
) -> Result<(), thirtyfour::prelude::WebDriverError>,
f: F,
) -> Result<T, Report<ChromeForTestingManagerError>>
where
F: for<'a> AsyncFnOnce(&'a crate::session::Session) -> Result<T, E>,
E: IntoReportCollection<SendSync>,
{
use crate::session::Session;
use futures::FutureExt;
let mut caps = self.mgr.prepare_caps(&self.loaded)?;
setup(&mut caps).context(ChromeForTestingManagerError::ConfigureSessionCapabilities)?;
let driver = thirtyfour::WebDriver::new(format!("http://localhost:{}", self.port), caps)
.await
.context(ChromeForTestingManagerError::StartWebDriverSession { port: self.port })?;
let session = Session { driver };
let maybe_panicked = core::panic::AssertUnwindSafe(f(&session))
.catch_unwind()
.await;
let user_result = match maybe_panicked {
Ok(result) => result.context(ChromeForTestingManagerError::RunSessionCallback),
Err(payload) => {
if let Err(quit_err) = session.quit().await {
tracing::error!(
"Failed to quit WebDriver session after user callback panic: {quit_err:?}"
);
}
std::panic::resume_unwind(payload);
}
};
let quit_result = session.quit().await;
match (user_result, quit_result) {
(Ok(value), Ok(())) => Ok(value),
(Ok(_), Err(quit_err)) => Err(quit_err),
(Err(user_err), Ok(())) => Err(user_err),
(Err(mut user_err), Err(quit_err)) => {
tracing::error!(
"Failed to quit WebDriver session after user failure: {quit_err:?}"
);
user_err
.children_mut()
.push(quit_err.into_dynamic().into_cloneable());
Err(user_err)
}
}
}
}