use std::ops::Deref;
use std::sync::Arc;
use anyhow::Context;
use getset::Getters;
use reqwest::Url;
use testcontainers::core::{Host, IntoContainerPort, WaitFor};
use testcontainers::runners::AsyncRunner;
use testcontainers::{ContainerAsync, GenericImage, ImageExt};
use crate::{Client, Doco, Result};
const DOCKER_HOST: &str = "host.docker.internal";
struct RunningService {
container: ContainerAsync<GenericImage>,
image: String,
}
impl RunningService {
async fn start(config: &crate::Service) -> Result<Self> {
let mut image = GenericImage::new(config.image(), config.tag());
if let Some(wait) = config.wait() {
image = image.with_wait_for(wait.clone());
}
let mut image = image.with_host("doco", Host::HostGateway);
for env in config.envs() {
image = image.with_env_var(env.name().clone(), env.value().clone());
}
for mount in config.mounts() {
image = image.with_mount(mount.clone());
}
if !config.cmd().is_empty() {
image = image.with_cmd(config.cmd().clone());
}
let container = image.start().await?;
Ok(Self {
container,
image: config.image().clone(),
})
}
async fn bridge_ip(&self) -> Result<std::net::IpAddr> {
self.container
.get_bridge_ip_address()
.await
.context("failed to get bridge IP for service")
}
}
struct RunningServer {
container: ContainerAsync<GenericImage>,
base_url: Url,
}
impl RunningServer {
async fn start(config: &crate::Server, services: &[RunningService]) -> Result<Self> {
let mut server =
GenericImage::new(config.image(), config.tag()).with_exposed_port(config.port().tcp());
if let Some(wait) = config.wait() {
server = server.with_wait_for(wait.clone());
}
let mut server = server.with_host(DOCKER_HOST, Host::HostGateway);
for service in services {
server = server.with_host(&service.image, Host::Addr(service.bridge_ip().await?));
}
for env in config.envs() {
server = server.with_env_var(env.name().clone(), env.value().clone());
}
for mount in config.mounts() {
server = server.with_mount(mount.clone());
}
if !config.cmd().is_empty() {
server = server.with_cmd(config.cmd().clone());
}
let container = server.start().await?;
let port = container.get_host_port_ipv4(config.port()).await?;
let base_url = format!("http://{DOCKER_HOST}:{port}").parse()?;
Ok(Self {
container,
base_url,
})
}
}
async fn create_driver(
selenium: &ContainerAsync<GenericImage>,
doco: &Doco,
) -> Result<thirtyfour::WebDriver> {
let mut caps = thirtyfour::DesiredCapabilities::firefox();
if *doco.headless() {
caps.set_headless()
.context("failed to set headless capability")?;
}
let driver = thirtyfour::WebDriver::new(
&format!(
"http://{}:{}",
selenium.get_host().await?,
selenium.get_host_port_ipv4(4444).await?
),
caps,
)
.await
.context("failed to connect to WebDriver")?;
if let Some(viewport) = doco.viewport() {
driver
.set_window_rect(0, 0, viewport.width(), viewport.height())
.await
.context("failed to set browser viewport")?;
}
Ok(driver)
}
#[derive(Getters)]
pub struct Session {
#[getset(get = "pub")]
client: Client,
driver: thirtyfour::WebDriver,
_selenium: Arc<ContainerAsync<GenericImage>>,
_server: ContainerAsync<GenericImage>,
_services: Vec<ContainerAsync<GenericImage>>,
}
impl Session {
pub(crate) async fn connect(doco: &Doco) -> Result<Self> {
println!("Initializing session...");
let selenium = Arc::new(Self::start_selenium().await?);
Self::with_selenium(doco, selenium).await
}
pub(crate) async fn with_selenium(
doco: &Doco,
selenium: Arc<ContainerAsync<GenericImage>>,
) -> Result<Self> {
let mut services = Vec::with_capacity(doco.services().len());
for config in doco.services() {
services.push(RunningService::start(config).await?);
}
let server = RunningServer::start(doco.server(), &services).await?;
let driver = create_driver(&selenium, doco).await?;
let client = Client::builder()
.base_url(server.base_url)
.client(driver.clone())
.build();
Ok(Self {
client,
driver,
_selenium: selenium,
_server: server.container,
_services: services.into_iter().map(|s| s.container).collect(),
})
}
pub async fn close(self) -> Result<()> {
self.driver.quit().await.ok();
Ok(())
}
pub(crate) async fn start_selenium() -> Result<ContainerAsync<GenericImage>> {
GenericImage::new("selenium/standalone-firefox", "latest")
.with_exposed_port(4444.tcp())
.with_wait_for(WaitFor::message_on_stdout("Started Selenium Standalone"))
.with_host(DOCKER_HOST, Host::HostGateway)
.start()
.await
.context("failed to start Selenium container")
}
}
impl Deref for Session {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::*;
use super::*;
#[test]
fn trait_send() {
assert_send::<Session>();
}
#[test]
fn trait_sync() {
assert_sync::<Session>();
}
#[test]
fn trait_unpin() {
assert_unpin::<Session>();
}
}