torrust_tracker/console/ci/e2e/
runner.rs

1//! Program to run E2E tests.
2//!
3//! You can execute it with (passing a TOML config file path):
4//!
5//! ```text
6//! cargo run --bin e2e_tests_runner -- --config-toml-path "./share/default/config/tracker.e2e.container.sqlite3.toml"
7//! ```
8//!
9//! Or:
10//!
11//! ```text
12//! TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.e2e.container.sqlite3.toml" cargo run --bin e2e_tests_runner"
13//! ```
14//!
15//! You can execute it with (directly passing TOML config):
16//!
17//! ```text
18//! TORRUST_TRACKER_CONFIG_TOML=$(cat "./share/default/config/tracker.e2e.container.sqlite3.toml") cargo run --bin e2e_tests_runner
19//! ```
20use std::path::PathBuf;
21
22use anyhow::Context;
23use clap::Parser;
24use tracing::level_filters::LevelFilter;
25
26use super::tracker_container::TrackerContainer;
27use crate::console::ci::e2e::docker::RunOptions;
28use crate::console::ci::e2e::logs_parser::RunningServices;
29use crate::console::ci::e2e::tracker_checker::{self};
30
31/* code-review:
32     - We use always the same docker image name. Should we use a random image name (tag)?
33     - We use the name image name we use in other workflows `torrust-tracker:local`.
34       Should we use a different one like `torrust-tracker:e2e`?
35     - We remove the container after running tests but not the container image.
36       Should we remove the image too?
37*/
38
39const CONTAINER_IMAGE: &str = "torrust-tracker:local";
40const CONTAINER_NAME_PREFIX: &str = "tracker_";
41
42#[derive(Parser, Debug)]
43#[clap(author, version, about, long_about = None)]
44struct Args {
45    /// Path to the JSON configuration file.
46    #[clap(short, long, env = "TORRUST_TRACKER_CONFIG_TOML_PATH")]
47    config_toml_path: Option<PathBuf>,
48
49    /// Direct configuration content in JSON.
50    #[clap(env = "TORRUST_TRACKER_CONFIG_TOML", hide_env_values = true)]
51    config_toml: Option<String>,
52}
53
54/// Script to run E2E tests.
55///
56/// # Errors
57///
58/// Will return an error if it can't load the tracker configuration from arguments.
59///
60/// # Panics
61///
62/// Will panic if it can't not perform any of the operations.
63pub fn run() -> anyhow::Result<()> {
64    tracing_stdout_init(LevelFilter::INFO);
65
66    let args = Args::parse();
67
68    let tracker_config = load_tracker_configuration(&args)?;
69
70    tracing::info!("tracker config:\n{tracker_config}");
71
72    let mut tracker_container = TrackerContainer::new(CONTAINER_IMAGE, CONTAINER_NAME_PREFIX);
73
74    tracker_container.build_image();
75
76    // code-review: if we want to use port 0 we don't know which ports we have to open.
77    // Besides, if we don't use port 0 we should get the port numbers from the tracker configuration.
78    // We could not use docker, but the intention was to create E2E tests including containerization.
79    let options = RunOptions {
80        env_vars: vec![("TORRUST_TRACKER_CONFIG_TOML".to_string(), tracker_config.to_string())],
81        ports: vec![
82            "6969:6969/udp".to_string(),
83            "7070:7070/tcp".to_string(),
84            "1212:1212/tcp".to_string(),
85            "1313:1313/tcp".to_string(),
86        ],
87    };
88
89    tracker_container.run(&options);
90
91    let running_services = tracker_container.running_services();
92
93    tracing::info!(
94        "Running services:\n {}",
95        serde_json::to_string_pretty(&running_services).expect("running services to be serializable to JSON")
96    );
97
98    assert_there_is_at_least_one_service_per_type(&running_services);
99
100    let tracker_checker_config =
101        serde_json::to_string_pretty(&running_services).expect("Running services should be serialized into JSON");
102
103    tracker_checker::run(&tracker_checker_config).expect("All tracker services should be running correctly");
104
105    // More E2E tests could be added here in the future.
106    // For example: `cargo test ...` for only E2E tests, using this shared test env.
107
108    tracker_container.stop();
109
110    tracker_container.remove();
111
112    tracing::info!("Tracker container final state:\n{:#?}", tracker_container);
113
114    Ok(())
115}
116
117fn tracing_stdout_init(filter: LevelFilter) {
118    tracing_subscriber::fmt().with_max_level(filter).init();
119    tracing::info!("Logging initialized");
120}
121
122fn load_tracker_configuration(args: &Args) -> anyhow::Result<String> {
123    match (args.config_toml_path.clone(), args.config_toml.clone()) {
124        (Some(config_path), _) => {
125            tracing::info!(
126                "Reading tracker configuration from file: {} ...",
127                config_path.to_string_lossy()
128            );
129            load_config_from_file(&config_path)
130        }
131        (_, Some(config_content)) => {
132            tracing::info!("Reading tracker configuration from env var ...");
133            Ok(config_content)
134        }
135        _ => Err(anyhow::anyhow!("No configuration provided")),
136    }
137}
138
139fn load_config_from_file(path: &PathBuf) -> anyhow::Result<String> {
140    let config = std::fs::read_to_string(path).with_context(|| format!("CSan't read config file {path:?}"))?;
141
142    Ok(config)
143}
144
145fn assert_there_is_at_least_one_service_per_type(running_services: &RunningServices) {
146    assert!(
147        !running_services.udp_trackers.is_empty(),
148        "At least one UDP tracker should be enabled in E2E tests configuration"
149    );
150    assert!(
151        !running_services.http_trackers.is_empty(),
152        "At least one HTTP tracker should be enabled in E2E tests configuration"
153    );
154    assert!(
155        !running_services.health_checks.is_empty(),
156        "At least one Health Check should be enabled in E2E tests configuration"
157    );
158}