Skip to content

Commit 14f88e9

Browse files
committed
Merge #643: E2E tests runner
ec13fb4 ci: [#634] run E2E tests in the testing workflow (Jose Celano) 4edcd2e ci: [#634] new script to run E2E tests (Jose Celano) 8e43205 ci: [#634] new dependency to make temp dirs (Jose Celano) Pull request description: A new binary to run E2E tests: - [x] Build the docker image. - [x] Run the docker image. - [x] Wait until the container is healthy. - [x] Parse logs to get running services. - [x] Build config file for the tracker_checker. - [x] Run the tracker_checker. - [x] Stop the container. ACKs for top commit: josecelano: ACK ec13fb4 Tree-SHA512: e3ae9d399cdf911b0ef8f14afd838c85e007996355632bf265baa640c6c611952f1feab038334a5a3fe1a567561b0fcbc81d56770d8908db9d8ae0c06f1758ab
2 parents dee86be + ec13fb4 commit 14f88e9

12 files changed

+646
-0
lines changed

.github/workflows/testing.yaml

+29
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,32 @@ jobs:
110110
- id: test
111111
name: Run Unit Tests
112112
run: cargo test --tests --benches --examples --workspace --all-targets --all-features
113+
114+
e2e:
115+
name: E2E
116+
runs-on: ubuntu-latest
117+
needs: unit
118+
119+
strategy:
120+
matrix:
121+
toolchain: [nightly]
122+
123+
steps:
124+
- id: setup
125+
name: Setup Toolchain
126+
uses: dtolnay/rust-toolchain@stable
127+
with:
128+
toolchain: ${{ matrix.toolchain }}
129+
components: llvm-tools-preview
130+
131+
- id: cache
132+
name: Enable Job Cache
133+
uses: Swatinem/rust-cache@v2
134+
135+
- id: checkout
136+
name: Checkout Repository
137+
uses: actions/checkout@v4
138+
139+
- id: test
140+
name: Run E2E Tests
141+
run: cargo run --bin e2e_tests_runner ./share/default/config/tracker.e2e.container.sqlite3.toml

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
@@ -71,6 +71,7 @@ tower-http = { version = "0", features = ["compression-full"] }
7171
uuid = { version = "1", features = ["v4"] }
7272
colored = "2.1.0"
7373
url = "2.5.0"
74+
tempfile = "3.9.0"
7475

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

cSpell.json

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"Swatinem",
117117
"Swiftbit",
118118
"taiki",
119+
"tempfile",
119120
"thiserror",
120121
"tlsv",
121122
"Torrentstorm",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
announce_interval = 120
2+
db_driver = "Sqlite3"
3+
db_path = "/var/lib/torrust/tracker/database/sqlite3.db"
4+
external_ip = "0.0.0.0"
5+
inactive_peer_cleanup_interval = 600
6+
log_level = "info"
7+
max_peer_timeout = 900
8+
min_announce_interval = 120
9+
mode = "public"
10+
on_reverse_proxy = false
11+
persistent_torrent_completed_stat = false
12+
remove_peerless_torrents = true
13+
tracker_usage_statistics = true
14+
15+
[[udp_trackers]]
16+
bind_address = "0.0.0.0:6969"
17+
enabled = true
18+
19+
[[http_trackers]]
20+
bind_address = "0.0.0.0:7070"
21+
enabled = true
22+
ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt"
23+
ssl_enabled = false
24+
ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key"
25+
26+
[http_api]
27+
bind_address = "0.0.0.0:1212"
28+
enabled = true
29+
ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt"
30+
ssl_enabled = false
31+
ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key"
32+
33+
# Please override the admin token setting the
34+
# `TORRUST_TRACKER_API_ADMIN_TOKEN`
35+
# environmental variable!
36+
37+
[http_api.access_tokens]
38+
admin = "MyAccessToken"
39+
40+
[health_check_api]
41+
bind_address = "0.0.0.0:1313"

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 share/default/config/tracker.e2e.container.sqlite3.toml
5+
//! ```
6+
use torrust_tracker::e2e;
7+
8+
fn main() {
9+
e2e::runner::run();
10+
}

src/e2e/docker.rs

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//! Docker command wrapper.
2+
use std::io;
3+
use std::process::{Command, Output};
4+
use std::thread::sleep;
5+
use std::time::{Duration, Instant};
6+
7+
use log::debug;
8+
9+
/// Docker command wrapper.
10+
pub struct Docker {}
11+
12+
pub struct RunningContainer {
13+
pub name: String,
14+
pub output: Output,
15+
}
16+
17+
impl Drop for RunningContainer {
18+
/// Ensures that the temporary container is stopped and removed when the
19+
/// struct goes out of scope.
20+
fn drop(&mut self) {
21+
let _unused = Docker::stop(self);
22+
let _unused = Docker::remove(&self.name);
23+
}
24+
}
25+
26+
impl Docker {
27+
/// Builds a Docker image from a given Dockerfile.
28+
///
29+
/// # Errors
30+
///
31+
/// Will fail if the docker build command fails.
32+
pub fn build(dockerfile: &str, tag: &str) -> io::Result<()> {
33+
let status = Command::new("docker")
34+
.args(["build", "-f", dockerfile, "-t", tag, "."])
35+
.status()?;
36+
37+
if status.success() {
38+
Ok(())
39+
} else {
40+
Err(io::Error::new(
41+
io::ErrorKind::Other,
42+
format!("Failed to build Docker image from dockerfile {dockerfile}"),
43+
))
44+
}
45+
}
46+
47+
/// Runs a Docker container from a given image with multiple environment variables.
48+
///
49+
/// # Arguments
50+
///
51+
/// * `image` - The Docker image to run.
52+
/// * `container` - The name for the Docker container.
53+
/// * `env_vars` - A slice of tuples, each representing an environment variable as ("KEY", "value").
54+
///
55+
/// # Errors
56+
///
57+
/// Will fail if the docker run command fails.
58+
pub fn run(image: &str, container: &str, env_vars: &[(String, String)], ports: &[String]) -> io::Result<RunningContainer> {
59+
let initial_args = vec![
60+
"run".to_string(),
61+
"--detach".to_string(),
62+
"--name".to_string(),
63+
container.to_string(),
64+
];
65+
66+
// Add environment variables
67+
let mut env_var_args: Vec<String> = vec![];
68+
for (key, value) in env_vars {
69+
env_var_args.push("--env".to_string());
70+
env_var_args.push(format!("{key}={value}"));
71+
}
72+
73+
// Add port mappings
74+
let mut port_args: Vec<String> = vec![];
75+
for port in ports {
76+
port_args.push("--publish".to_string());
77+
port_args.push(port.to_string());
78+
}
79+
80+
let args = [initial_args, env_var_args, port_args, [image.to_string()].to_vec()].concat();
81+
82+
debug!("Docker run args: {:?}", args);
83+
84+
let output = Command::new("docker").args(args).output()?;
85+
86+
if output.status.success() {
87+
Ok(RunningContainer {
88+
name: container.to_owned(),
89+
output,
90+
})
91+
} else {
92+
Err(io::Error::new(
93+
io::ErrorKind::Other,
94+
format!("Failed to run Docker image {image}"),
95+
))
96+
}
97+
}
98+
99+
/// Stops a Docker container.
100+
///
101+
/// # Errors
102+
///
103+
/// Will fail if the docker stop command fails.
104+
pub fn stop(container: &RunningContainer) -> io::Result<()> {
105+
let status = Command::new("docker").args(["stop", &container.name]).status()?;
106+
107+
if status.success() {
108+
Ok(())
109+
} else {
110+
Err(io::Error::new(
111+
io::ErrorKind::Other,
112+
format!("Failed to stop Docker container {}", container.name),
113+
))
114+
}
115+
}
116+
117+
/// Removes a Docker container.
118+
///
119+
/// # Errors
120+
///
121+
/// Will fail if the docker rm command fails.
122+
pub fn remove(container: &str) -> io::Result<()> {
123+
let status = Command::new("docker").args(["rm", "-f", container]).status()?;
124+
125+
if status.success() {
126+
Ok(())
127+
} else {
128+
Err(io::Error::new(
129+
io::ErrorKind::Other,
130+
format!("Failed to remove Docker container {container}"),
131+
))
132+
}
133+
}
134+
135+
/// Fetches logs from a Docker container.
136+
///
137+
/// # Errors
138+
///
139+
/// Will fail if the docker logs command fails.
140+
pub fn logs(container: &str) -> io::Result<String> {
141+
let output = Command::new("docker").args(["logs", container]).output()?;
142+
143+
if output.status.success() {
144+
Ok(String::from_utf8_lossy(&output.stdout).to_string())
145+
} else {
146+
Err(io::Error::new(
147+
io::ErrorKind::Other,
148+
format!("Failed to fetch logs from Docker container {container}"),
149+
))
150+
}
151+
}
152+
153+
/// Checks if a Docker container is healthy.
154+
#[must_use]
155+
pub fn wait_until_is_healthy(name: &str, timeout: Duration) -> bool {
156+
let start = Instant::now();
157+
158+
while start.elapsed() < timeout {
159+
let Ok(output) = Command::new("docker")
160+
.args(["ps", "-f", &format!("name={name}"), "--format", "{{.Status}}"])
161+
.output()
162+
else {
163+
return false;
164+
};
165+
166+
let output_str = String::from_utf8_lossy(&output.stdout);
167+
168+
if output_str.contains("(healthy)") {
169+
return true;
170+
}
171+
172+
sleep(Duration::from_secs(1));
173+
}
174+
175+
false
176+
}
177+
}

0 commit comments

Comments
 (0)