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 #[arg(id = "config-toml", long, value_delimiter(','))]
23 paths_toml: Vec<PathBuf>,
24
25 #[arg(id = "config-json", long, value_delimiter(','))]
27 paths_json: Vec<PathBuf>,
28
29 #[arg(id = "config-yaml", long, value_delimiter(','))]
31 paths_yaml: Vec<PathBuf>,
32
33 #[arg(value_delimiter(','))]
36 paths: Vec<PathBuf>,
37
38 #[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 #[arg(id = "junit-report", long, value_delimiter(','))]
53 junit_report_paths: Option<Vec<PathBuf>>,
54
55 #[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 }; if errors.is_empty() {
105 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 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 }; 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 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}