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