vrl/test/
test.rs

1use std::{collections::BTreeMap, fs, path::Path};
2
3use crate::compiler::function::Example;
4use crate::path::OwnedTargetPath;
5use crate::path::parse_value_path;
6use crate::test::{example_vrl_path, test_prefix};
7use crate::value::Value;
8
9#[derive(Debug)]
10pub struct Test {
11    pub name: String,
12    pub category: String,
13    pub error: Option<String>,
14    pub source: String,
15    pub object: Value,
16    pub result: String,
17    pub result_approx: bool,
18    pub skip: bool,
19    pub check_diagnostics: bool,
20    // paths set to read-only
21    pub read_only_paths: Vec<(OwnedTargetPath, bool)>,
22    pub source_file: String,
23    pub source_line: u32,
24    pub check_type_only: bool,
25}
26
27enum CaptureMode {
28    Result,
29    Object,
30    None,
31    Done,
32}
33
34impl Test {
35    pub fn from_path(path: &Path) -> Self {
36        let name = test_name(path);
37        let category = test_category(path);
38        let content = fs::read_to_string(path).expect("content");
39
40        let mut source = String::new();
41        let mut object = String::new();
42        let mut result = String::new();
43        let mut result_approx = false;
44
45        let mut read_only_paths = vec![];
46
47        let mut capture_mode = CaptureMode::None;
48        for mut line in content.lines() {
49            if line.starts_with('#') && !matches!(capture_mode, CaptureMode::Done) {
50                line = line.strip_prefix('#').expect("prefix");
51                line = line.strip_prefix(' ').unwrap_or(line);
52
53                if line.starts_with("object:") {
54                    capture_mode = CaptureMode::Object;
55                    line = line.strip_prefix("object:").expect("object").trim_start();
56                } else if line.starts_with("result: ~") {
57                    capture_mode = CaptureMode::Result;
58                    result_approx = true;
59                    line = line.strip_prefix("result: ~").expect("result").trim_start();
60                } else if line.starts_with("result:") {
61                    capture_mode = CaptureMode::Result;
62                    line = line.strip_prefix("result:").expect("result").trim_start();
63                } else if line.starts_with("read_only:") {
64                    let path_str = line.strip_prefix("read_only:").expect("read-only").trim();
65                    read_only_paths.push((
66                        OwnedTargetPath::event(parse_value_path(path_str).expect("valid path")),
67                        false,
68                    ));
69                    continue;
70                } else if line.starts_with("read_only_recursive:") {
71                    let path_str = line
72                        .strip_prefix("read_only_recursive:")
73                        .expect("read-only")
74                        .trim();
75                    read_only_paths.push((
76                        OwnedTargetPath::event(parse_value_path(path_str).expect("valid path")),
77                        true,
78                    ));
79                    continue;
80                } else if line.starts_with("read_only_metadata:") {
81                    let path_str = line
82                        .strip_prefix("read_only_metadata:")
83                        .expect("read_only_metadata")
84                        .trim();
85                    read_only_paths.push((
86                        OwnedTargetPath::metadata(parse_value_path(path_str).expect("valid path")),
87                        false,
88                    ));
89                    continue;
90                } else if line.starts_with("read_only_metadata_recursive:") {
91                    let path_str = line
92                        .strip_prefix("read_only_metadata_recursive:")
93                        .expect("read-read_only_metadata_recursive")
94                        .trim();
95                    read_only_paths.push((
96                        OwnedTargetPath::metadata(parse_value_path(path_str).expect("valid path")),
97                        true,
98                    ));
99                    continue;
100                }
101
102                match capture_mode {
103                    CaptureMode::None | CaptureMode::Done => continue,
104                    CaptureMode::Result => {
105                        result.push_str(line);
106                        result.push('\n');
107                    }
108                    CaptureMode::Object => {
109                        object.push_str(line);
110                    }
111                }
112            } else {
113                capture_mode = CaptureMode::Done;
114
115                source.push_str(line);
116                source.push('\n')
117            }
118        }
119
120        let mut error = None;
121        let object = if object.is_empty() {
122            Value::Object(BTreeMap::default())
123        } else {
124            serde_json::from_str::<'_, Value>(&object).unwrap_or_else(|err| {
125                error = Some(format!("unable to parse object as JSON: {err}"));
126                Value::Null
127            })
128        };
129
130        {
131            result = result.trim_end().to_owned();
132        }
133
134        Self {
135            name,
136            category,
137            error,
138            source,
139            object,
140            result,
141            result_approx,
142            skip: content.starts_with("# SKIP"),
143            check_diagnostics: content.starts_with("# DIAGNOSTICS"),
144            read_only_paths,
145            source_file: path.to_string_lossy().to_string(),
146            source_line: 1,
147            check_type_only: false,
148        }
149    }
150
151    pub fn from_example(func: impl ToString, example: &Example) -> Self {
152        let object = match example.input {
153            Some(input) => {
154                serde_json::from_str::<Value>(input).expect("example input should be valid JSON")
155            }
156            None => Value::Object(BTreeMap::default()),
157        };
158        let result = match example.result {
159            Ok(string) => string.to_owned(),
160            Err(err) => err.to_string(),
161        };
162
163        Self {
164            name: example.title.to_owned(),
165            category: format!("functions/{}", func.to_string()),
166            error: None,
167            source: example.source.to_owned(),
168            object,
169            result,
170            result_approx: false,
171            skip: false,
172            check_diagnostics: false,
173            read_only_paths: vec![],
174            source_file: example.file.to_owned(),
175            source_line: example.line,
176            check_type_only: !example.deterministic,
177        }
178    }
179}
180
181fn test_category(path: &Path) -> String {
182    if path == example_vrl_path() {
183        return "uncategorized".to_owned();
184    }
185
186    let stripped_path = path
187        .to_string_lossy()
188        .strip_prefix(test_prefix().as_str())
189        .expect("test")
190        .to_string();
191
192    stripped_path
193        .clone()
194        .rsplit_once('/')
195        .map_or(stripped_path, |x| x.0.to_owned())
196}
197
198fn test_name(path: &Path) -> String {
199    path.to_string_lossy()
200        .rsplit_once('/')
201        .unwrap()
202        .1
203        .trim_end_matches(".vrl")
204        .replace('_', " ")
205}