]> git.lizzy.rs Git - rust.git/blob - tests/system.rs
Merge pull request #221 from marcusklaas/diff-context
[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(catch_panic)]
12
13 extern crate rustfmt;
14 extern crate diff;
15 extern crate regex;
16 extern crate term;
17
18 use std::collections::{VecDeque, HashMap};
19 use std::fs;
20 use std::io::{self, Read, BufRead, BufReader};
21 use std::thread;
22 use rustfmt::*;
23 use rustfmt::config::Config;
24
25 static DIFF_CONTEXT_SIZE: usize = 3;
26
27 fn get_path_string(dir_entry: io::Result<fs::DirEntry>) -> String {
28     let path = dir_entry.ok().expect("Couldn't get DirEntry.").path();
29
30     path.to_str().expect("Couldn't stringify path.").to_owned()
31 }
32
33 // Integration tests. The files in the tests/source are formatted and compared
34 // to their equivalent in tests/target. The target file and config can be
35 // overriden by annotations in the source file. The input and output must match
36 // exactly.
37 // FIXME(#28) would be good to check for error messages and fail on them, or at
38 // least report.
39 #[test]
40 fn system_tests() {
41     // Get all files in the tests/source directory
42     let files = fs::read_dir("tests/source").ok().expect("Couldn't read source dir.");
43     // turn a DirEntry into a String that represents the relative path to the file
44     let files = files.map(get_path_string);
45
46     let (count, fails) = check_files(files);
47
48     // Display results
49     println!("Ran {} system tests.", count);
50     assert!(fails == 0, "{} system tests failed", fails);
51 }
52
53 // Idempotence tests. Files in tests/target are checked to be unaltered by
54 // rustfmt.
55 #[test]
56 fn idempotence_tests() {
57     // Get all files in the tests/target directory
58     let files = fs::read_dir("tests/target").ok().expect("Couldn't read target dir.");
59     let files = files.chain(fs::read_dir("tests").ok().expect("Couldn't read tests dir."));
60     let files = files.chain(fs::read_dir("src/bin").ok().expect("Couldn't read src dir."));
61     // turn a DirEntry into a String that represents the relative path to the file
62     let files = files.map(get_path_string);
63     // hack because there's no `IntoIterator` impl for `[T; N]`
64     let files = files.chain(Some("src/lib.rs".to_owned()).into_iter());
65
66     let (count, fails) = check_files(files);
67
68     // Display results
69     println!("Ran {} idempotent tests.", count);
70     assert!(fails == 0, "{} idempotent tests failed", fails);
71 }
72
73 // For each file, run rustfmt and collect the output.
74 // Returns the number of files checked and the number of failures.
75 fn check_files<I>(files: I) -> (u32, u32)
76     where I: Iterator<Item = String>
77 {
78     let mut count = 0;
79     let mut fails = 0;
80
81     for file_name in files.filter(|f| f.ends_with(".rs")) {
82         println!("Testing '{}'...", file_name);
83         if let Err(msg) = idempotent_check(file_name) {
84             print_mismatches(msg);
85             fails += 1;
86         }
87         count += 1;
88     }
89
90     (count, fails)
91 }
92
93 fn print_mismatches(result: HashMap<String, Vec<Mismatch>>) {
94     let mut t = term::stdout().unwrap();
95
96     for (file_name, diff) in result {
97         for mismatch in diff {
98             t.fg(term::color::BRIGHT_WHITE).unwrap();
99             writeln!(t, "\nMismatch at {}:{}:", file_name, mismatch.line_number).unwrap();
100
101             for line in mismatch.lines {
102                 match line {
103                     DiffLine::Context(ref str) => {
104                         t.fg(term::color::WHITE).unwrap();
105                         writeln!(t, " {}⏎", str).unwrap();
106                     }
107                     DiffLine::Expected(ref str) => {
108                         t.fg(term::color::GREEN).unwrap();
109                         writeln!(t, "+{}⏎", str).unwrap();
110                     }
111                     DiffLine::Resulting(ref str) => {
112                         t.fg(term::color::RED).unwrap();
113                         writeln!(t, "-{}⏎", str).unwrap();
114                     }
115                 }
116             }
117         }
118     }
119
120     assert!(t.reset().unwrap());
121 }
122
123 // Ick, just needed to get a &'static to handle_result.
124 static HANDLE_RESULT: &'static Fn(HashMap<String, String>) = &handle_result;
125
126 pub fn idempotent_check(filename: String) -> Result<(), HashMap<String, Vec<Mismatch>>> {
127     let sig_comments = read_significant_comments(&filename);
128     let mut config = get_config(sig_comments.get("config").map(|x| &(*x)[..]));
129     let args = vec!["rustfmt".to_owned(), filename];
130
131     for (key, val) in sig_comments {
132         if key != "target" && key != "config" {
133             config.override_value(&key, &val);
134         }
135     }
136
137     // this thread is not used for concurrency, but rather to workaround the issue that the passed
138     // function handle needs to have static lifetime. Instead of using a global RefCell, we use
139     // panic to return a result in case of failure. This has the advantage of smoothing the road to
140     // multithreaded rustfmt
141     thread::catch_panic(move || {
142         run(args, WriteMode::Return(HANDLE_RESULT), config);
143     }).map_err(|any|
144         *any.downcast().ok().expect("Downcast failed.")
145     )
146 }
147
148
149 // Reads test config file from comments and reads its contents.
150 fn get_config(config_file: Option<&str>) -> Box<Config> {
151     let config_file_name = match config_file {
152         None => return Box::new(Default::default()),
153         Some(file_name) => {
154             let mut full_path = "tests/config/".to_owned();
155             full_path.push_str(&file_name);
156             full_path
157         }
158     };
159
160     let mut def_config_file = fs::File::open(config_file_name).ok().expect("Couldn't open config.");
161     let mut def_config = String::new();
162     def_config_file.read_to_string(&mut def_config).ok().expect("Couldn't read config.");
163
164     Box::new(Config::from_toml(&def_config))
165 }
166
167 // Reads significant comments of the form: // rustfmt-key: value
168 // into a hash map.
169 fn read_significant_comments(file_name: &str) -> HashMap<String, String> {
170     let file = fs::File::open(file_name).ok().expect(&format!("Couldn't read file {}.", file_name));
171     let reader = BufReader::new(file);
172     let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)";
173     let regex = regex::Regex::new(&pattern).ok().expect("Failed creating pattern 1.");
174
175     // Matches lines containing significant comments or whitespace.
176     let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)")
177         .ok().expect("Failed creating pattern 2.");
178
179     reader.lines()
180           .map(|line| line.ok().expect("Failed getting line."))
181           .take_while(|line| line_regex.is_match(&line))
182           .filter_map(|line| {
183               regex.captures_iter(&line).next().map(|capture| {
184                   (capture.at(1).expect("Couldn't unwrap capture.").to_owned(),
185                    capture.at(2).expect("Couldn't unwrap capture.").to_owned())
186               })
187           })
188           .collect()
189 }
190
191 // Compare output to input.
192 // TODO: needs a better name, more explanation.
193 fn handle_result(result: HashMap<String, String>) {
194     let mut failures = HashMap::new();
195
196     for (file_name, fmt_text) in result {
197         // FIXME: reading significant comments again. Is there a way we can just
198         // pass the target to this function?
199         let sig_comments = read_significant_comments(&file_name);
200
201         // If file is in tests/source, compare to file with same name in tests/target.
202         let target = get_target(&file_name, sig_comments.get("target").map(|x| &(*x)[..]));
203         let mut f = fs::File::open(&target).ok().expect("Couldn't open target.");
204
205         let mut text = String::new();
206         // TODO: speedup by running through bytes iterator
207         f.read_to_string(&mut text).ok().expect("Failed reading target.");
208         if fmt_text != text {
209             let diff = make_diff(&fmt_text, &text, DIFF_CONTEXT_SIZE);
210             failures.insert(file_name, diff);
211         }
212     }
213
214     if !failures.is_empty() {
215         panic!(failures);
216     }
217 }
218
219 // Map source file paths to their target paths.
220 fn get_target(file_name: &str, target: Option<&str>) -> String {
221     if file_name.starts_with("tests/source/") {
222         let base = target.unwrap_or(file_name.trim_left_matches("tests/source/"));
223
224         format!("tests/target/{}", base)
225     } else {
226         file_name.to_owned()
227     }
228 }
229
230 pub enum DiffLine {
231     Context(String),
232     Expected(String),
233     Resulting(String),
234 }
235
236 pub struct Mismatch {
237     line_number: u32,
238     pub lines: Vec<DiffLine>,
239 }
240
241 impl Mismatch {
242     fn new(line_number: u32) -> Mismatch {
243         Mismatch { line_number: line_number, lines: Vec::new() }
244     }
245 }
246
247 // Produces a diff between the expected output and actual output of rustfmt.
248 fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec<Mismatch> {
249     let mut line_number = 1;
250     let mut context_queue: VecDeque<&str> = VecDeque::with_capacity(context_size);
251     let mut lines_since_mismatch = context_size + 1;
252     let mut results = Vec::new();
253     let mut mismatch = Mismatch::new(0);
254
255     for result in diff::lines(expected, actual) {
256         match result {
257             diff::Result::Left(str) => {
258                 if lines_since_mismatch >= context_size {
259                     results.push(mismatch);
260                     mismatch = Mismatch::new(line_number - context_queue.len() as u32);
261                 }
262
263                 while let Some(line) = context_queue.pop_front() {
264                     mismatch.lines.push(DiffLine::Context(line.to_owned()));
265                 }
266
267                 mismatch.lines.push(DiffLine::Resulting(str.to_owned()));
268                 lines_since_mismatch = 0;
269             }
270             diff::Result::Right(str) => {
271                 if lines_since_mismatch >= context_size {
272                     results.push(mismatch);
273                     mismatch = Mismatch::new(line_number - context_queue.len() as u32);
274                 }
275
276                 while let Some(line) = context_queue.pop_front() {
277                     mismatch.lines.push(DiffLine::Context(line.to_owned()));
278                 }
279
280                 mismatch.lines.push(DiffLine::Expected(str.to_owned()));
281                 line_number += 1;
282                 lines_since_mismatch = 0;
283             }
284             diff::Result::Both(str, _) => {
285                 if context_queue.len() >= context_size {
286                     let _ = context_queue.pop_front();
287                 }
288
289                 if lines_since_mismatch < context_size {
290                     mismatch.lines.push(DiffLine::Context(str.to_owned()));
291                 } else {
292                     context_queue.push_back(str);
293                 }
294
295                 line_number += 1;
296                 lines_since_mismatch += 1;
297             }
298         }
299     }
300
301     results.push(mismatch);
302     results.remove(0);
303
304     results
305 }