mod common;
use color_eyre::Result;
use common::with_docker_cleanup;
use docktopus::parser::ComposeParser;
use docktopus::{ComposeConfig, DockerBuilder, DockerError};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::fs;
use uuid::Uuid;
const OPTIMISM_FIXTURES: &str = "./fixtures/simple-optimism-node";
const TEST_BASE_DIR: &str = "./test-data";
pub struct OptimismTestContext {
pub builder: DockerBuilder,
pub config: ComposeConfig,
pub test_dir: PathBuf,
}
#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc, missing_docs)]
impl OptimismTestContext {
pub async fn new(test_id: &str) -> Result<Self> {
let builder = DockerBuilder::new().await?;
let test_dir = PathBuf::from(TEST_BASE_DIR)
.join("optimism")
.join(format!("test-{}", Uuid::new_v4()));
let compose_path = PathBuf::from(OPTIMISM_FIXTURES).join("docker-compose.yml");
let env_path = PathBuf::from("./tests/optimism_env.env");
let mut config = ComposeParser::new()
.env_file(env_path)
.parse_from_path(&compose_path)?;
for service in config.services.values_mut() {
let mut labels = service.labels.clone().unwrap_or_default();
labels.insert("test_id".to_string(), test_id.to_string());
service.labels = Some(labels);
if let Some(
"ethereumoptimism/replica-healthcheck:latest"
| "ethereumoptimism/l2geth:latest"
| "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101411.4",
) = service.image.as_deref()
{
service.platform = Some("linux/amd64".to_string());
}
}
Ok(Self {
builder,
config,
test_dir,
})
}
pub async fn deploy(&mut self) -> Result<HashMap<String, String>> {
self.setup_directories().await?;
let dockerfile_path = self
.test_dir
.join("docker/dockerfiles/Dockerfile.bedrock-init");
if !dockerfile_path.exists() {
return Err(DockerError::ValidationError(format!(
"Dockerfile not found at expected path: {}",
dockerfile_path.display()
))
.into());
}
println!("Found Dockerfile at: {}", dockerfile_path.display());
self.builder
.deploy_compose_with_base_dir(&mut self.config, self.test_dir.clone())
.await
.map_err(Into::into)
}
pub async fn setup_directories(&self) -> Result<()> {
println!("\n=== Setting up test directories ===");
println!("Test directory: {}", self.test_dir.display());
fs::create_dir_all(&self.test_dir).await.map_err(|e| {
DockerError::ValidationError(format!("Failed to create test directory: {}", e))
})?;
let dirs_to_copy = [
"docker/prometheus",
"docker/grafana/provisioning",
"docker/grafana/dashboards",
"docker/influxdb",
"scripts",
"envs/common",
"envs/op-mainnet",
"envs/op-mainnet/config",
];
for dir in dirs_to_copy {
let src_dir = Path::new(OPTIMISM_FIXTURES).join(dir);
let dst_dir = self.test_dir.join(dir);
println!("\nCopying directory:");
println!(" From: {}", src_dir.display());
println!(" To: {}", dst_dir.display());
if let Some(parent) = dst_dir.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
DockerError::ValidationError(format!(
"Failed to create parent directory {}: {}",
parent.display(),
e
))
})?;
}
if src_dir.exists() {
tokio::process::Command::new("cp")
.arg("-r")
.arg(&src_dir)
.arg(dst_dir.parent().unwrap())
.status()
.await
.map_err(|e| {
DockerError::ValidationError(format!(
"Failed to copy directory {}: {}",
dir, e
))
})?;
} else {
println!(
"Warning: Source directory does not exist: {}",
src_dir.display()
);
fs::create_dir_all(&dst_dir).await.map_err(|e| {
DockerError::ValidationError(format!(
"Failed to create directory {}: {}",
dst_dir.display(),
e
))
})?;
}
}
println!("\n=== Creating additional directories ===");
let additional_dirs = ["shared", "downloads", "torrents/op-mainnet"];
for dir in additional_dirs {
let dir_path = self.test_dir.join(dir);
println!("Creating directory: {}", dir_path.display());
fs::create_dir_all(&dir_path).await.map_err(|e| {
DockerError::ValidationError(format!(
"Failed to create directory {}: {}",
dir_path.display(),
e
))
})?;
}
println!("\n=== Copying compose files ===");
let compose_src = Path::new(OPTIMISM_FIXTURES).join("docker-compose.yml");
let compose_dst = self.test_dir.join("docker-compose.yml");
println!("Copying docker-compose.yml:");
println!(" From: {}", compose_src.display());
println!(" To: {}", compose_dst.display());
fs::copy(&compose_src, &compose_dst).await.map_err(|e| {
DockerError::ValidationError(format!("Failed to copy docker-compose.yml: {}", e))
})?;
let env_src = PathBuf::from("tests/optimism_env.env");
let env_dst = self.test_dir.join(".env");
println!("Copying env file:");
println!(" From: {}", env_src.display());
println!(" To: {}", env_dst.display());
fs::copy(&env_src, &env_dst).await.map_err(|e| {
DockerError::ValidationError(format!("Failed to copy optimism_env.env to .env: {}", e))
})?;
println!("\n=== Setting up Docker build context ===");
let dockerfiles_dir = self.test_dir.join("docker/dockerfiles");
fs::create_dir_all(&dockerfiles_dir).await.map_err(|e| {
DockerError::ValidationError(format!("Failed to create dockerfiles directory: {}", e))
})?;
let dockerfile_src =
Path::new(OPTIMISM_FIXTURES).join("docker/dockerfiles/Dockerfile.bedrock-init");
let dockerfile_dst = dockerfiles_dir.join("Dockerfile.bedrock-init");
println!("Copying Dockerfile:");
println!(" From: {}", dockerfile_src.display());
println!(" To: {}", dockerfile_dst.display());
fs::copy(&dockerfile_src, &dockerfile_dst)
.await
.map_err(|e| {
DockerError::ValidationError(format!(
"Failed to copy Dockerfile.bedrock-init: {}",
e
))
})?;
match fs::read_to_string(&dockerfile_dst).await {
Ok(content) => {
println!("Verified Dockerfile contents ({} bytes)", content.len());
println!("First few lines:");
for line in content.lines().take(5) {
println!(" {}", line);
}
}
Err(e) => println!("Warning: Could not read copied Dockerfile: {}", e),
}
println!("\nFinal dockerfiles directory contents:");
if let Ok(mut entries) = fs::read_dir(&dockerfiles_dir).await {
while let Ok(Some(entry)) = entries.next_entry().await {
println!(" {}", entry.path().display());
}
}
Ok(())
}
}
#[tokio::test]
async fn test_optimism_node_deployment() -> Result<()> {
with_docker_cleanup(|test_id| {
Box::pin(async move {
let mut ctx = OptimismTestContext::new(&test_id).await?;
let container_ids = ctx.deploy().await?;
for (service_name, container_id) in container_ids {
println!(
"Verifying container for service {} with id {}",
service_name, container_id
);
let mut retries = 5;
let mut container_running = false;
while retries > 0 {
if let Ok(containers) = ctx
.builder
.client()
.list_containers(Some(bollard::container::ListContainersOptions {
all: true,
filters: {
let mut filters = HashMap::new();
filters.insert("id".to_string(), vec![container_id.clone()]);
filters
},
..Default::default()
}))
.await
{
if !containers.is_empty() {
container_running = true;
break;
}
}
retries -= 1;
tokio::time::sleep(Duration::from_millis(500)).await;
}
assert!(
container_running,
"Container for service {} should be running",
service_name
);
}
Ok(())
})
})
.await
}