use crate::ChromeForTestingManagerError;
use crate::mgr::{ChromeForTestingManager, LoadedChromePackage};
use crate::output::{DriverOutputInspectors, DriverOutputListener};
use crate::port::{Port, PortRequest};
#[cfg(feature = "thirtyfour")]
use crate::session_builder::{DefaultCaps, DefaultConfig, SessionBuilder};
use crate::version::VersionRequest;
use chrome_for_testing::Channel;
use rootcause::prelude::ResultExt;
use rootcause::{Report, report};
use std::fmt::{Debug, Formatter};
use std::path::PathBuf;
use std::process::ExitStatus;
use std::time::Duration;
use tokio::runtime::RuntimeFlavor;
use tokio_process_tools::{
BroadcastOutputStream, GracefulShutdown, ReliableWithBackpressure, ReplayEnabled,
TerminateOnDrop,
};
use typed_builder::TypedBuilder;
#[must_use]
pub(crate) fn default_graceful_shutdown() -> GracefulShutdown {
let timeout = Duration::from_secs(3);
GracefulShutdown::builder()
.unix_sigterm(timeout)
.windows_ctrl_break(timeout)
.build()
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct ChromedriverRunConfig {
#[builder(default = VersionRequest::LatestIn(Channel::Stable), setter(into))]
pub version: VersionRequest,
#[builder(default = PortRequest::Any, setter(into))]
pub port: PortRequest,
#[builder(default, setter(strip_option(fallback = output_listener_opt)))]
pub output_listener: Option<DriverOutputListener>,
#[builder(default, setter(strip_option(fallback = cache_dir_opt)))]
pub cache_dir: Option<PathBuf>,
#[builder(default = default_graceful_shutdown())]
pub graceful_shutdown: GracefulShutdown,
}
impl Default for ChromedriverRunConfig {
fn default() -> Self {
Self::builder().build()
}
}
pub struct Chromedriver {
pub(crate) mgr: ChromeForTestingManager,
pub(crate) loaded: LoadedChromePackage,
process:
Option<TerminateOnDrop<BroadcastOutputStream<ReliableWithBackpressure, ReplayEnabled>>>,
output_inspectors: Option<DriverOutputInspectors>,
port: Port,
graceful_shutdown: GracefulShutdown,
}
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)
.field("graceful_shutdown", &self.graceful_shutdown)
.finish()
}
}
impl Chromedriver {
pub async fn run_default() -> Result<Chromedriver, Report<ChromeForTestingManagerError>> {
Self::run(ChromedriverRunConfig::default()).await
}
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 = match config.cache_dir {
Some(cache_dir) => ChromeForTestingManager::new_with_cache_dir(cache_dir)?,
None => ChromeForTestingManager::new()?,
};
let selected = mgr.resolve_version(config.version).await?;
let loaded = mgr.download(selected).await?;
let graceful_shutdown = config.graceful_shutdown;
let (process_handle, actual_port, output_inspectors) = mgr
.launch_chromedriver(
&loaded,
config.port,
config.output_listener,
graceful_shutdown.clone(),
)
.await?;
Ok(Chromedriver {
process: Some(process_handle.terminate_on_drop(graceful_shutdown.clone())),
output_inspectors: Some(output_inspectors),
port: actual_port,
loaded,
mgr,
graceful_shutdown,
})
}
#[must_use]
pub fn port(&self) -> Port {
self.port
}
#[expect(clippy::missing_panics_doc)] pub async fn terminate(mut self) -> Result<ExitStatus, Report<ChromeForTestingManagerError>> {
let _output_inspectors = self.output_inspectors.take();
self.process
.take()
.expect("present")
.terminate(self.graceful_shutdown)
.await
.context(ChromeForTestingManagerError::TerminateChromedriver { port: self.port })
}
#[cfg(feature = "thirtyfour")]
#[must_use]
pub fn session(&self) -> SessionBuilder<'_, DefaultCaps, DefaultConfig> {
SessionBuilder::new(self)
}
}
#[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();
}
#[test]
fn builder_port_accepts_u16_via_setter_into() {
let config = ChromedriverRunConfig::builder().port(8080u16).build();
assert_that!(config.port).is_equal_to(PortRequest::Specific(Port(8080)));
}
#[test]
fn builder_version_accepts_channel_via_setter_into() {
let config = ChromedriverRunConfig::builder()
.version(Channel::Beta)
.build();
assert_that!(config.version).is_equal_to(VersionRequest::LatestIn(Channel::Beta));
}
#[test]
fn builder_accepts_cache_dir_and_graceful_shutdown() {
let shutdown = GracefulShutdown::builder()
.unix_sigterm(Duration::from_secs(1))
.windows_ctrl_break(Duration::from_secs(2))
.build();
let config = ChromedriverRunConfig::builder()
.cache_dir(PathBuf::from("/tmp/cft-cache"))
.graceful_shutdown(shutdown.clone())
.build();
assert_that!(config.cache_dir).is_equal_to(Some(PathBuf::from("/tmp/cft-cache")));
assert_that!(config.graceful_shutdown).is_equal_to(shutdown);
}
#[test]
fn default_graceful_shutdown_uses_three_second_sigterm_and_ctrl_break() {
let expected = GracefulShutdown::builder()
.unix_sigterm(Duration::from_secs(3))
.windows_ctrl_break(Duration::from_secs(3))
.build();
assert_that!(default_graceful_shutdown()).is_equal_to(expected);
}
}