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.
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.
11 #![feature(rustc_private)]
16 extern crate rustfmt_nightly as rustfmt;
19 use std::collections::HashMap;
21 use std::io::{self, BufRead, BufReader, Read};
22 use std::iter::Peekable;
23 use std::path::{Path, PathBuf};
27 use rustfmt::config::{Color, Config, ReportTactic};
28 use rustfmt::filemap::{write_system_newlines, FileMap};
29 use rustfmt::rustfmt_diff::*;
31 const DIFF_CONTEXT_SIZE: usize = 3;
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
36 fn get_test_files(path: &Path, recursive: bool) -> Vec<PathBuf> {
37 let mut files = vec![];
39 for entry in fs::read_dir(path).expect(&format!(
40 "Couldn't read directory {}",
41 path.to_str().unwrap()
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") {
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()
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())),
71 .take_while(|l| l.starts_with("//"))
72 .any(|l| l.starts_with(&format!("// rustfmt-{}", config_name))),
74 "config option file {} does not contain expected config name",
83 fn verify_config_test_names() {
85 Path::new("tests/source/configs"),
86 Path::new("tests/target/configs"),
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();
92 let config_name = path.file_name().unwrap().to_str().unwrap();
94 // Make sure that config name is used in the files in the directory.
95 verify_config_used(&path, &config_name);
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
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);
112 println!("Ran {} system tests.", count);
113 assert_eq!(fails, 0, "{} system tests failed", fails);
116 // Do the same for tests/coverage-source directory
117 // the only difference is the coverage mode
119 fn coverage_tests() {
120 let files = get_test_files(Path::new("tests/coverage/source"), true);
121 let (_reports, count, fails) = check_files(files);
123 println!("Ran {} tests in coverage mode.", count);
124 assert_eq!(fails, 0, "{} tests failed", fails);
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));
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);
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();
145 let mut expected_file = fs::File::open(&expected_filename).expect("Couldn't open target");
146 let mut expected_text = String::new();
148 .read_to_string(&mut expected_text)
149 .expect("Failed reading target");
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");
160 // Idempotence tests. Files in tests/target are checked to be unaltered by
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);
169 println!("Ran {} idempotent tests.", count);
170 assert_eq!(fails, 0, "{} idempotent tests failed", fails);
173 // Run rustfmt on itself. This operation must be idempotent. We also check that
174 // no warnings are emitted.
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"));
182 let (reports, count, fails) = check_files(files);
183 let mut warnings = 0;
186 println!("Ran {} self tests.", count);
187 assert_eq!(fails, 0, "{} self tests failed", fails);
189 for format_report in reports {
190 println!("{}", format_report);
191 warnings += format_report.warning_count();
197 "Rustfmt's code generated {} warnings",
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");
220 // FIXME(#1990) restore this 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")
229 // .expect("failed to execute child");
232 // let stdin = child.stdin.as_mut().expect("failed to get stdin");
234 // .write_all(input.as_bytes())
235 // .expect("failed to write stdin");
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());
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());
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) {
258 let mut reports = vec![];
260 for file_name in files {
261 debug!("Testing '{}'...", file_name.display());
263 match idempotent_check(file_name) {
264 Ok(ref report) if report.has_warnings() => {
265 print!("{}", report);
268 Ok(report) => reports.push(report),
270 if let IdempotentCheckError::Mismatch(msg) = err {
271 print_mismatches(msg);
280 (reports, count, fails)
283 fn print_mismatches(result: HashMap<PathBuf, Vec<Mismatch>>) {
284 let mut t = term::stdout().unwrap();
286 for (file_name, diff) in result {
289 |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num),
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))
305 get_config(filename.with_extension("toml").file_name().map(Path::new))
308 for (key, val) in &sig_comments {
309 if key != "target" && key != "config" {
310 config.override_value(key, val);
314 // Don't generate warnings for to-do items.
315 config.set().report_todo(ReportTactic::Never);
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()
326 pub enum IdempotentCheckError {
327 Mismatch(HashMap<PathBuf, Vec<Mismatch>>),
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);
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);
351 let target = sig_comments.get("target").map(|x| &(*x)[..]);
353 handle_result(write_result, target).map(|_| format_report)
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
359 fn get_config(config_file: Option<&Path>) -> Config {
360 let config_file_name = match config_file {
361 None => return Default::default(),
363 let mut full_path = PathBuf::from("tests/config/");
364 full_path.push(file_name);
365 if !full_path.exists() {
366 return Default::default();
372 let mut def_config_file = fs::File::open(config_file_name).expect("Couldn't open config");
373 let mut def_config = String::new();
375 .read_to_string(&mut def_config)
376 .expect("Couldn't read config");
378 Config::from_toml(&def_config).expect("Invalid toml")
381 // Reads significant comments of the form: // rustfmt-key: value
383 fn read_significant_comments(file_name: &Path) -> HashMap<String, String> {
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");
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");
396 .map(|line| line.expect("Failed getting line"))
397 .take_while(|line| line_regex.is_match(line))
399 regex.captures_iter(&line).next().map(|capture| {
403 .expect("Couldn't unwrap capture")
408 .expect("Couldn't unwrap capture")
417 // Compare output to input.
418 // TODO: needs a better name, more explanation.
420 result: HashMap<PathBuf, String>,
421 target: Option<&str>,
422 ) -> Result<(), IdempotentCheckError> {
423 let mut failures = HashMap::new();
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);
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);
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);
440 "Empty diff? Maybe due to a missing a newline at the end of a file?"
442 failures.insert(file_name, diff);
446 if failures.is_empty() {
449 Err(IdempotentCheckError::Mismatch(failures))
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
457 .position(|c| c.as_os_str() == "source")
459 let mut target_file_name = PathBuf::new();
460 for (i, c) in file_name.components().enumerate() {
462 target_file_name.push("target");
464 target_file_name.push(c.as_os_str());
467 if let Some(replace_name) = target {
468 target_file_name.with_file_name(replace_name)
473 // This is either and idempotence check or a self check
479 fn rustfmt_diff_make_diff_tests() {
480 let diff = make_diff("a\nb\nc\nd", "a\ne\nc\nd", 3);
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()),
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![]);
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());
511 struct CharsIgnoreNewlineRepr<'a>(Peekable<Chars<'a>>);
513 impl<'a> Iterator for CharsIgnoreNewlineRepr<'a> {
515 fn next(&mut self) -> Option<char> {
516 self.0.next().map(|c| {
518 if *self.0.peek().unwrap_or(&'\0') == '\n' {
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"));