From 98c9c6e9ef758e3ec27131d7d35114135453fbef Mon Sep 17 00:00:00 2001 From: Marcus Klaas Date: Thu, 28 May 2015 19:23:07 +0200 Subject: [PATCH] implement framework for system tests --- Cargo.lock | 14 ++ Cargo.toml | 1 + tests/config/small_tabs.toml | 11 + tests/idem.rs | 134 ----------- tests/idem/hello.rs | 5 - tests/source/hello.rs | 6 + tests/source/hello2.rs | 8 + tests/system.rs | 214 ++++++++++++++++++ tests/{idem => target}/attrib-extern-crate.rs | 0 tests/{idem => target}/attrib.rs | 0 tests/{idem => target}/comments-fn.rs | 0 tests/{idem => target}/fn.rs | 0 tests/target/hello.rs | 8 + tests/{idem => target}/imports.rs | 0 tests/{idem => target}/long-fn-1.rs | 0 tests/{idem => target}/mod-1.rs | 0 tests/{idem => target}/paren.rs | 0 tests/{idem => target}/skip.rs | 0 tests/{idem => target}/struct_lits.rs | 0 tests/{idem => target}/structs.rs | 0 tests/{idem => target}/trait.rs | 0 21 files changed, 262 insertions(+), 139 deletions(-) create mode 100644 tests/config/small_tabs.toml delete mode 100644 tests/idem.rs delete mode 100644 tests/idem/hello.rs create mode 100644 tests/source/hello.rs create mode 100644 tests/source/hello2.rs create mode 100644 tests/system.rs rename tests/{idem => target}/attrib-extern-crate.rs (100%) rename tests/{idem => target}/attrib.rs (100%) rename tests/{idem => target}/comments-fn.rs (100%) rename tests/{idem => target}/fn.rs (100%) create mode 100644 tests/target/hello.rs rename tests/{idem => target}/imports.rs (100%) rename tests/{idem => target}/long-fn-1.rs (100%) rename tests/{idem => target}/mod-1.rs (100%) rename tests/{idem => target}/paren.rs (100%) rename tests/{idem => target}/skip.rs (100%) rename tests/{idem => target}/struct_lits.rs (100%) rename tests/{idem => target}/structs.rs (100%) rename tests/{idem => target}/trait.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 297e4f65fe1..975888e0267 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,6 +3,7 @@ name = "rustfmt" version = "0.0.1" dependencies = [ "diff 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "strings 0.0.1 (git+https://github.com/nrc/strings.rs.git)", "toml 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", @@ -13,6 +14,19 @@ name = "diff" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "regex" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "regex-syntax 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rustc-serialize" version = "0.3.14" diff --git a/Cargo.toml b/Cargo.toml index f76d45d39c7..57c47fec54c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ rustc-serialize = "0.3.14" [dev-dependencies] diff = "0.1.0" +regex = "0.1" diff --git a/tests/config/small_tabs.toml b/tests/config/small_tabs.toml new file mode 100644 index 00000000000..316ee8ae288 --- /dev/null +++ b/tests/config/small_tabs.toml @@ -0,0 +1,11 @@ +max_width = 100 +ideal_width = 80 +leeway = 5 +tab_spaces = 2 +newline_style = "Unix" +fn_brace_style = "SameLineWhere" +fn_return_indent = "WithArgs" +fn_args_paren_newline = true +struct_trailing_comma = true +struct_lit_trailing_comma = "Vertical" +enum_trailing_comma = true diff --git a/tests/idem.rs b/tests/idem.rs deleted file mode 100644 index 50329b258d1..00000000000 --- a/tests/idem.rs +++ /dev/null @@ -1,134 +0,0 @@ -// 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 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -#![feature(catch_panic)] - -extern crate rustfmt; -extern crate diff; - -use std::collections::HashMap; -use std::fs; -use std::io::Read; -use std::thread; -use rustfmt::*; - -// For now, the only supported regression tests are idempotent tests - 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 idempotent_tests() { - println!("Idempotent tests:"); - - // Get all files in the tests/idem directory - let files = fs::read_dir("tests/idem").unwrap(); - let files = files.chain(fs::read_dir("tests").unwrap()); - let files = files.chain(fs::read_dir("src/bin").unwrap()); - // turn a DirEntry into a String that represents the relative path to the file - let files = files.map(|e| e.unwrap().path().to_str().unwrap().to_owned()); - // hack because there's no `IntoIterator` impl for `[T; N]` - let files = files.chain(Some("src/lib.rs".to_owned()).into_iter()); - - // For each file, run rustfmt and collect the output - let mut count = 0; - let mut fails = 0; - for file_name in files.filter(|f| f.ends_with(".rs")) { - println!("Testing '{}'...", file_name); - match idempotent_check(file_name) { - Ok(()) => {}, - Err(m) => { - print_mismatches(m); - fails += 1; - }, - } - count += 1; - } - - // Display results - println!("Ran {} idempotent tests; {} failures.", count, fails); - assert!(fails == 0, "{} idempotent tests failed", fails); -} - -// Compare output to input. -fn print_mismatches(result: HashMap) { - for (_, fmt_text) in result { - println!("{}", fmt_text); - } -} - -// Ick, just needed to get a &'static to handle_result. -static HANDLE_RESULT: &'static Fn(HashMap) = &handle_result; - -pub fn idempotent_check(filename: String) -> Result<(), HashMap> { - let args = vec!["rustfmt".to_owned(), filename]; - let mut def_config_file = fs::File::open("default.toml").unwrap(); - let mut def_config = String::new(); - def_config_file.read_to_string(&mut def_config).unwrap(); - // this thread is not used for concurrency, but rather to workaround the issue that the passed - // function handle needs to have static lifetime. Instead of using a global RefCell, we use - // panic to return a result in case of failure. This has the advantage of smoothing the road to - // multithreaded rustfmt - thread::catch_panic(move || { - run(args, WriteMode::Return(HANDLE_RESULT), &def_config); - }).map_err(|any| - *any.downcast().unwrap() - ) -} - -// Compare output to input. -fn handle_result(result: HashMap) { - let mut failures = HashMap::new(); - - for (file_name, fmt_text) in result { - let mut f = fs::File::open(&file_name).unwrap(); - let mut text = String::new(); - // TODO: speedup by running through bytes iterator - f.read_to_string(&mut text).unwrap(); - if fmt_text != text { - let diff_str = make_diff(&file_name, &fmt_text, &text); - failures.insert(file_name, diff_str); - } - } - if !failures.is_empty() { - panic!(failures); - } -} - - -fn make_diff(file_name: &str, expected: &str, actual: &str) -> String { - let mut line_number = 1; - let mut prev_both = true; - let mut text = String::new(); - - for result in diff::lines(expected, actual) { - match result { - diff::Result::Left(str) => { - if prev_both { - text.push_str(&format!("Mismatch @ {}:{}\n", file_name, line_number)); - } - text.push_str(&format!("-{}⏎\n", str)); - prev_both = false; - } - diff::Result::Right(str) => { - if prev_both { - text.push_str(&format!("Mismatch @ {}:{}\n", file_name, line_number)); - } - text.push_str(&format!("+{}⏎\n", str)); - prev_both = false; - line_number += 1; - } - diff::Result::Both(..) => { - line_number += 1; - prev_both = true; - } - } - } - - text -} diff --git a/tests/idem/hello.rs b/tests/idem/hello.rs deleted file mode 100644 index 593d943cbdb..00000000000 --- a/tests/idem/hello.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Smoke test - hello world. - -fn main() { - println!("Hello world!"); -} diff --git a/tests/source/hello.rs b/tests/source/hello.rs new file mode 100644 index 00000000000..f892e6debb1 --- /dev/null +++ b/tests/source/hello.rs @@ -0,0 +1,6 @@ +// rustfmt-config: small_tabs.toml +// rustfmt-target: hello.rs + +// Smoke test - hello world. + +fn main() { println!("Hello world!"); } diff --git a/tests/source/hello2.rs b/tests/source/hello2.rs new file mode 100644 index 00000000000..48af7de3887 --- /dev/null +++ b/tests/source/hello2.rs @@ -0,0 +1,8 @@ +// rustfmt-config: small_tabs.toml +// rustfmt-target: hello.rs + +// Smoke test - hello world. + +fn main( ) { +println!("Hello world!"); +} diff --git a/tests/system.rs b/tests/system.rs new file mode 100644 index 00000000000..1cdd43abbfa --- /dev/null +++ b/tests/system.rs @@ -0,0 +1,214 @@ +// 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 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![feature(catch_panic)] + +extern crate rustfmt; +extern crate diff; +extern crate regex; + +use std::collections::HashMap; +use std::fs; +use std::io::{self, Read, BufRead, BufReader}; +use std::thread; +use rustfmt::*; + +fn get_path_string(dir_entry: io::Result) -> String { + let path = dir_entry.ok().expect("Couldn't get DirEntry.").path(); + + path.to_str().expect("Couldn't stringify path.").to_owned() +} + +// For now, the only supported regression tests are idempotent tests - 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/target directory + let files = fs::read_dir("tests/target").ok().expect("Couldn't read dir 1."); + let files = files.chain(fs::read_dir("tests").ok().expect("Couldn't read dir 2.")); + let files = files.chain(fs::read_dir("src/bin").ok().expect("Couldn't read dir 3.")); + // turn a DirEntry into a String that represents the relative path to the file + let files = files.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 (count, fails) = check_files(files); + + // Display results + println!("Ran {} idempotent tests.", count); + assert!(fails == 0, "{} idempotent tests failed", fails); + + // Get all files in the tests/source directory + let files = fs::read_dir("tests/source").ok().expect("Couldn't read dir 4."); + // turn a DirEntry into a String that represents the relative path to the file + let files = files.map(get_path_string); + + let (count, fails) = check_files(files); + + // Display results + println!("Ran {} system tests.", count); + assert!(fails == 0, "{} system tests failed", fails); +} + +// For each file, run rustfmt and collect the output. +// Returns the number of files checked and the number of failures. +fn check_files(files: I) -> (u32, u32) + where I: Iterator +{ + let mut count = 0; + let mut fails = 0; + + for file_name in files.filter(|f| f.ends_with(".rs")) { + println!("Testing '{}'...", file_name); + match idempotent_check(file_name) { + Ok(()) => {}, + Err(m) => { + print_mismatches(m); + fails += 1; + }, + } + count += 1; + } + + (count, fails) +} + +fn print_mismatches(result: HashMap) { + for (_, fmt_text) in result { + println!("{}", fmt_text); + } +} + +// Ick, just needed to get a &'static to handle_result. +static HANDLE_RESULT: &'static Fn(HashMap) = &handle_result; + +pub fn idempotent_check(filename: String) -> Result<(), HashMap> { + let config = get_config(&filename); + let args = vec!["rustfmt".to_owned(), filename]; + // this thread is not used for concurrency, but rather to workaround the issue that the passed + // function handle needs to have static lifetime. Instead of using a global RefCell, we use + // panic to return a result in case of failure. This has the advantage of smoothing the road to + // multithreaded rustfmt + thread::catch_panic(move || { + run(args, WriteMode::Return(HANDLE_RESULT), &config); + }).map_err(|any| + *any.downcast().ok().expect("Downcast failed.") + ) +} + +// Reads test config file from comments and loads it +fn get_config(file_name: &str) -> String { + let config_file_name = read_significant_comment(file_name, "config") + .map(|file_name| { + let mut full_path = "tests/config/".to_owned(); + full_path.push_str(&file_name); + full_path + }) + .unwrap_or("default.toml".to_owned()); + + let mut def_config_file = fs::File::open(config_file_name).ok().expect("Couldn't open config."); + let mut def_config = String::new(); + def_config_file.read_to_string(&mut def_config).ok().expect("Couldn't read config."); + + def_config +} + +fn read_significant_comment(file_name: &str, option: &str) -> Option { + let file = fs::File::open(file_name).ok().expect("Couldn't read file for comment."); + let reader = BufReader::new(file); + let pattern = format!("^\\s*//\\s*rustfmt-{}:\\s*(\\S+)", option); + let regex = regex::Regex::new(&pattern).ok().expect("Failed creating pattern 1."); + + // matches exactly the lines containing significant comments or whitespace + let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[:alpha:]+:\s*\S+)") + .ok().expect("Failed creating pattern 2."); + + reader.lines() + .map(|line| line.ok().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() + }) + }) + .next() +} + +// Compare output to input. +fn handle_result(result: HashMap) { + 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_file_name = get_target(&file_name); + let mut f = fs::File::open(&target_file_name).ok().expect("Couldn't open target."); + + let mut text = String::new(); + // TODO: speedup by running through bytes iterator + f.read_to_string(&mut text).ok().expect("Failed reading target."); + if fmt_text != text { + let diff_str = make_diff(&file_name, &fmt_text, &text); + failures.insert(file_name, diff_str); + } + } + if !failures.is_empty() { + panic!(failures); + } +} + +// Map source file paths to their target paths. +fn get_target(file_name: &str) -> String { + if file_name.starts_with("tests/source/") { + let target = read_significant_comment(file_name, "target"); + let base = target.unwrap_or(file_name.trim_left_matches("tests/source/").to_owned()); + + let mut target_file = "tests/target/".to_owned(); + target_file.push_str(&base); + + target_file + } else { + file_name.to_owned() + } +} + +// Produces a diff string between the expected output and actual output of +// rustfmt on a given file +fn make_diff(file_name: &str, expected: &str, actual: &str) -> String { + let mut line_number = 1; + let mut prev_both = true; + let mut text = String::new(); + + for result in diff::lines(expected, actual) { + match result { + diff::Result::Left(str) => { + if prev_both { + text.push_str(&format!("Mismatch @ {}:{}\n", file_name, line_number)); + } + text.push_str(&format!("-{}⏎\n", str)); + prev_both = false; + } + diff::Result::Right(str) => { + if prev_both { + text.push_str(&format!("Mismatch @ {}:{}\n", file_name, line_number)); + } + text.push_str(&format!("+{}⏎\n", str)); + prev_both = false; + line_number += 1; + } + diff::Result::Both(..) => { + line_number += 1; + prev_both = true; + } + } + } + + text +} diff --git a/tests/idem/attrib-extern-crate.rs b/tests/target/attrib-extern-crate.rs similarity index 100% rename from tests/idem/attrib-extern-crate.rs rename to tests/target/attrib-extern-crate.rs diff --git a/tests/idem/attrib.rs b/tests/target/attrib.rs similarity index 100% rename from tests/idem/attrib.rs rename to tests/target/attrib.rs diff --git a/tests/idem/comments-fn.rs b/tests/target/comments-fn.rs similarity index 100% rename from tests/idem/comments-fn.rs rename to tests/target/comments-fn.rs diff --git a/tests/idem/fn.rs b/tests/target/fn.rs similarity index 100% rename from tests/idem/fn.rs rename to tests/target/fn.rs diff --git a/tests/target/hello.rs b/tests/target/hello.rs new file mode 100644 index 00000000000..d9f90b0b5e8 --- /dev/null +++ b/tests/target/hello.rs @@ -0,0 +1,8 @@ +// rustfmt-config: small_tabs.toml +// rustfmt-target: hello.rs + +// Smoke test - hello world. + +fn main() { + println!("Hello world!"); +} diff --git a/tests/idem/imports.rs b/tests/target/imports.rs similarity index 100% rename from tests/idem/imports.rs rename to tests/target/imports.rs diff --git a/tests/idem/long-fn-1.rs b/tests/target/long-fn-1.rs similarity index 100% rename from tests/idem/long-fn-1.rs rename to tests/target/long-fn-1.rs diff --git a/tests/idem/mod-1.rs b/tests/target/mod-1.rs similarity index 100% rename from tests/idem/mod-1.rs rename to tests/target/mod-1.rs diff --git a/tests/idem/paren.rs b/tests/target/paren.rs similarity index 100% rename from tests/idem/paren.rs rename to tests/target/paren.rs diff --git a/tests/idem/skip.rs b/tests/target/skip.rs similarity index 100% rename from tests/idem/skip.rs rename to tests/target/skip.rs diff --git a/tests/idem/struct_lits.rs b/tests/target/struct_lits.rs similarity index 100% rename from tests/idem/struct_lits.rs rename to tests/target/struct_lits.rs diff --git a/tests/idem/structs.rs b/tests/target/structs.rs similarity index 100% rename from tests/idem/structs.rs rename to tests/target/structs.rs diff --git a/tests/idem/trait.rs b/tests/target/trait.rs similarity index 100% rename from tests/idem/trait.rs rename to tests/target/trait.rs