1#![allow(clippy::print_stdout)] #![allow(clippy::print_stderr)] use std::path::{MAIN_SEPARATOR, PathBuf};
5use std::{collections::BTreeMap, env, str::FromStr, time::Instant};
6
7use ansi_term::Colour;
8use chrono::{DateTime, SecondsFormat, Utc};
9
10pub use test::Test;
11
12use crate::compiler::{
13 CompilationResult, CompileConfig, Function, Program, SecretTarget, TargetValueRef, TimeZone,
14 VrlRuntime, compile_with_external,
15 runtime::{Runtime, Terminate},
16 state::{ExternalEnv, RuntimeState},
17 value::VrlValueConvert,
18};
19use crate::diagnostic::{DiagnosticList, Formatter};
20use crate::value::Secrets;
21use crate::value::Value;
22
23#[allow(clippy::module_inception)]
24mod test;
25
26fn measure_time<F, R>(f: F) -> (R, std::time::Duration)
27where
28 F: FnOnce() -> R, {
30 let start = Instant::now();
31 let result = f(); let duration = start.elapsed();
33 (result, duration) }
35
36pub struct TestConfig {
37 pub fail_early: bool,
38 pub verbose: bool,
39 pub no_diff: bool,
40 pub timings: bool,
41 pub runtime: VrlRuntime,
42 pub timezone: TimeZone,
43}
44
45#[derive(Clone)]
46struct FailedTest {
47 name: String,
48 category: String,
49 source_file: String,
50 source_line: u32,
51}
52
53pub fn test_dir() -> PathBuf {
54 PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap())
55}
56
57pub fn test_prefix() -> String {
58 let mut prefix = test_dir().join("tests").to_string_lossy().to_string();
59 prefix.push(MAIN_SEPARATOR);
60 prefix
61}
62
63pub fn example_vrl_path() -> PathBuf {
64 test_dir().join("tests").join("example.vrl")
65}
66
67pub fn get_tests_from_functions(functions: Vec<Box<dyn Function>>) -> Vec<Test> {
68 let mut tests = vec![];
69 functions.into_iter().for_each(|function| {
70 if let Some(closure) = function.closure() {
71 closure.inputs.iter().for_each(|input| {
72 let test = Test::from_example(
73 format!("{} (closure)", function.identifier()),
74 &input.example,
75 );
76 tests.push(test);
77 });
78 }
79
80 function.examples().iter().for_each(|example| {
81 let test = Test::from_example(function.identifier(), example);
82 tests.push(test)
83 })
84 });
85
86 tests
87}
88
89pub fn run_tests<T>(
90 tests: Vec<Test>,
91 cfg: &TestConfig,
92 functions: &[Box<dyn Function>],
93 compile_config_provider: impl Fn() -> (CompileConfig, T),
94 finalize_config: impl Fn(T),
95) {
96 let total_count = tests.len();
97 let mut failed_count = 0;
98 let mut warnings_count = 0;
99 let mut category = "".to_owned();
100 let mut failed_tests: Vec<FailedTest> = Vec::new();
101
102 for mut test in tests {
103 if category != test.category {
104 category.clone_from(&test.category);
105 println!("{}", Colour::Fixed(3).bold().paint(category.to_string()));
106 }
107
108 if let Some(err) = test.error {
109 println!("{}", Colour::Purple.bold().paint("INVALID"));
110 println!("{}", Colour::Red.paint(err));
111 failed_count += 1;
112 continue;
113 }
114
115 let mut name = test.name.clone();
116 name.truncate(58);
117
118 let dots = if name.len() >= 60 { 0 } else { 60 - name.len() };
119 print!(" {}{}", name, Colour::Fixed(240).paint(".".repeat(dots)));
120
121 if test.skip {
122 println!("{}", Colour::Yellow.bold().paint("SKIPPED"));
123 continue;
124 }
125
126 let (mut config, config_metadata) = (compile_config_provider)();
127 for (path, recursive) in &test.read_only_paths {
129 config.set_read_only_path(path.clone(), *recursive);
130 }
131
132 let (result, compile_duration) = measure_time(|| {
133 compile_with_external(&test.source, functions, &ExternalEnv::default(), config)
134 });
135 let compile_timing_fmt = if cfg.timings {
136 format!("comp: {compile_duration:>9.3?}")
137 } else {
138 String::new()
139 };
140
141 let failed = match result {
142 Ok(CompilationResult {
143 program,
144 warnings,
145 config: _,
146 }) => {
147 warnings_count += warnings.len();
148
149 if test.check_diagnostics {
150 process_compilation_diagnostics(&test, cfg, warnings, compile_timing_fmt)
151 } else if warnings.is_empty() {
152 let run_start = Instant::now();
153
154 finalize_config(config_metadata);
155 let result = run_vrl(program, &mut test.object, cfg.timezone, cfg.runtime);
156 let run_end = run_start.elapsed();
157
158 let timings = {
159 let timings_color = if run_end.as_millis() > 10 { 1 } else { 245 };
160 let timings_fmt = if cfg.timings {
161 format!(" ({compile_timing_fmt}, run: {run_end:>9.3?})")
162 } else {
163 String::new()
164 };
165 Colour::Fixed(timings_color).paint(timings_fmt).to_string()
166 };
167
168 process_result(result, &mut test, cfg, timings)
169 } else {
170 println!("{} (diagnostics)", Colour::Red.bold().paint("FAILED"));
171 let formatter = Formatter::new(&test.source, warnings);
172 println!("{formatter}");
173 true
175 }
176 }
177 Err(diagnostics) => {
178 warnings_count += diagnostics.warnings().len();
179 process_compilation_diagnostics(&test, cfg, diagnostics, compile_timing_fmt)
180 }
181 };
182 if failed {
183 failed_count += 1;
184 failed_tests.push(FailedTest {
185 name: test.name.clone(),
186 category: test.category.clone(),
187 source_file: test.source_file.clone(),
188 source_line: test.source_line,
189 });
190 }
191 }
192
193 print_result(total_count, failed_count, warnings_count, failed_tests);
194}
195
196fn process_result(
197 result: Result<Value, Terminate>,
198 test: &mut Test,
199 config: &TestConfig,
200 timings: String,
201) -> bool {
202 if test.skip {
203 return false;
204 }
205
206 match result {
207 Ok(got) => {
208 let got_value = vrl_value_to_json_value(got);
209 let mut failed = false;
210
211 let match_mode = if test.check_type_only {
212 MatchMode::TypeOnly
213 } else {
214 MatchMode::Exact
215 };
216
217 let want = test.result.clone();
218 let want_value = if want.starts_with("r'") && want.ends_with('\'') {
219 match regex::Regex::new(&want[2..want.len() - 1].replace("\\'", "'")) {
220 Ok(regex) => regex.to_string().into(),
221 Err(_) => want.into(),
222 }
223 } else if want.starts_with("t'") && want.ends_with('\'') {
224 match DateTime::<Utc>::from_str(&want[2..want.len() - 1]) {
225 Ok(dt) => dt.to_rfc3339_opts(SecondsFormat::AutoSi, true).into(),
226 Err(_) => want.into(),
227 }
228 } else if want.starts_with("s'") && want.ends_with('\'') {
229 want[2..want.len() - 1].into()
230 } else {
231 serde_json::from_str::<'_, serde_json::Value>(want.trim()).unwrap_or_else(|err| {
232 eprintln!("{err}");
233 want.into()
234 })
235 };
236
237 if match_mode.matches(&got_value, &want_value) {
238 print!(
239 "{timings}{}",
240 Colour::Green.bold().paint(match_mode.ok_label())
241 );
242 } else {
243 print!("{}", Colour::Red.bold().paint(match_mode.fail_label()));
244
245 if !config.no_diff {
246 let want = serde_json::to_string_pretty(&want_value).unwrap();
247 let got = serde_json::to_string_pretty(&got_value).unwrap();
248
249 let diff = prettydiff::diff_lines(&want, &got);
250 println!(" {diff}");
251 }
252
253 failed = true;
254 }
255 println!();
256
257 if config.verbose {
258 println!("{got_value:#}");
259 }
260
261 if failed && config.fail_early {
262 std::process::exit(1)
263 }
264 failed
265 }
266 Err(err) => {
267 let mut failed = false;
268 let got = err.to_string().trim().to_owned();
269 let want = test.result.clone().trim().to_owned();
270
271 if (test.result_approx && compare_partial_diagnostic(&got, &want)) || got == want {
272 println!("{}{}", Colour::Green.bold().paint("OK"), timings);
273 } else if matches!(err, Terminate::Abort { .. }) {
274 let want =
275 serde_json::from_str::<'_, serde_json::Value>(&want).unwrap_or_else(|err| {
276 eprintln!("{err}");
277 want.into()
278 });
279
280 let got = vrl_value_to_json_value(test.object.clone());
281 if got == want {
282 println!("{}{}", Colour::Green.bold().paint("OK"), timings);
283 } else {
284 println!("{} (abort)", Colour::Red.bold().paint("FAILED"));
285
286 if !config.no_diff {
287 let want = serde_json::to_string_pretty(&want).unwrap();
288 let got = serde_json::to_string_pretty(&got).unwrap();
289 let diff = prettydiff::diff_lines(&want, &got);
290 println!("{diff}");
291 }
292
293 failed = true;
294 }
295 } else {
296 println!("{} (runtime)", Colour::Red.bold().paint("FAILED"));
297
298 if !config.no_diff {
299 let diff = prettydiff::diff_lines(&want, &got);
300 println!("{diff}");
301 }
302
303 failed = true;
304 }
305
306 if config.verbose {
307 println!("{err:#}");
308 }
309
310 if failed && config.fail_early {
311 std::process::exit(1)
312 }
313 failed
314 }
315 }
316}
317
318fn process_compilation_diagnostics(
319 test: &Test,
320 cfg: &TestConfig,
321 diagnostics: DiagnosticList,
322 compile_timing_fmt: String,
323) -> bool {
324 let mut failed = false;
325
326 let mut formatter = Formatter::new(&test.source, diagnostics);
327
328 let got = formatter.to_string();
329 let got = got.trim();
330
331 let want = test.result.clone();
332 let want = want.trim();
333
334 if (test.result_approx && compare_partial_diagnostic(got, want)) || got == want {
335 let timings = {
336 let timings_fmt = if cfg.timings {
337 format!(" ({compile_timing_fmt})")
338 } else {
339 String::new()
340 };
341 Colour::Fixed(245).paint(timings_fmt).to_string()
342 };
343 println!("{}{timings}", Colour::Green.bold().paint("OK"));
344 } else {
345 println!("{} (compilation)", Colour::Red.bold().paint("FAILED"));
346
347 if !cfg.no_diff {
348 let diff = prettydiff::diff_lines(want, got);
349 println!("{diff}");
350 }
351
352 formatter.enable_colors(true);
354 println!("{formatter:#}");
355
356 failed = true;
357 }
358
359 if cfg.verbose && !failed {
360 formatter.enable_colors(true);
362 println!("{formatter:#}");
363 }
364
365 if failed && cfg.fail_early {
366 std::process::exit(1)
367 }
368 failed
369}
370
371fn print_result(
372 total_count: usize,
373 failed_count: usize,
374 warnings_count: usize,
375 failed_tests: Vec<FailedTest>,
376) {
377 let code = i32::from(failed_count > 0);
378
379 println!("\n");
380
381 let passed_count = total_count - failed_count;
382 if failed_count > 0 {
383 println!(
384 "Overall result: {}\n\n Number failed: {}\n Number passed: {}",
385 Colour::Red.bold().paint("FAILED"),
386 Colour::Red.bold().paint(failed_count.to_string()),
387 Colour::Green.bold().paint(passed_count.to_string())
388 );
389 } else {
390 println!(
391 "Overall result: {}\n Number passed: {}",
392 Colour::Green.bold().paint("SUCCESS"),
393 Colour::Green.bold().paint(passed_count.to_string())
394 );
395 }
396 println!(
397 " Number warnings: {}",
398 Colour::Yellow.bold().paint(warnings_count.to_string())
399 );
400
401 if !failed_tests.is_empty() {
402 println!("\n{}", Colour::Red.bold().paint("Failed tests:"));
403 for test in failed_tests {
404 println!(
405 " {} - {}:{}",
406 Colour::Yellow.paint(format!("{}/{}", test.category, test.name)),
407 test.source_file,
408 test.source_line
409 );
410 }
411 }
412
413 std::process::exit(code)
414}
415
416fn compare_partial_diagnostic(got: &str, want: &str) -> bool {
417 got.lines()
418 .filter(|line| line.trim().starts_with("error[E"))
419 .zip(want.trim().lines())
420 .all(|(got, want)| got.contains(want))
421}
422
423fn vrl_value_to_json_value(value: Value) -> serde_json::Value {
424 use serde_json::Value::*;
425
426 match value {
427 v @ Value::Bytes(_) => String(v.try_bytes_utf8_lossy().unwrap().into_owned()),
428 Value::Integer(v) => v.into(),
429 Value::Float(v) => v.into_inner().into(),
430 Value::Boolean(v) => v.into(),
431 Value::Object(v) => v
432 .into_iter()
433 .map(|(k, v)| (k, vrl_value_to_json_value(v)))
434 .collect::<serde_json::Value>(),
435 Value::Array(v) => v
436 .into_iter()
437 .map(vrl_value_to_json_value)
438 .collect::<serde_json::Value>(),
439 Value::Timestamp(v) => v.to_rfc3339_opts(SecondsFormat::AutoSi, true).into(),
440 Value::Regex(v) => v.to_string().into(),
441 Value::Null => Null,
442 }
443}
444
445enum MatchMode {
446 Exact,
447 TypeOnly,
448}
449
450impl MatchMode {
451 fn matches(&self, got: &serde_json::Value, want: &serde_json::Value) -> bool {
452 match self {
453 MatchMode::Exact => got == want,
454 MatchMode::TypeOnly => std::mem::discriminant(got) == std::mem::discriminant(want),
455 }
456 }
457
458 fn ok_label(&self) -> &'static str {
459 match self {
460 MatchMode::Exact => "OK",
461 MatchMode::TypeOnly => "OK (type match)",
462 }
463 }
464
465 fn fail_label(&self) -> &'static str {
466 match self {
467 MatchMode::Exact => "FAILED (expectation)",
468 MatchMode::TypeOnly => "FAILED (type mismatch)",
469 }
470 }
471}
472
473fn run_vrl(
474 program: Program,
475 test_object: &mut Value,
476 timezone: TimeZone,
477 vrl_runtime: VrlRuntime,
478) -> Result<Value, Terminate> {
479 let mut metadata = Value::from(BTreeMap::new());
480 let mut target = TargetValueRef {
481 value: test_object,
482 metadata: &mut metadata,
483 secrets: &mut Secrets::new(),
484 };
485
486 target.insert_secret("my_secret", "secret value");
488 target.insert_secret("datadog_api_key", "secret value");
489
490 match vrl_runtime {
491 VrlRuntime::Ast => {
492 let mut runtime = Runtime::new(RuntimeState::default());
494 runtime.resolve(&mut target, &program, &timezone)
495 }
496 }
497}