Skip to content

Commit

Permalink
Merge torrust#643: E2E tests runner
Browse files Browse the repository at this point in the history
ec13fb4 ci: [torrust#634] run E2E tests in the testing workflow (Jose Celano)
4edcd2e ci: [torrust#634] new script to run E2E tests (Jose Celano)
8e43205 ci: [torrust#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
  • Loading branch information
josecelano committed Jan 25, 2024
2 parents dee86be + ec13fb4 commit 14f88e9
Show file tree
Hide file tree
Showing 12 changed files with 646 additions and 0 deletions.
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

0 comments on commit 14f88e9

Please sign in to comment.