1 use std::collections::HashMap;
4 use std::io::{self, BufRead, BufReader, Read, Write};
5 use std::iter::Peekable;
7 use std::path::{Path, PathBuf};
8 use std::process::{Command, Stdio};
12 use crate::config::{Color, Config, EmitMode, FileName, NewlineStyle, ReportTactic};
13 use crate::formatting::{ReportedErrors, SourceFile};
14 use crate::rustfmt_diff::{make_diff, print_diff, DiffLine, Mismatch, ModifiedChunk, OutputWriter};
15 use crate::source_file;
16 use crate::{is_nightly_channel, FormatReport, FormatReportFormatterBuilder, Input, Session};
18 mod configuration_snippet;
21 const DIFF_CONTEXT_SIZE: usize = 3;
23 // A list of files on which we want to skip testing.
24 const SKIP_FILE_WHITE_LIST: &[&str] = &[
25 // We want to make sure that the `skip_children` is correctly working,
26 // so we do not want to test this file directly.
27 "configs/skip_children/foo/mod.rs",
28 "issue-3434/no_entry.rs",
29 "issue-3665/sub_mod.rs",
30 // Testing for issue-3779
32 // These files and directory are a part of modules defined inside `cfg_if!`.
38 // These files and directory are a part of modules defined inside `cfg_attr(..)`.
47 let _ = env_logger::builder().is_test(true).try_init();
51 /// The size of the stack of the thread that run tests.
55 impl Default for TestSetting {
56 fn default() -> Self {
58 stack_size: 8_388_608, // 8MB
63 fn run_test_with<F>(test_setting: &TestSetting, f: F)
68 thread::Builder::new()
69 .stack_size(test_setting.stack_size)
71 .expect("Failed to create a test thread")
73 .expect("Failed to join a test thread")
76 fn is_subpath<P>(path: &Path, subpath: &P) -> bool
80 (0..path.components().count())
84 .take(subpath.as_ref().components().count())
86 .any(|c| c.zip(subpath.as_ref().components()).all(|(a, b)| a == b))
89 fn is_file_skip(path: &Path) -> bool {
92 .any(|file_path| is_subpath(path, file_path))
95 // Returns a `Vec` containing `PathBuf`s of files with an `rs` extension in the
96 // given path. The `recursive` argument controls if files from subdirectories
98 fn get_test_files(path: &Path, recursive: bool) -> Vec<PathBuf> {
99 let mut files = vec![];
101 for entry in fs::read_dir(path).expect(&format!(
102 "couldn't read directory {}",
103 path.to_str().unwrap()
105 let entry = entry.expect("couldn't get `DirEntry`");
106 let path = entry.path();
107 if path.is_dir() && recursive {
108 files.append(&mut get_test_files(&path, recursive));
109 } else if path.extension().map_or(false, |f| f == "rs") && !is_file_skip(&path) {
117 fn verify_config_used(path: &Path, config_name: &str) {
118 for entry in fs::read_dir(path).expect(&format!(
119 "couldn't read {} directory",
120 path.to_str().unwrap()
122 let entry = entry.expect("couldn't get directory entry");
123 let path = entry.path();
124 if path.extension().map_or(false, |f| f == "rs") {
125 // check if "// rustfmt-<config_name>:" appears in the file.
126 let filebuf = BufReader::new(
127 fs::File::open(&path)
128 .unwrap_or_else(|_| panic!("couldn't read file {}", path.display())),
134 .take_while(|l| l.starts_with("//"))
135 .any(|l| l.starts_with(&format!("// rustfmt-{}", config_name))),
137 "config option file {} does not contain expected config name",
146 fn verify_config_test_names() {
149 Path::new("tests/source/configs"),
150 Path::new("tests/target/configs"),
152 for entry in fs::read_dir(path).expect("couldn't read configs directory") {
153 let entry = entry.expect("couldn't get directory entry");
154 let path = entry.path();
156 let config_name = path.file_name().unwrap().to_str().unwrap();
158 // Make sure that config name is used in the files in the directory.
159 verify_config_used(&path, config_name);
165 // This writes to the terminal using the same approach (via `term::stdout` or
166 // `println!`) that is used by `rustfmt::rustfmt_diff::print_diff`. Writing
167 // using only one or the other will cause the output order to differ when
168 // `print_diff` selects the approach not used.
169 fn write_message(msg: &str) {
170 let mut writer = OutputWriter::new(Color::Auto);
171 writer.writeln(msg, None);
174 // Integration tests. The files in `tests/source` are formatted and compared
175 // to their equivalent in `tests/target`. The target file and config can be
176 // overridden by annotations in the source file. The input and output must match
181 run_test_with(&TestSetting::default(), || {
182 // Get all files in the tests/source directory.
183 let files = get_test_files(Path::new("tests/source"), true);
184 let (_reports, count, fails) = check_files(files, &None);
187 println!("Ran {} system tests.", count);
188 assert_eq!(fails, 0, "{} system tests failed", fails);
191 "Expected a minimum of {} system tests to be executed",
197 // Do the same for tests/coverage-source directory.
198 // The only difference is the coverage mode.
200 fn coverage_tests() {
202 let files = get_test_files(Path::new("tests/coverage/source"), true);
203 let (_reports, count, fails) = check_files(files, &None);
205 println!("Ran {} tests in coverage mode.", count);
206 assert_eq!(fails, 0, "{} tests failed", fails);
210 fn checkstyle_test() {
212 let filename = "tests/writemode/source/fn-single-line.rs";
213 let expected_filename = "tests/writemode/target/checkstyle.xml";
214 assert_output(Path::new(filename), Path::new(expected_filename));
220 let filename = "tests/writemode/source/json.rs";
221 let expected_filename = "tests/writemode/target/output.json";
222 assert_output(Path::new(filename), Path::new(expected_filename));
228 use std::io::BufRead;
230 // Test "modified" output
231 let filename = "tests/writemode/source/modified.rs";
232 let mut data = Vec::new();
233 let mut config = Config::default();
236 .emit_mode(crate::config::EmitMode::ModifiedLines);
239 let mut session = Session::new(config, Some(&mut data));
240 session.format(Input::File(filename.into())).unwrap();
243 let mut lines = data.lines();
244 let mut chunks = Vec::new();
245 while let Some(Ok(header)) = lines.next() {
246 // Parse the header line
247 let values: Vec<_> = header
249 .map(|s| s.parse::<u32>().unwrap())
251 assert_eq!(values.len(), 3);
252 let line_number_orig = values[0];
253 let lines_removed = values[1];
254 let num_added = values[2];
255 let mut added_lines = Vec::new();
256 for _ in 0..num_added {
257 added_lines.push(lines.next().unwrap().unwrap());
259 chunks.push(ModifiedChunk {
272 lines: vec!["fn blah() {}".into()],
277 lines: vec!["#[cfg(a, b)]".into(), "fn main() {}".into()],
283 // Helper function for comparing the results of rustfmt
284 // to a known output file generated by one of the write modes.
285 fn assert_output(source: &Path, expected_filename: &Path) {
286 let config = read_config(source);
287 let (_, source_file, _) = format_file(source, config.clone());
289 // Populate output by writing to a vec.
290 let mut out = vec![];
291 let _ = source_file::write_all_files(&source_file, &mut out, &config);
292 let output = String::from_utf8(out).unwrap();
294 let mut expected_file = fs::File::open(&expected_filename).expect("couldn't open target");
295 let mut expected_text = String::new();
297 .read_to_string(&mut expected_text)
298 .expect("Failed reading target");
300 let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE);
301 if !compare.is_empty() {
302 let mut failures = HashMap::new();
303 failures.insert(source.to_owned(), compare);
304 print_mismatches_default_message(failures);
305 panic!("Text does not match expected output");
309 // Idempotence tests. Files in tests/target are checked to be unaltered by
312 fn idempotence_tests() {
314 run_test_with(&TestSetting::default(), || {
315 // these tests require nightly
316 if !is_nightly_channel!() {
319 // Get all files in the tests/target directory.
320 let files = get_test_files(Path::new("tests/target"), true);
321 let (_reports, count, fails) = check_files(files, &None);
324 println!("Ran {} idempotent tests.", count);
325 assert_eq!(fails, 0, "{} idempotent tests failed", fails);
328 "Expected a minimum of {} idempotent tests to be executed",
334 // Run rustfmt on itself. This operation must be idempotent. We also check that
335 // no warnings are emitted.
339 // Issue-3443: these tests require nightly
340 if !is_nightly_channel!() {
343 let mut files = get_test_files(Path::new("tests"), false);
344 let bin_directories = vec!["cargo-fmt", "git-rustfmt", "bin", "format-diff"];
345 for dir in bin_directories {
346 let mut path = PathBuf::from("src");
348 path.push("main.rs");
351 files.push(PathBuf::from("src/lib.rs"));
353 let (reports, count, fails) = check_files(files, &Some(PathBuf::from("rustfmt.toml")));
354 let mut warnings = 0;
357 println!("Ran {} self tests.", count);
358 assert_eq!(fails, 0, "{} self tests failed", fails);
360 for format_report in reports {
363 FormatReportFormatterBuilder::new(&format_report).build()
365 warnings += format_report.warning_count();
370 "Rustfmt's code generated {} warnings",
376 fn stdin_formatting_smoke_test() {
378 let input = Input::Text("fn main () {}".to_owned());
379 let mut config = Config::default();
380 config.set().emit_mode(EmitMode::Stdout);
381 let mut buf: Vec<u8> = vec![];
383 let mut session = Session::new(config, Some(&mut buf));
384 session.format(input).unwrap();
385 assert!(session.has_no_errors());
389 assert_eq!(buf, "stdin:\n\nfn main() {}\n".as_bytes());
391 assert_eq!(buf, "stdin:\n\nfn main() {}\r\n".as_bytes());
395 fn stdin_parser_panic_caught() {
398 for text in ["{", "}"].iter().cloned().map(String::from) {
399 let mut buf = vec![];
400 let mut session = Session::new(Default::default(), Some(&mut buf));
401 let _ = session.format(Input::Text(text));
403 assert!(session.has_parsing_errors());
407 /// Ensures that `EmitMode::ModifiedLines` works with input from `stdin`. Useful
408 /// when embedding Rustfmt (e.g. inside RLS).
410 fn stdin_works_with_modified_lines() {
412 let input = "\nfn\n some( )\n{\n}\nfn main () {}\n";
413 let output = "1 6 2\nfn some() {}\nfn main() {}\n";
415 let input = Input::Text(input.to_owned());
416 let mut config = Config::default();
417 config.set().newline_style(NewlineStyle::Unix);
418 config.set().emit_mode(EmitMode::ModifiedLines);
419 let mut buf: Vec<u8> = vec![];
421 let mut session = Session::new(config, Some(&mut buf));
422 session.format(input).unwrap();
423 let errors = ReportedErrors {
427 assert_eq!(session.errors, errors);
429 assert_eq!(buf, output.as_bytes());
433 fn stdin_disable_all_formatting_test() {
435 match option_env!("CFG_RELEASE_CHANNEL") {
436 None | Some("nightly") => {}
437 // These tests require nightly.
440 let input = String::from("fn main() { println!(\"This should not be formatted.\"); }");
441 let mut child = Command::new(rustfmt().to_str().unwrap())
442 .stdin(Stdio::piped())
443 .stdout(Stdio::piped())
444 .arg("--config-path=./tests/config/disable_all_formatting.toml")
446 .expect("failed to execute child");
449 let stdin = child.stdin.as_mut().expect("failed to get stdin");
451 .write_all(input.as_bytes())
452 .expect("failed to write stdin");
455 let output = child.wait_with_output().expect("failed to wait on child");
456 assert!(output.status.success());
457 assert!(output.stderr.is_empty());
458 assert_eq!(input, String::from_utf8(output.stdout).unwrap());
462 fn format_lines_errors_are_reported() {
464 let long_identifier = String::from_utf8(vec![b'a'; 239]).unwrap();
465 let input = Input::Text(format!("fn {}() {{}}", long_identifier));
466 let mut config = Config::default();
467 config.set().error_on_line_overflow(true);
468 let mut session = Session::<io::Stdout>::new(config, None);
469 session.format(input).unwrap();
470 assert!(session.has_formatting_errors());
474 fn format_lines_errors_are_reported_with_tabs() {
476 let long_identifier = String::from_utf8(vec![b'a'; 97]).unwrap();
477 let input = Input::Text(format!("fn a() {{\n\t{}\n}}", long_identifier));
478 let mut config = Config::default();
479 config.set().error_on_line_overflow(true);
480 config.set().hard_tabs(true);
481 let mut session = Session::<io::Stdout>::new(config, None);
482 session.format(input).unwrap();
483 assert!(session.has_formatting_errors());
486 // For each file, run rustfmt and collect the output.
487 // Returns the number of files checked and the number of failures.
488 fn check_files(files: Vec<PathBuf>, opt_config: &Option<PathBuf>) -> (Vec<FormatReport>, u32, u32) {
491 let mut reports = vec![];
493 for file_name in files {
494 let sig_comments = read_significant_comments(&file_name);
495 if sig_comments.contains_key("unstable") && !is_nightly_channel!() {
497 "Skipping '{}' because it requires unstable \
498 features which are only available on nightly...",
504 debug!("Testing '{}'...", file_name.display());
506 match idempotent_check(&file_name, &opt_config) {
507 Ok(ref report) if report.has_warnings() => {
508 print!("{}", FormatReportFormatterBuilder::new(&report).build());
511 Ok(report) => reports.push(report),
513 if let IdempotentCheckError::Mismatch(msg) = err {
514 print_mismatches_default_message(msg);
523 (reports, count, fails)
526 fn print_mismatches_default_message(result: HashMap<PathBuf, Vec<Mismatch>>) {
527 for (file_name, diff) in result {
528 let mismatch_msg_formatter =
529 |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num);
530 print_diff(diff, &mismatch_msg_formatter, &Default::default());
533 if let Some(mut t) = term::stdout() {
534 t.reset().unwrap_or(());
538 fn print_mismatches<T: Fn(u32) -> String>(
539 result: HashMap<PathBuf, Vec<Mismatch>>,
540 mismatch_msg_formatter: T,
542 for (_file_name, diff) in result {
543 print_diff(diff, &mismatch_msg_formatter, &Default::default());
546 if let Some(mut t) = term::stdout() {
547 t.reset().unwrap_or(());
551 fn read_config(filename: &Path) -> Config {
552 let sig_comments = read_significant_comments(filename);
553 // Look for a config file. If there is a 'config' property in the significant comments, use
554 // that. Otherwise, if there are no significant comments at all, look for a config file with
555 // the same name as the test file.
556 let mut config = if !sig_comments.is_empty() {
557 get_config(sig_comments.get("config").map(Path::new))
559 get_config(filename.with_extension("toml").file_name().map(Path::new))
562 for (key, val) in &sig_comments {
563 if key != "target" && key != "config" && key != "unstable" {
564 config.override_value(key, val);
565 if config.is_default(key) {
566 warn!("Default value {} used explicitly for {}", val, key);
571 // Don't generate warnings for to-do items.
572 config.set().report_todo(ReportTactic::Never);
577 fn format_file<P: Into<PathBuf>>(filepath: P, config: Config) -> (bool, SourceFile, FormatReport) {
578 let filepath = filepath.into();
579 let input = Input::File(filepath);
580 let mut session = Session::<io::Stdout>::new(config, None);
581 let result = session.format(input).unwrap();
582 let parsing_errors = session.has_parsing_errors();
583 let mut source_file = SourceFile::new();
584 mem::swap(&mut session.source_file, &mut source_file);
585 (parsing_errors, source_file, result)
588 enum IdempotentCheckError {
589 Mismatch(HashMap<PathBuf, Vec<Mismatch>>),
595 opt_config: &Option<PathBuf>,
596 ) -> Result<FormatReport, IdempotentCheckError> {
597 let sig_comments = read_significant_comments(filename);
598 let config = if let Some(ref config_file_path) = opt_config {
599 Config::from_toml_path(config_file_path).expect("`rustfmt.toml` not found")
601 read_config(filename)
603 let (parsing_errors, source_file, format_report) = format_file(filename, config);
605 return Err(IdempotentCheckError::Parse);
608 let mut write_result = HashMap::new();
609 for (filename, text) in source_file {
610 if let FileName::Real(ref filename) = filename {
611 write_result.insert(filename.to_owned(), text);
615 let target = sig_comments.get("target").map(|x| &(*x)[..]);
617 handle_result(write_result, target).map(|_| format_report)
620 // Reads test config file using the supplied (optional) file name. If there's no file name or the
621 // file doesn't exist, just return the default config. Otherwise, the file must be read
623 fn get_config(config_file: Option<&Path>) -> Config {
624 let config_file_name = match config_file {
625 None => return Default::default(),
627 let mut full_path = PathBuf::from("tests/config/");
628 full_path.push(file_name);
629 if !full_path.exists() {
630 return Default::default();
636 let mut def_config_file = fs::File::open(config_file_name).expect("couldn't open config");
637 let mut def_config = String::new();
639 .read_to_string(&mut def_config)
640 .expect("Couldn't read config");
642 Config::from_toml(&def_config, Path::new("tests/config/")).expect("invalid TOML")
645 // Reads significant comments of the form: `// rustfmt-key: value` into a hash map.
646 fn read_significant_comments(file_name: &Path) -> HashMap<String, String> {
647 let file = fs::File::open(file_name)
648 .unwrap_or_else(|_| panic!("couldn't read file {}", file_name.display()));
649 let reader = BufReader::new(file);
650 let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)";
651 let regex = regex::Regex::new(pattern).expect("failed creating pattern 1");
653 // Matches lines containing significant comments or whitespace.
654 let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)")
655 .expect("failed creating pattern 2");
659 .map(|line| line.expect("failed getting line"))
660 .take_while(|line| line_regex.is_match(line))
662 regex.captures_iter(&line).next().map(|capture| {
666 .expect("couldn't unwrap capture")
671 .expect("couldn't unwrap capture")
680 // Compares output to input.
681 // TODO: needs a better name, more explanation.
683 result: HashMap<PathBuf, String>,
684 target: Option<&str>,
685 ) -> Result<(), IdempotentCheckError> {
686 let mut failures = HashMap::new();
688 for (file_name, fmt_text) in result {
689 // If file is in tests/source, compare to file with same name in tests/target.
690 let target = get_target(&file_name, target);
691 let open_error = format!("couldn't open target {:?}", target);
692 let mut f = fs::File::open(&target).expect(&open_error);
694 let mut text = String::new();
695 let read_error = format!("failed reading target {:?}", target);
696 f.read_to_string(&mut text).expect(&read_error);
698 // Ignore LF and CRLF difference for Windows.
699 if !string_eq_ignore_newline_repr(&fmt_text, &text) {
700 let diff = make_diff(&text, &fmt_text, DIFF_CONTEXT_SIZE);
703 "Empty diff? Maybe due to a missing a newline at the end of a file?"
705 failures.insert(file_name, diff);
709 if failures.is_empty() {
712 Err(IdempotentCheckError::Mismatch(failures))
716 // Maps source file paths to their target paths.
717 fn get_target(file_name: &Path, target: Option<&str>) -> PathBuf {
718 if let Some(n) = file_name
720 .position(|c| c.as_os_str() == "source")
722 let mut target_file_name = PathBuf::new();
723 for (i, c) in file_name.components().enumerate() {
725 target_file_name.push("target");
727 target_file_name.push(c.as_os_str());
730 if let Some(replace_name) = target {
731 target_file_name.with_file_name(replace_name)
736 // This is either and idempotence check or a self check.
742 fn rustfmt_diff_make_diff_tests() {
744 let diff = make_diff("a\nb\nc\nd", "a\ne\nc\nd", 3);
751 DiffLine::Context("a".into()),
752 DiffLine::Resulting("b".into()),
753 DiffLine::Expected("e".into()),
754 DiffLine::Context("c".into()),
755 DiffLine::Context("d".into()),
762 fn rustfmt_diff_no_diff_test() {
764 let diff = make_diff("a\nb\nc\nd", "a\nb\nc\nd", 3);
765 assert_eq!(diff, vec![]);
768 // Compare strings without distinguishing between CRLF and LF
769 fn string_eq_ignore_newline_repr(left: &str, right: &str) -> bool {
770 let left = CharsIgnoreNewlineRepr(left.chars().peekable());
771 let right = CharsIgnoreNewlineRepr(right.chars().peekable());
775 struct CharsIgnoreNewlineRepr<'a>(Peekable<Chars<'a>>);
777 impl<'a> Iterator for CharsIgnoreNewlineRepr<'a> {
780 fn next(&mut self) -> Option<char> {
781 self.0.next().map(|c| {
783 if *self.0.peek().unwrap_or(&'\0') == '\n' {
797 fn string_eq_ignore_newline_repr_test() {
799 assert!(string_eq_ignore_newline_repr("", ""));
800 assert!(!string_eq_ignore_newline_repr("", "abc"));
801 assert!(!string_eq_ignore_newline_repr("abc", ""));
802 assert!(string_eq_ignore_newline_repr("a\nb\nc\rd", "a\nb\r\nc\rd"));
803 assert!(string_eq_ignore_newline_repr("a\r\n\r\n\r\nb", "a\n\n\nb"));
804 assert!(!string_eq_ignore_newline_repr("a\r\nbcd", "a\nbcdefghijk"));
811 fn make_temp_file(file_name: &'static str) -> TempFile {
815 // Used in the Rust build system.
816 let target_dir = var("RUSTFMT_TEST_DIR").unwrap_or_else(|_| ".".to_owned());
817 let path = Path::new(&target_dir).join(file_name);
819 let mut file = File::create(&path).expect("couldn't create temp file");
820 let content = "fn main() {}\n";
821 file.write_all(content.as_bytes())
822 .expect("couldn't write temp file");
826 impl Drop for TempFile {
828 use std::fs::remove_file;
829 remove_file(&self.path).expect("couldn't delete temp file");
833 fn rustfmt() -> PathBuf {
834 let mut me = env::current_exe().expect("failed to get current executable");
835 // Chop of the test name.
840 // If we run `cargo test --release`, we might only have a release build.
848 me.is_file() || me.with_extension("exe").is_file(),
850 "no rustfmt bin, try running `cargo build --release` before testing"
852 "no rustfmt bin, try running `cargo build` before testing"
859 fn verify_check_works() {
861 let temp_file = make_temp_file("temp_check.rs");
863 Command::new(rustfmt().to_str().unwrap())
865 .arg(temp_file.path.to_str().unwrap())
867 .expect("run with check option failed");