diff options
Diffstat (limited to 'src/libtest/lib.rs')
| -rw-r--r-- | src/libtest/lib.rs | 1577 |
1 files changed, 1577 insertions, 0 deletions
diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs new file mode 100644 index 00000000000..226dd75d740 --- /dev/null +++ b/src/libtest/lib.rs @@ -0,0 +1,1577 @@ +// 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. + +// Support code for rustc's built in test runner generator. Currently, +// none of this is meant for users. It is intended to support the +// simplest interface possible for representing and running tests +// while providing a base that other test frameworks may build off of. + +#[crate_id = "test#0.10-pre"]; +#[comment = "Rust internal test library only used by rustc"]; +#[license = "MIT/ASL2"]; +#[crate_type = "rlib"]; +#[crate_type = "dylib"]; + +#[feature(asm)]; + +extern crate collections; +extern crate extra; +extern crate getopts; +extern crate serialize; +extern crate term; + +use collections::TreeMap; +use extra::json::ToJson; +use extra::json; +use extra::stats::Stats; +use extra::stats; +use extra::time::precise_time_ns; +use getopts::{OptGroup, optflag, optopt}; +use serialize::Decodable; +use term::Terminal; +use term::color::{Color, RED, YELLOW, GREEN, CYAN}; + +use std::cmp; +use std::io; +use std::io::{File, PortReader, ChanWriter}; +use std::io::stdio::StdWriter; +use std::str; +use std::task; +use std::to_str::ToStr; +use std::f64; +use std::os; + +// to be used by rustc to compile tests in libtest +pub mod test { + pub use {BenchHarness, TestName, TestResult, TestDesc, + TestDescAndFn, TestOpts, TrFailed, TrIgnored, TrOk, + Metric, MetricMap, MetricAdded, MetricRemoved, + MetricChange, Improvement, Regression, LikelyNoise, + StaticTestFn, StaticTestName, DynTestName, DynTestFn, + run_test, test_main, test_main_static, filter_tests, + parse_opts}; +} + +// 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 +// colons. This way if some test runner wants to arrange the tests +// hierarchically it may. + +#[deriving(Clone)] +pub enum TestName { + StaticTestName(&'static str), + DynTestName(~str) +} +impl ToStr for TestName { + fn to_str(&self) -> ~str { + match (*self).clone() { + StaticTestName(s) => s.to_str(), + DynTestName(s) => s.to_str() + } + } +} + +#[deriving(Clone)] +enum NamePadding { PadNone, PadOnLeft, PadOnRight } + +impl TestDesc { + fn padded_name(&self, column_count: uint, align: NamePadding) -> ~str { + use std::num::Saturating; + let name = self.name.to_str(); + let fill = column_count.saturating_sub(name.len()); + let pad = " ".repeat(fill); + match align { + PadNone => name, + PadOnLeft => pad.append(name), + PadOnRight => name.append(pad), + } + } +} + +/// Represents a benchmark function. +pub trait TDynBenchFn { + fn run(&self, harness: &mut BenchHarness); +} + +// A function that runs a test. If the function returns successfully, +// the test succeeds; if the function fails then the test fails. We +// may need to come up with a more clever definition of test in order +// to support isolation of tests into tasks. +pub enum TestFn { + StaticTestFn(extern fn()), + StaticBenchFn(extern fn(&mut BenchHarness)), + StaticMetricFn(proc(&mut MetricMap)), + DynTestFn(proc()), + DynMetricFn(proc(&mut MetricMap)), + DynBenchFn(~TDynBenchFn) +} + +impl TestFn { + fn padding(&self) -> NamePadding { + match self { + &StaticTestFn(..) => PadNone, + &StaticBenchFn(..) => PadOnRight, + &StaticMetricFn(..) => PadOnRight, + &DynTestFn(..) => PadNone, + &DynMetricFn(..) => PadOnRight, + &DynBenchFn(..) => PadOnRight, + } + } +} + +// Structure passed to BenchFns +pub struct BenchHarness { + priv iterations: u64, + priv ns_start: u64, + priv ns_end: u64, + bytes: u64 +} + +// The definition of a single test. A test runner will run a list of +// these. +#[deriving(Clone)] +pub struct TestDesc { + name: TestName, + ignore: bool, + should_fail: bool +} + +pub struct TestDescAndFn { + desc: TestDesc, + testfn: TestFn, +} + +#[deriving(Clone, Encodable, Decodable, Eq)] +pub struct Metric { + priv value: f64, + priv noise: f64 +} + +impl Metric { + pub fn new(value: f64, noise: f64) -> Metric { + Metric {value: value, noise: noise} + } +} + +#[deriving(Eq)] +pub struct MetricMap(TreeMap<~str,Metric>); + +impl Clone for MetricMap { + fn clone(&self) -> MetricMap { + let MetricMap(ref map) = *self; + MetricMap(map.clone()) + } +} + +/// Analysis of a single change in metric +#[deriving(Eq)] +pub enum MetricChange { + LikelyNoise, + MetricAdded, + MetricRemoved, + Improvement(f64), + Regression(f64) +} + +pub type MetricDiff = TreeMap<~str,MetricChange>; + +// The default console test runner. It accepts the command line +// arguments and a vector of test_descs. +pub fn test_main(args: &[~str], tests: ~[TestDescAndFn]) { + let opts = + match parse_opts(args) { + Some(Ok(o)) => o, + Some(Err(msg)) => fail!("{}", msg), + None => return + }; + match run_tests_console(&opts, tests) { + Ok(true) => {} + Ok(false) => fail!("Some tests failed"), + Err(e) => fail!("io error when running tests: {}", e), + } +} + +// A variant optimized for invocation with a static test vector. +// This will fail (intentionally) when fed any dynamic tests, because +// it is copying the static values out into a dynamic vector and cannot +// copy dynamic values. It is doing this because from this point on +// a ~[TestDescAndFn] is used in order to effect ownership-transfer +// semantics into parallel test runners, which in turn requires a ~[] +// rather than a &[]. +pub fn test_main_static(args: &[~str], tests: &[TestDescAndFn]) { + let owned_tests = tests.map(|t| { + match t.testfn { + StaticTestFn(f) => + TestDescAndFn { testfn: StaticTestFn(f), desc: t.desc.clone() }, + + StaticBenchFn(f) => + TestDescAndFn { testfn: StaticBenchFn(f), desc: t.desc.clone() }, + + _ => { + fail!("non-static tests passed to test::test_main_static"); + } + } + }); + test_main(args, owned_tests) +} + +pub struct TestOpts { + filter: Option<~str>, + run_ignored: bool, + run_tests: bool, + run_benchmarks: bool, + ratchet_metrics: Option<Path>, + ratchet_noise_percent: Option<f64>, + save_metrics: Option<Path>, + test_shard: Option<(uint,uint)>, + logfile: Option<Path> +} + +/// Result of parsing the options. +pub type OptRes = Result<TestOpts, ~str>; + +fn optgroups() -> ~[getopts::OptGroup] { + ~[getopts::optflag("", "ignored", "Run ignored tests"), + getopts::optflag("", "test", "Run tests and not benchmarks"), + getopts::optflag("", "bench", "Run benchmarks instead of tests"), + getopts::optflag("h", "help", "Display this message (longer with --help)"), + getopts::optopt("", "save-metrics", "Location to save bench metrics", + "PATH"), + getopts::optopt("", "ratchet-metrics", + "Location to load and save metrics from. The metrics \ + loaded are cause benchmarks to fail if they run too \ + slowly", "PATH"), + getopts::optopt("", "ratchet-noise-percent", + "Tests within N% of the recorded metrics will be \ + considered as passing", "PERCENTAGE"), + getopts::optopt("", "logfile", "Write logs to the specified file instead \ + of stdout", "PATH"), + getopts::optopt("", "test-shard", "run shard A, of B shards, worth of the testsuite", + "A.B")] +} + +fn usage(binary: &str, helpstr: &str) { + let message = format!("Usage: {} [OPTIONS] [FILTER]", binary); + println!("{}", getopts::usage(message, optgroups())); + println!(""); + if helpstr == "help" { + println!("{}", "\ +The FILTER is matched against the name of all tests to run, and if any tests +have a substring match, only those tests are run. + +By default, all tests are run in parallel. This can be altered with the +RUST_TEST_TASKS environment variable when running tests (set it to 1). + +Test Attributes: + + #[test] - Indicates a function is a test to be run. This function + takes no arguments. + #[bench] - Indicates a function is a benchmark to be run. This + function takes one argument (test::BenchHarness). + #[should_fail] - This function (also labeled with #[test]) will only pass if + the code causes a failure (an assertion failure or fail!) + #[ignore] - When applied to a function which is already attributed as a + test, then the test runner will ignore these tests during + normal test runs. Running with --ignored will run these + tests. This may also be written as #[ignore(cfg(...))] to + ignore the test on certain configurations."); + } +} + +// Parses command line arguments into test options +pub fn parse_opts(args: &[~str]) -> Option<OptRes> { + let args_ = args.tail(); + let matches = + match getopts::getopts(args_, optgroups()) { + Ok(m) => m, + Err(f) => return Some(Err(f.to_err_msg())) + }; + + if matches.opt_present("h") { usage(args[0], "h"); return None; } + if matches.opt_present("help") { usage(args[0], "help"); return None; } + + let filter = + if matches.free.len() > 0 { + Some((matches).free[0].clone()) + } else { + None + }; + + let run_ignored = matches.opt_present("ignored"); + + let logfile = matches.opt_str("logfile"); + let logfile = logfile.map(|s| Path::new(s)); + + let run_benchmarks = matches.opt_present("bench"); + let run_tests = ! run_benchmarks || + matches.opt_present("test"); + + let ratchet_metrics = matches.opt_str("ratchet-metrics"); + let ratchet_metrics = ratchet_metrics.map(|s| Path::new(s)); + + let ratchet_noise_percent = matches.opt_str("ratchet-noise-percent"); + let ratchet_noise_percent = ratchet_noise_percent.map(|s| from_str::<f64>(s).unwrap()); + + let save_metrics = matches.opt_str("save-metrics"); + let save_metrics = save_metrics.map(|s| Path::new(s)); + + let test_shard = matches.opt_str("test-shard"); + let test_shard = opt_shard(test_shard); + + let test_opts = TestOpts { + filter: filter, + run_ignored: run_ignored, + run_tests: run_tests, + run_benchmarks: run_benchmarks, + ratchet_metrics: ratchet_metrics, + ratchet_noise_percent: ratchet_noise_percent, + save_metrics: save_metrics, + test_shard: test_shard, + logfile: logfile + }; + + Some(Ok(test_opts)) +} + +pub fn opt_shard(maybestr: Option<~str>) -> Option<(uint,uint)> { + match maybestr { + None => None, + Some(s) => { + let vector = s.split('.').to_owned_vec(); + if vector.len() == 2 { + match (from_str::<uint>(vector[0]), + from_str::<uint>(vector[1])) { + (Some(a), Some(b)) => Some((a, b)), + _ => None + } + } else { + None + } + } + } +} + + +#[deriving(Clone, Eq)] +pub struct BenchSamples { + priv ns_iter_summ: stats::Summary, + priv mb_s: uint +} + +#[deriving(Clone, Eq)] +pub enum TestResult { + TrOk, + TrFailed, + TrIgnored, + TrMetrics(MetricMap), + TrBench(BenchSamples), +} + +enum OutputLocation<T> { + Pretty(term::Terminal<T>), + Raw(T), +} + +struct ConsoleTestState<T> { + log_out: Option<File>, + out: OutputLocation<T>, + use_color: bool, + total: uint, + passed: uint, + failed: uint, + ignored: uint, + measured: uint, + metrics: MetricMap, + failures: ~[(TestDesc, ~[u8])], + max_name_len: uint, // number of columns to fill when aligning names +} + +impl<T: Writer> ConsoleTestState<T> { + pub fn new(opts: &TestOpts, + _: Option<T>) -> io::IoResult<ConsoleTestState<StdWriter>> { + let log_out = match opts.logfile { + Some(ref path) => Some(if_ok!(File::create(path))), + None => None + }; + let out = match term::Terminal::new(io::stdout()) { + Err(_) => Raw(io::stdout()), + Ok(t) => Pretty(t) + }; + Ok(ConsoleTestState { + out: out, + log_out: log_out, + use_color: use_color(), + total: 0u, + passed: 0u, + failed: 0u, + ignored: 0u, + measured: 0u, + metrics: MetricMap::new(), + failures: ~[], + max_name_len: 0u, + }) + } + + pub fn write_ok(&mut self) -> io::IoResult<()> { + self.write_pretty("ok", term::color::GREEN) + } + + pub fn write_failed(&mut self) -> io::IoResult<()> { + self.write_pretty("FAILED", term::color::RED) + } + + pub fn write_ignored(&mut self) -> io::IoResult<()> { + self.write_pretty("ignored", term::color::YELLOW) + } + + pub fn write_metric(&mut self) -> io::IoResult<()> { + self.write_pretty("metric", term::color::CYAN) + } + + pub fn write_bench(&mut self) -> io::IoResult<()> { + self.write_pretty("bench", term::color::CYAN) + } + + pub fn write_added(&mut self) -> io::IoResult<()> { + self.write_pretty("added", term::color::GREEN) + } + + pub fn write_improved(&mut self) -> io::IoResult<()> { + self.write_pretty("improved", term::color::GREEN) + } + + pub fn write_removed(&mut self) -> io::IoResult<()> { + self.write_pretty("removed", term::color::YELLOW) + } + + pub fn write_regressed(&mut self) -> io::IoResult<()> { + self.write_pretty("regressed", term::color::RED) + } + + pub fn write_pretty(&mut self, + word: &str, + color: term::color::Color) -> io::IoResult<()> { + match self.out { + Pretty(ref mut term) => { + if self.use_color { + if_ok!(term.fg(color)); + } + if_ok!(term.write(word.as_bytes())); + if self.use_color { + if_ok!(term.reset()); + } + Ok(()) + } + Raw(ref mut stdout) => stdout.write(word.as_bytes()) + } + } + + pub fn write_plain(&mut self, s: &str) -> io::IoResult<()> { + match self.out { + Pretty(ref mut term) => term.write(s.as_bytes()), + Raw(ref mut stdout) => stdout.write(s.as_bytes()) + } + } + + pub fn write_run_start(&mut self, len: uint) -> io::IoResult<()> { + 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::IoResult<()> { + 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::IoResult<()> { + if_ok!(match *result { + TrOk => self.write_ok(), + TrFailed => self.write_failed(), + TrIgnored => self.write_ignored(), + TrMetrics(ref mm) => { + if_ok!(self.write_metric()); + self.write_plain(format!(": {}", fmt_metrics(mm))) + } + TrBench(ref bs) => { + if_ok!(self.write_bench()); + self.write_plain(format!(": {}", fmt_bench_samples(bs))) + } + }); + self.write_plain("\n") + } + + pub fn write_log(&mut self, test: &TestDesc, + result: &TestResult) -> io::IoResult<()> { + match self.log_out { + None => Ok(()), + Some(ref mut o) => { + let s = format!("{} {}\n", match *result { + TrOk => ~"ok", + TrFailed => ~"failed", + TrIgnored => ~"ignored", + TrMetrics(ref mm) => fmt_metrics(mm), + TrBench(ref bs) => fmt_bench_samples(bs) + }, test.name.to_str()); + o.write(s.as_bytes()) + } + } + } + + pub fn write_failures(&mut self) -> io::IoResult<()> { + if_ok!(self.write_plain("\nfailures:\n")); + let mut failures = ~[]; + let mut fail_out = ~""; + for &(ref f, ref stdout) in self.failures.iter() { + failures.push(f.name.to_str()); + if stdout.len() > 0 { + fail_out.push_str(format!("---- {} stdout ----\n\t", + f.name.to_str())); + let output = str::from_utf8_lossy(*stdout); + fail_out.push_str(output.as_slice().replace("\n", "\n\t")); + fail_out.push_str("\n"); + } + } + if fail_out.len() > 0 { + if_ok!(self.write_plain("\n")); + if_ok!(self.write_plain(fail_out)); + } + + if_ok!(self.write_plain("\nfailures:\n")); + failures.sort(); + for name in failures.iter() { + if_ok!(self.write_plain(format!(" {}\n", name.to_str()))); + } + Ok(()) + } + + pub fn write_metric_diff(&mut self, diff: &MetricDiff) -> io::IoResult<()> { + let mut noise = 0; + let mut improved = 0; + let mut regressed = 0; + let mut added = 0; + let mut removed = 0; + + for (k, v) in diff.iter() { + match *v { + LikelyNoise => noise += 1, + MetricAdded => { + added += 1; + if_ok!(self.write_added()); + if_ok!(self.write_plain(format!(": {}\n", *k))); + } + MetricRemoved => { + removed += 1; + if_ok!(self.write_removed()); + if_ok!(self.write_plain(format!(": {}\n", *k))); + } + Improvement(pct) => { + improved += 1; + if_ok!(self.write_plain(format!(": {}", *k))); + if_ok!(self.write_improved()); + if_ok!(self.write_plain(format!(" by {:.2f}%\n", pct as f64))); + } + Regression(pct) => { + regressed += 1; + if_ok!(self.write_plain(format!(": {}", *k))); + if_ok!(self.write_regressed()); + if_ok!(self.write_plain(format!(" by {:.2f}%\n", pct as f64))); + } + } + } + if_ok!(self.write_plain(format!("result of ratchet: {} metrics added, \ + {} removed, {} improved, {} regressed, \ + {} noise\n", + added, removed, improved, regressed, + noise))); + if regressed == 0 { + if_ok!(self.write_plain("updated ratchet file\n")); + } else { + if_ok!(self.write_plain("left ratchet file untouched\n")); + } + Ok(()) + } + + pub fn write_run_finish(&mut self, + ratchet_metrics: &Option<Path>, + ratchet_pct: Option<f64>) -> io::IoResult<bool> { + assert!(self.passed + self.failed + self.ignored + self.measured == self.total); + + let ratchet_success = match *ratchet_metrics { + None => true, + Some(ref pth) => { + if_ok!(self.write_plain(format!("\nusing metrics ratcher: {}\n", + pth.display()))); + match ratchet_pct { + None => (), + Some(pct) => + if_ok!(self.write_plain(format!("with noise-tolerance \ + forced to: {}%\n", + pct))) + } + let (diff, ok) = self.metrics.ratchet(pth, ratchet_pct); + if_ok!(self.write_metric_diff(&diff)); + ok + } + }; + + let test_success = self.failed == 0u; + if !test_success { + if_ok!(self.write_failures()); + } + + let success = ratchet_success && test_success; + + if_ok!(self.write_plain("\ntest result: ")); + if success { + // There's no parallelism at this point so it's safe to use color + if_ok!(self.write_ok()); + } else { + if_ok!(self.write_failed()); + } + let s = format!(". {} passed; {} failed; {} ignored; {} measured\n\n", + self.passed, self.failed, self.ignored, self.measured); + if_ok!(self.write_plain(s)); + return Ok(success); + } +} + +pub fn fmt_metrics(mm: &MetricMap) -> ~str { + let MetricMap(ref mm) = *mm; + let v : ~[~str] = mm.iter() + .map(|(k,v)| format!("{}: {} (+/- {})", + *k, + v.value as f64, + v.noise as f64)) + .collect(); + v.connect(", ") +} + +pub fn fmt_bench_samples(bs: &BenchSamples) -> ~str { + if bs.mb_s != 0 { + format!("{:>9} ns/iter (+/- {}) = {} MB/s", + bs.ns_iter_summ.median as uint, + (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as uint, + bs.mb_s) + } else { + format!("{:>9} ns/iter (+/- {})", + bs.ns_iter_summ.median as uint, + (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as uint) + } +} + +// A simple console test runner +pub fn run_tests_console(opts: &TestOpts, + tests: ~[TestDescAndFn]) -> io::IoResult<bool> { + fn callback<T: Writer>(event: &TestEvent, + st: &mut ConsoleTestState<T>) -> io::IoResult<()> { + debug!("callback(event={:?})", event); + match (*event).clone() { + TeFiltered(ref filtered_tests) => st.write_run_start(filtered_tests.len()), + TeWait(ref test, padding) => st.write_test_start(test, padding), + TeResult(test, result, stdout) => { + if_ok!(st.write_log(&test, &result)); + if_ok!(st.write_result(&result)); + match result { + TrOk => st.passed += 1, + TrIgnored => st.ignored += 1, + TrMetrics(mm) => { + let tname = test.name.to_str(); + let MetricMap(mm) = mm; + for (k,v) in mm.iter() { + st.metrics.insert_metric(tname + "." + *k, + v.value, v.noise); + } + st.measured += 1 + } + TrBench(bs) => { + st.metrics.insert_metric(test.name.to_str(), + bs.ns_iter_summ.median, + bs.ns_iter_summ.max - bs.ns_iter_summ.min); + st.measured += 1 + } + TrFailed => { + st.failed += 1; + st.failures.push((test, stdout)); + } + } + Ok(()) + } + } + } + let mut st = if_ok!(ConsoleTestState::new(opts, None::<StdWriter>)); + fn len_if_padded(t: &TestDescAndFn) -> uint { + match t.testfn.padding() { + PadNone => 0u, + PadOnLeft | PadOnRight => t.desc.name.to_str().len(), + } + } + match tests.iter().max_by(|t|len_if_padded(*t)) { + Some(t) => { + let n = t.desc.name.to_str(); + debug!("Setting max_name_len from: {}", n); + st.max_name_len = n.len(); + }, + None => {} + } + if_ok!(run_tests(opts, tests, |x| callback(&x, &mut st))); + match opts.save_metrics { + None => (), + Some(ref pth) => { + if_ok!(st.metrics.save(pth)); + if_ok!(st.write_plain(format!("\nmetrics saved to: {}", + pth.display()))); + } + } + return st.write_run_finish(&opts.ratchet_metrics, opts.ratchet_noise_percent); +} + +#[test] +fn should_sort_failures_before_printing_them() { + use std::io::MemWriter; + use std::str; + + let test_a = TestDesc { + name: StaticTestName("a"), + ignore: false, + should_fail: false + }; + + let test_b = TestDesc { + name: StaticTestName("b"), + ignore: false, + should_fail: false + }; + + let mut st = ConsoleTestState { + log_out: None, + out: Raw(MemWriter::new()), + use_color: false, + total: 0u, + passed: 0u, + failed: 0u, + ignored: 0u, + measured: 0u, + max_name_len: 10u, + metrics: MetricMap::new(), + failures: ~[(test_b, ~[]), (test_a, ~[])] + }; + + st.write_failures().unwrap(); + let s = match st.out { + Raw(ref m) => str::from_utf8_lossy(m.get_ref()), + Pretty(_) => unreachable!() + }; + + let apos = s.as_slice().find_str("a").unwrap(); + let bpos = s.as_slice().find_str("b").unwrap(); + assert!(apos < bpos); +} + +fn use_color() -> bool { return get_concurrency() == 1; } + +#[deriving(Clone)] +enum TestEvent { + TeFiltered(~[TestDesc]), + TeWait(TestDesc, NamePadding), + TeResult(TestDesc, TestResult, ~[u8] /* stdout */), +} + +pub type MonitorMsg = (TestDesc, TestResult, ~[u8] /* stdout */); + +fn run_tests(opts: &TestOpts, + tests: ~[TestDescAndFn], + callback: |e: TestEvent| -> io::IoResult<()>) -> io::IoResult<()> { + let filtered_tests = filter_tests(opts, tests); + let filtered_descs = filtered_tests.map(|t| t.desc.clone()); + + if_ok!(callback(TeFiltered(filtered_descs))); + + let (filtered_tests, filtered_benchs_and_metrics) = + filtered_tests.partition(|e| { + match e.testfn { + StaticTestFn(_) | DynTestFn(_) => true, + _ => false + } + }); + + // It's tempting to just spawn all the tests at once, but since we have + // many tests that run in other processes we would be making a big mess. + let concurrency = get_concurrency(); + debug!("using {} test tasks", concurrency); + + let mut remaining = filtered_tests; + remaining.reverse(); + let mut pending = 0; + + let (p, ch) = Chan::<MonitorMsg>::new(); + + while pending > 0 || !remaining.is_empty() { + while pending < concurrency && !remaining.is_empty() { + let test = remaining.pop().unwrap(); + if concurrency == 1 { + // We are doing one test at a time so we can print the name + // of the test before we run it. Useful for debugging tests + // that hang forever. + if_ok!(callback(TeWait(test.desc.clone(), test.testfn.padding()))); + } + run_test(!opts.run_tests, test, ch.clone()); + pending += 1; + } + + let (desc, result, stdout) = p.recv(); + if concurrency != 1 { + if_ok!(callback(TeWait(desc.clone(), PadNone))); + } + if_ok!(callback(TeResult(desc, result, stdout))); + pending -= 1; + } + + // All benchmarks run at the end, in serial. + // (this includes metric fns) + for b in filtered_benchs_and_metrics.move_iter() { + if_ok!(callback(TeWait(b.desc.clone(), b.testfn.padding()))); + run_test(!opts.run_benchmarks, b, ch.clone()); + let (test, result, stdout) = p.recv(); + if_ok!(callback(TeResult(test, result, stdout))); + } + Ok(()) +} + +fn get_concurrency() -> uint { + use std::rt; + match os::getenv("RUST_TEST_TASKS") { + Some(s) => { + let opt_n: Option<uint> = FromStr::from_str(s); + match opt_n { + Some(n) if n > 0 => n, + _ => fail!("RUST_TEST_TASKS is `{}`, should be a positive integer.", s) + } + } + None => { + rt::default_sched_threads() + } + } +} + +pub fn filter_tests( + opts: &TestOpts, + tests: ~[TestDescAndFn]) -> ~[TestDescAndFn] +{ + let mut filtered = tests; + + // Remove tests that don't match the test filter + filtered = if opts.filter.is_none() { + filtered + } else { + let filter_str = match opts.filter { + Some(ref f) => (*f).clone(), + None => ~"" + }; + + fn filter_fn(test: TestDescAndFn, filter_str: &str) -> + Option<TestDescAndFn> { + if test.desc.name.to_str().contains(filter_str) { + return Some(test); + } else { + return None; + } + } + + filtered.move_iter().filter_map(|x| filter_fn(x, filter_str)).collect() + }; + + // Maybe pull out the ignored test and unignore them + filtered = if !opts.run_ignored { + filtered + } else { + fn filter(test: TestDescAndFn) -> Option<TestDescAndFn> { + if test.desc.ignore { + let TestDescAndFn {desc, testfn} = test; + Some(TestDescAndFn { + desc: TestDesc {ignore: false, ..desc}, + testfn: testfn + }) + } else { + None + } + }; + filtered.move_iter().filter_map(|x| filter(x)).collect() + }; + + // Sort the tests alphabetically + filtered.sort_by(|t1, t2| t1.desc.name.to_str().cmp(&t2.desc.name.to_str())); + + // Shard the remaining tests, if sharding requested. + match opts.test_shard { + None => filtered, + Some((a,b)) => + filtered.move_iter().enumerate() + .filter(|&(i,_)| i % b == a) + .map(|(_,t)| t) + .to_owned_vec() + } +} + +pub fn run_test(force_ignore: bool, + test: TestDescAndFn, + monitor_ch: Chan<MonitorMsg>) { + + let TestDescAndFn {desc, testfn} = test; + + if force_ignore || desc.ignore { + monitor_ch.send((desc, TrIgnored, ~[])); + return; + } + + fn run_test_inner(desc: TestDesc, + monitor_ch: Chan<MonitorMsg>, + testfn: proc()) { + spawn(proc() { + let (p, c) = Chan::new(); + let mut reader = PortReader::new(p); + let stdout = ChanWriter::new(c.clone()); + let stderr = ChanWriter::new(c); + let mut task = task::task().named(match desc.name { + DynTestName(ref name) => name.clone().into_maybe_owned(), + StaticTestName(name) => name.into_maybe_owned(), + }); + task.opts.stdout = Some(~stdout as ~Writer); + task.opts.stderr = Some(~stderr as ~Writer); + let result_future = task.future_result(); + task.spawn(testfn); + + let stdout = reader.read_to_end().unwrap(); + let task_result = result_future.recv(); + let test_result = calc_result(&desc, task_result.is_ok()); + monitor_ch.send((desc.clone(), test_result, stdout)); + }) + } + + match testfn { + DynBenchFn(bencher) => { + let bs = ::bench::benchmark(|harness| bencher.run(harness)); + monitor_ch.send((desc, TrBench(bs), ~[])); + return; + } + StaticBenchFn(benchfn) => { + let bs = ::bench::benchmark(|harness| benchfn(harness)); + monitor_ch.send((desc, TrBench(bs), ~[])); + return; + } + DynMetricFn(f) => { + let mut mm = MetricMap::new(); + f(&mut mm); + monitor_ch.send((desc, TrMetrics(mm), ~[])); + return; + } + StaticMetricFn(f) => { + let mut mm = MetricMap::new(); + f(&mut mm); + monitor_ch.send((desc, TrMetrics(mm), ~[])); + return; + } + DynTestFn(f) => run_test_inner(desc, monitor_ch, f), + StaticTestFn(f) => run_test_inner(desc, monitor_ch, proc() f()) + } +} + +fn calc_result(desc: &TestDesc, task_succeeded: bool) -> TestResult { + if task_succeeded { + if desc.should_fail { TrFailed } + else { TrOk } + } else { + if desc.should_fail { TrOk } + else { TrFailed } + } +} + + +impl ToJson for Metric { + fn to_json(&self) -> json::Json { + let mut map = ~TreeMap::new(); + map.insert(~"value", json::Number(self.value)); + map.insert(~"noise", json::Number(self.noise)); + json::Object(map) + } +} + +impl MetricMap { + + pub fn new() -> MetricMap { + MetricMap(TreeMap::new()) + } + + /// Load MetricDiff from a file. + /// + /// # Failure + /// + /// This function will fail if the path does not exist or the path does not + /// contain a valid metric map. + pub fn load(p: &Path) -> MetricMap { + assert!(p.exists()); + let mut f = File::open(p).unwrap(); + let value = json::from_reader(&mut f as &mut io::Reader).unwrap(); + let mut decoder = json::Decoder::new(value); + MetricMap(Decodable::decode(&mut decoder)) + } + + /// Write MetricDiff to a file. + pub fn save(&self, p: &Path) -> io::IoResult<()> { + let mut file = if_ok!(File::create(p)); + let MetricMap(ref map) = *self; + map.to_json().to_pretty_writer(&mut file) + } + + /// Compare against another MetricMap. Optionally compare all + /// measurements in the maps using the provided `noise_pct` as a + /// percentage of each value to consider noise. If `None`, each + /// measurement's noise threshold is independently chosen as the + /// maximum of that measurement's recorded noise quantity in either + /// map. + pub fn compare_to_old(&self, old: &MetricMap, + noise_pct: Option<f64>) -> MetricDiff { + let mut diff : MetricDiff = TreeMap::new(); + let MetricMap(ref selfmap) = *self; + let MetricMap(ref old) = *old; + for (k, vold) in old.iter() { + let r = match selfmap.find(k) { + None => MetricRemoved, + Some(v) => { + let delta = (v.value - vold.value); + let noise = match noise_pct { + None => f64::max(vold.noise.abs(), v.noise.abs()), + Some(pct) => vold.value * pct / 100.0 + }; + if delta.abs() <= noise { + LikelyNoise + } else { + let pct = delta.abs() / cmp::max(vold.value, f64::EPSILON) * 100.0; + if vold.noise < 0.0 { + // When 'noise' is negative, it means we want + // to see deltas that go up over time, and can + // only tolerate slight negative movement. + if delta < 0.0 { + Regression(pct) + } else { + Improvement(pct) + } + } else { + // When 'noise' is positive, it means we want + // to see deltas that go down over time, and + // can only tolerate slight positive movements. + if delta < 0.0 { + Improvement(pct) + } else { + Regression(pct) + } + } + } + } + }; + diff.insert((*k).clone(), r); + } + let MetricMap(ref map) = *self; + for (k, _) in map.iter() { + if !diff.contains_key(k) { + diff.insert((*k).clone(), MetricAdded); + } + } + diff + } + + /// Insert a named `value` (+/- `noise`) metric into the map. The value + /// must be non-negative. The `noise` indicates the uncertainty of the + /// metric, which doubles as the "noise range" of acceptable + /// pairwise-regressions on this named value, when comparing from one + /// metric to the next using `compare_to_old`. + /// + /// If `noise` is positive, then it means this metric is of a value + /// you want to see grow smaller, so a change larger than `noise` in the + /// positive direction represents a regression. + /// + /// If `noise` is negative, then it means this metric is of a value + /// you want to see grow larger, so a change larger than `noise` in the + /// negative direction represents a regression. + pub fn insert_metric(&mut self, name: &str, value: f64, noise: f64) { + let m = Metric { + value: value, + noise: noise + }; + let MetricMap(ref mut map) = *self; + map.insert(name.to_owned(), m); + } + + /// Attempt to "ratchet" an external metric file. This involves loading + /// metrics from a metric file (if it exists), comparing against + /// the metrics in `self` using `compare_to_old`, and rewriting the + /// file to contain the metrics in `self` if none of the + /// `MetricChange`s are `Regression`. Returns the diff as well + /// as a boolean indicating whether the ratchet succeeded. + pub fn ratchet(&self, p: &Path, pct: Option<f64>) -> (MetricDiff, bool) { + let old = if p.exists() { + MetricMap::load(p) + } else { + MetricMap::new() + }; + + let diff : MetricDiff = self.compare_to_old(&old, pct); + let ok = diff.iter().all(|(_, v)| { + match *v { + Regression(_) => false, + _ => true + } + }); + + if ok { + debug!("rewriting file '{:?}' with updated metrics", p); + self.save(p).unwrap(); + } + return (diff, ok) + } +} + + +// Benchmarking + +/// A function that is opaque to the optimizer, to allow benchmarks to +/// pretend to use outputs to assist in avoiding dead-code +/// elimination. +/// +/// This function is a no-op, and does not even read from `dummy`. +pub fn black_box<T>(dummy: T) { + // we need to "use" the argument in some way LLVM can't + // introspect. + unsafe {asm!("" : : "r"(&dummy))} +} + + +impl BenchHarness { + /// Callback for benchmark functions to run in their body. + pub fn iter<T>(&mut self, inner: || -> T) { + self.ns_start = precise_time_ns(); + let k = self.iterations; + for _ in range(0u64, k) { + black_box(inner()); + } + self.ns_end = precise_time_ns(); + } + + pub fn ns_elapsed(&mut self) -> u64 { + if self.ns_start == 0 || self.ns_end == 0 { + 0 + } else { + self.ns_end - self.ns_start + } + } + + pub fn ns_per_iter(&mut self) -> u64 { + if self.iterations == 0 { + 0 + } else { + self.ns_elapsed() / cmp::max(self.iterations, 1) + } + } + + pub fn bench_n(&mut self, n: u64, f: |&mut BenchHarness|) { + self.iterations = n; + debug!("running benchmark for {} iterations", + n as uint); + f(self); + } + + // This is a more statistics-driven benchmark algorithm + pub fn auto_bench(&mut self, f: |&mut BenchHarness|) -> stats::Summary { + + // Initial bench run to get ballpark figure. + let mut n = 1_u64; + self.bench_n(n, |x| f(x)); + + // Try to estimate iter count for 1ms falling back to 1m + // iterations if first run took < 1ns. + if self.ns_per_iter() == 0 { + n = 1_000_000; + } else { + n = 1_000_000 / cmp::max(self.ns_per_iter(), 1); + } + // if the first run took more than 1ms we don't want to just + // be left doing 0 iterations on every loop. The unfortunate + // side effect of not being able to do as many runs is + // automatically handled by the statistical analysis below + // (i.e. larger error bars). + if n == 0 { n = 1; } + + debug!("Initial run took {} ns, iter count that takes 1ms estimated as {}", + self.ns_per_iter(), n); + + let mut total_run = 0; + let samples : &mut [f64] = [0.0_f64, ..50]; + loop { + let loop_start = precise_time_ns(); + + for p in samples.mut_iter() { + self.bench_n(n, |x| f(x)); + *p = self.ns_per_iter() as f64; + }; + + stats::winsorize(samples, 5.0); + let summ = stats::Summary::new(samples); + + for p in samples.mut_iter() { + self.bench_n(5 * n, |x| f(x)); + *p = self.ns_per_iter() as f64; + }; + + stats::winsorize(samples, 5.0); + let summ5 = stats::Summary::new(samples); + + debug!("{} samples, median {}, MAD={}, MADP={}", + samples.len(), + summ.median as f64, + summ.median_abs_dev as f64, + summ.median_abs_dev_pct as f64); + + let now = precise_time_ns(); + let loop_run = now - loop_start; + + // If we've run for 100ms and seem to have converged to a + // stable median. + if loop_run > 100_000_000 && + summ.median_abs_dev_pct < 1.0 && + summ.median - summ5.median < summ5.median_abs_dev { + return summ5; + } + + total_run += loop_run; + // Longest we ever run for is 3s. + if total_run > 3_000_000_000 { + return summ5; + } + + n *= 2; + } + } +} + +pub mod bench { + use std::cmp; + use super::{BenchHarness, BenchSamples}; + + pub fn benchmark(f: |&mut BenchHarness|) -> BenchSamples { + let mut bs = BenchHarness { + iterations: 0, + ns_start: 0, + ns_end: 0, + bytes: 0 + }; + + let ns_iter_summ = bs.auto_bench(f); + + let ns_iter = cmp::max(ns_iter_summ.median as u64, 1); + let iter_s = 1_000_000_000 / ns_iter; + let mb_s = (bs.bytes * iter_s) / 1_000_000; + + BenchSamples { + ns_iter_summ: ns_iter_summ, + mb_s: mb_s as uint + } + } +} + +#[cfg(test)] +mod tests { + use test::{TrFailed, TrIgnored, TrOk, filter_tests, parse_opts, + TestDesc, TestDescAndFn, TestOpts, run_test, + Metric, MetricMap, MetricAdded, MetricRemoved, + Improvement, Regression, LikelyNoise, + StaticTestName, DynTestName, DynTestFn}; + use extra::tempfile::TempDir; + + #[test] + pub fn do_not_run_ignored_tests() { + fn f() { fail!(); } + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: true, + should_fail: false + }, + testfn: DynTestFn(proc() f()), + }; + let (p, ch) = Chan::new(); + run_test(false, desc, ch); + let (_, res, _) = p.recv(); + assert!(res != TrOk); + } + + #[test] + pub fn ignored_tests_result_in_ignored() { + fn f() { } + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: true, + should_fail: false + }, + testfn: DynTestFn(proc() f()), + }; + let (p, ch) = Chan::new(); + run_test(false, desc, ch); + let (_, res, _) = p.recv(); + assert_eq!(res, TrIgnored); + } + + #[test] + fn test_should_fail() { + fn f() { fail!(); } + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + should_fail: true + }, + testfn: DynTestFn(proc() f()), + }; + let (p, ch) = Chan::new(); + run_test(false, desc, ch); + let (_, res, _) = p.recv(); + assert_eq!(res, TrOk); + } + + #[test] + fn test_should_fail_but_succeeds() { + fn f() { } + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + should_fail: true + }, + testfn: DynTestFn(proc() f()), + }; + let (p, ch) = Chan::new(); + run_test(false, desc, ch); + let (_, res, _) = p.recv(); + assert_eq!(res, TrFailed); + } + + #[test] + fn first_free_arg_should_be_a_filter() { + let args = ~[~"progname", ~"filter"]; + let opts = match parse_opts(args) { + Some(Ok(o)) => o, + _ => fail!("Malformed arg in first_free_arg_should_be_a_filter") + }; + assert!("filter" == opts.filter.clone().unwrap()); + } + + #[test] + fn parse_ignored_flag() { + let args = ~[~"progname", ~"filter", ~"--ignored"]; + let opts = match parse_opts(args) { + Some(Ok(o)) => o, + _ => fail!("Malformed arg in parse_ignored_flag") + }; + assert!((opts.run_ignored)); + } + + #[test] + pub fn filter_for_ignored_option() { + // When we run ignored tests the test filter should filter out all the + // unignored tests and flip the ignore flag on the rest to false + + let opts = TestOpts { + filter: None, + run_ignored: true, + logfile: None, + run_tests: true, + run_benchmarks: false, + ratchet_noise_percent: None, + ratchet_metrics: None, + save_metrics: None, + test_shard: None + }; + + let tests = ~[ + TestDescAndFn { + desc: TestDesc { + name: StaticTestName("1"), + ignore: true, + should_fail: false, + }, + testfn: DynTestFn(proc() {}), + }, + TestDescAndFn { + desc: TestDesc { + name: StaticTestName("2"), + ignore: false, + should_fail: false + }, + testfn: DynTestFn(proc() {}), + }, + ]; + let filtered = filter_tests(&opts, tests); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].desc.name.to_str(), ~"1"); + assert!(filtered[0].desc.ignore == false); + } + + #[test] + pub fn sort_tests() { + let opts = TestOpts { + filter: None, + run_ignored: false, + logfile: None, + run_tests: true, + run_benchmarks: false, + ratchet_noise_percent: None, + ratchet_metrics: None, + save_metrics: None, + test_shard: None + }; + + let names = + ~[~"sha1::test", ~"int::test_to_str", ~"int::test_pow", + ~"test::do_not_run_ignored_tests", + ~"test::ignored_tests_result_in_ignored", + ~"test::first_free_arg_should_be_a_filter", + ~"test::parse_ignored_flag", ~"test::filter_for_ignored_option", + ~"test::sort_tests"]; + let tests = + { + fn testfn() { } + let mut tests = ~[]; + for name in names.iter() { + let test = TestDescAndFn { + desc: TestDesc { + name: DynTestName((*name).clone()), + ignore: false, + should_fail: false + }, + testfn: DynTestFn(testfn), + }; + tests.push(test); + } + tests + }; + let filtered = filter_tests(&opts, tests); + + let expected = + ~[~"int::test_pow", ~"int::test_to_str", ~"sha1::test", + ~"test::do_not_run_ignored_tests", + ~"test::filter_for_ignored_option", + ~"test::first_free_arg_should_be_a_filter", + ~"test::ignored_tests_result_in_ignored", + ~"test::parse_ignored_flag", + ~"test::sort_tests"]; + + for (a, b) in expected.iter().zip(filtered.iter()) { + assert!(*a == b.desc.name.to_str()); + } + } + + #[test] + pub fn test_metricmap_compare() { + let mut m1 = MetricMap::new(); + let mut m2 = MetricMap::new(); + m1.insert_metric("in-both-noise", 1000.0, 200.0); + m2.insert_metric("in-both-noise", 1100.0, 200.0); + + m1.insert_metric("in-first-noise", 1000.0, 2.0); + m2.insert_metric("in-second-noise", 1000.0, 2.0); + + m1.insert_metric("in-both-want-downwards-but-regressed", 1000.0, 10.0); + m2.insert_metric("in-both-want-downwards-but-regressed", 2000.0, 10.0); + + m1.insert_metric("in-both-want-downwards-and-improved", 2000.0, 10.0); + m2.insert_metric("in-both-want-downwards-and-improved", 1000.0, 10.0); + + m1.insert_metric("in-both-want-upwards-but-regressed", 2000.0, -10.0); + m2.insert_metric("in-both-want-upwards-but-regressed", 1000.0, -10.0); + + m1.insert_metric("in-both-want-upwards-and-improved", 1000.0, -10.0); + m2.insert_metric("in-both-want-upwards-and-improved", 2000.0, -10.0); + + let diff1 = m2.compare_to_old(&m1, None); + + assert_eq!(*(diff1.find(&~"in-both-noise").unwrap()), LikelyNoise); + assert_eq!(*(diff1.find(&~"in-first-noise").unwrap()), MetricRemoved); + assert_eq!(*(diff1.find(&~"in-second-noise").unwrap()), MetricAdded); + assert_eq!(*(diff1.find(&~"in-both-want-downwards-but-regressed").unwrap()), + Regression(100.0)); + assert_eq!(*(diff1.find(&~"in-both-want-downwards-and-improved").unwrap()), + Improvement(50.0)); + assert_eq!(*(diff1.find(&~"in-both-want-upwards-but-regressed").unwrap()), + Regression(50.0)); + assert_eq!(*(diff1.find(&~"in-both-want-upwards-and-improved").unwrap()), + Improvement(100.0)); + assert_eq!(diff1.len(), 7); + + let diff2 = m2.compare_to_old(&m1, Some(200.0)); + + assert_eq!(*(diff2.find(&~"in-both-noise").unwrap()), LikelyNoise); + assert_eq!(*(diff2.find(&~"in-first-noise").unwrap()), MetricRemoved); + assert_eq!(*(diff2.find(&~"in-second-noise").unwrap()), MetricAdded); + assert_eq!(*(diff2.find(&~"in-both-want-downwards-but-regressed").unwrap()), LikelyNoise); + assert_eq!(*(diff2.find(&~"in-both-want-downwards-and-improved").unwrap()), LikelyNoise); + assert_eq!(*(diff2.find(&~"in-both-want-upwards-but-regressed").unwrap()), LikelyNoise); + assert_eq!(*(diff2.find(&~"in-both-want-upwards-and-improved").unwrap()), LikelyNoise); + assert_eq!(diff2.len(), 7); + } + + #[test] + pub fn ratchet_test() { + + let dpth = TempDir::new("test-ratchet").expect("missing test for ratchet"); + let pth = dpth.path().join("ratchet.json"); + + let mut m1 = MetricMap::new(); + m1.insert_metric("runtime", 1000.0, 2.0); + m1.insert_metric("throughput", 50.0, 2.0); + + let mut m2 = MetricMap::new(); + m2.insert_metric("runtime", 1100.0, 2.0); + m2.insert_metric("throughput", 50.0, 2.0); + + m1.save(&pth).unwrap(); + + // Ask for a ratchet that should fail to advance. + let (diff1, ok1) = m2.ratchet(&pth, None); + assert_eq!(ok1, false); + assert_eq!(diff1.len(), 2); + assert_eq!(*(diff1.find(&~"runtime").unwrap()), Regression(10.0)); + assert_eq!(*(diff1.find(&~"throughput").unwrap()), LikelyNoise); + + // Check that it was not rewritten. + let m3 = MetricMap::load(&pth); + let MetricMap(m3) = m3; + assert_eq!(m3.len(), 2); + assert_eq!(*(m3.find(&~"runtime").unwrap()), Metric::new(1000.0, 2.0)); + assert_eq!(*(m3.find(&~"throughput").unwrap()), Metric::new(50.0, 2.0)); + + // Ask for a ratchet with an explicit noise-percentage override, + // that should advance. + let (diff2, ok2) = m2.ratchet(&pth, Some(10.0)); + assert_eq!(ok2, true); + assert_eq!(diff2.len(), 2); + assert_eq!(*(diff2.find(&~"runtime").unwrap()), LikelyNoise); + assert_eq!(*(diff2.find(&~"throughput").unwrap()), LikelyNoise); + + // Check that it was rewritten. + let m4 = MetricMap::load(&pth); + let MetricMap(m4) = m4; + assert_eq!(m4.len(), 2); + assert_eq!(*(m4.find(&~"runtime").unwrap()), Metric::new(1100.0, 2.0)); + assert_eq!(*(m4.find(&~"throughput").unwrap()), Metric::new(50.0, 2.0)); + } +} + |
