Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,32 @@ jobs:
- id: test
name: Run Unit Tests
run: cargo test --tests --benches --examples --workspace --all-targets --all-features

e2e:
name: E2E
runs-on: ubuntu-latest
needs: unit

strategy:
matrix:
toolchain: [nightly]

steps:
- id: setup
name: Setup Toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.toolchain }}
components: llvm-tools-preview

- id: cache
name: Enable Job Cache
uses: Swatinem/rust-cache@v2

- id: checkout
name: Checkout Repository
uses: actions/checkout@v4

- id: test
name: Run E2E Tests
run: cargo run --bin e2e_tests_runner ./share/default/config/tracker.e2e.container.sqlite3.toml
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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"] }
Expand Down
1 change: 1 addition & 0 deletions cSpell.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"Swatinem",
"Swiftbit",
"taiki",
"tempfile",
"thiserror",
"tlsv",
"Torrentstorm",
Expand Down
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
}
}
Loading