use std::future::{Future, IntoFuture};
use std::panic::AssertUnwindSafe;
use futures_util::FutureExt;
use crate::WebDriver;
use crate::error::{WebDriverError, WebDriverResult};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum BrowserTestError<E = WebDriverError> {
#[error("failed to start browser session: {0}")]
Setup(WebDriverError),
#[error("browser test failed: {0}")]
Body(E),
#[error("browser cleanup failed: {0}")]
Cleanup(WebDriverError),
#[error("browser test failed: {body}; browser cleanup also failed: {cleanup}")]
BodyAndCleanup {
body: E,
cleanup: WebDriverError,
},
}
impl<E> From<WebDriverError> for BrowserTestError<E> {
fn from(error: WebDriverError) -> Self {
Self::Setup(error)
}
}
impl<E> BrowserTestError<E> {
pub fn body_error(&self) -> Option<&E> {
match self {
Self::Body(error)
| Self::BodyAndCleanup {
body: error,
..
} => Some(error),
Self::Setup(_) | Self::Cleanup(_) => None,
}
}
pub fn cleanup_error(&self) -> Option<&WebDriverError> {
match self {
Self::Cleanup(error)
| Self::BodyAndCleanup {
cleanup: error,
..
} => Some(error),
Self::Setup(_) | Self::Body(_) => None,
}
}
}
pub async fn run_browser_test<S, F, Fut, T, E>(
session: S,
test: F,
) -> Result<T, BrowserTestError<E>>
where
S: IntoFuture<Output = WebDriverResult<WebDriver>>,
F: FnOnce(WebDriver) -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let driver = session.into_future().await.map_err(BrowserTestError::Setup)?;
let body_driver = driver.clone();
let body_result = AssertUnwindSafe(async move { test(body_driver).await }).catch_unwind().await;
let cleanup_result = AssertUnwindSafe(driver.quit()).catch_unwind().await;
match body_result {
Ok(body_result) => match cleanup_result {
Ok(cleanup_result) => match (body_result, cleanup_result) {
(Ok(value), Ok(())) => Ok(value),
(Ok(_), Err(cleanup)) => Err(BrowserTestError::Cleanup(cleanup)),
(Err(body), Ok(())) => Err(BrowserTestError::Body(body)),
(Err(body), Err(cleanup)) => Err(BrowserTestError::BodyAndCleanup {
body,
cleanup,
}),
},
Err(cleanup_panic) => std::panic::resume_unwind(cleanup_panic),
},
Err(body_panic) => {
match cleanup_result {
Ok(Ok(())) => {}
Ok(Err(error)) => {
tracing::warn!(
%error,
"browser cleanup failed while unwinding a browser test panic"
);
}
Err(_) => {
tracing::warn!("browser cleanup panicked while unwinding a browser test panic");
}
}
std::panic::resume_unwind(body_panic)
}
}
}
#[cfg(test)]
mod tests {
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use bytes::Bytes;
use http::{Request, Response};
use super::*;
use crate::common::config::WebDriverConfig;
use crate::session::handle::SessionHandle;
use crate::session::http::{Body, HttpClient};
use crate::{Capabilities, SessionId};
#[derive(Debug, PartialEq, Eq)]
struct BodyError(&'static str);
impl Display for BodyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for BodyError {}
#[derive(Debug, Default)]
struct ClientState {
delete_attempts: AtomicUsize,
fail_first_delete: AtomicBool,
panic_first_delete: AtomicBool,
}
#[derive(Debug, Clone)]
struct RecordingClient(Arc<ClientState>);
#[async_trait::async_trait]
impl HttpClient for RecordingClient {
async fn send(&self, request: Request<Body<'_>>) -> WebDriverResult<Response<Bytes>> {
assert_eq!(request.method(), http::Method::DELETE);
assert_eq!(request.uri().path(), "/session/test-session");
self.0.delete_attempts.fetch_add(1, Ordering::SeqCst);
if self.0.panic_first_delete.swap(false, Ordering::SeqCst) {
panic!("cleanup panic");
}
if self.0.fail_first_delete.swap(false, Ordering::SeqCst) {
return Err(WebDriverError::RequestFailed("cleanup failed".to_string()));
}
Ok(Response::builder()
.status(200)
.body(Bytes::from_static(br#"{"value":null}"#))
.expect("valid response"))
}
async fn new(&self) -> Arc<dyn HttpClient> {
Arc::new(self.clone())
}
}
fn test_driver(fail_first_delete: bool) -> (WebDriver, Arc<ClientState>) {
test_driver_with_cleanup(fail_first_delete, false)
}
fn test_driver_with_cleanup(
fail_first_delete: bool,
panic_first_delete: bool,
) -> (WebDriver, Arc<ClientState>) {
let state = Arc::new(ClientState {
delete_attempts: AtomicUsize::new(0),
fail_first_delete: AtomicBool::new(fail_first_delete),
panic_first_delete: AtomicBool::new(panic_first_delete),
});
let client: Arc<dyn HttpClient> = Arc::new(RecordingClient(Arc::clone(&state)));
let handle = SessionHandle::new_with_config_guard_and_caps(
client,
"http://localhost:4444",
SessionId::from("test-session"),
WebDriverConfig::default(),
None,
Capabilities::new(),
)
.expect("valid test session");
(
WebDriver {
handle: Arc::new(handle),
},
state,
)
}
#[tokio::test]
async fn returns_body_value_after_successful_cleanup() {
let (driver, state) = test_driver(false);
let result =
run_browser_test(async { Ok(driver) }, |_| async { Ok::<_, BodyError>(42) }).await;
assert_eq!(result.expect("test succeeds"), 42);
assert_eq!(state.delete_attempts.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn setup_error_does_not_invoke_body() {
let body_invoked = Arc::new(AtomicBool::new(false));
let invoked = Arc::clone(&body_invoked);
let result = run_browser_test(
async {
Err::<WebDriver, _>(WebDriverError::SessionCreateError("setup failed".to_string()))
},
move |_| {
invoked.store(true, Ordering::SeqCst);
async { Ok::<(), BodyError>(()) }
},
)
.await;
assert!(matches!(result, Err(BrowserTestError::Setup(_))));
assert!(!body_invoked.load(Ordering::SeqCst));
}
#[tokio::test]
async fn body_error_is_returned_after_successful_cleanup() {
let (driver, state) = test_driver(false);
let result = run_browser_test(async { Ok(driver) }, |_| async {
Err::<(), _>(BodyError("body failed"))
})
.await;
assert!(matches!(result, Err(BrowserTestError::Body(BodyError("body failed")))));
assert_eq!(state.delete_attempts.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn cleanup_error_is_returned_after_successful_body() {
let (driver, state) = test_driver(true);
let result =
run_browser_test(async { Ok(driver) }, |_| async { Ok::<_, BodyError>(()) }).await;
assert!(matches!(result, Err(BrowserTestError::Cleanup(_))));
assert!(state.delete_attempts.load(Ordering::SeqCst) >= 1);
}
#[tokio::test]
async fn body_and_cleanup_errors_are_both_retained() {
let (driver, state) = test_driver(true);
let result = run_browser_test(async { Ok(driver) }, |_| async {
Err::<(), _>(BodyError("body failed"))
})
.await;
let error = result.expect_err("both operations fail");
assert_eq!(error.body_error(), Some(&BodyError("body failed")));
assert!(error.cleanup_error().is_some());
assert!(matches!(error, BrowserTestError::BodyAndCleanup { .. }));
assert!(state.delete_attempts.load(Ordering::SeqCst) >= 1);
}
#[tokio::test]
async fn cleanup_runs_when_invoking_body_panics() {
let (driver, state) = test_driver(false);
let result = AssertUnwindSafe(run_browser_test(
async { Ok(driver) },
|_| -> std::future::Ready<Result<(), BodyError>> { panic!("body invocation panic") },
))
.catch_unwind()
.await;
assert!(result.is_err());
assert_eq!(state.delete_attempts.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn cleanup_runs_when_polling_body_panics() {
let (driver, state) = test_driver(false);
let result = AssertUnwindSafe(run_browser_test(async { Ok(driver) }, |_| async {
panic!("body future panic");
#[allow(unreachable_code)]
Ok::<(), BodyError>(())
}))
.catch_unwind()
.await;
assert!(result.is_err());
assert_eq!(state.delete_attempts.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn cleanup_failure_does_not_replace_body_panic() {
let (driver, state) = test_driver(true);
let result = AssertUnwindSafe(run_browser_test(async { Ok(driver) }, |_| async {
panic!("original body panic");
#[allow(unreachable_code)]
Ok::<(), BodyError>(())
}))
.catch_unwind()
.await;
let panic = result.expect_err("the original panic resumes");
assert_eq!(panic.downcast_ref::<&str>(), Some(&"original body panic"));
assert!(state.delete_attempts.load(Ordering::SeqCst) >= 1);
}
#[tokio::test]
async fn cleanup_panic_does_not_replace_body_panic() {
let (driver, state) = test_driver_with_cleanup(false, true);
let result = AssertUnwindSafe(run_browser_test(async { Ok(driver) }, |_| async {
panic!("original body panic");
#[allow(unreachable_code)]
Ok::<(), BodyError>(())
}))
.catch_unwind()
.await;
let panic = result.expect_err("the original panic resumes");
assert_eq!(panic.downcast_ref::<&str>(), Some(&"original body panic"));
assert!(state.delete_attempts.load(Ordering::SeqCst) >= 1);
}
}