]> git.lizzy.rs Git - rust.git/blob - src/test/mod.rs
Merge pull request #3454 from rchaser53/issue-3434
[rust.git] / src / test / mod.rs
1 use std::collections::{HashMap, HashSet};
2 use std::env;
3 use std::fs;
4 use std::io::{self, BufRead, BufReader, Read, Write};
5 use std::iter::{Enumerate, Peekable};
6 use std::mem;
7 use std::path::{Path, PathBuf};
8 use std::process::{Command, Stdio};
9 use std::str::Chars;
10
11 use crate::config::{Color, Config, EmitMode, FileName, NewlineStyle, ReportTactic};
12 use crate::formatting::{ReportedErrors, SourceFile};
13 use crate::rustfmt_diff::{make_diff, print_diff, DiffLine, Mismatch, ModifiedChunk, OutputWriter};
14 use crate::source_file;
15 use crate::{FormatReport, Input, Session};
16
17 const DIFF_CONTEXT_SIZE: usize = 3;
18 const CONFIGURATIONS_FILE_NAME: &str = "Configurations.md";
19
20 // A list of files on which we want to skip testing.
21 const SKIP_FILE_WHITE_LIST: &[&str] = &[
22     // We want to make sure that the `skip_children` is correctly working,
23     // so we do not want to test this file directly.
24     "configs/skip_children/foo/mod.rs",
25     "issue-3434/no_entry.rs",
26 ];
27
28 fn is_file_skip(path: &Path) -> bool {
29     SKIP_FILE_WHITE_LIST
30         .iter()
31         .any(|file_path| path.ends_with(file_path))
32 }
33
34 // Returns a `Vec` containing `PathBuf`s of files with an  `rs` extension in the
35 // given path. The `recursive` argument controls if files from subdirectories
36 // are also returned.
37 fn get_test_files(path: &Path, recursive: bool) -> Vec<PathBuf> {
38     let mut files = vec![];
39     if path.is_dir() {
40         for entry in fs::read_dir(path).expect(&format!(
41             "couldn't read directory {}",
42             path.to_str().unwrap()
43         )) {
44             let entry = entry.expect("couldn't get `DirEntry`");
45             let path = entry.path();
46             if path.is_dir() && recursive {
47                 files.append(&mut get_test_files(&path, recursive));
48             } else if path.extension().map_or(false, |f| f == "rs") && !is_file_skip(&path) {
49                 files.push(path);
50             }
51         }
52     }
53     files
54 }
55
56 fn verify_config_used(path: &Path, config_name: &str) {
57     for entry in fs::read_dir(path).expect(&format!(
58         "couldn't read {} directory",
59         path.to_str().unwrap()
60     )) {
61         let entry = entry.expect("couldn't get directory entry");
62         let path = entry.path();
63         if path.extension().map_or(false, |f| f == "rs") {
64             // check if "// rustfmt-<config_name>:" appears in the file.
65             let filebuf = BufReader::new(
66                 fs::File::open(&path).expect(&format!("couldn't read file {}", path.display())),
67             );
68             assert!(
69                 filebuf
70                     .lines()
71                     .map(|l| l.unwrap())
72                     .take_while(|l| l.starts_with("//"))
73                     .any(|l| l.starts_with(&format!("// rustfmt-{}", config_name))),
74                 format!(
75                     "config option file {} does not contain expected config name",
76                     path.display()
77                 )
78             );
79         }
80     }
81 }
82
83 #[test]
84 fn verify_config_test_names() {
85     for path in &[
86         Path::new("tests/source/configs"),
87         Path::new("tests/target/configs"),
88     ] {
89         for entry in fs::read_dir(path).expect("couldn't read configs directory") {
90             let entry = entry.expect("couldn't get directory entry");
91             let path = entry.path();
92             if path.is_dir() {
93                 let config_name = path.file_name().unwrap().to_str().unwrap();
94
95                 // Make sure that config name is used in the files in the directory.
96                 verify_config_used(&path, config_name);
97             }
98         }
99     }
100 }
101
102 // This writes to the terminal using the same approach (via `term::stdout` or
103 // `println!`) that is used by `rustfmt::rustfmt_diff::print_diff`. Writing
104 // using only one or the other will cause the output order to differ when
105 // `print_diff` selects the approach not used.
106 fn write_message(msg: &str) {
107     let mut writer = OutputWriter::new(Color::Auto);
108     writer.writeln(msg, None);
109 }
110
111 // Integration tests. The files in `tests/source` are formatted and compared
112 // to their equivalent in `tests/target`. The target file and config can be
113 // overridden by annotations in the source file. The input and output must match
114 // exactly.
115 #[test]
116 fn system_tests() {
117     // Get all files in the tests/source directory.
118     let files = get_test_files(Path::new("tests/source"), true);
119     let (_reports, count, fails) = check_files(files, &None);
120
121     // Display results.
122     println!("Ran {} system tests.", count);
123     assert_eq!(fails, 0, "{} system tests failed", fails);
124 }
125
126 // Do the same for tests/coverage-source directory.
127 // The only difference is the coverage mode.
128 #[test]
129 fn coverage_tests() {
130     let files = get_test_files(Path::new("tests/coverage/source"), true);
131     let (_reports, count, fails) = check_files(files, &None);
132
133     println!("Ran {} tests in coverage mode.", count);
134     assert_eq!(fails, 0, "{} tests failed", fails);
135 }
136
137 #[test]
138 fn checkstyle_test() {
139     let filename = "tests/writemode/source/fn-single-line.rs";
140     let expected_filename = "tests/writemode/target/checkstyle.xml";
141     assert_output(Path::new(filename), Path::new(expected_filename));
142 }
143
144 #[test]
145 fn modified_test() {
146     use std::io::BufRead;
147
148     // Test "modified" output
149     let filename = "tests/writemode/source/modified.rs";
150     let mut data = Vec::new();
151     let mut config = Config::default();
152     config
153         .set()
154         .emit_mode(crate::config::EmitMode::ModifiedLines);
155
156     {
157         let mut session = Session::new(config, Some(&mut data));
158         session.format(Input::File(filename.into())).unwrap();
159     }
160
161     let mut lines = data.lines();
162     let mut chunks = Vec::new();
163     while let Some(Ok(header)) = lines.next() {
164         // Parse the header line
165         let values: Vec<_> = header
166             .split(' ')
167             .map(|s| s.parse::<u32>().unwrap())
168             .collect();
169         assert_eq!(values.len(), 3);
170         let line_number_orig = values[0];
171         let lines_removed = values[1];
172         let num_added = values[2];
173         let mut added_lines = Vec::new();
174         for _ in 0..num_added {
175             added_lines.push(lines.next().unwrap().unwrap());
176         }
177         chunks.push(ModifiedChunk {
178             line_number_orig,
179             lines_removed,
180             lines: added_lines,
181         });
182     }
183
184     assert_eq!(
185         chunks,
186         vec![
187             ModifiedChunk {
188                 line_number_orig: 4,
189                 lines_removed: 4,
190                 lines: vec!["fn blah() {}".into()],
191             },
192             ModifiedChunk {
193                 line_number_orig: 9,
194                 lines_removed: 6,
195                 lines: vec!["#[cfg(a, b)]".into(), "fn main() {}".into()],
196             },
197         ],
198     );
199 }
200
201 // Helper function for comparing the results of rustfmt
202 // to a known output file generated by one of the write modes.
203 fn assert_output(source: &Path, expected_filename: &Path) {
204     let config = read_config(source);
205     let (_, source_file, _) = format_file(source, config.clone());
206
207     // Populate output by writing to a vec.
208     let mut out = vec![];
209     let _ = source_file::write_all_files(&source_file, &mut out, &config);
210     let output = String::from_utf8(out).unwrap();
211
212     let mut expected_file = fs::File::open(&expected_filename).expect("couldn't open target");
213     let mut expected_text = String::new();
214     expected_file
215         .read_to_string(&mut expected_text)
216         .expect("Failed reading target");
217
218     let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE);
219     if !compare.is_empty() {
220         let mut failures = HashMap::new();
221         failures.insert(source.to_owned(), compare);
222         print_mismatches_default_message(failures);
223         assert!(false, "Text does not match expected output");
224     }
225 }
226
227 // Idempotence tests. Files in tests/target are checked to be unaltered by
228 // rustfmt.
229 #[test]
230 fn idempotence_tests() {
231     match option_env!("CFG_RELEASE_CHANNEL") {
232         None | Some("nightly") => {}
233         _ => return, // these tests require nightly
234     }
235     // Get all files in the tests/target directory.
236     let files = get_test_files(Path::new("tests/target"), true);
237     let (_reports, count, fails) = check_files(files, &None);
238
239     // Display results.
240     println!("Ran {} idempotent tests.", count);
241     assert_eq!(fails, 0, "{} idempotent tests failed", fails);
242 }
243
244 // Run rustfmt on itself. This operation must be idempotent. We also check that
245 // no warnings are emitted.
246 #[test]
247 fn self_tests() {
248     match option_env!("CFG_RELEASE_CHANNEL") {
249         None | Some("nightly") => {}
250         _ => return, // Issue-3443: these tests require nightly
251     }
252     let mut files = get_test_files(Path::new("tests"), false);
253     let bin_directories = vec!["cargo-fmt", "git-rustfmt", "bin", "format-diff"];
254     for dir in bin_directories {
255         let mut path = PathBuf::from("src");
256         path.push(dir);
257         path.push("main.rs");
258         files.push(path);
259     }
260     files.push(PathBuf::from("src/lib.rs"));
261
262     let (reports, count, fails) = check_files(files, &Some(PathBuf::from("rustfmt.toml")));
263     let mut warnings = 0;
264
265     // Display results.
266     println!("Ran {} self tests.", count);
267     assert_eq!(fails, 0, "{} self tests failed", fails);
268
269     for format_report in reports {
270         println!("{}", format_report);
271         warnings += format_report.warning_count();
272     }
273
274     assert_eq!(
275         warnings, 0,
276         "Rustfmt's code generated {} warnings",
277         warnings
278     );
279 }
280
281 #[test]
282 fn stdin_formatting_smoke_test() {
283     let input = Input::Text("fn main () {}".to_owned());
284     let mut config = Config::default();
285     config.set().emit_mode(EmitMode::Stdout);
286     let mut buf: Vec<u8> = vec![];
287     {
288         let mut session = Session::new(config, Some(&mut buf));
289         session.format(input).unwrap();
290         assert!(session.has_no_errors());
291     }
292
293     #[cfg(not(windows))]
294     assert_eq!(buf, "fn main() {}\n".as_bytes());
295     #[cfg(windows)]
296     assert_eq!(buf, "fn main() {}\r\n".as_bytes());
297 }
298
299 #[test]
300 fn stdin_parser_panic_caught() {
301     // See issue #3239.
302     for text in ["{", "}"].iter().cloned().map(String::from) {
303         let mut buf = vec![];
304         let mut session = Session::new(Default::default(), Some(&mut buf));
305         let _ = session.format(Input::Text(text));
306
307         assert!(session.has_parsing_errors());
308     }
309 }
310
311 /// Ensures that `EmitMode::ModifiedLines` works with input from `stdin`. Useful
312 /// when embedding Rustfmt (e.g. inside RLS).
313 #[test]
314 fn stdin_works_with_modified_lines() {
315     let input = "\nfn\n some( )\n{\n}\nfn main () {}\n";
316     let output = "1 6 2\nfn some() {}\nfn main() {}\n";
317
318     let input = Input::Text(input.to_owned());
319     let mut config = Config::default();
320     config.set().newline_style(NewlineStyle::Unix);
321     config.set().emit_mode(EmitMode::ModifiedLines);
322     let mut buf: Vec<u8> = vec![];
323     {
324         let mut session = Session::new(config, Some(&mut buf));
325         session.format(input).unwrap();
326         let errors = ReportedErrors {
327             has_diff: true,
328             ..Default::default()
329         };
330         assert_eq!(session.errors, errors);
331     }
332     assert_eq!(buf, output.as_bytes());
333 }
334
335 #[test]
336 fn stdin_disable_all_formatting_test() {
337     match option_env!("CFG_RELEASE_CHANNEL") {
338         None | Some("nightly") => {}
339         // These tests require nightly.
340         _ => return,
341     }
342     let input = String::from("fn main() { println!(\"This should not be formatted.\"); }");
343     let mut child = Command::new(rustfmt().to_str().unwrap())
344         .stdin(Stdio::piped())
345         .stdout(Stdio::piped())
346         .arg("--config-path=./tests/config/disable_all_formatting.toml")
347         .spawn()
348         .expect("failed to execute child");
349
350     {
351         let stdin = child.stdin.as_mut().expect("failed to get stdin");
352         stdin
353             .write_all(input.as_bytes())
354             .expect("failed to write stdin");
355     }
356
357     let output = child.wait_with_output().expect("failed to wait on child");
358     assert!(output.status.success());
359     assert!(output.stderr.is_empty());
360     assert_eq!(input, String::from_utf8(output.stdout).unwrap());
361 }
362
363 #[test]
364 fn format_lines_errors_are_reported() {
365     let long_identifier = String::from_utf8(vec![b'a'; 239]).unwrap();
366     let input = Input::Text(format!("fn {}() {{}}", long_identifier));
367     let mut config = Config::default();
368     config.set().error_on_line_overflow(true);
369     let mut session = Session::<io::Stdout>::new(config, None);
370     session.format(input).unwrap();
371     assert!(session.has_formatting_errors());
372 }
373
374 #[test]
375 fn format_lines_errors_are_reported_with_tabs() {
376     let long_identifier = String::from_utf8(vec![b'a'; 97]).unwrap();
377     let input = Input::Text(format!("fn a() {{\n\t{}\n}}", long_identifier));
378     let mut config = Config::default();
379     config.set().error_on_line_overflow(true);
380     config.set().hard_tabs(true);
381     let mut session = Session::<io::Stdout>::new(config, None);
382     session.format(input).unwrap();
383     assert!(session.has_formatting_errors());
384 }
385
386 // For each file, run rustfmt and collect the output.
387 // Returns the number of files checked and the number of failures.
388 fn check_files(files: Vec<PathBuf>, opt_config: &Option<PathBuf>) -> (Vec<FormatReport>, u32, u32) {
389     let mut count = 0;
390     let mut fails = 0;
391     let mut reports = vec![];
392
393     for file_name in files {
394         debug!("Testing '{}'...", file_name.display());
395
396         match idempotent_check(&file_name, &opt_config) {
397             Ok(ref report) if report.has_warnings() => {
398                 print!("{}", report);
399                 fails += 1;
400             }
401             Ok(report) => reports.push(report),
402             Err(err) => {
403                 if let IdempotentCheckError::Mismatch(msg) = err {
404                     print_mismatches_default_message(msg);
405                 }
406                 fails += 1;
407             }
408         }
409
410         count += 1;
411     }
412
413     (reports, count, fails)
414 }
415
416 fn print_mismatches_default_message(result: HashMap<PathBuf, Vec<Mismatch>>) {
417     for (file_name, diff) in result {
418         let mismatch_msg_formatter =
419             |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num);
420         print_diff(diff, &mismatch_msg_formatter, &Default::default());
421     }
422
423     if let Some(mut t) = term::stdout() {
424         t.reset().unwrap_or(());
425     }
426 }
427
428 fn print_mismatches<T: Fn(u32) -> String>(
429     result: HashMap<PathBuf, Vec<Mismatch>>,
430     mismatch_msg_formatter: T,
431 ) {
432     for (_file_name, diff) in result {
433         print_diff(diff, &mismatch_msg_formatter, &Default::default());
434     }
435
436     if let Some(mut t) = term::stdout() {
437         t.reset().unwrap_or(());
438     }
439 }
440
441 fn read_config(filename: &Path) -> Config {
442     let sig_comments = read_significant_comments(filename);
443     // Look for a config file. If there is a 'config' property in the significant comments, use
444     // that. Otherwise, if there are no significant comments at all, look for a config file with
445     // the same name as the test file.
446     let mut config = if !sig_comments.is_empty() {
447         get_config(sig_comments.get("config").map(Path::new))
448     } else {
449         get_config(filename.with_extension("toml").file_name().map(Path::new))
450     };
451
452     for (key, val) in &sig_comments {
453         if key != "target" && key != "config" {
454             config.override_value(key, val);
455             if config.is_default(key) {
456                 warn!("Default value {} used explicitly for {}", val, key);
457             }
458         }
459     }
460
461     // Don't generate warnings for to-do items.
462     config.set().report_todo(ReportTactic::Never);
463
464     config
465 }
466
467 fn format_file<P: Into<PathBuf>>(filepath: P, config: Config) -> (bool, SourceFile, FormatReport) {
468     let filepath = filepath.into();
469     let input = Input::File(filepath);
470     let mut session = Session::<io::Stdout>::new(config, None);
471     let result = session.format(input).unwrap();
472     let parsing_errors = session.has_parsing_errors();
473     let mut source_file = SourceFile::new();
474     mem::swap(&mut session.source_file, &mut source_file);
475     (parsing_errors, source_file, result)
476 }
477
478 enum IdempotentCheckError {
479     Mismatch(HashMap<PathBuf, Vec<Mismatch>>),
480     Parse,
481 }
482
483 fn idempotent_check(
484     filename: &PathBuf,
485     opt_config: &Option<PathBuf>,
486 ) -> Result<FormatReport, IdempotentCheckError> {
487     let sig_comments = read_significant_comments(filename);
488     let config = if let Some(ref config_file_path) = opt_config {
489         Config::from_toml_path(config_file_path).expect("`rustfmt.toml` not found")
490     } else {
491         read_config(filename)
492     };
493     let (parsing_errors, source_file, format_report) = format_file(filename, config);
494     if parsing_errors {
495         return Err(IdempotentCheckError::Parse);
496     }
497
498     let mut write_result = HashMap::new();
499     for (filename, text) in source_file {
500         if let FileName::Real(ref filename) = filename {
501             write_result.insert(filename.to_owned(), text);
502         }
503     }
504
505     let target = sig_comments.get("target").map(|x| &(*x)[..]);
506
507     handle_result(write_result, target).map(|_| format_report)
508 }
509
510 // Reads test config file using the supplied (optional) file name. If there's no file name or the
511 // file doesn't exist, just return the default config. Otherwise, the file must be read
512 // successfully.
513 fn get_config(config_file: Option<&Path>) -> Config {
514     let config_file_name = match config_file {
515         None => return Default::default(),
516         Some(file_name) => {
517             let mut full_path = PathBuf::from("tests/config/");
518             full_path.push(file_name);
519             if !full_path.exists() {
520                 return Default::default();
521             };
522             full_path
523         }
524     };
525
526     let mut def_config_file = fs::File::open(config_file_name).expect("couldn't open config");
527     let mut def_config = String::new();
528     def_config_file
529         .read_to_string(&mut def_config)
530         .expect("Couldn't read config");
531
532     Config::from_toml(&def_config, Path::new("tests/config/")).expect("invalid TOML")
533 }
534
535 // Reads significant comments of the form: `// rustfmt-key: value` into a hash map.
536 fn read_significant_comments(file_name: &Path) -> HashMap<String, String> {
537     let file =
538         fs::File::open(file_name).expect(&format!("couldn't read file {}", file_name.display()));
539     let reader = BufReader::new(file);
540     let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)";
541     let regex = regex::Regex::new(pattern).expect("failed creating pattern 1");
542
543     // Matches lines containing significant comments or whitespace.
544     let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)")
545         .expect("failed creating pattern 2");
546
547     reader
548         .lines()
549         .map(|line| line.expect("failed getting line"))
550         .take_while(|line| line_regex.is_match(line))
551         .filter_map(|line| {
552             regex.captures_iter(&line).next().map(|capture| {
553                 (
554                     capture
555                         .get(1)
556                         .expect("couldn't unwrap capture")
557                         .as_str()
558                         .to_owned(),
559                     capture
560                         .get(2)
561                         .expect("couldn't unwrap capture")
562                         .as_str()
563                         .to_owned(),
564                 )
565             })
566         })
567         .collect()
568 }
569
570 // Compares output to input.
571 // TODO: needs a better name, more explanation.
572 fn handle_result(
573     result: HashMap<PathBuf, String>,
574     target: Option<&str>,
575 ) -> Result<(), IdempotentCheckError> {
576     let mut failures = HashMap::new();
577
578     for (file_name, fmt_text) in result {
579         // If file is in tests/source, compare to file with same name in tests/target.
580         let target = get_target(&file_name, target);
581         let open_error = format!("couldn't open target {:?}", &target);
582         let mut f = fs::File::open(&target).expect(&open_error);
583
584         let mut text = String::new();
585         let read_error = format!("failed reading target {:?}", &target);
586         f.read_to_string(&mut text).expect(&read_error);
587
588         // Ignore LF and CRLF difference for Windows.
589         if !string_eq_ignore_newline_repr(&fmt_text, &text) {
590             let diff = make_diff(&text, &fmt_text, DIFF_CONTEXT_SIZE);
591             assert!(
592                 !diff.is_empty(),
593                 "Empty diff? Maybe due to a missing a newline at the end of a file?"
594             );
595             failures.insert(file_name, diff);
596         }
597     }
598
599     if failures.is_empty() {
600         Ok(())
601     } else {
602         Err(IdempotentCheckError::Mismatch(failures))
603     }
604 }
605
606 // Maps source file paths to their target paths.
607 fn get_target(file_name: &Path, target: Option<&str>) -> PathBuf {
608     if let Some(n) = file_name
609         .components()
610         .position(|c| c.as_os_str() == "source")
611     {
612         let mut target_file_name = PathBuf::new();
613         for (i, c) in file_name.components().enumerate() {
614             if i == n {
615                 target_file_name.push("target");
616             } else {
617                 target_file_name.push(c.as_os_str());
618             }
619         }
620         if let Some(replace_name) = target {
621             target_file_name.with_file_name(replace_name)
622         } else {
623             target_file_name
624         }
625     } else {
626         // This is either and idempotence check or a self check.
627         file_name.to_owned()
628     }
629 }
630
631 #[test]
632 fn rustfmt_diff_make_diff_tests() {
633     let diff = make_diff("a\nb\nc\nd", "a\ne\nc\nd", 3);
634     assert_eq!(
635         diff,
636         vec![Mismatch {
637             line_number: 1,
638             line_number_orig: 1,
639             lines: vec![
640                 DiffLine::Context("a".into()),
641                 DiffLine::Resulting("b".into()),
642                 DiffLine::Expected("e".into()),
643                 DiffLine::Context("c".into()),
644                 DiffLine::Context("d".into()),
645             ],
646         }]
647     );
648 }
649
650 #[test]
651 fn rustfmt_diff_no_diff_test() {
652     let diff = make_diff("a\nb\nc\nd", "a\nb\nc\nd", 3);
653     assert_eq!(diff, vec![]);
654 }
655
656 // Compare strings without distinguishing between CRLF and LF
657 fn string_eq_ignore_newline_repr(left: &str, right: &str) -> bool {
658     let left = CharsIgnoreNewlineRepr(left.chars().peekable());
659     let right = CharsIgnoreNewlineRepr(right.chars().peekable());
660     left.eq(right)
661 }
662
663 struct CharsIgnoreNewlineRepr<'a>(Peekable<Chars<'a>>);
664
665 impl<'a> Iterator for CharsIgnoreNewlineRepr<'a> {
666     type Item = char;
667
668     fn next(&mut self) -> Option<char> {
669         self.0.next().map(|c| {
670             if c == '\r' {
671                 if *self.0.peek().unwrap_or(&'\0') == '\n' {
672                     self.0.next();
673                     '\n'
674                 } else {
675                     '\r'
676                 }
677             } else {
678                 c
679             }
680         })
681     }
682 }
683
684 #[test]
685 fn string_eq_ignore_newline_repr_test() {
686     assert!(string_eq_ignore_newline_repr("", ""));
687     assert!(!string_eq_ignore_newline_repr("", "abc"));
688     assert!(!string_eq_ignore_newline_repr("abc", ""));
689     assert!(string_eq_ignore_newline_repr("a\nb\nc\rd", "a\nb\r\nc\rd"));
690     assert!(string_eq_ignore_newline_repr("a\r\n\r\n\r\nb", "a\n\n\nb"));
691     assert!(!string_eq_ignore_newline_repr("a\r\nbcd", "a\nbcdefghijk"));
692 }
693
694 // This enum is used to represent one of three text features in Configurations.md: a block of code
695 // with its starting line number, the name of a rustfmt configuration option, or the value of a
696 // rustfmt configuration option.
697 enum ConfigurationSection {
698     CodeBlock((String, u32)), // (String: block of code, u32: line number of code block start)
699     ConfigName(String),
700     ConfigValue(String),
701 }
702
703 impl ConfigurationSection {
704     fn get_section<I: Iterator<Item = String>>(
705         file: &mut Enumerate<I>,
706     ) -> Option<ConfigurationSection> {
707         lazy_static! {
708             static ref CONFIG_NAME_REGEX: regex::Regex =
709                 regex::Regex::new(r"^## `([^`]+)`").expect("failed creating configuration pattern");
710             static ref CONFIG_VALUE_REGEX: regex::Regex =
711                 regex::Regex::new(r#"^#### `"?([^`"]+)"?`"#)
712                     .expect("failed creating configuration value pattern");
713         }
714
715         loop {
716             match file.next() {
717                 Some((i, line)) => {
718                     if line.starts_with("```rust") {
719                         // Get the lines of the code block.
720                         let lines: Vec<String> = file
721                             .map(|(_i, l)| l)
722                             .take_while(|l| !l.starts_with("```"))
723                             .collect();
724                         let block = format!("{}\n", lines.join("\n"));
725
726                         // +1 to translate to one-based indexing
727                         // +1 to get to first line of code (line after "```")
728                         let start_line = (i + 2) as u32;
729
730                         return Some(ConfigurationSection::CodeBlock((block, start_line)));
731                     } else if let Some(c) = CONFIG_NAME_REGEX.captures(&line) {
732                         return Some(ConfigurationSection::ConfigName(String::from(&c[1])));
733                     } else if let Some(c) = CONFIG_VALUE_REGEX.captures(&line) {
734                         return Some(ConfigurationSection::ConfigValue(String::from(&c[1])));
735                     }
736                 }
737                 None => return None, // reached the end of the file
738             }
739         }
740     }
741 }
742
743 // This struct stores the information about code blocks in the configurations
744 // file, formats the code blocks, and prints formatting errors.
745 struct ConfigCodeBlock {
746     config_name: Option<String>,
747     config_value: Option<String>,
748     code_block: Option<String>,
749     code_block_start: Option<u32>,
750 }
751
752 impl ConfigCodeBlock {
753     fn new() -> ConfigCodeBlock {
754         ConfigCodeBlock {
755             config_name: None,
756             config_value: None,
757             code_block: None,
758             code_block_start: None,
759         }
760     }
761
762     fn set_config_name(&mut self, name: Option<String>) {
763         self.config_name = name;
764         self.config_value = None;
765     }
766
767     fn set_config_value(&mut self, value: Option<String>) {
768         self.config_value = value;
769     }
770
771     fn set_code_block(&mut self, code_block: String, code_block_start: u32) {
772         self.code_block = Some(code_block);
773         self.code_block_start = Some(code_block_start);
774     }
775
776     fn get_block_config(&self) -> Config {
777         let mut config = Config::default();
778         if self.config_value.is_some() && self.config_value.is_some() {
779             config.override_value(
780                 self.config_name.as_ref().unwrap(),
781                 self.config_value.as_ref().unwrap(),
782             );
783         }
784         config
785     }
786
787     fn code_block_valid(&self) -> bool {
788         // We never expect to not have a code block.
789         assert!(self.code_block.is_some() && self.code_block_start.is_some());
790
791         // See if code block begins with #![rustfmt::skip].
792         let fmt_skip = self
793             .code_block
794             .as_ref()
795             .unwrap()
796             .lines()
797             .nth(0)
798             .unwrap_or("")
799             == "#![rustfmt::skip]";
800
801         if self.config_name.is_none() && !fmt_skip {
802             write_message(&format!(
803                 "No configuration name for {}:{}",
804                 CONFIGURATIONS_FILE_NAME,
805                 self.code_block_start.unwrap()
806             ));
807             return false;
808         }
809         if self.config_value.is_none() && !fmt_skip {
810             write_message(&format!(
811                 "No configuration value for {}:{}",
812                 CONFIGURATIONS_FILE_NAME,
813                 self.code_block_start.unwrap()
814             ));
815             return false;
816         }
817         true
818     }
819
820     fn has_parsing_errors<T: Write>(&self, session: &Session<'_, T>) -> bool {
821         if session.has_parsing_errors() {
822             write_message(&format!(
823                 "\u{261d}\u{1f3fd} Cannot format {}:{}",
824                 CONFIGURATIONS_FILE_NAME,
825                 self.code_block_start.unwrap()
826             ));
827             return true;
828         }
829
830         false
831     }
832
833     fn print_diff(&self, compare: Vec<Mismatch>) {
834         let mut mismatches = HashMap::new();
835         mismatches.insert(PathBuf::from(CONFIGURATIONS_FILE_NAME), compare);
836         print_mismatches(mismatches, |line_num| {
837             format!(
838                 "\nMismatch at {}:{}:",
839                 CONFIGURATIONS_FILE_NAME,
840                 line_num + self.code_block_start.unwrap() - 1
841             )
842         });
843     }
844
845     fn formatted_has_diff(&self, text: &str) -> bool {
846         let compare = make_diff(self.code_block.as_ref().unwrap(), text, DIFF_CONTEXT_SIZE);
847         if !compare.is_empty() {
848             self.print_diff(compare);
849             return true;
850         }
851
852         false
853     }
854
855     // Return a bool indicating if formatting this code block is an idempotent
856     // operation. This function also triggers printing any formatting failure
857     // messages.
858     fn formatted_is_idempotent(&self) -> bool {
859         // Verify that we have all of the expected information.
860         if !self.code_block_valid() {
861             return false;
862         }
863
864         let input = Input::Text(self.code_block.as_ref().unwrap().to_owned());
865         let mut config = self.get_block_config();
866         config.set().emit_mode(EmitMode::Stdout);
867         let mut buf: Vec<u8> = vec![];
868
869         {
870             let mut session = Session::new(config, Some(&mut buf));
871             session.format(input).unwrap();
872             if self.has_parsing_errors(&session) {
873                 return false;
874             }
875         }
876
877         !self.formatted_has_diff(&String::from_utf8(buf).unwrap())
878     }
879
880     // Extract a code block from the iterator. Behavior:
881     // - Rust code blocks are identifed by lines beginning with "```rust".
882     // - One explicit configuration setting is supported per code block.
883     // - Rust code blocks with no configuration setting are illegal and cause an
884     //   assertion failure, unless the snippet begins with #![rustfmt::skip].
885     // - Configuration names in Configurations.md must be in the form of
886     //   "## `NAME`".
887     // - Configuration values in Configurations.md must be in the form of
888     //   "#### `VALUE`".
889     fn extract<I: Iterator<Item = String>>(
890         file: &mut Enumerate<I>,
891         prev: Option<&ConfigCodeBlock>,
892         hash_set: &mut HashSet<String>,
893     ) -> Option<ConfigCodeBlock> {
894         let mut code_block = ConfigCodeBlock::new();
895         code_block.config_name = prev.and_then(|cb| cb.config_name.clone());
896
897         loop {
898             match ConfigurationSection::get_section(file) {
899                 Some(ConfigurationSection::CodeBlock((block, start_line))) => {
900                     code_block.set_code_block(block, start_line);
901                     break;
902                 }
903                 Some(ConfigurationSection::ConfigName(name)) => {
904                     assert!(
905                         Config::is_valid_name(&name),
906                         "an unknown configuration option was found: {}",
907                         name
908                     );
909                     assert!(
910                         hash_set.remove(&name),
911                         "multiple configuration guides found for option {}",
912                         name
913                     );
914                     code_block.set_config_name(Some(name));
915                 }
916                 Some(ConfigurationSection::ConfigValue(value)) => {
917                     code_block.set_config_value(Some(value));
918                 }
919                 None => return None, // end of file was reached
920             }
921         }
922
923         Some(code_block)
924     }
925 }
926
927 #[test]
928 fn configuration_snippet_tests() {
929     // Read Configurations.md and build a `Vec` of `ConfigCodeBlock` structs with one
930     // entry for each Rust code block found.
931     fn get_code_blocks() -> Vec<ConfigCodeBlock> {
932         let mut file_iter = BufReader::new(
933             fs::File::open(Path::new(CONFIGURATIONS_FILE_NAME))
934                 .expect(&format!("couldn't read file {}", CONFIGURATIONS_FILE_NAME)),
935         )
936         .lines()
937         .map(|l| l.unwrap())
938         .enumerate();
939         let mut code_blocks: Vec<ConfigCodeBlock> = Vec::new();
940         let mut hash_set = Config::hash_set();
941
942         while let Some(cb) =
943             ConfigCodeBlock::extract(&mut file_iter, code_blocks.last(), &mut hash_set)
944         {
945             code_blocks.push(cb);
946         }
947
948         for name in hash_set {
949             if !Config::is_hidden_option(&name) {
950                 panic!("{} does not have a configuration guide", name);
951             }
952         }
953
954         code_blocks
955     }
956
957     let blocks = get_code_blocks();
958     let failures = blocks
959         .iter()
960         .map(|b| b.formatted_is_idempotent())
961         .fold(0, |acc, r| acc + (!r as u32));
962
963     // Display results.
964     println!("Ran {} configurations tests.", blocks.len());
965     assert_eq!(failures, 0, "{} configurations tests failed", failures);
966 }
967
968 struct TempFile {
969     path: PathBuf,
970 }
971
972 fn make_temp_file(file_name: &'static str) -> TempFile {
973     use std::env::var;
974     use std::fs::File;
975
976     // Used in the Rust build system.
977     let target_dir = var("RUSTFMT_TEST_DIR").unwrap_or_else(|_| ".".to_owned());
978     let path = Path::new(&target_dir).join(file_name);
979
980     let mut file = File::create(&path).expect("couldn't create temp file");
981     let content = "fn main() {}\n";
982     file.write_all(content.as_bytes())
983         .expect("couldn't write temp file");
984     TempFile { path }
985 }
986
987 impl Drop for TempFile {
988     fn drop(&mut self) {
989         use std::fs::remove_file;
990         remove_file(&self.path).expect("couldn't delete temp file");
991     }
992 }
993
994 fn rustfmt() -> PathBuf {
995     let mut me = env::current_exe().expect("failed to get current executable");
996     // Chop of the test name.
997     me.pop();
998     // Chop off `deps`.
999     me.pop();
1000
1001     // If we run `cargo test --release`, we might only have a release build.
1002     if cfg!(release) {
1003         // `../release/`
1004         me.pop();
1005         me.push("release");
1006     }
1007     me.push("rustfmt");
1008     assert!(
1009         me.is_file() || me.with_extension("exe").is_file(),
1010         if cfg!(release) {
1011             "no rustfmt bin, try running `cargo build --release` before testing"
1012         } else {
1013             "no rustfmt bin, try running `cargo build` before testing"
1014         }
1015     );
1016     me
1017 }
1018
1019 #[test]
1020 fn verify_check_works() {
1021     let temp_file = make_temp_file("temp_check.rs");
1022
1023     Command::new(rustfmt().to_str().unwrap())
1024         .arg("--check")
1025         .arg(temp_file.path.to_str().unwrap())
1026         .status()
1027         .expect("run with check option failed");
1028 }