Skip to main content

vector/
unit_test.rs

1#![allow(missing_docs)]
2use std::{
3    fs::File,
4    io::prelude::*,
5    path::PathBuf,
6    time::{Duration, Instant},
7};
8
9use clap::Parser;
10use colored::*;
11use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
12
13use crate::{
14    config::{self, UnitTestResult},
15    signal,
16};
17
18#[derive(Parser, Debug)]
19#[command(rename_all = "kebab-case")]
20pub struct Opts {
21    /// Vector config files in TOML format to test.
22    #[arg(id = "config-toml", long, value_delimiter(','))]
23    paths_toml: Vec<PathBuf>,
24
25    /// Vector config files in JSON format to test.
26    #[arg(id = "config-json", long, value_delimiter(','))]
27    paths_json: Vec<PathBuf>,
28
29    /// Vector config files in YAML format to test.
30    #[arg(id = "config-yaml", long, value_delimiter(','))]
31    paths_yaml: Vec<PathBuf>,
32
33    /// Any number of Vector config files to test. If none are specified the
34    /// default config path `/etc/vector/vector.yaml` will be targeted.
35    #[arg(value_delimiter(','))]
36    paths: Vec<PathBuf>,
37
38    /// Read configuration from files in one or more directories.
39    /// File format is detected from the file name.
40    ///
41    /// Files not ending in .toml, .json, .yaml, or .yml will be ignored.
42    #[arg(
43        id = "config-dir",
44        short = 'C',
45        long,
46        env = "VECTOR_CONFIG_DIR",
47        value_delimiter(',')
48    )]
49    pub config_dirs: Vec<PathBuf>,
50
51    /// Output path for JUnit reports
52    #[arg(id = "junit-report", long, value_delimiter(','))]
53    junit_report_paths: Option<Vec<PathBuf>>,
54
55    /// Allow interpolation of environment variables in configuration files. Enabling this may
56    /// expose environment secrets into your Vector configuration.
57    #[arg(
58        long,
59        env = "VECTOR_DANGEROUSLY_ALLOW_ENV_VAR_INTERPOLATION",
60        default_value = "false"
61    )]
62    pub dangerously_allow_env_var_interpolation: bool,
63}
64
65impl Opts {
66    fn paths_with_formats(&self) -> Vec<config::ConfigPath> {
67        config::merge_path_lists(vec![
68            (&self.paths, None),
69            (&self.paths_toml, Some(config::Format::Toml)),
70            (&self.paths_json, Some(config::Format::Json)),
71            (&self.paths_yaml, Some(config::Format::Yaml)),
72        ])
73        .map(|(path, hint)| config::ConfigPath::File(path, hint))
74        .chain(
75            self.config_dirs
76                .iter()
77                .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
78        )
79        .collect()
80    }
81}
82
83#[derive(Debug)]
84pub struct JUnitReporter<'a> {
85    report: Report,
86    test_suite: TestSuite,
87    output_paths: Option<&'a Vec<PathBuf>>,
88}
89
90impl<'a> JUnitReporter<'a> {
91    fn new(paths: Option<&'a Vec<PathBuf>>) -> Self {
92        Self {
93            report: Report::new("Vector Unit Tests"),
94            test_suite: TestSuite::new("Test Suite"),
95            output_paths: paths,
96        }
97    }
98
99    fn add_test_result(&mut self, name: &str, errors: &[String], time: Duration) {
100        if self.output_paths.is_none() {
101            return;
102        }; // early return in case no output paths were specified
103
104        if errors.is_empty() {
105            // successful test
106            let mut test_case = TestCase::new(name.to_owned(), TestCaseStatus::success());
107            test_case.set_time(time);
108            self.test_suite.add_test_case(test_case);
109        } else {
110            // failed test
111            let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
112            status.set_description(errors.join("\n"));
113            let mut test_case = TestCase::new(name.to_owned(), status);
114            test_case.set_time(time);
115            self.test_suite.add_test_case(test_case);
116        }
117    }
118
119    fn write_reports(mut self, time: Duration) -> Result<(), String> {
120        if self.output_paths.is_none() {
121            return Ok(());
122        }; // early return in case no output paths were specified
123
124        // create a report from the test cases
125        self.test_suite.set_time(time);
126        self.report.add_test_suite(self.test_suite);
127
128        let report_bytes = match self.report.to_string() {
129            Ok(report_string) => report_string.into_bytes(),
130            Err(error) => return Err(error.to_string()),
131        };
132
133        for path in self.output_paths.unwrap() {
134            // safe to unwrap because of the check above
135            match File::create(path) {
136                Ok(mut file) => match file.write_all(&report_bytes) {
137                    Ok(()) => {}
138                    Err(error) => return Err(error.to_string()),
139                },
140                Err(error) => return Err(error.to_string()),
141            }
142        }
143
144        Ok(())
145    }
146}
147
148pub async fn cmd(opts: &Opts, signal_handler: &mut signal::SignalHandler) -> exitcode::ExitCode {
149    let mut aggregated_test_errors: Vec<(String, Vec<String>)> = Vec::new();
150
151    let paths = opts.paths_with_formats();
152    let paths = match config::process_paths(&paths) {
153        Some(paths) => paths,
154        None => return exitcode::CONFIG,
155    };
156
157    let mut junit_reporter = JUnitReporter::new(opts.junit_report_paths.as_ref());
158
159    #[allow(clippy::print_stdout)]
160    {
161        println!("Running tests");
162    }
163    match config::build_unit_tests_main(&paths, signal_handler).await {
164        Ok(tests) => {
165            if tests.is_empty() {
166                #[allow(clippy::print_stdout)]
167                {
168                    println!("{}", "No tests found.".yellow());
169                }
170            } else {
171                let test_suite_start = Instant::now();
172
173                for test in tests {
174                    let name = test.name.clone();
175
176                    let test_case_start = Instant::now();
177                    let UnitTestResult { errors } = test.run().await;
178                    let test_case_elapsed = test_case_start.elapsed();
179
180                    junit_reporter.add_test_result(&name, &errors, test_case_elapsed);
181
182                    if !errors.is_empty() {
183                        #[allow(clippy::print_stdout)]
184                        {
185                            println!("test {} ... {}", name, "failed".red());
186                        }
187                        aggregated_test_errors.push((name, errors));
188                    } else {
189                        #[allow(clippy::print_stdout)]
190                        {
191                            println!("test {} ... {}", name, "passed".green());
192                        }
193                    }
194                }
195
196                let test_suite_elapsed = test_suite_start.elapsed();
197                match junit_reporter.write_reports(test_suite_elapsed) {
198                    Ok(()) => {}
199                    Err(error) => {
200                        error!("Failed to write test output:\n{}.", error);
201                        return exitcode::CONFIG;
202                    }
203                }
204            }
205        }
206        Err(errors) => {
207            #[allow(clippy::print_stderr)]
208            {
209                eprintln!("Failed to execute tests:\n{}.", errors.join("\n"));
210            }
211            return exitcode::CONFIG;
212        }
213    }
214
215    if !aggregated_test_errors.is_empty() {
216        #[allow(clippy::print_stdout)]
217        {
218            println!("\nfailures:");
219        }
220        for (test_name, fails) in aggregated_test_errors {
221            #[allow(clippy::print_stdout)]
222            {
223                println!("\ntest {test_name}:\n");
224            }
225            for fail in fails {
226                #[allow(clippy::print_stdout)]
227                {
228                    println!("{fail}\n");
229                }
230            }
231        }
232
233        exitcode::CONFIG
234    } else {
235        exitcode::OK
236    }
237}