Skip to content

Commit 083af0e

Browse files
committed
feat: [torrust#634] E2E test runner: build, run and stop tracker image
1 parent 91c268a commit 083af0e

9 files changed

+281
-0
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ tower-http = { version = "0", features = ["compression-full"] }
7070
uuid = { version = "1", features = ["v4"] }
7171
colored = "2.1.0"
7272
url = "2.5.0"
73+
tempfile = "3.9.0"
7374

7475
[dev-dependencies]
7576
criterion = { version = "0.5.1", features = ["async_tokio"] }

cSpell.json

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"Swatinem",
115115
"Swiftbit",
116116
"taiki",
117+
"tempfile",
117118
"thiserror",
118119
"tlsv",
119120
"Torrentstorm",

src/bin/e2e_tests_runner.rs

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! Program to run E2E tests.
2+
//!
3+
//! ```text
4+
//! cargo run --bin e2e_tests_runner
5+
//! ```
6+
use torrust_tracker::e2e::runner;
7+
8+
fn main() {
9+
runner::run();
10+
}

src/e2e/docker.rs

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use std::io;
2+
use std::process::{Child, Command, Output, Stdio};
3+
use std::thread::sleep;
4+
use std::time::{Duration, Instant};
5+
6+
pub struct Docker {}
7+
8+
impl Docker {
9+
/// Builds a Docker image from a given Dockerfile.
10+
///
11+
/// # Errors
12+
///
13+
/// Will fail if the docker build command fails.
14+
pub fn build(dockerfile: &str, tag: &str) -> io::Result<()> {
15+
let status = Command::new("docker")
16+
.args(["build", "-f", dockerfile, "-t", tag, "."])
17+
.status()?;
18+
19+
if status.success() {
20+
Ok(())
21+
} else {
22+
Err(io::Error::new(io::ErrorKind::Other, "Failed to build Docker image"))
23+
}
24+
}
25+
26+
/// Runs a Docker container from a given image.
27+
///
28+
/// # Errors
29+
///
30+
/// Will fail if the docker run command fails.
31+
pub fn run(image: &str, name: &str) -> io::Result<Output> {
32+
let output = Command::new("docker")
33+
.args(["run", "--detach", "--name", name, image])
34+
.output()?;
35+
36+
if output.status.success() {
37+
Ok(output)
38+
} else {
39+
Err(io::Error::new(io::ErrorKind::Other, "Failed to run Docker container"))
40+
}
41+
}
42+
43+
/// Runs a Docker container from a given image in the background.
44+
///
45+
/// # Errors
46+
///
47+
/// Will fail if the docker run command fails to start.
48+
pub fn run_spawned(image: &str, name: &str) -> io::Result<Child> {
49+
let child = Command::new("docker")
50+
.args(["run", "--name", name, image])
51+
.stdin(Stdio::null()) // Ignore stdin
52+
.stdout(Stdio::null()) // Ignore stdout
53+
.stderr(Stdio::null()) // Ignore stderr
54+
.spawn()?;
55+
56+
Ok(child)
57+
}
58+
59+
/// Stops a Docker container.
60+
///
61+
/// # Errors
62+
///
63+
/// Will fail if the docker stop command fails.
64+
pub fn stop(name: &str) -> io::Result<()> {
65+
let status = Command::new("docker").args(["stop", name]).status()?;
66+
67+
if status.success() {
68+
Ok(())
69+
} else {
70+
Err(io::Error::new(io::ErrorKind::Other, "Failed to stop Docker container"))
71+
}
72+
}
73+
74+
/// Removes a Docker container.
75+
///
76+
/// # Errors
77+
///
78+
/// Will fail if the docker rm command fails.
79+
pub fn remove(name: &str) -> io::Result<()> {
80+
let status = Command::new("docker").args(["rm", "-f", name]).status()?;
81+
82+
if status.success() {
83+
Ok(())
84+
} else {
85+
Err(io::Error::new(io::ErrorKind::Other, "Failed to remove Docker container"))
86+
}
87+
}
88+
89+
/// Fetches logs from a Docker container.
90+
///
91+
/// # Errors
92+
///
93+
/// Will fail if the docker logs command fails.
94+
pub fn logs(container_name: &str) -> io::Result<String> {
95+
let output = Command::new("docker").args(["logs", container_name]).output()?;
96+
97+
if output.status.success() {
98+
Ok(String::from_utf8_lossy(&output.stdout).to_string())
99+
} else {
100+
Err(io::Error::new(
101+
io::ErrorKind::Other,
102+
"Failed to fetch logs from Docker container",
103+
))
104+
}
105+
}
106+
107+
/// Checks if a Docker container is healthy.
108+
#[must_use]
109+
pub fn wait_until_is_healthy(name: &str, timeout: Duration) -> bool {
110+
let start = Instant::now();
111+
112+
while start.elapsed() < timeout {
113+
let Ok(output) = Command::new("docker")
114+
.args(["ps", "-f", &format!("name={name}"), "--format", "{{.Status}}"])
115+
.output()
116+
else {
117+
return false;
118+
};
119+
120+
let output_str = String::from_utf8_lossy(&output.stdout);
121+
122+
if output_str.contains("(healthy)") {
123+
return true;
124+
}
125+
126+
sleep(Duration::from_secs(1));
127+
}
128+
129+
false
130+
}
131+
}

src/e2e/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod docker;
2+
pub mod runner;
3+
pub mod temp_dir;

src/e2e/runner.rs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use std::env;
2+
use std::time::Duration;
3+
4+
use rand::distributions::Alphanumeric;
5+
use rand::Rng;
6+
7+
use crate::e2e::docker::Docker;
8+
use crate::e2e::temp_dir::Handler;
9+
10+
/// # Panics
11+
///
12+
/// Will panic if:
13+
///
14+
/// - It can't build the docker image.
15+
/// - It can't create a temp dir.
16+
/// - It can't change to the new temp dir.
17+
/// - It can't revert the dit to the previous one.
18+
pub fn run() {
19+
/* todo:
20+
21+
- [x] Build the docker image.
22+
- [x] Run the docker image.
23+
- [x] Wait until the container is healthy.
24+
- [ ] Parse logs to get running services.
25+
- [ ] Build config file for the tracker_checker.
26+
- [ ] Run the tracker_checker.
27+
- [x] Stop the container.
28+
29+
*/
30+
31+
//Docker::build("./Containerfile", "local").expect("A tracker local docker image should be built");
32+
33+
println!(
34+
"Current dir: {:?}",
35+
env::current_dir().expect("It should return the current dir")
36+
);
37+
38+
println!("Create temp dir ...");
39+
let temp_dir_handler = Handler::new().expect("A temp dir should be created");
40+
println!("Temp dir created: {:?}", temp_dir_handler.temp_dir);
41+
42+
println!("Change dir to: {:?}", temp_dir_handler.temp_dir);
43+
temp_dir_handler
44+
.change_to_temp_dir()
45+
.expect("The app should change dir to the temp dir");
46+
47+
let container_name = generate_random_container_name("tracker_");
48+
49+
println!("Running docker tracker image: {container_name} ...");
50+
Docker::run("local", &container_name).expect("A tracker local docker image should be running");
51+
52+
println!("Waiting for the container {container_name} to be healthy ...");
53+
let is_healthy = Docker::wait_until_is_healthy(&container_name, Duration::from_secs(10));
54+
55+
if !is_healthy {
56+
println!("Unhealthy container: {container_name}");
57+
println!("Stopping container: {container_name} ...");
58+
Docker::stop(&container_name).expect("A tracker local docker image should be stopped");
59+
panic!("Unhealthy container: {container_name}");
60+
}
61+
62+
println!("Container {container_name} is healthy ...");
63+
64+
println!("Stopping docker tracker image: {container_name} ...");
65+
Docker::stop(&container_name).expect("A tracker local docker image should be stopped");
66+
67+
println!("Revert current dir to: {:?}", temp_dir_handler.original_dir);
68+
temp_dir_handler
69+
.revert_to_original_dir()
70+
.expect("The app should revert dir from temp dir to the original one");
71+
}
72+
73+
fn generate_random_container_name(prefix: &str) -> String {
74+
let rand_string: String = rand::thread_rng()
75+
.sample_iter(&Alphanumeric)
76+
.take(20)
77+
.map(char::from)
78+
.collect();
79+
80+
format!("{prefix}{rand_string}")
81+
}

src/e2e/temp_dir.rs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::path::PathBuf;
2+
use std::{env, io};
3+
4+
use tempfile::TempDir;
5+
6+
pub struct Handler {
7+
pub temp_dir: TempDir,
8+
pub original_dir: PathBuf,
9+
}
10+
11+
impl Handler {
12+
/// Creates a new temporary directory and remembers the current working directory.
13+
///
14+
/// # Errors
15+
///
16+
/// Will error if:
17+
///
18+
/// - It can't create the temp dir.
19+
/// - It can't get the current dir.
20+
pub fn new() -> io::Result<Self> {
21+
let temp_dir = TempDir::new()?;
22+
let original_dir = env::current_dir()?;
23+
24+
Ok(Handler { temp_dir, original_dir })
25+
}
26+
27+
/// Changes the current working directory to the temporary directory.
28+
///
29+
/// # Errors
30+
///
31+
/// Will error if it can't change the current di to the temp dir.
32+
pub fn change_to_temp_dir(&self) -> io::Result<()> {
33+
env::set_current_dir(self.temp_dir.path())
34+
}
35+
36+
/// Changes the current working directory back to the original directory.
37+
///
38+
/// # Errors
39+
///
40+
/// Will error if it can't revert the current dir to the original one.
41+
pub fn revert_to_original_dir(&self) -> io::Result<()> {
42+
env::set_current_dir(&self.original_dir)
43+
}
44+
}
45+
46+
impl Drop for Handler {
47+
/// Ensures that the temporary directory is deleted when the struct goes out of scope.
48+
fn drop(&mut self) {
49+
// The temporary directory is automatically deleted when `TempDir` is dropped.
50+
// We can add additional cleanup here if necessary.
51+
}
52+
}

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ pub mod app;
473473
pub mod bootstrap;
474474
pub mod checker;
475475
pub mod core;
476+
pub mod e2e;
476477
pub mod servers;
477478
pub mod shared;
478479

0 commit comments

Comments
 (0)