]> git.lizzy.rs Git - rust.git/blob - tests/system.rs
Testing that config option tests use the expected config option
[rust.git] / tests / system.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 #![feature(rustc_private)]
12
13 #[macro_use]
14 extern crate log;
15 extern crate regex;
16 extern crate rustfmt_nightly as rustfmt;
17 extern crate term;
18
19 use std::collections::HashMap;
20 use std::fs;
21 use std::io::{self, BufRead, BufReader, Read};
22 use std::iter::Peekable;
23 use std::path::{Path, PathBuf};
24 use std::str::Chars;
25
26 use rustfmt::*;
27 use rustfmt::config::{Color, Config, ReportTactic};
28 use rustfmt::filemap::{write_system_newlines, FileMap};
29 use rustfmt::rustfmt_diff::*;
30
31 const DIFF_CONTEXT_SIZE: usize = 3;
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 DirEntry");
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 // Integration tests. The files in the tests/source are formatted and compared
102 // to their equivalent in tests/target. The target file and config can be
103 // overridden by annotations in the source file. The input and output must match
104 // exactly.
105 #[test]
106 fn system_tests() {
107     // Get all files in the tests/source directory.
108     let files = get_test_files(Path::new("tests/source"), true);
109     let (_reports, count, fails) = check_files(files);
110
111     // Display results.
112     println!("Ran {} system tests.", count);
113     assert_eq!(fails, 0, "{} system tests failed", fails);
114 }
115
116 // Do the same for tests/coverage-source directory
117 // the only difference is the coverage mode
118 #[test]
119 fn coverage_tests() {
120     let files = get_test_files(Path::new("tests/coverage/source"), true);
121     let (_reports, count, fails) = check_files(files);
122
123     println!("Ran {} tests in coverage mode.", count);
124     assert_eq!(fails, 0, "{} tests failed", fails);
125 }
126
127 #[test]
128 fn checkstyle_test() {
129     let filename = "tests/writemode/source/fn-single-line.rs";
130     let expected_filename = "tests/writemode/target/checkstyle.xml";
131     assert_output(Path::new(filename), Path::new(expected_filename));
132 }
133
134 // Helper function for comparing the results of rustfmt
135 // to a known output file generated by one of the write modes.
136 fn assert_output(source: &Path, expected_filename: &Path) {
137     let config = read_config(source);
138     let (_error_summary, file_map, _report) = format_file(source, &config);
139
140     // Populate output by writing to a vec.
141     let mut out = vec![];
142     let _ = filemap::write_all_files(&file_map, &mut out, &config);
143     let output = String::from_utf8(out).unwrap();
144
145     let mut expected_file = fs::File::open(&expected_filename).expect("Couldn't open target");
146     let mut expected_text = String::new();
147     expected_file
148         .read_to_string(&mut expected_text)
149         .expect("Failed reading target");
150
151     let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE);
152     if !compare.is_empty() {
153         let mut failures = HashMap::new();
154         failures.insert(source.to_owned(), compare);
155         print_mismatches(failures);
156         assert!(false, "Text does not match expected output");
157     }
158 }
159
160 // Idempotence tests. Files in tests/target are checked to be unaltered by
161 // rustfmt.
162 #[test]
163 fn idempotence_tests() {
164     // Get all files in the tests/target directory.
165     let files = get_test_files(Path::new("tests/target"), true);
166     let (_reports, count, fails) = check_files(files);
167
168     // Display results.
169     println!("Ran {} idempotent tests.", count);
170     assert_eq!(fails, 0, "{} idempotent tests failed", fails);
171 }
172
173 // Run rustfmt on itself. This operation must be idempotent. We also check that
174 // no warnings are emitted.
175 #[test]
176 fn self_tests() {
177     let mut files = get_test_files(Path::new("src/bin"), false);
178     files.append(&mut get_test_files(Path::new("tests"), false));
179     files.push(PathBuf::from("src/lib.rs"));
180     files.push(PathBuf::from("build.rs"));
181
182     let (reports, count, fails) = check_files(files);
183     let mut warnings = 0;
184
185     // Display results.
186     println!("Ran {} self tests.", count);
187     assert_eq!(fails, 0, "{} self tests failed", fails);
188
189     for format_report in reports {
190         println!("{}", format_report);
191         warnings += format_report.warning_count();
192     }
193
194     assert_eq!(
195         warnings,
196         0,
197         "Rustfmt's code generated {} warnings",
198         warnings
199     );
200 }
201
202 #[test]
203 fn stdin_formatting_smoke_test() {
204     let input = Input::Text("fn main () {}".to_owned());
205     let config = Config::default();
206     let (error_summary, file_map, _report) =
207         format_input::<io::Stdout>(input, &config, None).unwrap();
208     assert!(error_summary.has_no_errors());
209     for &(ref file_name, ref text) in &file_map {
210         if let FileName::Custom(ref file_name) = *file_name {
211             if file_name == "stdin" {
212                 assert_eq!(text.to_string(), "fn main() {}\n");
213                 return;
214             }
215         }
216     }
217     panic!("no stdin");
218 }
219
220 // FIXME(#1990) restore this test
221 // #[test]
222 // fn stdin_disable_all_formatting_test() {
223 //     let input = String::from("fn main() { println!(\"This should not be formatted.\"); }");
224 //     let mut child = Command::new("./target/debug/rustfmt")
225 //         .stdin(Stdio::piped())
226 //         .stdout(Stdio::piped())
227 //         .arg("--config-path=./tests/config/disable_all_formatting.toml")
228 //         .spawn()
229 //         .expect("failed to execute child");
230
231 //     {
232 //         let stdin = child.stdin.as_mut().expect("failed to get stdin");
233 //         stdin
234 //             .write_all(input.as_bytes())
235 //             .expect("failed to write stdin");
236 //     }
237 //     let output = child.wait_with_output().expect("failed to wait on child");
238 //     assert!(output.status.success());
239 //     assert!(output.stderr.is_empty());
240 //     assert_eq!(input, String::from_utf8(output.stdout).unwrap());
241 // }
242
243 #[test]
244 fn format_lines_errors_are_reported() {
245     let long_identifier = String::from_utf8(vec![b'a'; 239]).unwrap();
246     let input = Input::Text(format!("fn {}() {{}}", long_identifier));
247     let config = Config::default();
248     let (error_summary, _file_map, _report) =
249         format_input::<io::Stdout>(input, &config, None).unwrap();
250     assert!(error_summary.has_formatting_errors());
251 }
252
253 // For each file, run rustfmt and collect the output.
254 // Returns the number of files checked and the number of failures.
255 fn check_files(files: Vec<PathBuf>) -> (Vec<FormatReport>, u32, u32) {
256     let mut count = 0;
257     let mut fails = 0;
258     let mut reports = vec![];
259
260     for file_name in files {
261         debug!("Testing '{}'...", file_name.display());
262
263         match idempotent_check(file_name) {
264             Ok(ref report) if report.has_warnings() => {
265                 print!("{}", report);
266                 fails += 1;
267             }
268             Ok(report) => reports.push(report),
269             Err(err) => {
270                 if let IdempotentCheckError::Mismatch(msg) = err {
271                     print_mismatches(msg);
272                 }
273                 fails += 1;
274             }
275         }
276
277         count += 1;
278     }
279
280     (reports, count, fails)
281 }
282
283 fn print_mismatches(result: HashMap<PathBuf, Vec<Mismatch>>) {
284     let mut t = term::stdout().unwrap();
285
286     for (file_name, diff) in result {
287         print_diff(
288             diff,
289             |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num),
290             Color::Auto,
291         );
292     }
293
294     t.reset().unwrap();
295 }
296
297 fn read_config(filename: &Path) -> Config {
298     let sig_comments = read_significant_comments(filename);
299     // Look for a config file... If there is a 'config' property in the significant comments, use
300     // that. Otherwise, if there are no significant comments at all, look for a config file with
301     // the same name as the test file.
302     let mut config = if !sig_comments.is_empty() {
303         get_config(sig_comments.get("config").map(Path::new))
304     } else {
305         get_config(filename.with_extension("toml").file_name().map(Path::new))
306     };
307
308     for (key, val) in &sig_comments {
309         if key != "target" && key != "config" {
310             config.override_value(key, val);
311         }
312     }
313
314     // Don't generate warnings for to-do items.
315     config.set().report_todo(ReportTactic::Never);
316
317     config
318 }
319
320 fn format_file<P: Into<PathBuf>>(filepath: P, config: &Config) -> (Summary, FileMap, FormatReport) {
321     let filepath = filepath.into();
322     let input = Input::File(filepath);
323     format_input::<io::Stdout>(input, config, None).unwrap()
324 }
325
326 pub enum IdempotentCheckError {
327     Mismatch(HashMap<PathBuf, Vec<Mismatch>>),
328     Parse,
329 }
330
331 pub fn idempotent_check(filename: PathBuf) -> Result<FormatReport, IdempotentCheckError> {
332     let sig_comments = read_significant_comments(&filename);
333     let config = read_config(&filename);
334     let (error_summary, file_map, format_report) = format_file(filename, &config);
335     if error_summary.has_parsing_errors() {
336         return Err(IdempotentCheckError::Parse);
337     }
338
339     let mut write_result = HashMap::new();
340     for &(ref filename, ref text) in &file_map {
341         let mut v = Vec::new();
342         // Won't panic, as we're not doing any IO.
343         write_system_newlines(&mut v, text, &config).unwrap();
344         // Won't panic, we are writing correct utf8.
345         let one_result = String::from_utf8(v).unwrap();
346         if let FileName::Real(ref filename) = *filename {
347             write_result.insert(filename.to_owned(), one_result);
348         }
349     }
350
351     let target = sig_comments.get("target").map(|x| &(*x)[..]);
352
353     handle_result(write_result, target).map(|_| format_report)
354 }
355
356 // Reads test config file using the supplied (optional) file name. If there's no file name or the
357 // file doesn't exist, just return the default config. Otherwise, the file must be read
358 // successfully.
359 fn get_config(config_file: Option<&Path>) -> Config {
360     let config_file_name = match config_file {
361         None => return Default::default(),
362         Some(file_name) => {
363             let mut full_path = PathBuf::from("tests/config/");
364             full_path.push(file_name);
365             if !full_path.exists() {
366                 return Default::default();
367             };
368             full_path
369         }
370     };
371
372     let mut def_config_file = fs::File::open(config_file_name).expect("Couldn't open config");
373     let mut def_config = String::new();
374     def_config_file
375         .read_to_string(&mut def_config)
376         .expect("Couldn't read config");
377
378     Config::from_toml(&def_config).expect("Invalid toml")
379 }
380
381 // Reads significant comments of the form: // rustfmt-key: value
382 // into a hash map.
383 fn read_significant_comments(file_name: &Path) -> HashMap<String, String> {
384     let file =
385         fs::File::open(file_name).expect(&format!("Couldn't read file {}", file_name.display()));
386     let reader = BufReader::new(file);
387     let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)";
388     let regex = regex::Regex::new(pattern).expect("Failed creating pattern 1");
389
390     // Matches lines containing significant comments or whitespace.
391     let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)")
392         .expect("Failed creating pattern 2");
393
394     reader
395         .lines()
396         .map(|line| line.expect("Failed getting line"))
397         .take_while(|line| line_regex.is_match(line))
398         .filter_map(|line| {
399             regex.captures_iter(&line).next().map(|capture| {
400                 (
401                     capture
402                         .get(1)
403                         .expect("Couldn't unwrap capture")
404                         .as_str()
405                         .to_owned(),
406                     capture
407                         .get(2)
408                         .expect("Couldn't unwrap capture")
409                         .as_str()
410                         .to_owned(),
411                 )
412             })
413         })
414         .collect()
415 }
416
417 // Compare output to input.
418 // TODO: needs a better name, more explanation.
419 fn handle_result(
420     result: HashMap<PathBuf, String>,
421     target: Option<&str>,
422 ) -> Result<(), IdempotentCheckError> {
423     let mut failures = HashMap::new();
424
425     for (file_name, fmt_text) in result {
426         // If file is in tests/source, compare to file with same name in tests/target.
427         let target = get_target(&file_name, target);
428         let open_error = format!("Couldn't open target {:?}", &target);
429         let mut f = fs::File::open(&target).expect(&open_error);
430
431         let mut text = String::new();
432         let read_error = format!("Failed reading target {:?}", &target);
433         f.read_to_string(&mut text).expect(&read_error);
434
435         // Ignore LF and CRLF difference for Windows.
436         if !string_eq_ignore_newline_repr(&fmt_text, &text) {
437             let diff = make_diff(&text, &fmt_text, DIFF_CONTEXT_SIZE);
438             assert!(
439                 !diff.is_empty(),
440                 "Empty diff? Maybe due to a missing a newline at the end of a file?"
441             );
442             failures.insert(file_name, diff);
443         }
444     }
445
446     if failures.is_empty() {
447         Ok(())
448     } else {
449         Err(IdempotentCheckError::Mismatch(failures))
450     }
451 }
452
453 // Map source file paths to their target paths.
454 fn get_target(file_name: &Path, target: Option<&str>) -> PathBuf {
455     if let Some(n) = file_name
456         .components()
457         .position(|c| c.as_os_str() == "source")
458     {
459         let mut target_file_name = PathBuf::new();
460         for (i, c) in file_name.components().enumerate() {
461             if i == n {
462                 target_file_name.push("target");
463             } else {
464                 target_file_name.push(c.as_os_str());
465             }
466         }
467         if let Some(replace_name) = target {
468             target_file_name.with_file_name(replace_name)
469         } else {
470             target_file_name
471         }
472     } else {
473         // This is either and idempotence check or a self check
474         file_name.to_owned()
475     }
476 }
477
478 #[test]
479 fn rustfmt_diff_make_diff_tests() {
480     let diff = make_diff("a\nb\nc\nd", "a\ne\nc\nd", 3);
481     assert_eq!(
482         diff,
483         vec![
484             Mismatch {
485                 line_number: 1,
486                 lines: vec![
487                     DiffLine::Context("a".into()),
488                     DiffLine::Resulting("b".into()),
489                     DiffLine::Expected("e".into()),
490                     DiffLine::Context("c".into()),
491                     DiffLine::Context("d".into()),
492                 ],
493             },
494         ]
495     );
496 }
497
498 #[test]
499 fn rustfmt_diff_no_diff_test() {
500     let diff = make_diff("a\nb\nc\nd", "a\nb\nc\nd", 3);
501     assert_eq!(diff, vec![]);
502 }
503
504 // Compare strings without distinguishing between CRLF and LF
505 fn string_eq_ignore_newline_repr(left: &str, right: &str) -> bool {
506     let left = CharsIgnoreNewlineRepr(left.chars().peekable());
507     let right = CharsIgnoreNewlineRepr(right.chars().peekable());
508     left.eq(right)
509 }
510
511 struct CharsIgnoreNewlineRepr<'a>(Peekable<Chars<'a>>);
512
513 impl<'a> Iterator for CharsIgnoreNewlineRepr<'a> {
514     type Item = char;
515     fn next(&mut self) -> Option<char> {
516         self.0.next().map(|c| {
517             if c == '\r' {
518                 if *self.0.peek().unwrap_or(&'\0') == '\n' {
519                     self.0.next();
520                     '\n'
521                 } else {
522                     '\r'
523                 }
524             } else {
525                 c
526             }
527         })
528     }
529 }
530
531 #[test]
532 fn string_eq_ignore_newline_repr_test() {
533     assert!(string_eq_ignore_newline_repr("", ""));
534     assert!(!string_eq_ignore_newline_repr("", "abc"));
535     assert!(!string_eq_ignore_newline_repr("abc", ""));
536     assert!(string_eq_ignore_newline_repr("a\nb\nc\rd", "a\nb\r\nc\rd"));
537     assert!(string_eq_ignore_newline_repr("a\r\n\r\n\r\nb", "a\n\n\nb"));
538     assert!(!string_eq_ignore_newline_repr("a\r\nbcd", "a\nbcdefghijk"));
539 }