1#[derive(PartialEq, Eq, Debug)]
2pub struct Spec {
3 pub file_name: String,
4 pub message: String,
5 pub file_text: String,
6 pub expected_text: String,
7 pub is_only: bool,
8 pub is_trace: bool,
9 pub skip: bool,
10 pub skip_format_twice: bool,
11 pub config: SpecConfigMap,
12}
13
14pub type SpecConfigMap = serde_json::Map<String, serde_json::Value>;
15
16#[derive(Debug, Clone)]
17pub struct ParseSpecOptions {
18 pub default_file_name: &'static str,
20}
21
22pub fn parse_specs(file_text: String, options: &ParseSpecOptions) -> Vec<Spec> {
23 let file_text = file_text.replace("\r\n", "\n");
25 let (file_path, file_text) = parse_file_path(file_text, options);
26 let (config, file_text) = parse_config(file_text);
27 let lines = file_text.split('\n').collect::<Vec<_>>();
28 let spec_starts = get_spec_starts(&file_path, &lines);
29 let mut specs = Vec::new();
30
31 for i in 0..spec_starts.len() {
32 let start_index = spec_starts[i];
33 let end_index = if spec_starts.len() == i + 1 { lines.len() } else { spec_starts[i + 1] };
34 let message_line = lines[start_index];
35 let spec = parse_single_spec(&file_path, message_line, &lines[(start_index + 1)..end_index], &config);
36
37 specs.push(spec);
38 }
39
40 return specs;
41
42 fn parse_file_path(file_text: String, options: &ParseSpecOptions) -> (String, String) {
43 if !file_text.starts_with("--") {
44 return (options.default_file_name.into(), file_text);
45 }
46 let last_index = file_text.find("--\n").expect("Could not find final --");
47
48 (file_text["--".len()..last_index].trim().into(), file_text[(last_index + "--\n".len())..].into())
49 }
50
51 fn parse_config(file_text: String) -> (SpecConfigMap, String) {
52 if !file_text.starts_with("~~") {
53 return (Default::default(), file_text);
54 }
55 let last_index = file_text.find("~~\n").expect("Could not find final ~~\\n");
56
57 let config_text = file_text["~~".len()..last_index].replace('\n', "");
58 let config_text = config_text.trim();
59 let mut config: SpecConfigMap = Default::default();
60
61 if config_text.starts_with('{') {
62 config = serde_json::from_str(config_text).expect("Error parsing config json.");
63 } else {
64 for item in config_text.split(',') {
65 let first_colon = item.find(':').expect("Could not find colon in config option.");
66 let key = item[0..first_colon].trim();
67 let value = item[first_colon + ":".len()..].trim();
68
69 config.insert(
70 key.into(),
71 match value.parse::<bool>() {
72 Ok(value) => value.into(),
73 Err(_) => match value.parse::<i32>() {
74 Ok(value) => value.into(),
75 Err(_) => value.into(),
76 },
77 },
78 );
79 }
80 }
81
82 (config, file_text[(last_index + "~~\n".len())..].into())
83 }
84
85 fn get_spec_starts(file_name: &str, lines: &[&str]) -> Vec<usize> {
86 let mut result = Vec::new();
87 let message_separator = get_message_separator(file_name);
88
89 if !lines.first().unwrap().starts_with(message_separator) {
90 panic!("All spec files should start with a message. (ex. {0} Message {0})", message_separator);
91 }
92
93 for (i, line) in lines.iter().enumerate() {
94 if line.starts_with(message_separator) {
95 result.push(i);
96 }
97 }
98
99 result
100 }
101
102 fn parse_single_spec(file_name: &str, message_line: &str, lines: &[&str], config: &SpecConfigMap) -> Spec {
103 let file_text = lines.join("\n");
104 let parts = file_text.split("[expect]").collect::<Vec<&str>>();
105 let start_text = parts[0][0..parts[0].len() - "\n".len()].into(); let expected_text = parts[1]["\n".len()..].into(); let lower_case_message_line = message_line.to_ascii_lowercase();
108 let message_separator = get_message_separator(file_name);
109 let is_trace = lower_case_message_line.contains("(trace)");
110
111 Spec {
112 file_name: String::from(file_name),
113 message: message_line[message_separator.len()..message_line.len() - message_separator.len()]
114 .trim()
115 .into(),
116 file_text: start_text,
117 expected_text,
118 is_only: lower_case_message_line.contains("(only)") || is_trace,
119 is_trace,
120 skip: lower_case_message_line.contains("(skip)"),
121 skip_format_twice: lower_case_message_line.contains("(skip-format-twice)"),
122 config: config.clone(),
123 }
124 }
125
126 fn get_message_separator(file_name: &str) -> &'static str {
127 if file_name.ends_with(".md") {
128 "!!"
129 } else {
130 "=="
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn it_parses() {
141 let specs = parse_specs(
142 vec![
143 "== message 1 ==",
144 "start",
145 "multiple",
146 "",
147 "[expect]",
148 "expected",
149 "multiple",
150 "",
151 "== message 2 (only) (skip) (skip-format-twice) ==",
152 "start2",
153 "",
154 "[expect]",
155 "expected2",
156 "",
157 "== message 3 (trace) ==",
158 "test",
159 "",
160 "[expect]",
161 "test",
162 "",
163 ]
164 .join("\n"),
165 &ParseSpecOptions { default_file_name: "test.ts" },
166 );
167
168 assert_eq!(specs.len(), 3);
169 assert_eq!(
170 specs[0],
171 Spec {
172 file_name: "test.ts".into(),
173 file_text: "start\nmultiple\n".into(),
174 expected_text: "expected\nmultiple\n".into(),
175 message: "message 1".into(),
176 is_only: false,
177 is_trace: false,
178 skip: false,
179 skip_format_twice: false,
180 config: Default::default(),
181 }
182 );
183 assert_eq!(
184 specs[1],
185 Spec {
186 file_name: "test.ts".into(),
187 file_text: "start2\n".into(),
188 expected_text: "expected2\n".into(),
189 message: "message 2 (only) (skip) (skip-format-twice)".into(),
190 is_only: true,
191 is_trace: false,
192 skip: true,
193 skip_format_twice: true,
194 config: Default::default(),
195 }
196 );
197 assert_eq!(
198 specs[2],
199 Spec {
200 file_name: "test.ts".into(),
201 file_text: "test\n".into(),
202 expected_text: "test\n".into(),
203 message: "message 3 (trace)".into(),
204 is_only: true,
205 is_trace: true,
206 skip: false,
207 skip_format_twice: false,
208 config: Default::default(),
209 }
210 );
211 }
212
213 #[test]
214 fn it_parses_with_file_name() {
215 let specs = parse_specs(
216 vec!["-- asdf.ts --", "== message ==", "start", "[expect]", "expected"].join("\n"),
217 &ParseSpecOptions { default_file_name: "test.ts" },
218 );
219
220 assert_eq!(specs.len(), 1);
221 assert_eq!(
222 specs[0],
223 Spec {
224 file_name: "asdf.ts".into(),
225 file_text: "start".into(),
226 expected_text: "expected".into(),
227 message: "message".into(),
228 is_only: false,
229 is_trace: false,
230 skip: false,
231 skip_format_twice: false,
232 config: Default::default(),
233 }
234 );
235 }
236
237 #[test]
238 fn it_parses_with_config() {
239 let specs = parse_specs(
240 vec![
241 "-- asdf.ts --",
242 "~~ test.test: other, lineWidth: 40 ~~",
243 "== message ==",
244 "start",
245 "[expect]",
246 "expected",
247 ]
248 .join("\n"),
249 &ParseSpecOptions { default_file_name: "test.ts" },
250 );
251
252 assert_eq!(specs.len(), 1);
253 assert_eq!(
254 specs[0],
255 Spec {
256 file_name: "asdf.ts".into(),
257 file_text: "start".into(),
258 expected_text: "expected".into(),
259 message: "message".into(),
260 is_only: false,
261 is_trace: false,
262 skip: false,
263 skip_format_twice: false,
264 config: [("test.test".into(), "other".into()), ("lineWidth".into(), 40.into())]
265 .iter()
266 .cloned()
267 .collect(),
268 }
269 );
270 }
271
272 #[test]
273 fn it_parses_markdown() {
274 let specs = parse_specs(
275 vec![
276 "!! message 1 !!",
277 "start",
278 "multiple",
279 "",
280 "[expect]",
281 "expected",
282 "multiple",
283 "",
284 "!! message 2 (only) (skip) (skip-format-twice) !!",
285 "start2",
286 "",
287 "[expect]",
288 "expected2",
289 "",
290 ]
291 .join("\n"),
292 &ParseSpecOptions { default_file_name: "test.md" },
293 );
294
295 assert_eq!(specs.len(), 2);
296 assert_eq!(
297 specs[0],
298 Spec {
299 file_name: "test.md".into(),
300 file_text: "start\nmultiple\n".into(),
301 expected_text: "expected\nmultiple\n".into(),
302 message: "message 1".into(),
303 is_only: false,
304 is_trace: false,
305 skip: false,
306 skip_format_twice: false,
307 config: Default::default(),
308 }
309 );
310 assert_eq!(
311 specs[1],
312 Spec {
313 file_name: "test.md".into(),
314 file_text: "start2\n".into(),
315 expected_text: "expected2\n".into(),
316 message: "message 2 (only) (skip) (skip-format-twice)".into(),
317 is_only: true,
318 is_trace: false,
319 skip: true,
320 skip_format_twice: true,
321 config: Default::default(),
322 }
323 );
324 }
325}