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") { "!!" } else { "==" }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn it_parses() {
137 let specs = parse_specs(
138 vec![
139 "== message 1 ==",
140 "start",
141 "multiple",
142 "",
143 "[expect]",
144 "expected",
145 "multiple",
146 "",
147 "== message 2 (only) (skip) (skip-format-twice) ==",
148 "start2",
149 "",
150 "[expect]",
151 "expected2",
152 "",
153 "== message 3 (trace) ==",
154 "test",
155 "",
156 "[expect]",
157 "test",
158 "",
159 ]
160 .join("\n"),
161 &ParseSpecOptions { default_file_name: "test.ts" },
162 );
163
164 assert_eq!(specs.len(), 3);
165 assert_eq!(
166 specs[0],
167 Spec {
168 file_name: "test.ts".into(),
169 file_text: "start\nmultiple\n".into(),
170 expected_text: "expected\nmultiple\n".into(),
171 message: "message 1".into(),
172 is_only: false,
173 is_trace: false,
174 skip: false,
175 skip_format_twice: false,
176 config: Default::default(),
177 }
178 );
179 assert_eq!(
180 specs[1],
181 Spec {
182 file_name: "test.ts".into(),
183 file_text: "start2\n".into(),
184 expected_text: "expected2\n".into(),
185 message: "message 2 (only) (skip) (skip-format-twice)".into(),
186 is_only: true,
187 is_trace: false,
188 skip: true,
189 skip_format_twice: true,
190 config: Default::default(),
191 }
192 );
193 assert_eq!(
194 specs[2],
195 Spec {
196 file_name: "test.ts".into(),
197 file_text: "test\n".into(),
198 expected_text: "test\n".into(),
199 message: "message 3 (trace)".into(),
200 is_only: true,
201 is_trace: true,
202 skip: false,
203 skip_format_twice: false,
204 config: Default::default(),
205 }
206 );
207 }
208
209 #[test]
210 fn it_parses_with_file_name() {
211 let specs = parse_specs(
212 vec!["-- asdf.ts --", "== message ==", "start", "[expect]", "expected"].join("\n"),
213 &ParseSpecOptions { default_file_name: "test.ts" },
214 );
215
216 assert_eq!(specs.len(), 1);
217 assert_eq!(
218 specs[0],
219 Spec {
220 file_name: "asdf.ts".into(),
221 file_text: "start".into(),
222 expected_text: "expected".into(),
223 message: "message".into(),
224 is_only: false,
225 is_trace: false,
226 skip: false,
227 skip_format_twice: false,
228 config: Default::default(),
229 }
230 );
231 }
232
233 #[test]
234 fn it_parses_with_config() {
235 let specs = parse_specs(
236 vec![
237 "-- asdf.ts --",
238 "~~ test.test: other, lineWidth: 40 ~~",
239 "== message ==",
240 "start",
241 "[expect]",
242 "expected",
243 ]
244 .join("\n"),
245 &ParseSpecOptions { default_file_name: "test.ts" },
246 );
247
248 assert_eq!(specs.len(), 1);
249 assert_eq!(
250 specs[0],
251 Spec {
252 file_name: "asdf.ts".into(),
253 file_text: "start".into(),
254 expected_text: "expected".into(),
255 message: "message".into(),
256 is_only: false,
257 is_trace: false,
258 skip: false,
259 skip_format_twice: false,
260 config: [("test.test".into(), "other".into()), ("lineWidth".into(), 40.into())]
261 .iter()
262 .cloned()
263 .collect(),
264 }
265 );
266 }
267
268 #[test]
269 fn it_parses_markdown() {
270 let specs = parse_specs(
271 vec![
272 "!! message 1 !!",
273 "start",
274 "multiple",
275 "",
276 "[expect]",
277 "expected",
278 "multiple",
279 "",
280 "!! message 2 (only) (skip) (skip-format-twice) !!",
281 "start2",
282 "",
283 "[expect]",
284 "expected2",
285 "",
286 ]
287 .join("\n"),
288 &ParseSpecOptions { default_file_name: "test.md" },
289 );
290
291 assert_eq!(specs.len(), 2);
292 assert_eq!(
293 specs[0],
294 Spec {
295 file_name: "test.md".into(),
296 file_text: "start\nmultiple\n".into(),
297 expected_text: "expected\nmultiple\n".into(),
298 message: "message 1".into(),
299 is_only: false,
300 is_trace: false,
301 skip: false,
302 skip_format_twice: false,
303 config: Default::default(),
304 }
305 );
306 assert_eq!(
307 specs[1],
308 Spec {
309 file_name: "test.md".into(),
310 file_text: "start2\n".into(),
311 expected_text: "expected2\n".into(),
312 message: "message 2 (only) (skip) (skip-format-twice)".into(),
313 is_only: true,
314 is_trace: false,
315 skip: true,
316 skip_format_twice: true,
317 config: Default::default(),
318 }
319 );
320 }
321}