Skip to content

Commit

Permalink
ci: [#634] new script to run E2E tests
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
josecelano committed Jan 24, 2024
1 parent 8e43205 commit 4edcd2e
Show file tree
Hide file tree
Showing 8 changed files with 614 additions and 0 deletions.
41 changes: 41 additions & 0 deletions share/default/config/tracker.e2e.container.sqlite3.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
announce_interval = 120
db_driver = "Sqlite3"
db_path = "/var/lib/torrust/tracker/database/sqlite3.db"
external_ip = "0.0.0.0"
inactive_peer_cleanup_interval = 600
log_level = "info"
max_peer_timeout = 900
min_announce_interval = 120
mode = "public"
on_reverse_proxy = false
persistent_torrent_completed_stat = false
remove_peerless_torrents = true
tracker_usage_statistics = true

[[udp_trackers]]
bind_address = "0.0.0.0:6969"
enabled = true

[[http_trackers]]
bind_address = "0.0.0.0:7070"
enabled = true
ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt"
ssl_enabled = false
ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key"

[http_api]
bind_address = "0.0.0.0:1212"
enabled = true
ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt"
ssl_enabled = false
ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key"

# Please override the admin token setting the
# `TORRUST_TRACKER_API_ADMIN_TOKEN`
# environmental variable!

[http_api.access_tokens]
admin = "MyAccessToken"

[health_check_api]
bind_address = "0.0.0.0:1313"
10 changes: 10 additions & 0 deletions src/bin/e2e_tests_runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Program to run E2E tests.
//!
//! ```text
//! cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml
//! ```
use torrust_tracker::e2e;

fn main() {
e2e::runner::run();
}
177 changes: 177 additions & 0 deletions src/e2e/docker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//! Docker command wrapper.
use std::io;
use std::process::{Command, Output};
use std::thread::sleep;
use std::time::{Duration, Instant};

use log::debug;

/// Docker command wrapper.
pub struct Docker {}

pub struct RunningContainer {
pub name: String,
pub output: Output,
}

impl Drop for RunningContainer {
/// Ensures that the temporary container is stopped and removed when the
/// struct goes out of scope.
fn drop(&mut self) {
let _unused = Docker::stop(self);
let _unused = Docker::remove(&self.name);
}
}

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,
format!("Failed to build Docker image from dockerfile {dockerfile}"),
))
}
}

/// Runs a Docker container from a given image with multiple environment variables.
///
/// # Arguments
///
/// * `image` - The Docker image to run.
/// * `container` - The name for the Docker container.
/// * `env_vars` - A slice of tuples, each representing an environment variable as ("KEY", "value").
///
/// # Errors
///
/// Will fail if the docker run command fails.
pub fn run(image: &str, container: &str, env_vars: &[(String, String)], ports: &[String]) -> io::Result<RunningContainer> {
let initial_args = vec![
"run".to_string(),
"--detach".to_string(),
"--name".to_string(),
container.to_string(),
];

// Add environment variables
let mut env_var_args: Vec<String> = vec![];
for (key, value) in env_vars {
env_var_args.push("--env".to_string());
env_var_args.push(format!("{key}={value}"));
}

// Add port mappings
let mut port_args: Vec<String> = vec![];
for port in ports {
port_args.push("--publish".to_string());
port_args.push(port.to_string());
}

let args = [initial_args, env_var_args, port_args, [image.to_string()].to_vec()].concat();

debug!("Docker run args: {:?}", args);

let output = Command::new("docker").args(args).output()?;

if output.status.success() {
Ok(RunningContainer {
name: container.to_owned(),
output,
})
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to run Docker image {image}"),
))
}
}

/// Stops a Docker container.
///
/// # Errors
///
/// Will fail if the docker stop command fails.
pub fn stop(container: &RunningContainer) -> io::Result<()> {
let status = Command::new("docker").args(["stop", &container.name]).status()?;

if status.success() {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to stop Docker container {}", container.name),
))
}
}

/// Removes a Docker container.
///
/// # Errors
///
/// Will fail if the docker rm command fails.
pub fn remove(container: &str) -> io::Result<()> {
let status = Command::new("docker").args(["rm", "-f", container]).status()?;

if status.success() {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to remove Docker container {container}"),
))
}
}

/// Fetches logs from a Docker container.
///
/// # Errors
///
/// Will fail if the docker logs command fails.
pub fn logs(container: &str) -> io::Result<String> {
let output = Command::new("docker").args(["logs", container]).output()?;

if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to fetch logs from Docker container {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
}
}
114 changes: 114 additions & 0 deletions src/e2e/logs_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Utilities to parse Torrust Tracker logs.
use serde::{Deserialize, Serialize};

const UDP_TRACKER_PATTERN: &str = "[UDP Tracker][INFO] Starting on: udp://";
const HTTP_TRACKER_PATTERN: &str = "[HTTP Tracker][INFO] Starting on: ";
const HEALTH_CHECK_PATTERN: &str = "[Health Check API][INFO] Starting on: ";

#[derive(Serialize, Deserialize, Debug, Default)]
pub struct RunningServices {
pub udp_trackers: Vec<String>,
pub http_trackers: Vec<String>,
pub health_checks: Vec<String>,
}

impl RunningServices {
/// It parses the tracker logs to extract the running services.
///
/// For example, from this logs:
///
/// ```text
/// Loading default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ...
/// 2024-01-24T16:36:14.614898789+00:00 [torrust_tracker::bootstrap::logging][INFO] logging initialized.
/// 2024-01-24T16:36:14.615586025+00:00 [UDP Tracker][INFO] Starting on: udp://0.0.0.0:6969
/// 2024-01-24T16:36:14.615623705+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled
/// 2024-01-24T16:36:14.615694484+00:00 [HTTP Tracker][INFO] Starting on: http://0.0.0.0:7070
/// 2024-01-24T16:36:14.615710534+00:00 [HTTP Tracker][INFO] Started on: http://0.0.0.0:7070
/// 2024-01-24T16:36:14.615716574+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled
/// 2024-01-24T16:36:14.615764904+00:00 [API][INFO] Starting on http://127.0.0.1:1212
/// 2024-01-24T16:36:14.615767264+00:00 [API][INFO] Started on http://127.0.0.1:1212
/// 2024-01-24T16:36:14.615777574+00:00 [Health Check API][INFO] Starting on: http://127.0.0.1:1313
/// 2024-01-24T16:36:14.615791124+00:00 [Health Check API][INFO] Started on: http://127.0.0.1:1313
/// ```
///
/// It would extract these services:
///
/// ```json
/// {
/// "udp_trackers": [
/// "127.0.0.1:6969"
/// ],
/// "http_trackers": [
/// "http://127.0.0.1:7070"
/// ],
/// "health_checks": [
/// "http://127.0.0.1:1313/health_check"
/// ]
/// }
/// ```
#[must_use]
pub fn parse_from_logs(logs: &str) -> Self {
let mut udp_trackers: Vec<String> = Vec::new();
let mut http_trackers: Vec<String> = Vec::new();
let mut health_checks: Vec<String> = Vec::new();

for line in logs.lines() {
if let Some(address) = Self::extract_address_if_matches(line, UDP_TRACKER_PATTERN) {
udp_trackers.push(address);
} else if let Some(address) = Self::extract_address_if_matches(line, HTTP_TRACKER_PATTERN) {
http_trackers.push(address);
} else if let Some(address) = Self::extract_address_if_matches(line, HEALTH_CHECK_PATTERN) {
health_checks.push(format!("{address}/health_check"));
}
}

Self {
udp_trackers,
http_trackers,
health_checks,
}
}

fn extract_address_if_matches(line: &str, pattern: &str) -> Option<String> {
line.find(pattern)
.map(|start| Self::replace_wildcard_ip_with_localhost(line[start + pattern.len()..].trim()))
}

fn replace_wildcard_ip_with_localhost(address: &str) -> String {
address.replace("0.0.0.0", "127.0.0.1")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_should_parse_from_logs_with_valid_logs() {
let logs = "\
[UDP Tracker][INFO] Starting on: udp://0.0.0.0:8080\n\
[HTTP Tracker][INFO] Starting on: 0.0.0.0:9090\n\
[Health Check API][INFO] Starting on: 0.0.0.0:10010";
let running_services = RunningServices::parse_from_logs(logs);

assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:8080"]);
assert_eq!(running_services.http_trackers, vec!["127.0.0.1:9090"]);
assert_eq!(running_services.health_checks, vec!["127.0.0.1:10010/health_check"]);
}

#[test]
fn it_should_ignore_logs_with_no_matching_lines() {
let logs = "[Other Service][INFO] Starting on: 0.0.0.0:7070";
let running_services = RunningServices::parse_from_logs(logs);

assert!(running_services.udp_trackers.is_empty());
assert!(running_services.http_trackers.is_empty());
assert!(running_services.health_checks.is_empty());
}

#[test]
fn it_should_replace_wildcard_ip_with_localhost() {
let address = "0.0.0.0:8080";
assert_eq!(RunningServices::replace_wildcard_ip_with_localhost(address), "127.0.0.1:8080");
}
}
4 changes: 4 additions & 0 deletions src/e2e/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod docker;
pub mod logs_parser;
pub mod runner;
pub mod temp_dir;
Loading

0 comments on commit 4edcd2e

Please sign in to comment.