Refactoring needed in order to have test json output.

This commit is contained in:
Gilad Naaman
2017-11-08 06:06:16 +02:00
parent a97cd17f5d
commit d24f9af31c
2 changed files with 308 additions and 232 deletions

246
src/libtest/formatters.rs Normal file
View File

@@ -0,0 +1,246 @@
// Copyright 2012-2014 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.
use super::*;
pub(crate) trait OutputFormatter {
fn write_run_start(&mut self, len: usize) -> io::Result<()>;
fn write_test_start(&mut self,
test: &TestDesc,
align: NamePadding,
max_name_len: usize) -> io::Result<()>;
fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()>;
fn write_result(&mut self, result: &TestResult) -> io::Result<()>;
fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool>;
}
pub(crate) struct HumanFormatter<T> {
out: OutputLocation<T>,
terse: bool,
use_color: bool,
test_count: usize,
}
impl<T: Write> HumanFormatter<T> {
pub fn new(out: OutputLocation<T>, use_color: bool, terse: bool) -> Self {
HumanFormatter {
out,
terse,
use_color,
test_count: 0,
}
}
#[cfg(test)]
pub fn output_location(&self) -> &OutputLocation<T> {
&self.out
}
pub fn write_ok(&mut self) -> io::Result<()> {
self.write_short_result("ok", ".", term::color::GREEN)
}
pub fn write_failed(&mut self) -> io::Result<()> {
self.write_short_result("FAILED", "F", term::color::RED)
}
pub fn write_ignored(&mut self) -> io::Result<()> {
self.write_short_result("ignored", "i", term::color::YELLOW)
}
pub fn write_allowed_fail(&mut self) -> io::Result<()> {
self.write_short_result("FAILED (allowed)", "a", term::color::YELLOW)
}
pub fn write_bench(&mut self) -> io::Result<()> {
self.write_pretty("bench", term::color::CYAN)
}
pub fn write_short_result(&mut self, verbose: &str, quiet: &str, color: term::color::Color)
-> io::Result<()> {
if self.terse {
self.write_pretty(quiet, color)?;
if self.test_count % QUIET_MODE_MAX_COLUMN == QUIET_MODE_MAX_COLUMN - 1 {
// we insert a new line every 100 dots in order to flush the
// screen when dealing with line-buffered output (e.g. piping to
// `stamp` in the rust CI).
self.write_plain("\n")?;
}
self.test_count += 1;
Ok(())
} else {
self.write_pretty(verbose, color)?;
self.write_plain("\n")
}
}
pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> {
match self.out {
Pretty(ref mut term) => {
if self.use_color {
term.fg(color)?;
}
term.write_all(word.as_bytes())?;
if self.use_color {
term.reset()?;
}
term.flush()
}
Raw(ref mut stdout) => {
stdout.write_all(word.as_bytes())?;
stdout.flush()
}
}
}
pub fn write_plain<S: AsRef<str>>(&mut self, s: S) -> io::Result<()> {
let s = s.as_ref();
self.out.write_all(s.as_bytes())?;
self.out.flush()
}
pub fn write_outputs(&mut self, state: &ConsoleTestState) -> io::Result<()> {
self.write_plain("\nsuccesses:\n")?;
let mut successes = Vec::new();
let mut stdouts = String::new();
for &(ref f, ref stdout) in &state.not_failures {
successes.push(f.name.to_string());
if !stdout.is_empty() {
stdouts.push_str(&format!("---- {} stdout ----\n\t", f.name));
let output = String::from_utf8_lossy(stdout);
stdouts.push_str(&output);
stdouts.push_str("\n");
}
}
if !stdouts.is_empty() {
self.write_plain("\n")?;
self.write_plain(&stdouts)?;
}
self.write_plain("\nsuccesses:\n")?;
successes.sort();
for name in &successes {
self.write_plain(&format!(" {}\n", name))?;
}
Ok(())
}
pub fn write_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> {
self.write_plain("\nfailures:\n")?;
let mut failures = Vec::new();
let mut fail_out = String::new();
for &(ref f, ref stdout) in &state.failures {
failures.push(f.name.to_string());
if !stdout.is_empty() {
fail_out.push_str(&format!("---- {} stdout ----\n\t", f.name));
let output = String::from_utf8_lossy(stdout);
fail_out.push_str(&output);
fail_out.push_str("\n");
}
}
if !fail_out.is_empty() {
self.write_plain("\n")?;
self.write_plain(&fail_out)?;
}
self.write_plain("\nfailures:\n")?;
failures.sort();
for name in &failures {
self.write_plain(&format!(" {}\n", name))?;
}
Ok(())
}
}
impl<T: Write> OutputFormatter for HumanFormatter<T> {
fn write_run_start(&mut self, len: usize) -> io::Result<()> {
let noun = if len != 1 {
"tests"
} else {
"test"
};
self.write_plain(&format!("\nrunning {} {}\n", len, noun))
}
fn write_test_start(&mut self,
test: &TestDesc,
align: NamePadding,
max_name_len: usize) -> io::Result<()> {
if self.terse && align != PadOnRight {
Ok(())
}
else {
let name = test.padded_name(max_name_len, align);
self.write_plain(&format!("test {} ... ", name))
}
}
fn write_result(&mut self, result: &TestResult) -> io::Result<()> {
match *result {
TrOk => self.write_ok(),
TrFailed | TrFailedMsg(_) => self.write_failed(),
TrIgnored => self.write_ignored(),
TrAllowedFail => self.write_allowed_fail(),
TrBench(ref bs) => {
self.write_bench()?;
self.write_plain(&format!(": {}\n", fmt_bench_samples(bs)))
}
}
}
fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> {
self.write_plain(&format!("test {} has been running for over {} seconds\n",
desc.name,
TEST_WARN_TIMEOUT_S))
}
fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
if state.options.display_output {
self.write_outputs(state)?;
}
let success = state.failed == 0;
if !success {
self.write_failures(state)?;
}
self.write_plain("\ntest result: ")?;
if success {
// There's no parallelism at this point so it's safe to use color
self.write_pretty("ok", term::color::GREEN)?;
} else {
self.write_pretty("FAILED", term::color::RED)?;
}
let s = if state.allowed_fail > 0 {
format!(
". {} passed; {} failed ({} allowed); {} ignored; {} measured; {} filtered out\n\n",
state.passed,
state.failed + state.allowed_fail,
state.allowed_fail,
state.ignored,
state.measured,
state.filtered_out)
} else {
format!(
". {} passed; {} failed; {} ignored; {} measured; {} filtered out\n\n",
state.passed,
state.failed,
state.ignored,
state.measured,
state.filtered_out)
};
self.write_plain(&s)?;
Ok(success)
}
}

View File

@@ -84,6 +84,9 @@ pub mod test {
} }
pub mod stats; pub mod stats;
mod formatters;
use formatters::*;
// The name of a test. By convention this follows the rules for rust // The name of a test. By convention this follows the rules for rust
// paths; i.e. it should be a series of identifiers separated by double // paths; i.e. it should be a series of identifiers separated by double
@@ -359,7 +362,8 @@ fn optgroups() -> getopts::Options {
in parallel", "n_threads") in parallel", "n_threads")
.optmulti("", "skip", "Skip tests whose names contain FILTER (this flag can \ .optmulti("", "skip", "Skip tests whose names contain FILTER (this flag can \
be used multiple times)","FILTER") be used multiple times)","FILTER")
.optflag("q", "quiet", "Display one character per test instead of one line") .optflag("q", "quiet", "Display one character per test instead of one line.\
Equivalent to --format=terse")
.optflag("", "exact", "Exactly match filters rather than by substring") .optflag("", "exact", "Exactly match filters rather than by substring")
.optopt("", "color", "Configure coloring of output: .optopt("", "color", "Configure coloring of output:
auto = colorize if stdout is a tty and tests are run on serially (default); auto = colorize if stdout is a tty and tests are run on serially (default);
@@ -507,11 +511,24 @@ enum OutputLocation<T> {
Raw(T), Raw(T),
} }
struct ConsoleTestState<T> { impl<T: Write> Write for OutputLocation<T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
Pretty(ref mut term) => term.write(buf),
Raw(ref mut stdout) => stdout.write(buf)
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
Pretty(ref mut term) => term.flush(),
Raw(ref mut stdout) => stdout.flush()
}
}
}
struct ConsoleTestState {
log_out: Option<File>, log_out: Option<File>,
out: OutputLocation<T>,
use_color: bool,
quiet: bool,
total: usize, total: usize,
passed: usize, passed: usize,
failed: usize, failed: usize,
@@ -526,22 +543,15 @@ struct ConsoleTestState<T> {
options: Options, options: Options,
} }
impl<T: Write> ConsoleTestState<T> { impl ConsoleTestState {
pub fn new(opts: &TestOpts, _: Option<T>) -> io::Result<ConsoleTestState<io::Stdout>> { pub fn new(opts: &TestOpts) -> io::Result<ConsoleTestState> {
let log_out = match opts.logfile { let log_out = match opts.logfile {
Some(ref path) => Some(File::create(path)?), Some(ref path) => Some(File::create(path)?),
None => None, None => None,
}; };
let out = match term::stdout() {
None => Raw(io::stdout()),
Some(t) => Pretty(t),
};
Ok(ConsoleTestState { Ok(ConsoleTestState {
out,
log_out, log_out,
use_color: use_color(opts),
quiet: opts.quiet,
total: 0, total: 0,
passed: 0, passed: 0,
failed: 0, failed: 0,
@@ -557,114 +567,6 @@ impl<T: Write> ConsoleTestState<T> {
}) })
} }
pub fn write_ok(&mut self) -> io::Result<()> {
self.write_short_result("ok", ".", term::color::GREEN)
}
pub fn write_failed(&mut self) -> io::Result<()> {
self.write_short_result("FAILED", "F", term::color::RED)
}
pub fn write_ignored(&mut self) -> io::Result<()> {
self.write_short_result("ignored", "i", term::color::YELLOW)
}
pub fn write_allowed_fail(&mut self) -> io::Result<()> {
self.write_short_result("FAILED (allowed)", "a", term::color::YELLOW)
}
pub fn write_bench(&mut self) -> io::Result<()> {
self.write_pretty("bench", term::color::CYAN)
}
pub fn write_short_result(&mut self, verbose: &str, quiet: &str, color: term::color::Color)
-> io::Result<()> {
if self.quiet {
self.write_pretty(quiet, color)?;
if self.current_test_count() % QUIET_MODE_MAX_COLUMN == QUIET_MODE_MAX_COLUMN - 1 {
// we insert a new line every 100 dots in order to flush the
// screen when dealing with line-buffered output (e.g. piping to
// `stamp` in the rust CI).
self.write_plain("\n")?;
}
Ok(())
} else {
self.write_pretty(verbose, color)?;
self.write_plain("\n")
}
}
pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> {
match self.out {
Pretty(ref mut term) => {
if self.use_color {
term.fg(color)?;
}
term.write_all(word.as_bytes())?;
if self.use_color {
term.reset()?;
}
term.flush()
}
Raw(ref mut stdout) => {
stdout.write_all(word.as_bytes())?;
stdout.flush()
}
}
}
pub fn write_plain<S: AsRef<str>>(&mut self, s: S) -> io::Result<()> {
let s = s.as_ref();
match self.out {
Pretty(ref mut term) => {
term.write_all(s.as_bytes())?;
term.flush()
}
Raw(ref mut stdout) => {
stdout.write_all(s.as_bytes())?;
stdout.flush()
}
}
}
pub fn write_run_start(&mut self, len: usize) -> io::Result<()> {
self.total = len;
let noun = if len != 1 {
"tests"
} else {
"test"
};
self.write_plain(&format!("\nrunning {} {}\n", len, noun))
}
pub fn write_test_start(&mut self, test: &TestDesc, align: NamePadding) -> io::Result<()> {
if self.quiet && align != PadOnRight {
Ok(())
} else {
let name = test.padded_name(self.max_name_len, align);
self.write_plain(&format!("test {} ... ", name))
}
}
pub fn write_result(&mut self, result: &TestResult) -> io::Result<()> {
match *result {
TrOk => self.write_ok(),
TrFailed | TrFailedMsg(_) => self.write_failed(),
TrIgnored => self.write_ignored(),
TrAllowedFail => self.write_allowed_fail(),
TrBench(ref bs) => {
self.write_bench()?;
self.write_plain(&format!(": {}\n", fmt_bench_samples(bs)))
}
}
}
pub fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> {
self.write_plain(&format!("test {} has been running for over {} seconds\n",
desc.name,
TEST_WARN_TIMEOUT_S))
}
pub fn write_log<S: AsRef<str>>(&mut self, msg: S) -> io::Result<()> { pub fn write_log<S: AsRef<str>>(&mut self, msg: S) -> io::Result<()> {
let msg = msg.as_ref(); let msg = msg.as_ref();
match self.log_out { match self.log_out {
@@ -687,101 +589,9 @@ impl<T: Write> ConsoleTestState<T> {
test.name)) test.name))
} }
pub fn write_failures(&mut self) -> io::Result<()> {
self.write_plain("\nfailures:\n")?;
let mut failures = Vec::new();
let mut fail_out = String::new();
for &(ref f, ref stdout) in &self.failures {
failures.push(f.name.to_string());
if !stdout.is_empty() {
fail_out.push_str(&format!("---- {} stdout ----\n\t", f.name));
let output = String::from_utf8_lossy(stdout);
fail_out.push_str(&output);
fail_out.push_str("\n");
}
}
if !fail_out.is_empty() {
self.write_plain("\n")?;
self.write_plain(&fail_out)?;
}
self.write_plain("\nfailures:\n")?;
failures.sort();
for name in &failures {
self.write_plain(&format!(" {}\n", name))?;
}
Ok(())
}
pub fn write_outputs(&mut self) -> io::Result<()> {
self.write_plain("\nsuccesses:\n")?;
let mut successes = Vec::new();
let mut stdouts = String::new();
for &(ref f, ref stdout) in &self.not_failures {
successes.push(f.name.to_string());
if !stdout.is_empty() {
stdouts.push_str(&format!("---- {} stdout ----\n\t", f.name));
let output = String::from_utf8_lossy(stdout);
stdouts.push_str(&output);
stdouts.push_str("\n");
}
}
if !stdouts.is_empty() {
self.write_plain("\n")?;
self.write_plain(&stdouts)?;
}
self.write_plain("\nsuccesses:\n")?;
successes.sort();
for name in &successes {
self.write_plain(&format!(" {}\n", name))?;
}
Ok(())
}
fn current_test_count(&self) -> usize { fn current_test_count(&self) -> usize {
self.passed + self.failed + self.ignored + self.measured + self.allowed_fail self.passed + self.failed + self.ignored + self.measured + self.allowed_fail
} }
pub fn write_run_finish(&mut self) -> io::Result<bool> {
assert!(self.current_test_count() == self.total);
if self.options.display_output {
self.write_outputs()?;
}
let success = self.failed == 0;
if !success {
self.write_failures()?;
}
self.write_plain("\ntest result: ")?;
if success {
// There's no parallelism at this point so it's safe to use color
self.write_pretty("ok", term::color::GREEN)?;
} else {
self.write_pretty("FAILED", term::color::RED)?;
}
let s = if self.allowed_fail > 0 {
format!(
". {} passed; {} failed ({} allowed); {} ignored; {} measured; {} filtered out\n\n",
self.passed,
self.failed + self.allowed_fail,
self.allowed_fail,
self.ignored,
self.measured,
self.filtered_out)
} else {
format!(
". {} passed; {} failed; {} ignored; {} measured; {} filtered out\n\n",
self.passed,
self.failed,
self.ignored,
self.measured,
self.filtered_out)
};
self.write_plain(&s)?;
return Ok(success);
}
} }
// Format a number with thousands separators // Format a number with thousands separators
@@ -827,7 +637,12 @@ pub fn fmt_bench_samples(bs: &BenchSamples) -> String {
// List the tests to console, and optionally to logfile. Filters are honored. // List the tests to console, and optionally to logfile. Filters are honored.
pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<()> { pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<()> {
let mut st = ConsoleTestState::new(opts, None::<io::Stdout>)?; let output = match term::stdout() {
None => Raw(io::stdout()),
Some(t) => Pretty(t),
};
let mut out = HumanFormatter::new(output, use_color(opts), opts.quiet);
let mut st = ConsoleTestState::new(opts)?;
let mut ntest = 0; let mut ntest = 0;
let mut nbench = 0; let mut nbench = 0;
@@ -842,7 +657,7 @@ pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Res
StaticBenchFn(..) | DynBenchFn(..) => { nbench += 1; "benchmark" }, StaticBenchFn(..) | DynBenchFn(..) => { nbench += 1; "benchmark" },
}; };
st.write_plain(format!("{}: {}\n", name, fntype))?; out.write_plain(format!("{}: {}\n", name, fntype))?;
st.write_log(format!("{} {}\n", fntype, name))?; st.write_log(format!("{} {}\n", fntype, name))?;
} }
@@ -868,15 +683,21 @@ pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Res
// A simple console test runner // A simple console test runner
pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<bool> { pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<bool> {
fn callback<T: Write>(event: &TestEvent, st: &mut ConsoleTestState<T>) -> io::Result<()> { fn callback(event: &TestEvent,
st: &mut ConsoleTestState,
out: &mut OutputFormatter) -> io::Result<()> {
match (*event).clone() { match (*event).clone() {
TeFiltered(ref filtered_tests) => st.write_run_start(filtered_tests.len()), TeFiltered(ref filtered_tests) => {
st.total = filtered_tests.len();
out.write_run_start(filtered_tests.len())
},
TeFilteredOut(filtered_out) => Ok(st.filtered_out = filtered_out), TeFilteredOut(filtered_out) => Ok(st.filtered_out = filtered_out),
TeWait(ref test, padding) => st.write_test_start(test, padding), TeWait(ref test, padding) => out.write_test_start(test, padding, st.max_name_len),
TeTimeout(ref test) => st.write_timeout(test), TeTimeout(ref test) => out.write_timeout(test),
TeResult(test, result, stdout) => { TeResult(test, result, stdout) => {
st.write_log_result(&test, &result)?; st.write_log_result(&test, &result)?;
st.write_result(&result)?; out.write_result(&result)?;
match result { match result {
TrOk => { TrOk => {
st.passed += 1; st.passed += 1;
@@ -908,7 +729,14 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Resu
} }
} }
let mut st = ConsoleTestState::new(opts, None::<io::Stdout>)?; let output = match term::stdout() {
None => Raw(io::stdout()),
Some(t) => Pretty(t),
};
let mut out = HumanFormatter::new(output, use_color(opts), opts.quiet);
let mut st = ConsoleTestState::new(opts)?;
fn len_if_padded(t: &TestDescAndFn) -> usize { fn len_if_padded(t: &TestDescAndFn) -> usize {
match t.testfn.padding() { match t.testfn.padding() {
PadNone => 0, PadNone => 0,
@@ -919,8 +747,11 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Resu
let n = t.desc.name.as_slice(); let n = t.desc.name.as_slice();
st.max_name_len = n.len(); st.max_name_len = n.len();
} }
run_tests(opts, tests, |x| callback(&x, &mut st))?; run_tests(opts, tests, |x| callback(&x, &mut st, &mut out))?;
return st.write_run_finish();
assert!(st.current_test_count() == st.total);
return out.write_run_finish(&st);
} }
#[test] #[test]
@@ -939,11 +770,10 @@ fn should_sort_failures_before_printing_them() {
allow_fail: false, allow_fail: false,
}; };
let mut st = ConsoleTestState { let mut out = HumanFormatter::new(Raw(Vec::new()), false, false);
let st = ConsoleTestState {
log_out: None, log_out: None,
out: Raw(Vec::new()),
use_color: false,
quiet: false,
total: 0, total: 0,
passed: 0, passed: 0,
failed: 0, failed: 0,
@@ -958,10 +788,10 @@ fn should_sort_failures_before_printing_them() {
not_failures: Vec::new(), not_failures: Vec::new(),
}; };
st.write_failures().unwrap(); out.write_failures(&st).unwrap();
let s = match st.out { let s = match out.output_location() {
Raw(ref m) => String::from_utf8_lossy(&m[..]), &Raw(ref m) => String::from_utf8_lossy(&m[..]),
Pretty(_) => unreachable!(), &Pretty(_) => unreachable!(),
}; };
let apos = s.find("a").unwrap(); let apos = s.find("a").unwrap();