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 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}