1#[cfg(all(
3 test,
4 feature = "sources-demo_logs",
5 feature = "transforms-remap",
6 feature = "transforms-route",
7 feature = "transforms-filter",
8 feature = "transforms-reduce",
9 feature = "sinks-console"
10))]
11mod tests;
12mod unit_test_components;
13
14use std::{
15 collections::{BTreeMap, HashMap, HashSet},
16 sync::Arc,
17};
18
19use futures_util::{StreamExt, stream::FuturesUnordered};
20use indexmap::IndexMap;
21use tokio::sync::{
22 Mutex,
23 oneshot::{self, Receiver},
24};
25use uuid::Uuid;
26use vrl::{
27 compiler::{Context, TargetValue, TimeZone, state::RuntimeState},
28 diagnostic::Formatter,
29 value,
30};
31
32pub use self::unit_test_components::{
33 UnitTestSinkCheck, UnitTestSinkConfig, UnitTestSinkResult, UnitTestSourceConfig,
34 UnitTestStreamSinkConfig, UnitTestStreamSourceConfig,
35};
36use super::{OutputId, compiler::expand_globs, graph::Graph, transform::get_transform_output_ids};
37use crate::{
38 conditions::Condition,
39 config::{
40 self, ComponentKey, Config, ConfigBuilder, ConfigPath, SinkOuter, SourceOuter,
41 TestDefinition, TestInput, TestOutput, loading, loading::ConfigBuilderLoader,
42 },
43 event::{Event, EventMetadata, LogEvent},
44 signal,
45 topology::{
46 RunningTopology,
47 builder::{TopologyPieces, TopologyPiecesBuilder},
48 },
49};
50
51pub struct UnitTest {
52 pub name: String,
53 config: Config,
54 pieces: TopologyPieces,
55 test_result_rxs: Vec<Receiver<UnitTestSinkResult>>,
56}
57
58pub struct UnitTestResult {
59 pub errors: Vec<String>,
60}
61
62impl UnitTest {
63 pub async fn run(self) -> UnitTestResult {
64 let diff = config::ConfigDiff::initial(&self.config);
65 let (topology, _) = RunningTopology::start_validated(self.config, diff, self.pieces)
66 .await
67 .unwrap();
68 topology.sources_finished().await;
69 let _stop_complete = topology.stop();
70
71 let mut in_flight = self
72 .test_result_rxs
73 .into_iter()
74 .collect::<FuturesUnordered<_>>();
75
76 let mut errors = Vec::new();
77 while let Some(partial_result) = in_flight.next().await {
78 let partial_result = partial_result.expect(
79 "An unexpected error occurred while executing unit tests. Please try again.",
80 );
81 errors.extend(partial_result.test_errors);
82 }
83
84 UnitTestResult { errors }
85 }
86}
87
88fn init_log_schema_from_paths(
93 config_paths: &[ConfigPath],
94 deny_if_set: bool,
95) -> Result<(), Vec<String>> {
96 let builder = ConfigBuilderLoader::default().load_from_paths(config_paths)?;
97 vector_lib::config::init_log_schema(builder.global.log_schema, deny_if_set);
98 Ok(())
99}
100
101pub async fn build_unit_tests_main(
102 paths: &[ConfigPath],
103 signal_handler: &mut signal::SignalHandler,
104) -> Result<Vec<UnitTest>, Vec<String>> {
105 init_log_schema_from_paths(paths, false)?;
106 let secrets_backends_loader =
107 loading::loader_from_paths(loading::SecretBackendLoader::default(), paths)?;
108 let secrets = secrets_backends_loader
109 .retrieve_secrets(signal_handler)
110 .await
111 .map_err(|e| vec![e])?;
112
113 let config_builder = ConfigBuilderLoader::default()
114 .secrets(secrets)
115 .load_from_paths(paths)?;
116
117 build_unit_tests(config_builder).await
118}
119
120pub async fn build_unit_tests(
121 mut config_builder: ConfigBuilder,
122) -> Result<Vec<UnitTest>, Vec<String>> {
123 config_builder.sources = Default::default();
125 config_builder.sinks = Default::default();
126
127 let test_definitions = std::mem::take(&mut config_builder.tests);
128 let mut tests = Vec::new();
129 let mut build_errors = Vec::new();
130 let metadata = UnitTestBuildMetadata::initialize(&mut config_builder)?;
131
132 for mut test_definition in test_definitions {
133 let test_name = test_definition.name.clone();
134 let legacy_input = std::mem::take(&mut test_definition.input);
136 if let Some(input) = legacy_input {
137 test_definition.inputs.push(input);
138 }
139 match build_unit_test(&metadata, test_definition, config_builder.clone()).await {
140 Ok(test) => tests.push(test),
141 Err(errors) => {
142 let mut test_error = errors.join("\n");
143 test_error = test_error.replace('\n', "\n ");
145 test_error.insert_str(0, &format!("Failed to build test '{test_name}':\n "));
146 build_errors.push(test_error);
147 }
148 }
149 }
150
151 if build_errors.is_empty() {
152 Ok(tests)
153 } else {
154 Err(build_errors)
155 }
156}
157
158pub struct UnitTestBuildMetadata {
159 available_insert_targets: HashSet<ComponentKey>,
161 source_ids: HashMap<ComponentKey, String>,
163 template_sources: IndexMap<ComponentKey, UnitTestSourceConfig>,
166 sink_ids: HashMap<OutputId, String>,
168}
169
170impl UnitTestBuildMetadata {
171 pub fn initialize(config_builder: &mut ConfigBuilder) -> Result<Self, Vec<String>> {
172 let random_id = Uuid::new_v4().to_string();
174
175 let available_insert_targets = config_builder
176 .transforms
177 .keys()
178 .cloned()
179 .collect::<HashSet<_>>();
180
181 let source_ids = available_insert_targets
182 .iter()
183 .map(|key| (key.clone(), format!("{}-{}-{}", key, "source", random_id)))
184 .collect::<HashMap<_, _>>();
185
186 let mut template_sources = IndexMap::new();
188 for (key, transform) in config_builder.transforms.iter_mut() {
189 let test_source_id = source_ids
190 .get(key)
191 .expect("Missing test source for a transform")
192 .clone();
193 transform.inputs.extend(Some(test_source_id));
194
195 template_sources.insert(key.clone(), UnitTestSourceConfig::default());
196 }
197
198 let builder = config_builder.clone();
199 let available_extract_targets = builder
200 .transforms
201 .iter()
202 .flat_map(|(key, transform)| {
203 get_transform_output_ids(
204 transform.inner.as_ref(),
205 key.clone(),
206 builder.schema.log_namespace(),
207 )
208 })
209 .collect::<HashSet<_>>();
210
211 let sink_ids = available_extract_targets
212 .iter()
213 .map(|key| {
214 (
215 key.clone(),
216 format!(
217 "{}-{}-{}",
218 key.to_string().replace('.', "-"),
219 "sink",
220 random_id
221 ),
222 )
223 })
224 .collect::<HashMap<_, _>>();
225
226 Ok(Self {
227 available_insert_targets,
228 source_ids,
229 template_sources,
230 sink_ids,
231 })
232 }
233
234 pub fn hydrate_into_sources(
236 &self,
237 inputs: &[TestInput],
238 ) -> Result<IndexMap<ComponentKey, SourceOuter>, Vec<String>> {
239 let inputs = build_and_validate_inputs(inputs, &self.available_insert_targets)?;
240 let mut template_sources = self.template_sources.clone();
241 Ok(inputs
242 .into_iter()
243 .map(|(insert_at, events)| {
244 let mut source_config =
245 template_sources
246 .shift_remove(&insert_at)
247 .unwrap_or_else(|| {
248 panic!(
252 "Invalid input: cannot insert at {:?}",
253 insert_at.to_string()
254 )
255 });
256 source_config.events.extend(events);
257 let id: &str = self
258 .source_ids
259 .get(&insert_at)
260 .expect("Corresponding source must exist")
261 .as_ref();
262 (ComponentKey::from(id), SourceOuter::new(source_config))
263 })
264 .collect::<IndexMap<_, _>>())
265 }
266
267 pub fn hydrate_into_sinks(
269 &self,
270 test_name: &str,
271 outputs: &[TestOutput],
272 no_outputs_from: &[OutputId],
273 ) -> Result<
274 (
275 Vec<Receiver<UnitTestSinkResult>>,
276 IndexMap<ComponentKey, SinkOuter<String>>,
277 ),
278 Vec<String>,
279 > {
280 if outputs.is_empty() && no_outputs_from.is_empty() {
281 return Err(vec![
282 "unit test must contain at least one of `outputs` or `no_outputs_from`."
283 .to_string(),
284 ]);
285 }
286 let outputs = build_outputs(outputs)?;
287
288 let mut template_sinks = IndexMap::new();
289 let mut test_result_rxs = Vec::new();
290 for (ids, built) in outputs {
292 let (tx, rx) = oneshot::channel();
293 let sink_ids = ids.clone();
294 let sink_config = UnitTestSinkConfig {
295 test_name: test_name.to_string(),
296 transform_ids: ids.iter().map(|id| id.to_string()).collect(),
297 result_tx: Arc::new(Mutex::new(Some(tx))),
298 check: UnitTestSinkCheck::Checks {
299 conditions: built.conditions,
300 expected_event_count: built.expected_event_count,
301 },
302 };
303
304 test_result_rxs.push(rx);
305 template_sinks.insert(sink_ids, sink_config);
306 }
307
308 for id in no_outputs_from {
310 let (tx, rx) = oneshot::channel();
311 let sink_config = UnitTestSinkConfig {
312 test_name: test_name.to_string(),
313 transform_ids: vec![id.to_string()],
314 result_tx: Arc::new(Mutex::new(Some(tx))),
315 check: UnitTestSinkCheck::NoOutputs,
316 };
317
318 test_result_rxs.push(rx);
319 template_sinks.insert(vec![id.clone()], sink_config);
320 }
321
322 let sinks = template_sinks
323 .into_iter()
324 .map(|(transform_ids, sink_config)| {
325 let transform_ids_str = transform_ids
326 .iter()
327 .map(|s| s.to_string())
328 .collect::<Vec<_>>();
329 let sink_ids = transform_ids
330 .iter()
331 .map(|transform_id| {
332 self.sink_ids
333 .get(transform_id)
334 .expect("Sink does not exist")
335 .as_str()
336 })
337 .collect::<Vec<_>>();
338 let sink_id = sink_ids.join(",");
339 (
340 ComponentKey::from(sink_id),
341 SinkOuter::new(transform_ids_str, sink_config),
342 )
343 })
344 .collect::<IndexMap<_, _>>();
345
346 Ok((test_result_rxs, sinks))
347 }
348}
349
350fn get_relevant_test_components(
352 sources: &[&ComponentKey],
353 graph: &Graph,
354) -> Result<HashSet<String>, Vec<String>> {
355 graph.check_for_cycles().map_err(|error| vec![error])?;
356 let mut errors = Vec::new();
357 let mut components = HashSet::new();
358 for source in sources {
359 let paths = graph.paths_to_sink_from(source);
360 if paths.is_empty() {
361 errors.push(format!(
362 "Unable to complete topology between input target '{}' and output target(s)",
363 source
364 .to_string()
365 .rsplit_once("-source-")
366 .unwrap_or(("", ""))
367 .0
368 ));
369 } else {
370 for path in paths {
371 components.extend(path.into_iter().map(|key| key.to_string()));
372 }
373 }
374 }
375
376 if errors.is_empty() {
377 Ok(components)
378 } else {
379 Err(errors)
380 }
381}
382
383async fn build_unit_test(
384 metadata: &UnitTestBuildMetadata,
385 test: TestDefinition<String>,
386 mut config_builder: ConfigBuilder,
387) -> Result<UnitTest, Vec<String>> {
388 let transform_only_config = config_builder.clone();
389 let transform_only_graph = Graph::new_unchecked(
390 &transform_only_config.sources,
391 &transform_only_config.transforms,
392 &transform_only_config.sinks,
393 transform_only_config.schema,
394 transform_only_config
395 .global
396 .wildcard_matching
397 .unwrap_or_default(),
398 );
399 let test = test.resolve_outputs(&transform_only_graph)?;
400
401 let sources = metadata.hydrate_into_sources(&test.inputs)?;
402 let (test_result_rxs, sinks) =
403 metadata.hydrate_into_sinks(&test.name, &test.outputs, &test.no_outputs_from)?;
404
405 config_builder.sources = sources;
406 config_builder.sinks = sinks;
407 expand_globs(&mut config_builder);
408
409 let graph = Graph::new_unchecked(
410 &config_builder.sources,
411 &config_builder.transforms,
412 &config_builder.sinks,
413 config_builder.schema,
414 config_builder.global.wildcard_matching.unwrap_or_default(),
415 );
416
417 let mut valid_components = get_relevant_test_components(
418 config_builder.sources.keys().collect::<Vec<_>>().as_ref(),
419 &graph,
420 )?;
421
422 let unexpanded_transforms = valid_components
424 .iter()
425 .filter_map(|component| {
426 component
427 .split_once('.')
428 .map(|(original_name, _)| original_name.to_string())
429 })
430 .collect::<Vec<_>>();
431 valid_components.extend(unexpanded_transforms);
432
433 config_builder
437 .enrichment_tables
438 .iter()
439 .filter_map(|(key, c)| c.as_sink(key).map(|(_, sink)| sink.inputs))
440 .for_each(|i| valid_components.extend(i));
441
442 config_builder.transforms = config_builder
444 .transforms
445 .into_iter()
446 .filter(|(key, _)| valid_components.contains(&key.to_string()))
447 .collect();
448
449 let graph = Graph::new_unchecked(
451 &config_builder.sources,
452 &config_builder.transforms,
453 &config_builder.sinks,
454 config_builder.schema,
455 config_builder.global.wildcard_matching.unwrap_or_default(),
456 );
457 let valid_inputs = graph.input_map()?;
458 for (_, transform) in config_builder.transforms.iter_mut() {
459 let inputs = std::mem::take(&mut transform.inputs);
460 transform.inputs = inputs
461 .into_iter()
462 .filter(|input| valid_inputs.contains_key(input))
463 .collect();
464 }
465
466 if let Some(sink) = get_loose_end_outputs_sink(&config_builder) {
467 config_builder
468 .sinks
469 .insert(ComponentKey::from(Uuid::new_v4().to_string()), sink);
470 }
471 let config = config_builder.build()?;
472 let diff = config::ConfigDiff::initial(&config);
473 let pieces = TopologyPiecesBuilder::new(&config, &diff).build().await?;
474
475 Ok(UnitTest {
476 name: test.name,
477 config,
478 pieces,
479 test_result_rxs,
480 })
481}
482
483fn get_loose_end_outputs_sink(config: &ConfigBuilder) -> Option<SinkOuter<String>> {
490 let config = config.clone();
491 let transform_ids = config.transforms.iter().flat_map(|(key, transform)| {
492 get_transform_output_ids(
493 transform.inner.as_ref(),
494 key.clone(),
495 config.schema.log_namespace(),
496 )
497 .map(|output| output.to_string())
498 .collect::<Vec<_>>()
499 });
500
501 let mut loose_end_outputs = Vec::new();
502 for id in transform_ids {
503 if !config
504 .transforms
505 .iter()
506 .any(|(_, transform)| transform.inputs.contains(&id))
507 && !config
508 .sinks
509 .iter()
510 .any(|(_, sink)| sink.inputs.contains(&id))
511 {
512 loose_end_outputs.push(id);
513 }
514 }
515
516 if loose_end_outputs.is_empty() {
517 None
518 } else {
519 let noop_sink = UnitTestSinkConfig {
520 test_name: "".to_string(),
521 transform_ids: vec![],
522 result_tx: Arc::new(Mutex::new(None)),
523 check: UnitTestSinkCheck::NoOp,
524 };
525 Some(SinkOuter::new(loose_end_outputs, noop_sink))
526 }
527}
528
529fn build_and_validate_inputs(
530 test_inputs: &[TestInput],
531 available_insert_targets: &HashSet<ComponentKey>,
532) -> Result<HashMap<ComponentKey, Vec<Event>>, Vec<String>> {
533 let mut inputs = HashMap::new();
534 let mut errors = Vec::new();
535 if test_inputs.is_empty() {
536 errors.push("must specify at least one input.".to_string());
537 return Err(errors);
538 }
539
540 for (index, input) in test_inputs.iter().enumerate() {
541 if available_insert_targets.contains(&input.insert_at) {
542 match build_input_event(input) {
543 Ok(input_event) => {
544 inputs
545 .entry(input.insert_at.clone())
546 .and_modify(|events: &mut Vec<Event>| {
547 events.push(input_event.clone());
548 })
549 .or_insert_with(|| vec![input_event]);
550 }
551 Err(error) => errors.push(error),
552 }
553 } else {
554 errors.push(format!(
555 "inputs[{}]: unable to locate target transform '{}'",
556 index, input.insert_at
557 ))
558 }
559 }
560
561 if errors.is_empty() {
562 Ok(inputs)
563 } else {
564 Err(errors)
565 }
566}
567
568#[derive(Default)]
569pub(super) struct BuiltOutput {
570 pub(super) expected_event_count: Option<usize>,
571 pub(super) conditions: Vec<Vec<Condition>>,
572}
573
574fn build_outputs(
575 test_outputs: &[TestOutput],
576) -> Result<IndexMap<Vec<OutputId>, BuiltOutput>, Vec<String>> {
577 let mut outputs: IndexMap<Vec<OutputId>, BuiltOutput> = IndexMap::new();
578 let mut errors = Vec::new();
579
580 for output in test_outputs {
581 let mut conditions = Vec::new();
582 for (index, condition) in output
583 .conditions
584 .clone()
585 .unwrap_or_default()
586 .iter()
587 .enumerate()
588 {
589 match condition.build(&Default::default(), &Default::default()) {
590 Ok(condition) => conditions.push(condition),
591 Err(error) => errors.push(format!(
592 "failed to create test condition '{index}': {error}"
593 )),
594 }
595 }
596
597 let expected_event_count = output.expected_event_count;
598 if expected_event_count == Some(0) && !conditions.is_empty() {
599 errors.push(format!(
600 "output for {:?} has expected_event_count of 0 but also defines conditions; \
601 conditions cannot be evaluated when no events are expected",
602 output.extract_from
603 ));
604 }
605 outputs
606 .entry(output.extract_from.clone().to_vec())
607 .and_modify(|existing| {
608 if let (Some(prev), Some(new)) =
609 (existing.expected_event_count, expected_event_count)
610 {
611 if prev != new {
612 errors.push(format!(
613 "conflicting expected_event_count for extract_from {:?}: {} vs {}",
614 output.extract_from, prev, new
615 ));
616 }
617 } else if existing.expected_event_count.is_none() {
618 existing.expected_event_count = expected_event_count;
619 }
620 existing.conditions.push(conditions.clone());
621 })
622 .or_insert_with(|| BuiltOutput {
623 expected_event_count,
624 conditions: vec![conditions.clone()],
625 });
626 }
627
628 for (extract_from, built) in &outputs {
632 if built.expected_event_count == Some(0) && built.conditions.iter().any(|c| !c.is_empty()) {
633 errors.push(format!(
634 "output for {extract_from:?} has expected_event_count of 0 but also defines conditions; \
635 conditions cannot be evaluated when no events are expected",
636 ));
637 }
638 }
639
640 if errors.is_empty() {
641 Ok(outputs)
642 } else {
643 Err(errors)
644 }
645}
646
647fn build_input_event(input: &TestInput) -> Result<Event, String> {
648 match input.type_str.as_ref() {
649 "raw" => match input.value.as_ref() {
650 Some(v) => Ok(Event::Log(LogEvent::from_str_legacy(v.clone()))),
651 None => Err("input type 'raw' requires the field 'value'".to_string()),
652 },
653 "vrl" => {
654 if let Some(source) = &input.source {
655 let result = vrl::compiler::compile(source, &vector_vrl_functions::all())
656 .map_err(|e| Formatter::new(source, e.clone()).to_string())?;
657
658 let mut target = TargetValue {
659 value: value!({}),
660 metadata: value::Value::Object(BTreeMap::new()),
661 secrets: value::Secrets::default(),
662 };
663
664 let mut state = RuntimeState::default();
665 let timezone = TimeZone::default();
666 let mut ctx = Context::new(&mut target, &mut state, &timezone);
667
668 result
669 .program
670 .resolve(&mut ctx)
671 .map(|_| {
672 Event::Log(LogEvent::from_parts(
673 target.value.clone(),
674 EventMetadata::default_with_value(target.metadata.clone()),
675 ))
676 })
677 .map_err(|e| e.to_string())
678 } else {
679 Err("input type 'vrl' requires the field 'source'".to_string())
680 }
681 }
682 "log" => {
683 if let Some(log_fields) = &input.log_fields {
684 let mut event = LogEvent::from_str_legacy("");
685 for (path, value) in log_fields {
686 event
687 .parse_path_and_insert(path, value.clone())
688 .map_err(|e| e.to_string())?;
689 }
690 Ok(event.into())
691 } else {
692 Err("input type 'log' requires the field 'log_fields'".to_string())
693 }
694 }
695 "metric" => {
696 if let Some(metric) = &input.metric {
697 Ok(Event::Metric(metric.clone()))
698 } else {
699 Err("input type 'metric' requires the field 'metric'".to_string())
700 }
701 }
702 _ => Err(format!(
703 "unrecognized input type '{}', expected one of: 'raw', 'log' or 'metric'",
704 input.type_str
705 )),
706 }
707}