1use std::{
6 collections::VecDeque,
7 io::ErrorKind,
8 path::{Path, PathBuf},
9};
10
11const TEST_INPUT_FILE_EXTENSION: &'static str = "input";
14const TEST_VALID_FILE_EXTENSION: &'static str = "valid";
15const TEST_NEW_VALID_FILE_EXTENSION: &'static str = "valid.new";
16
17pub fn testdata<P, F>(path: P, callback: F)
63where
64 P: AsRef<Path>,
65 F: FnMut(Vec<String>) -> Vec<String>,
66{
67 let result = testdata_to_result(path, callback);
68
69 for it in result.tests.iter() {
70 if it.success {
71 println!("passed: {}", it.name);
72 } else {
73 println!("failed: {}", it.name);
74 }
75 }
76
77 if !result.success() {
78 let mut failed_count = 0;
79
80 for it in result.tests.iter() {
81 if !it.success {
82 failed_count += 1;
83
84 if let Some(expected) = &it.expect {
85 eprintln!(
86 "\n=> `{}` output did not match `{}`:",
87 it.name, it.valid_file
88 );
89
90 let diff = super::diff::lines(&it.actual, expected);
91 eprintln!("\n{}", diff);
92 } else {
93 eprintln!("\n=> `{}` for test `{}` not found", it.valid_file, it.name);
94 eprintln!(
95 ".. created `{}.new` with the current test output",
96 it.valid_file
97 );
98 }
99 }
100 }
101
102 eprintln!("\n===== Failed tests =====\n");
103 for it in result.tests.iter() {
104 if !it.success {
105 eprintln!("- {}", it.name);
106 }
107 }
108 eprintln!();
109
110 panic!(
111 "{} test case{} failed",
112 failed_count,
113 if failed_count != 1 { "s" } else { "" }
114 );
115 }
116}
117
118#[derive(Debug)]
120struct TestDataResult {
121 pub tests: Vec<TestDataResultItem>,
122}
123
124#[derive(Debug)]
127struct TestDataResultItem {
128 pub success: bool,
130
131 pub name: String,
133
134 pub valid_file: String,
136
137 pub expect: Option<Vec<String>>,
140
141 pub actual: Vec<String>,
143}
144
145impl TestDataResult {
146 pub fn success(&self) -> bool {
148 for it in self.tests.iter() {
149 if !it.success {
150 return false;
151 }
152 }
153 true
154 }
155}
156
157fn testdata_to_result<P, F>(test_path: P, mut test_callback: F) -> TestDataResult
158where
159 P: AsRef<Path>,
160 F: FnMut(Vec<String>) -> Vec<String>,
161{
162 let test_path = test_path.as_ref();
163
164 let mut test_results = Vec::new();
165 let test_inputs_with_name = collect_test_inputs_with_name(test_path);
166
167 for (input_path, test_name) in test_inputs_with_name.into_iter() {
168 let input_text = std::fs::read_to_string(&input_path).expect("reading test input file");
169 let input_lines = super::text::lines(input_text);
170
171 let mut test_succeeded = true;
172 let output_lines = test_callback(input_lines);
173 let output_text = output_lines.join("\n");
174
175 let mut valid_file_path = input_path.clone();
176 valid_file_path.set_extension(TEST_VALID_FILE_EXTENSION);
177
178 let expected_lines = match std::fs::read_to_string(&valid_file_path) {
179 Ok(raw_text) => {
180 let expected_lines = super::text::lines(raw_text);
181 let expected_text = expected_lines.join("\n");
182 if output_text != expected_text {
183 test_succeeded = false;
184 }
185 Some(expected_lines)
186 }
187 Err(err) => {
188 test_succeeded = false;
189 if err.kind() == ErrorKind::NotFound {
190 let mut new_valid_file_path = valid_file_path.clone();
193 new_valid_file_path.set_extension(TEST_NEW_VALID_FILE_EXTENSION);
194 std::fs::write(new_valid_file_path, output_text)
195 .expect("writing new test output");
196 } else {
197 panic!("failed to read output file for {}: {}", test_name, err);
199 }
200
201 None
204 }
205 };
206
207 let valid_file_name = valid_file_path.file_name().unwrap().to_string_lossy();
208 test_results.push(TestDataResultItem {
209 success: test_succeeded,
210 name: test_name,
211 valid_file: valid_file_name.into(),
212 expect: expected_lines,
213 actual: output_lines,
214 });
215 }
216
217 TestDataResult {
218 tests: test_results,
219 }
220}
221
222fn collect_test_inputs_with_name(root_path: &Path) -> Vec<(PathBuf, String)> {
223 let mut test_inputs_with_name = Vec::new();
224
225 let mut dirs_to_scan_with_name = VecDeque::new();
226 dirs_to_scan_with_name.push_back((root_path.to_owned(), String::new()));
227
228 while let Some((current_dir, current_name)) = dirs_to_scan_with_name.pop_front() {
229 let entries = std::fs::read_dir(¤t_dir).expect("reading test directory");
230 let entries = entries.map(|x| x.expect("reading test directory entry"));
231
232 let mut entries = entries.collect::<Vec<_>>();
234 entries.sort_by_key(|x| x.file_name());
235
236 for entry in entries {
237 let entry_path = entry.path();
238 let entry_name = if current_name.len() > 0 {
239 format!("{}/{}", current_name, entry.file_name().to_string_lossy())
240 } else {
241 entry.file_name().to_string_lossy().to_string()
242 };
243
244 let entry_info =
245 std::fs::metadata(&entry_path).expect("reading test directory metadata");
246 if entry_info.is_dir() {
247 dirs_to_scan_with_name.push_back((entry_path, entry_name));
248 } else if let Some(extension) = entry_path.extension() {
249 if extension == TEST_INPUT_FILE_EXTENSION {
250 test_inputs_with_name.push((entry_path, entry_name));
251 }
252 }
253 }
254 }
255
256 test_inputs_with_name
257}
258
259#[cfg(test)]
260#[cfg(feature = "temp")] mod test_testdata {
262 use super::{testdata, testdata_to_result};
263 use crate::{temp_dir, TempDir};
264
265 #[test]
266 fn runs_test_callback() {
267 let dir = temp_dir();
268 dir.create_file("some.input", "");
269 dir.create_file("some.valid", "");
270
271 let mut test_callback_was_called = false;
272 testdata(dir.path(), |input| {
273 test_callback_was_called = true;
274 input
275 });
276
277 assert!(test_callback_was_called);
278 }
279
280 #[test]
281 fn runs_test_callback_with_input() {
282 let dir = temp_dir();
283 dir.create_file("some.input", "the input");
284 dir.create_file("some.valid", "");
285
286 let mut test_callback_input = String::new();
287 testdata(dir.path(), |input| {
288 let input = input.join("\n");
289 test_callback_input.push_str(&input);
290 Vec::new()
291 });
292
293 assert_eq!(test_callback_input, "the input");
294 }
295
296 #[test]
297 fn fails_if_output_is_missing() {
298 let dir = temp_dir();
299 dir.create_file("test.input", "some input");
300
301 let res = testdata_to_result(dir.path(), |input| input);
302 assert!(!res.success());
303 }
304
305 #[test]
306 fn fails_if_output_is_different() {
307 let dir = temp_dir();
308 helper::write_case(&dir, "test.input", "some input", "some output");
309
310 let res = testdata_to_result(dir.path(), |input| input);
311 assert!(!res.success());
312 }
313
314 #[test]
315 fn runs_test_callback_for_each_input() {
316 let dir = temp_dir();
317 helper::write_case(&dir, "a.input", "input A", "");
318 helper::write_case(&dir, "b.input", "input B", "");
319 helper::write_case(&dir, "c.input", "input C", "");
320
321 let mut test_callback_inputs = Vec::new();
322 testdata(dir.path(), |input| {
323 let input = input.join("\n");
324 test_callback_inputs.push(input);
325 Vec::new()
326 });
327
328 let expected = vec![
329 "input A".to_string(),
330 "input B".to_string(),
331 "input C".to_string(),
332 ];
333 assert_eq!(test_callback_inputs, expected);
334 }
335
336 #[test]
337 fn recurses_into_subdirectories() {
338 let dir = temp_dir();
339 helper::write_case(&dir, "a1.input", "a1", "");
340 helper::write_case(&dir, "a2.input", "a2", "");
341 helper::write_case(&dir, "a3.input", "a3", "");
342 helper::write_case(&dir, "a1/a.input", "a1/a", "");
343 helper::write_case(&dir, "a1/b.input", "a1/b", "");
344 helper::write_case(&dir, "a2/a.input", "a2/a", "");
345 helper::write_case(&dir, "a2/b.input", "a2/b", "");
346 helper::write_case(&dir, "a2/sub/file.input", "a2/sub/file", "");
347
348 let mut test_callback_inputs = Vec::new();
349 testdata(dir.path(), |input| {
350 let input = input.join("\n");
351 test_callback_inputs.push(input);
352 Vec::new()
353 });
354
355 let expected = vec![
356 "a1".to_string(),
357 "a2".to_string(),
358 "a3".to_string(),
359 "a1/a".to_string(),
360 "a1/b".to_string(),
361 "a2/a".to_string(),
362 "a2/b".to_string(),
363 "a2/sub/file".to_string(),
364 ];
365 assert_eq!(test_callback_inputs, expected);
366 }
367
368 #[test]
369 fn fails_and_generate_an_output_file_if_one_does_not_exist() {
370 let dir = temp_dir();
371 dir.create_file("test.input", "Some Input");
372
373 let result = testdata_to_result(dir.path(), |input| {
374 input.into_iter().map(|x| x.to_lowercase()).collect()
375 });
376 assert!(!result.success());
377
378 let new_result_path = dir.path().join("test.valid.new");
379 assert!(new_result_path.is_file());
380
381 let new_result_text = std::fs::read_to_string(new_result_path).unwrap();
382 assert_eq!(new_result_text, "some input");
383 }
384
385 #[test]
386 fn trims_input_files() {
387 let dir = temp_dir();
388 helper::write_case(&dir, "test.input", "\n\nfirst\ntrim end: \nlast\n\n", "");
389
390 let mut test_input = Vec::new();
391 testdata(dir.path(), |input| {
392 test_input = input;
393 Vec::new()
394 });
395
396 assert_eq!(test_input, vec!["first", "trim end:", "last"]);
397 }
398
399 #[test]
400 fn trims_expected_output_files() {
401 let dir = temp_dir();
402 helper::write_case(
403 &dir,
404 "test.input",
405 "line 1\nline 2\nline 3",
406 "\n\nline 1\nline 2 \nline 3\n\n",
407 );
408 testdata(dir.path(), |input| input);
409 }
410
411 #[test]
412 fn ignores_line_break_differences_in_input_and_output() {
413 let dir = temp_dir();
414 helper::write_case(&dir, "a.input", "a\nb\nc", "c\r\nb\r\na");
415 helper::write_case(&dir, "b.input", "a\r\nb\r\nc", "c\nb\na");
416
417 testdata(dir.path(), |mut input| {
418 input.reverse();
419 input
420 });
421 }
422
423 #[test]
424 fn does_not_ignore_trailing_indentation_of_first_line() {
425 let dir = temp_dir();
426 helper::write_case(&dir, "test.input", "value", " value");
427 let res = testdata_to_result(dir.path(), |input| input);
428 assert!(!res.success());
429 }
430
431 #[test]
436 fn to_result_returns_ok_for_valid_case() {
437 let dir = temp_dir();
438 helper::write_case(&dir, "test.input", "abc\n123", "123\nabc");
439
440 let result = testdata_to_result(dir.path(), |mut input| {
441 input.reverse();
442 input
443 });
444
445 assert!(result.success());
446 assert_eq!(result.tests.len(), 1);
447 assert_eq!(result.tests[0].name, "test.input");
448 assert_eq!(result.tests[0].success, true);
449 }
450
451 #[test]
452 fn to_result_returns_an_item_for_each_case() {
453 let dir = temp_dir();
454 helper::write_case(&dir, "a.input", "A", "a");
455 helper::write_case(&dir, "b.input", "B", "b");
456 helper::write_case(&dir, "sub/some.input", "Some", "some");
457
458 let result = testdata_to_result(dir.path(), |input| {
459 input.into_iter().map(|x| x.to_lowercase()).collect()
460 });
461
462 assert_eq!(result.tests.len(), 3);
463 assert_eq!(result.tests[0].name, "a.input");
464 assert_eq!(result.tests[1].name, "b.input");
465 assert_eq!(result.tests[2].name, "sub/some.input");
466 }
467
468 #[test]
469 fn to_result_fails_if_output_does_not_match() {
470 let dir = temp_dir();
471 helper::write_case(&dir, "a.input", "Valid 1", "valid 1");
472 helper::write_case(&dir, "b.input", "Valid 2", "valid 2");
473 helper::write_case(
474 &dir,
475 "c.input",
476 "this should fail",
477 "invalid output for the test",
478 );
479
480 let result = testdata_to_result(dir.path(), |input| {
481 input.into_iter().map(|x| x.to_lowercase()).collect()
482 });
483
484 assert!(!result.success());
485 assert!(result.tests.len() == 3);
486 assert!(result.tests[0].success);
487 assert!(result.tests[1].success);
488 assert!(!result.tests[2].success);
489 }
490
491 mod helper {
496 use super::*;
497
498 pub fn write_case(dir: &TempDir, input_file: &str, input: &str, expected: &str) {
499 dir.create_file(input_file, input);
500
501 let suffix = format!(".input");
502 let basename = input_file.strip_suffix(&suffix).unwrap();
503 dir.create_file(&format!("{}.valid", basename), expected);
504 }
505 }
506}