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