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