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