Skip to content

Commit 4edcd2e

Browse files
committed
ci: [torrust#634] new script to run E2E tests
It uses Rust instead of Bash. You can run it with: ``` cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml ``` It will: - Build the tracker docker image. - Run the docker image. - Wait until the container is healthy. - Parse logs to get running services. - Build config file for the tracker_checker. - Run the tracker_checker. - Stop the container.
1 parent 8e43205 commit 4edcd2e

File tree

8 files changed

+614
-0
lines changed

8 files changed

+614
-0
lines changed
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+
}

src/e2e/logs_parser.rs

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! Utilities to parse Torrust Tracker logs.
2+
use serde::{Deserialize, Serialize};
3+
4+
const UDP_TRACKER_PATTERN: &str = "[UDP Tracker][INFO] Starting on: udp://";
5+
const HTTP_TRACKER_PATTERN: &str = "[HTTP Tracker][INFO] Starting on: ";
6+
const HEALTH_CHECK_PATTERN: &str = "[Health Check API][INFO] Starting on: ";
7+
8+
#[derive(Serialize, Deserialize, Debug, Default)]
9+
pub struct RunningServices {
10+
pub udp_trackers: Vec<String>,
11+
pub http_trackers: Vec<String>,
12+
pub health_checks: Vec<String>,
13+
}
14+
15+
impl RunningServices {
16+
/// It parses the tracker logs to extract the running services.
17+
///
18+
/// For example, from this logs:
19+
///
20+
/// ```text
21+
/// Loading default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ...
22+
/// 2024-01-24T16:36:14.614898789+00:00 [torrust_tracker::bootstrap::logging][INFO] logging initialized.
23+
/// 2024-01-24T16:36:14.615586025+00:00 [UDP Tracker][INFO] Starting on: udp://0.0.0.0:6969
24+
/// 2024-01-24T16:36:14.615623705+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled
25+
/// 2024-01-24T16:36:14.615694484+00:00 [HTTP Tracker][INFO] Starting on: http://0.0.0.0:7070
26+
/// 2024-01-24T16:36:14.615710534+00:00 [HTTP Tracker][INFO] Started on: http://0.0.0.0:7070
27+
/// 2024-01-24T16:36:14.615716574+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled
28+
/// 2024-01-24T16:36:14.615764904+00:00 [API][INFO] Starting on http://127.0.0.1:1212
29+
/// 2024-01-24T16:36:14.615767264+00:00 [API][INFO] Started on http://127.0.0.1:1212
30+
/// 2024-01-24T16:36:14.615777574+00:00 [Health Check API][INFO] Starting on: http://127.0.0.1:1313
31+
/// 2024-01-24T16:36:14.615791124+00:00 [Health Check API][INFO] Started on: http://127.0.0.1:1313
32+
/// ```
33+
///
34+
/// It would extract these services:
35+
///
36+
/// ```json
37+
/// {
38+
/// "udp_trackers": [
39+
/// "127.0.0.1:6969"
40+
/// ],
41+
/// "http_trackers": [
42+
/// "http://127.0.0.1:7070"
43+
/// ],
44+
/// "health_checks": [
45+
/// "http://127.0.0.1:1313/health_check"
46+
/// ]
47+
/// }
48+
/// ```
49+
#[must_use]
50+
pub fn parse_from_logs(logs: &str) -> Self {
51+
let mut udp_trackers: Vec<String> = Vec::new();
52+
let mut http_trackers: Vec<String> = Vec::new();
53+
let mut health_checks: Vec<String> = Vec::new();
54+
55+
for line in logs.lines() {
56+
if let Some(address) = Self::extract_address_if_matches(line, UDP_TRACKER_PATTERN) {
57+
udp_trackers.push(address);
58+
} else if let Some(address) = Self::extract_address_if_matches(line, HTTP_TRACKER_PATTERN) {
59+
http_trackers.push(address);
60+
} else if let Some(address) = Self::extract_address_if_matches(line, HEALTH_CHECK_PATTERN) {
61+
health_checks.push(format!("{address}/health_check"));
62+
}
63+
}
64+
65+
Self {
66+
udp_trackers,
67+
http_trackers,
68+
health_checks,
69+
}
70+
}
71+
72+
fn extract_address_if_matches(line: &str, pattern: &str) -> Option<String> {
73+
line.find(pattern)
74+
.map(|start| Self::replace_wildcard_ip_with_localhost(line[start + pattern.len()..].trim()))
75+
}
76+
77+
fn replace_wildcard_ip_with_localhost(address: &str) -> String {
78+
address.replace("0.0.0.0", "127.0.0.1")
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use super::*;
85+
86+
#[test]
87+
fn it_should_parse_from_logs_with_valid_logs() {
88+
let logs = "\
89+
[UDP Tracker][INFO] Starting on: udp://0.0.0.0:8080\n\
90+
[HTTP Tracker][INFO] Starting on: 0.0.0.0:9090\n\
91+
[Health Check API][INFO] Starting on: 0.0.0.0:10010";
92+
let running_services = RunningServices::parse_from_logs(logs);
93+
94+
assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:8080"]);
95+
assert_eq!(running_services.http_trackers, vec!["127.0.0.1:9090"]);
96+
assert_eq!(running_services.health_checks, vec!["127.0.0.1:10010/health_check"]);
97+
}
98+
99+
#[test]
100+
fn it_should_ignore_logs_with_no_matching_lines() {
101+
let logs = "[Other Service][INFO] Starting on: 0.0.0.0:7070";
102+
let running_services = RunningServices::parse_from_logs(logs);
103+
104+
assert!(running_services.udp_trackers.is_empty());
105+
assert!(running_services.http_trackers.is_empty());
106+
assert!(running_services.health_checks.is_empty());
107+
}
108+
109+
#[test]
110+
fn it_should_replace_wildcard_ip_with_localhost() {
111+
let address = "0.0.0.0:8080";
112+
assert_eq!(RunningServices::replace_wildcard_ip_with_localhost(address), "127.0.0.1:8080");
113+
}
114+
}

src/e2e/mod.rs

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

0 commit comments

Comments
 (0)