rust/tests/system.rs
Nick Cameron 9b05461666 Write each file as it is formatted (#991)
The old behaviour stored everything in memory until we were finished. Now we write as soon as we can.

This gives better behaviour when formatting large programs, since there is some progress indication. It also opens the door to optimising memory use by not storing everything in memory unless it is required (which it still might be). That is left as future work though.
2016-05-15 11:41:05 +02:00

351 lines
12 KiB
Rust

// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
extern crate rustfmt;
extern crate diff;
extern crate regex;
extern crate term;
use std::collections::HashMap;
use std::fs;
use std::io::{self, Read, BufRead, BufReader};
use std::path::{Path, PathBuf};
use rustfmt::*;
use rustfmt::filemap::{write_system_newlines, FileMap};
use rustfmt::config::{Config, ReportTactic};
use rustfmt::rustfmt_diff::*;
const DIFF_CONTEXT_SIZE: usize = 3;
fn get_path_string(dir_entry: io::Result<fs::DirEntry>) -> String {
let path = dir_entry.expect("Couldn't get DirEntry").path();
path.to_str().expect("Couldn't stringify path").to_owned()
}
// Integration tests. The files in the tests/source are formatted and compared
// to their equivalent in tests/target. The target file and config can be
// overriden by annotations in the source file. The input and output must match
// exactly.
// FIXME(#28) would be good to check for error messages and fail on them, or at
// least report.
#[test]
fn system_tests() {
// Get all files in the tests/source directory.
let files = fs::read_dir("tests/source").expect("Couldn't read source dir");
// Turn a DirEntry into a String that represents the relative path to the
// file.
let files = files.map(get_path_string);
let (_reports, count, fails) = check_files(files);
// Display results.
println!("Ran {} system tests.", count);
assert!(fails == 0, "{} system tests failed", fails);
}
// Do the same for tests/coverage-source directory
// the only difference is the coverage mode
#[test]
fn coverage_tests() {
let files = fs::read_dir("tests/coverage/source").expect("Couldn't read source dir");
let files = files.map(get_path_string);
let (_reports, count, fails) = check_files(files);
println!("Ran {} tests in coverage mode.", count);
assert!(fails == 0, "{} tests failed", fails);
}
#[test]
fn checkstyle_test() {
let filename = "tests/writemode/source/fn-single-line.rs";
let expected_filename = "tests/writemode/target/checkstyle.xml";
assert_output(filename, expected_filename);
}
// Helper function for comparing the results of rustfmt
// to a known output file generated by one of the write modes.
fn assert_output(source: &str, expected_filename: &str) {
let config = read_config(&source);
let (file_map, _report) = format_file(source, &config);
// Populate output by writing to a vec.
let mut out = vec![];
let _ = filemap::write_all_files(&file_map, &mut out, &config);
let output = String::from_utf8(out).unwrap();
let mut expected_file = fs::File::open(&expected_filename).expect("Couldn't open target");
let mut expected_text = String::new();
expected_file.read_to_string(&mut expected_text)
.expect("Failed reading target");
let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE);
if compare.len() > 0 {
let mut failures = HashMap::new();
failures.insert(source.to_string(), compare);
print_mismatches(failures);
assert!(false, "Text does not match expected output");
}
}
// Idempotence tests. Files in tests/target are checked to be unaltered by
// rustfmt.
#[test]
fn idempotence_tests() {
// Get all files in the tests/target directory.
let files = fs::read_dir("tests/target")
.expect("Couldn't read target dir")
.map(get_path_string);
let (_reports, count, fails) = check_files(files);
// Display results.
println!("Ran {} idempotent tests.", count);
assert!(fails == 0, "{} idempotent tests failed", fails);
}
// Run rustfmt on itself. This operation must be idempotent. We also check that
// no warnings are emitted.
#[test]
fn self_tests() {
let files = fs::read_dir("src/bin")
.expect("Couldn't read src dir")
.chain(fs::read_dir("tests").expect("Couldn't read tests dir"))
.map(get_path_string);
// Hack because there's no `IntoIterator` impl for `[T; N]`.
let files = files.chain(Some("src/lib.rs".to_owned()).into_iter());
let (reports, count, fails) = check_files(files);
let mut warnings = 0;
// Display results.
println!("Ran {} self tests.", count);
assert!(fails == 0, "{} self tests failed", fails);
for format_report in reports {
println!("{}", format_report);
warnings += format_report.warning_count();
}
assert!(warnings == 0,
"Rustfmt's code generated {} warnings",
warnings);
}
#[test]
fn stdin_formatting_smoke_test() {
let input = Input::Text("fn main () {}".to_owned());
let config = Config::default();
let (error_summary, file_map, _report) = format_input::<io::Stdout>(input, &config, None)
.unwrap();
assert!(error_summary.has_no_errors());
for &(ref file_name, ref text) in &file_map {
if file_name == "stdin" {
assert!(text.to_string() == "fn main() {}\n");
return;
}
}
panic!("no stdin");
}
#[test]
fn format_lines_errors_are_reported() {
let long_identifier = String::from_utf8(vec![b'a'; 239]).unwrap();
let input = Input::Text(format!("fn {}() {{}}", long_identifier));
let config = Config::default();
let (error_summary, _file_map, _report) = format_input::<io::Stdout>(input, &config, None)
.unwrap();
assert!(error_summary.has_formatting_errors());
}
// For each file, run rustfmt and collect the output.
// Returns the number of files checked and the number of failures.
fn check_files<I>(files: I) -> (Vec<FormatReport>, u32, u32)
where I: Iterator<Item = String>
{
let mut count = 0;
let mut fails = 0;
let mut reports = vec![];
for file_name in files.filter(|f| f.ends_with(".rs")) {
println!("Testing '{}'...", file_name);
match idempotent_check(file_name) {
Ok(report) => reports.push(report),
Err(msg) => {
print_mismatches(msg);
fails += 1;
}
}
count += 1;
}
(reports, count, fails)
}
fn print_mismatches(result: HashMap<String, Vec<Mismatch>>) {
let mut t = term::stdout().unwrap();
for (file_name, diff) in result {
print_diff(diff,
|line_num| format!("\nMismatch at {}:{}:", file_name, line_num));
}
t.reset().unwrap();
}
fn read_config(filename: &str) -> Config {
let sig_comments = read_significant_comments(&filename);
let mut config = get_config(sig_comments.get("config").map(|x| &(*x)[..]));
for (key, val) in &sig_comments {
if key != "target" && key != "config" {
config.override_value(key, val);
}
}
// Don't generate warnings for to-do items.
config.report_todo = ReportTactic::Never;
config
}
fn format_file<P: Into<PathBuf>>(filename: P, config: &Config) -> (FileMap, FormatReport) {
let input = Input::File(filename.into());
let (_error_summary, file_map, report) = format_input::<io::Stdout>(input, &config, None)
.unwrap();
return (file_map, report);
}
pub fn idempotent_check(filename: String) -> Result<FormatReport, HashMap<String, Vec<Mismatch>>> {
let sig_comments = read_significant_comments(&filename);
let config = read_config(&filename);
let (file_map, format_report) = format_file(filename, &config);
let mut write_result = HashMap::new();
for &(ref filename, ref text) in &file_map {
let mut v = Vec::new();
// Won't panic, as we're not doing any IO.
write_system_newlines(&mut v, text, &config).unwrap();
// Won't panic, we are writing correct utf8.
let one_result = String::from_utf8(v).unwrap();
write_result.insert(filename.clone(), one_result);
}
let target = sig_comments.get("target").map(|x| &(*x)[..]);
handle_result(write_result, target).map(|_| format_report)
}
// Reads test config file from comments and reads its contents.
fn get_config(config_file: Option<&str>) -> Config {
let config_file_name = match config_file {
None => return Default::default(),
Some(file_name) => {
let mut full_path = "tests/config/".to_owned();
full_path.push_str(&file_name);
full_path
}
};
let mut def_config_file = fs::File::open(config_file_name).expect("Couldn't open config");
let mut def_config = String::new();
def_config_file.read_to_string(&mut def_config).expect("Couldn't read config");
Config::from_toml(&def_config)
}
// Reads significant comments of the form: // rustfmt-key: value
// into a hash map.
fn read_significant_comments(file_name: &str) -> HashMap<String, String> {
let file = fs::File::open(file_name).expect(&format!("Couldn't read file {}", file_name));
let reader = BufReader::new(file);
let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)";
let regex = regex::Regex::new(&pattern).expect("Failed creating pattern 1");
// Matches lines containing significant comments or whitespace.
let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)")
.expect("Failed creating pattern 2");
reader.lines()
.map(|line| line.expect("Failed getting line"))
.take_while(|line| line_regex.is_match(&line))
.filter_map(|line| {
regex.captures_iter(&line).next().map(|capture| {
(capture.at(1).expect("Couldn't unwrap capture").to_owned(),
capture.at(2).expect("Couldn't unwrap capture").to_owned())
})
})
.collect()
}
// Compare output to input.
// TODO: needs a better name, more explanation.
fn handle_result(result: HashMap<String, String>,
target: Option<&str>)
-> Result<(), HashMap<String, Vec<Mismatch>>> {
let mut failures = HashMap::new();
for (file_name, fmt_text) in result {
// If file is in tests/source, compare to file with same name in tests/target.
let target = get_target(&file_name, target);
let mut f = fs::File::open(&target).expect("Couldn't open target");
let mut text = String::new();
f.read_to_string(&mut text).expect("Failed reading target");
if fmt_text != text {
let diff = make_diff(&text, &fmt_text, DIFF_CONTEXT_SIZE);
assert!(!diff.is_empty(),
"Empty diff? Maybe due to a missing a newline at the end of a file?");
failures.insert(file_name, diff);
}
}
if failures.is_empty() {
Ok(())
} else {
Err(failures)
}
}
// Map source file paths to their target paths.
fn get_target(file_name: &str, target: Option<&str>) -> String {
if file_name.contains("source") {
let target_file_name = file_name.replace("source", "target");
if let Some(replace_name) = target {
Path::new(&target_file_name)
.with_file_name(replace_name)
.into_os_string()
.into_string()
.unwrap()
} else {
target_file_name
}
} else {
// This is either and idempotence check or a self check
file_name.to_owned()
}
}
#[test]
fn rustfmt_diff_make_diff_tests() {
let diff = make_diff("a\nb\nc\nd", "a\ne\nc\nd", 3);
assert_eq!(diff,
vec![Mismatch {
line_number: 1,
lines: vec![DiffLine::Context("a".into()),
DiffLine::Resulting("b".into()),
DiffLine::Expected("e".into()),
DiffLine::Context("c".into()),
DiffLine::Context("d".into())],
}]);
}