From 083af0ed8f5a303ddea734f3ee9acab1818ec94d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Jan 2024 12:24:34 +0000 Subject: [PATCH] feat: [#634] E2E test runner: build, run and stop tracker image --- Cargo.lock | 1 + Cargo.toml | 1 + cSpell.json | 1 + src/bin/e2e_tests_runner.rs | 10 +++ src/e2e/docker.rs | 131 ++++++++++++++++++++++++++++++++++++ src/e2e/mod.rs | 3 + src/e2e/runner.rs | 81 ++++++++++++++++++++++ src/e2e/temp_dir.rs | 52 ++++++++++++++ src/lib.rs | 1 + 9 files changed, 281 insertions(+) create mode 100644 src/bin/e2e_tests_runner.rs create mode 100644 src/e2e/docker.rs create mode 100644 src/e2e/mod.rs create mode 100644 src/e2e/runner.rs create mode 100644 src/e2e/temp_dir.rs diff --git a/Cargo.lock b/Cargo.lock index 1f49aa986..dd5017e11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3457,6 +3457,7 @@ dependencies = [ "serde_with", "tdyne-peer-id", "tdyne-peer-id-registry", + "tempfile", "thiserror", "tokio", "torrust-tracker-configuration", diff --git a/Cargo.toml b/Cargo.toml index 9b7e71905..93abc9114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ tower-http = { version = "0", features = ["compression-full"] } uuid = { version = "1", features = ["v4"] } colored = "2.1.0" url = "2.5.0" +tempfile = "3.9.0" [dev-dependencies] criterion = { version = "0.5.1", features = ["async_tokio"] } diff --git a/cSpell.json b/cSpell.json index e02c6ed87..1e7e04b40 100644 --- a/cSpell.json +++ b/cSpell.json @@ -114,6 +114,7 @@ "Swatinem", "Swiftbit", "taiki", + "tempfile", "thiserror", "tlsv", "Torrentstorm", diff --git a/src/bin/e2e_tests_runner.rs b/src/bin/e2e_tests_runner.rs new file mode 100644 index 000000000..b1af451fa --- /dev/null +++ b/src/bin/e2e_tests_runner.rs @@ -0,0 +1,10 @@ +//! Program to run E2E tests. +//! +//! ```text +//! cargo run --bin e2e_tests_runner +//! ``` +use torrust_tracker::e2e::runner; + +fn main() { + runner::run(); +} diff --git a/src/e2e/docker.rs b/src/e2e/docker.rs new file mode 100644 index 000000000..6770cec87 --- /dev/null +++ b/src/e2e/docker.rs @@ -0,0 +1,131 @@ +use std::io; +use std::process::{Child, Command, Output, Stdio}; +use std::thread::sleep; +use std::time::{Duration, Instant}; + +pub struct Docker {} + +impl Docker { + /// Builds a Docker image from a given Dockerfile. + /// + /// # Errors + /// + /// Will fail if the docker build command fails. + pub fn build(dockerfile: &str, tag: &str) -> io::Result<()> { + let status = Command::new("docker") + .args(["build", "-f", dockerfile, "-t", tag, "."]) + .status()?; + + if status.success() { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::Other, "Failed to build Docker image")) + } + } + + /// Runs a Docker container from a given image. + /// + /// # Errors + /// + /// Will fail if the docker run command fails. + pub fn run(image: &str, name: &str) -> io::Result { + let output = Command::new("docker") + .args(["run", "--detach", "--name", name, image]) + .output()?; + + if output.status.success() { + Ok(output) + } else { + Err(io::Error::new(io::ErrorKind::Other, "Failed to run Docker container")) + } + } + + /// Runs a Docker container from a given image in the background. + /// + /// # Errors + /// + /// Will fail if the docker run command fails to start. + pub fn run_spawned(image: &str, name: &str) -> io::Result { + let child = Command::new("docker") + .args(["run", "--name", name, image]) + .stdin(Stdio::null()) // Ignore stdin + .stdout(Stdio::null()) // Ignore stdout + .stderr(Stdio::null()) // Ignore stderr + .spawn()?; + + Ok(child) + } + + /// Stops a Docker container. + /// + /// # Errors + /// + /// Will fail if the docker stop command fails. + pub fn stop(name: &str) -> io::Result<()> { + let status = Command::new("docker").args(["stop", name]).status()?; + + if status.success() { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::Other, "Failed to stop Docker container")) + } + } + + /// Removes a Docker container. + /// + /// # Errors + /// + /// Will fail if the docker rm command fails. + pub fn remove(name: &str) -> io::Result<()> { + let status = Command::new("docker").args(["rm", "-f", name]).status()?; + + if status.success() { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::Other, "Failed to remove Docker container")) + } + } + + /// Fetches logs from a Docker container. + /// + /// # Errors + /// + /// Will fail if the docker logs command fails. + pub fn logs(container_name: &str) -> io::Result { + let output = Command::new("docker").args(["logs", container_name]).output()?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Failed to fetch logs from Docker container", + )) + } + } + + /// Checks if a Docker container is healthy. + #[must_use] + pub fn wait_until_is_healthy(name: &str, timeout: Duration) -> bool { + let start = Instant::now(); + + while start.elapsed() < timeout { + let Ok(output) = Command::new("docker") + .args(["ps", "-f", &format!("name={name}"), "--format", "{{.Status}}"]) + .output() + else { + return false; + }; + + let output_str = String::from_utf8_lossy(&output.stdout); + + if output_str.contains("(healthy)") { + return true; + } + + sleep(Duration::from_secs(1)); + } + + false + } +} diff --git a/src/e2e/mod.rs b/src/e2e/mod.rs new file mode 100644 index 000000000..bc8a6d8d3 --- /dev/null +++ b/src/e2e/mod.rs @@ -0,0 +1,3 @@ +pub mod docker; +pub mod runner; +pub mod temp_dir; diff --git a/src/e2e/runner.rs b/src/e2e/runner.rs new file mode 100644 index 000000000..16d8d2592 --- /dev/null +++ b/src/e2e/runner.rs @@ -0,0 +1,81 @@ +use std::env; +use std::time::Duration; + +use rand::distributions::Alphanumeric; +use rand::Rng; + +use crate::e2e::docker::Docker; +use crate::e2e::temp_dir::Handler; + +/// # Panics +/// +/// Will panic if: +/// +/// - It can't build the docker image. +/// - It can't create a temp dir. +/// - It can't change to the new temp dir. +/// - It can't revert the dit to the previous one. +pub fn run() { + /* todo: + + - [x] Build the docker image. + - [x] Run the docker image. + - [x] Wait until the container is healthy. + - [ ] Parse logs to get running services. + - [ ] Build config file for the tracker_checker. + - [ ] Run the tracker_checker. + - [x] Stop the container. + + */ + + //Docker::build("./Containerfile", "local").expect("A tracker local docker image should be built"); + + println!( + "Current dir: {:?}", + env::current_dir().expect("It should return the current dir") + ); + + println!("Create temp dir ..."); + let temp_dir_handler = Handler::new().expect("A temp dir should be created"); + println!("Temp dir created: {:?}", temp_dir_handler.temp_dir); + + println!("Change dir to: {:?}", temp_dir_handler.temp_dir); + temp_dir_handler + .change_to_temp_dir() + .expect("The app should change dir to the temp dir"); + + let container_name = generate_random_container_name("tracker_"); + + println!("Running docker tracker image: {container_name} ..."); + Docker::run("local", &container_name).expect("A tracker local docker image should be running"); + + println!("Waiting for the container {container_name} to be healthy ..."); + let is_healthy = Docker::wait_until_is_healthy(&container_name, Duration::from_secs(10)); + + if !is_healthy { + println!("Unhealthy container: {container_name}"); + println!("Stopping container: {container_name} ..."); + Docker::stop(&container_name).expect("A tracker local docker image should be stopped"); + panic!("Unhealthy container: {container_name}"); + } + + println!("Container {container_name} is healthy ..."); + + println!("Stopping docker tracker image: {container_name} ..."); + Docker::stop(&container_name).expect("A tracker local docker image should be stopped"); + + println!("Revert current dir to: {:?}", temp_dir_handler.original_dir); + temp_dir_handler + .revert_to_original_dir() + .expect("The app should revert dir from temp dir to the original one"); +} + +fn generate_random_container_name(prefix: &str) -> String { + let rand_string: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(20) + .map(char::from) + .collect(); + + format!("{prefix}{rand_string}") +} diff --git a/src/e2e/temp_dir.rs b/src/e2e/temp_dir.rs new file mode 100644 index 000000000..0aeb7f2d5 --- /dev/null +++ b/src/e2e/temp_dir.rs @@ -0,0 +1,52 @@ +use std::path::PathBuf; +use std::{env, io}; + +use tempfile::TempDir; + +pub struct Handler { + pub temp_dir: TempDir, + pub original_dir: PathBuf, +} + +impl Handler { + /// Creates a new temporary directory and remembers the current working directory. + /// + /// # Errors + /// + /// Will error if: + /// + /// - It can't create the temp dir. + /// - It can't get the current dir. + pub fn new() -> io::Result { + let temp_dir = TempDir::new()?; + let original_dir = env::current_dir()?; + + Ok(Handler { temp_dir, original_dir }) + } + + /// Changes the current working directory to the temporary directory. + /// + /// # Errors + /// + /// Will error if it can't change the current di to the temp dir. + pub fn change_to_temp_dir(&self) -> io::Result<()> { + env::set_current_dir(self.temp_dir.path()) + } + + /// Changes the current working directory back to the original directory. + /// + /// # Errors + /// + /// Will error if it can't revert the current dir to the original one. + pub fn revert_to_original_dir(&self) -> io::Result<()> { + env::set_current_dir(&self.original_dir) + } +} + +impl Drop for Handler { + /// Ensures that the temporary directory is deleted when the struct goes out of scope. + fn drop(&mut self) { + // The temporary directory is automatically deleted when `TempDir` is dropped. + // We can add additional cleanup here if necessary. + } +} diff --git a/src/lib.rs b/src/lib.rs index 7b5d453a4..f239039bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -473,6 +473,7 @@ pub mod app; pub mod bootstrap; pub mod checker; pub mod core; +pub mod e2e; pub mod servers; pub mod shared;