From e5aa399e0d98b81b6454ec30f70aac8604fb7b11 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Wed, 13 Feb 2013 11:46:14 -0800 Subject: [PATCH] rustc and std: teach about #[bench], modernize to use quote_expr! some. --- src/compiletest/compiletest.rc | 16 +- src/libcore/either.rs | 2 +- src/libcore/num/num.rs | 2 +- src/librustc/front/test.rs | 458 ++++++++---------------- src/librustc/middle/check_const.rs | 12 +- src/libstd/stats.rs | 2 +- src/libstd/std.rc | 1 + src/libstd/test.rs | 513 +++++++++++++++++++++++---- src/test/run-pass/test-ignore-cfg.rs | 6 +- 9 files changed, 610 insertions(+), 402 deletions(-) diff --git a/src/compiletest/compiletest.rc b/src/compiletest/compiletest.rc index 6d3bfde0372..ccd1b899ce3 100644 --- a/src/compiletest/compiletest.rc +++ b/src/compiletest/compiletest.rc @@ -33,7 +33,7 @@ mod errors; use std::getopts; use std::test; -use core::result; +use core::{result, either}; use result::{Ok, Err}; use common::config; @@ -158,7 +158,11 @@ pub fn test_opts(config: config) -> test::TestOpts { test::TestOpts { filter: config.filter, run_ignored: config.run_ignored, - logfile: config.logfile.map(|s| s.to_str()), + logfile: copy config.logfile, + run_tests: true, + run_benchmarks: false, + save_results: option::None, + compare_results: option::None } } @@ -210,13 +214,15 @@ pub fn make_test(config: config, testfile: &Path) -> test::TestDescAndFn { } } -pub fn make_test_name(config: config, testfile: &Path) -> ~str { - fmt!("[%s] %s", mode_str(config.mode), testfile.to_str()) +pub fn make_test_name(config: config, testfile: &Path) -> test::TestName { + test::DynTestName(fmt!("[%s] %s", + mode_str(config.mode), + testfile.to_str())) } pub fn make_test_closure(config: config, testfile: &Path) -> test::TestFn { let testfile = testfile.to_str(); - fn~() { runtest::run(config, testfile) } + test::DynTestFn(fn~() { runtest::run(config, testfile) }) } // Local Variables: diff --git a/src/libcore/either.rs b/src/libcore/either.rs index 7efde62c4ca..b8f60c1a2d9 100644 --- a/src/libcore/either.rs +++ b/src/libcore/either.rs @@ -145,7 +145,7 @@ pub pure fn unwrap_right(eith: Either) -> U { } } -impl Either { +pub impl Either { #[inline(always)] fn either(&self, f_left: fn(&T) -> V, f_right: fn(&U) -> V) -> V { either(f_left, f_right, self) diff --git a/src/libcore/num/num.rs b/src/libcore/num/num.rs index 9ba53defd6e..bc5a42262ca 100644 --- a/src/libcore/num/num.rs +++ b/src/libcore/num/num.rs @@ -39,7 +39,7 @@ pub trait One { static pure fn one() -> Self; } -pub pure fn abs(v: T) -> T { +pub pure fn abs(v: T) -> T { if v < Zero::zero() { v.neg() } else { v } } diff --git a/src/librustc/front/test.rs b/src/librustc/front/test.rs index 34f0045f3fd..caa2035389d 100644 --- a/src/librustc/front/test.rs +++ b/src/librustc/front/test.rs @@ -21,18 +21,21 @@ use core::option; use core::vec; use syntax::ast_util::*; use syntax::attr; -use syntax::codemap::{dummy_sp, span}; +use syntax::codemap::{dummy_sp, span, ExpandedFrom}; use syntax::codemap; use syntax::fold; use syntax::print::pprust; use syntax::{ast, ast_util}; use syntax::attr::attrs_contains_name; +use syntax::ext::base::{mk_ctxt, ext_ctxt}; + type node_id_gen = fn@() -> ast::node_id; type test = { span: span, path: ~[ast::ident], + bench: bool, ignore: bool, should_fail: bool }; @@ -41,6 +44,7 @@ struct TestCtxt { sess: session::Session, crate: @ast::crate, path: ~[ast::ident], + ext_cx: ext_ctxt, testfns: ~[test] } @@ -68,10 +72,15 @@ fn generate_test_harness(sess: session::Session, let cx: @mut TestCtxt = @mut TestCtxt { sess: sess, crate: crate, + ext_cx: mk_ctxt(sess.parse_sess, copy sess.opts.cfg), path: ~[], testfns: ~[] }; + cx.ext_cx.bt_push(ExpandedFrom({call_site: dummy_sp(), + callie: {name: ~"test", + span: None}})); + let precursor = @fold::AstFoldFns { fold_crate: fold::wrap(|a,b| fold_crate(cx, a, b) ), fold_item: |a,b| fold_item(cx, a, b), @@ -79,6 +88,7 @@ fn generate_test_harness(sess: session::Session, let fold = fold::make_fold(precursor); let res = @fold.fold_crate(*crate); + cx.ext_cx.bt_pop(); return res; } @@ -86,7 +96,8 @@ fn strip_test_functions(crate: @ast::crate) -> @ast::crate { // When not compiling with --test we should not compile the // #[test] functions do config::strip_items(crate) |attrs| { - !attr::contains_name(attr::attr_metas(attrs), ~"test") + !attr::contains_name(attr::attr_metas(attrs), ~"test") && + !attr::contains_name(attr::attr_metas(attrs), ~"bench") } } @@ -132,7 +143,7 @@ fn fold_item(cx: @mut TestCtxt, &&i: @ast::item, fld: fold::ast_fold) debug!("current path: %s", ast_util::path_name_i(cx.path, cx.sess.parse_sess.interner)); - if is_test_fn(i) { + if is_test_fn(i) || is_bench_fn(i) { match i.node { ast::item_fn(_, purity, _, _) if purity == ast::unsafe_fn => { let sess = cx.sess; @@ -143,10 +154,12 @@ fn fold_item(cx: @mut TestCtxt, &&i: @ast::item, fld: fold::ast_fold) _ => { debug!("this is a test function"); let test = {span: i.span, - path: /*bad*/copy cx.path, ignore: is_ignored(cx, i), + path: /*bad*/copy cx.path, + bench: is_bench_fn(i), + ignore: is_ignored(cx, i), should_fail: should_fail(i)}; cx.testfns.push(test); - debug!("have %u test functions", cx.testfns.len()); + debug!("have %u test/bench functions", cx.testfns.len()); } } } @@ -176,6 +189,31 @@ fn is_test_fn(i: @ast::item) -> bool { return has_test_attr && has_test_signature(i); } +fn is_bench_fn(i: @ast::item) -> bool { + let has_bench_attr = + vec::len(attr::find_attrs_by_name(i.attrs, ~"bench")) > 0u; + + fn has_test_signature(i: @ast::item) -> bool { + match /*bad*/copy i.node { + ast::item_fn(decl, _, tps, _) => { + let input_cnt = vec::len(decl.inputs); + let no_output = match decl.output.node { + ast::ty_nil => true, + _ => false + }; + let tparm_cnt = vec::len(tps); + // NB: inadequate check, but we're running + // well before resolve, can't get too deep. + input_cnt == 1u + && no_output && tparm_cnt == 0u + } + _ => false + } + } + + return has_bench_attr && has_test_signature(i); +} + fn is_ignored(cx: @mut TestCtxt, i: @ast::item) -> bool { let ignoreattrs = attr::find_attrs_by_name(i.attrs, "ignore"); let ignoreitems = attr::attr_metas(ignoreattrs); @@ -194,7 +232,7 @@ fn should_fail(i: @ast::item) -> bool { vec::len(attr::find_attrs_by_name(i.attrs, ~"should_fail")) > 0u } -fn add_test_module(cx: @mut TestCtxt, +m: ast::_mod) -> ast::_mod { +fn add_test_module(cx: &TestCtxt, +m: ast::_mod) -> ast::_mod { let testmod = mk_test_module(cx); ast::_mod { items: vec::append_one(/*bad*/copy m.items, testmod), @@ -207,47 +245,84 @@ fn add_test_module(cx: @mut TestCtxt, +m: ast::_mod) -> ast::_mod { We're going to be building a module that looks more or less like: mod __test { - fn main(args: ~[str]) -> int { - std::test::test_main(args, tests()) + #[!resolve_unexported] + extern mod std (name = "std", vers = "..."); + fn main() { + #[main]; + std::test::test_main_static(::os::args(), tests) } - fn tests() -> ~[std::test::test_desc] { + const tests : &static/[std::test::TestDescAndFn] = &[ ... the list of tests in the crate ... - } + ]; } */ -fn mk_test_module(cx: @mut TestCtxt) -> @ast::item { +fn mk_std(cx: &TestCtxt) -> @ast::view_item { + let vers = ast::lit_str(@~"0.6"); + let vers = nospan(vers); + let mi = ast::meta_name_value(~"vers", vers); + let mi = nospan(mi); + let id_std = cx.sess.ident_of(~"std"); + let vi = if is_std(cx) { + ast::view_item_import( + ~[@nospan(ast::view_path_simple(id_std, + path_node(~[id_std]), + ast::type_value_ns, + cx.sess.next_node_id()))]) + } else { + ast::view_item_use(id_std, ~[@mi], + cx.sess.next_node_id()) + }; + let vi = ast::view_item { + node: vi, + attrs: ~[], + vis: ast::private, + span: dummy_sp() + }; + return @vi; +} + +fn mk_test_module(cx: &TestCtxt) -> @ast::item { + // Link to std - let std = mk_std(cx); - let view_items = if is_std(cx) { ~[] } else { ~[std] }; - // A function that generates a vector of test descriptors to feed to the - // test runner - let testsfn = mk_tests(cx); + let view_items = ~[mk_std(cx)]; + + // A constant vector of test descriptors. + let tests = mk_tests(cx); + // The synthesized main function which will call the console test runner // with our list of tests - let mainfn = mk_main(cx); + let ext_cx = cx.ext_cx; + let mainfn = (quote_item!( + pub fn main() { + #[main]; + std::test::test_main_static(::os::args(), tests); + } + )).get(); + let testmod = ast::_mod { view_items: view_items, - items: ~[mainfn, testsfn], + items: ~[mainfn, tests], }; let item_ = ast::item_mod(testmod); + // This attribute tells resolve to let us call unexported functions let resolve_unexported_attr = attr::mk_attr(attr::mk_word_item(~"!resolve_unexported")); - let sess = cx.sess; + let item = ast::item { - ident: sess.ident_of(~"__test"), + ident: cx.sess.ident_of(~"__test"), attrs: ~[resolve_unexported_attr], - id: sess.next_node_id(), + id: cx.sess.next_node_id(), node: item_, vis: ast::public, span: dummy_sp(), - }; + }; debug!("Synthetic test module:\n%s\n", - pprust::item_to_str(@copy item, sess.intr())); + pprust::item_to_str(@copy item, cx.sess.intr())); return @item; } @@ -258,10 +333,10 @@ fn nospan(t: T) -> codemap::spanned { fn path_node(+ids: ~[ast::ident]) -> @ast::path { @ast::path { span: dummy_sp(), - global: false, - idents: ids, - rp: None, - types: ~[] } + global: false, + idents: ids, + rp: None, + types: ~[] } } fn path_node_global(+ids: ~[ast::ident]) -> @ast::path { @@ -272,56 +347,22 @@ fn path_node_global(+ids: ~[ast::ident]) -> @ast::path { types: ~[] } } -fn mk_std(cx: @mut TestCtxt) -> @ast::view_item { - let vers = ast::lit_str(@~"0.6"); - let vers = nospan(vers); - let mi = ast::meta_name_value(~"vers", vers); - let mi = nospan(mi); - let sess = cx.sess; - let vi = ast::view_item_use(sess.ident_of(~"std"), - ~[@mi], - sess.next_node_id()); - let vi = ast::view_item { - node: vi, - attrs: ~[], - vis: ast::private, - span: dummy_sp() - }; - return @vi; -} +fn mk_tests(cx: &TestCtxt) -> @ast::item { -fn mk_tests(cx: @mut TestCtxt) -> @ast::item { - let ret_ty = mk_test_desc_and_fn_vec_ty(cx); - - let decl = ast::fn_decl { - inputs: ~[], - output: ret_ty, - cf: ast::return_val, - }; + let ext_cx = cx.ext_cx; // The vector of test_descs for this crate - let test_descs = mk_test_desc_and_fn_vec(cx); + let test_descs = mk_test_descs(cx); - let sess = cx.sess; - let body_: ast::blk_ = default_block(~[], - option::Some(test_descs), - sess.next_node_id()); - let body = nospan(body_); - - let item_ = ast::item_fn(decl, ast::impure_fn, ~[], body); - let item = ast::item { - ident: sess.ident_of(~"tests"), - attrs: ~[], - id: sess.next_node_id(), - node: item_, - vis: ast::public, - span: dummy_sp(), - }; - return @item; + (quote_item!( + pub const tests : &static/[self::std::test::TestDescAndFn] = + $test_descs + ; + )).get() } -fn is_std(cx: @mut TestCtxt) -> bool { +fn is_std(cx: &TestCtxt) -> bool { let is_std = { let items = attr::find_linkage_metas(cx.crate.node.attrs); match attr::last_meta_item_value_str_by_name(items, ~"name") { @@ -332,55 +373,11 @@ fn is_std(cx: @mut TestCtxt) -> bool { return is_std; } -fn mk_path(cx: @mut TestCtxt, +path: ~[ast::ident]) -> @ast::path { - // For tests that are inside of std we don't want to prefix - // the paths with std:: - let sess = cx.sess; - if is_std(cx) { - path_node_global(path) - } else { - path_node(~[ sess.ident_of(~"self"), sess.ident_of(~"std") ] + path) - } -} - -// The ast::Ty of ~[std::test::test_desc] -fn mk_test_desc_and_fn_vec_ty(cx: @mut TestCtxt) -> @ast::Ty { - let sess = cx.sess; - let test_desc_and_fn_ty_path = mk_path(cx, ~[ - sess.ident_of(~"test"), - sess.ident_of(~"TestDescAndFn") - ]); - - let test_desc_and_fn_ty = ast::Ty { - id: sess.next_node_id(), - node: ast::ty_path(test_desc_and_fn_ty_path, sess.next_node_id()), - span: dummy_sp(), - }; - - let vec_mt = ast::mt {ty: @test_desc_and_fn_ty, - mutbl: ast::m_imm}; - - let inner_ty = @ast::Ty { - id: sess.next_node_id(), - node: ast::ty_vec(vec_mt), - span: dummy_sp(), - }; - - @ast::Ty { - id: sess.next_node_id(), - node: ast::ty_uniq(ast::mt { ty: inner_ty, mutbl: ast::m_imm }), - span: dummy_sp(), - } -} - -fn mk_test_desc_and_fn_vec(cx: @mut TestCtxt) -> @ast::expr { +fn mk_test_descs(cx: &TestCtxt) -> @ast::expr { debug!("building test vector from %u tests", cx.testfns.len()); let mut descs = ~[]; - { - let testfns = &mut cx.testfns; - for testfns.each |test| { - descs.push(mk_test_desc_and_fn_rec(cx, *test)); - } + for cx.testfns.each |test| { + descs.push(mk_test_desc_and_fn_rec(cx, *test)); } let sess = cx.sess; @@ -394,223 +391,70 @@ fn mk_test_desc_and_fn_vec(cx: @mut TestCtxt) -> @ast::expr { @ast::expr { id: sess.next_node_id(), callee_id: sess.next_node_id(), - node: ast::expr_vstore(inner_expr, ast::expr_vstore_uniq), + node: ast::expr_vstore(inner_expr, ast::expr_vstore_slice), span: dummy_sp(), } } -fn mk_test_desc_and_fn_rec(cx: @mut TestCtxt, test: test) -> @ast::expr { +fn mk_test_desc_and_fn_rec(cx: &TestCtxt, test: test) -> @ast::expr { let span = test.span; let path = /*bad*/copy test.path; - let sess = cx.sess; - debug!("encoding %s", - ast_util::path_name_i(path, sess.parse_sess.interner)); + let ext_cx = cx.ext_cx; + + debug!("encoding %s", ast_util::path_name_i(path, + cx.sess.parse_sess.interner)); let name_lit: ast::lit = nospan(ast::lit_str(@ast_util::path_name_i( path, - sess.parse_sess.interner))); + cx.sess.parse_sess.interner))); - let name_expr_inner = @ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_lit(@name_lit), - span: span, + let name_expr = @ast::expr { + id: cx.sess.next_node_id(), + callee_id: cx.sess.next_node_id(), + node: ast::expr_lit(@name_lit), + span: span }; - let name_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_vstore(name_expr_inner, ast::expr_vstore_uniq), - span: dummy_sp(), - }; - - let name_field = nospan(ast::field_ { - mutbl: ast::m_imm, - ident: sess.ident_of(~"name"), - expr: @name_expr, - }); - - let ignore_lit: ast::lit = nospan(ast::lit_bool(test.ignore)); - - let ignore_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_lit(@ignore_lit), - span: span, - }; - - let ignore_field = nospan(ast::field_ { - mutbl: ast::m_imm, - ident: sess.ident_of(~"ignore"), - expr: @ignore_expr, - }); - - let fail_lit: ast::lit = nospan(ast::lit_bool(test.should_fail)); - - let fail_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_lit(@fail_lit), - span: span, - }; - - let fail_field = nospan(ast::field_ { - mutbl: ast::m_imm, - ident: sess.ident_of(~"should_fail"), - expr: @fail_expr, - }); - - let test_desc_path = - mk_path(cx, ~[ sess.ident_of(~"test"), sess.ident_of(~"TestDesc") ]); - - let desc_rec_ = ast::expr_struct( - test_desc_path, - ~[name_field, ignore_field, fail_field], - option::None - ); - - let desc_rec = @ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: desc_rec_, - span: span, - }; - - let desc_field = nospan(ast::field_ { - mutbl: ast::m_imm, - ident: sess.ident_of(~"desc"), - expr: desc_rec - }); - let fn_path = path_node_global(path); let fn_expr = @ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), + id: cx.sess.next_node_id(), + callee_id: cx.sess.next_node_id(), node: ast::expr_path(fn_path), span: span, }; - let fn_field = nospan(ast::field_ { - mutbl: ast::m_imm, - ident: sess.ident_of(~"testfn"), - expr: fn_expr, - }); - - let test_desc_and_fn_path = - mk_path(cx, ~[sess.ident_of(~"test"), - sess.ident_of(~"TestDescAndFn")]); - - let desc_and_fn_rec = @ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_struct(test_desc_and_fn_path, - ~[fn_field, desc_field], - option::None), - span: span, + let t_expr = if test.bench { + quote_expr!( self::std::test::StaticBenchFn($fn_expr) ) + } else { + quote_expr!( self::std::test::StaticTestFn($fn_expr) ) }; - return desc_and_fn_rec; -} - -fn mk_main(cx: @mut TestCtxt) -> @ast::item { - let sess = cx.sess; - let ret_ty = ast::Ty { - id: sess.next_node_id(), - node: ast::ty_nil, - span: dummy_sp(), + let ignore_expr = if test.ignore { + quote_expr!( true ) + } else { + quote_expr!( false ) }; - let decl = ast::fn_decl { - inputs: ~[], - output: @ret_ty, - cf: ast::return_val, + let fail_expr = if test.should_fail { + quote_expr!( true ) + } else { + quote_expr!( false ) }; - let test_main_call_expr = mk_test_main_call(cx); - - let body_: ast::blk_ = - default_block(~[], - option::Some(test_main_call_expr), - sess.next_node_id()); - let body = codemap::spanned { node: body_, span: dummy_sp() }; - - let item_ = ast::item_fn(decl, ast::impure_fn, ~[], body); - let item = ast::item { - ident: sess.ident_of(~"main"), - attrs: ~[attr::mk_attr(attr::mk_word_item(~"main"))], - id: sess.next_node_id(), - node: item_, - vis: ast::public, - span: dummy_sp(), - }; - return @item; -} - -fn mk_test_main_call(cx: @mut TestCtxt) -> @ast::expr { - // Call os::args to generate the vector of test_descs - let sess = cx.sess; - let args_path = path_node_global(~[ - sess.ident_of(~"os"), - sess.ident_of(~"args") - ]); - - let args_path_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_path(args_path), - span: dummy_sp(), - }; - - let args_call_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_call(@args_path_expr, ~[], ast::NoSugar), - span: dummy_sp(), - }; - - // Call __test::test to generate the vector of test_descs - let test_path = path_node(~[ sess.ident_of(~"tests") ]); - - let test_path_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_path(test_path), - span: dummy_sp(), - }; - - let test_call_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_call(@test_path_expr, ~[], ast::NoSugar), - span: dummy_sp(), - }; - - // Call std::test::test_main - let test_main_path = mk_path(cx, ~[ - sess.ident_of(~"test"), - sess.ident_of(~"test_main") - ]); - - let test_main_path_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_path(test_main_path), - span: dummy_sp(), - }; - - let test_main_call_expr = ast::expr { - id: sess.next_node_id(), - callee_id: sess.next_node_id(), - node: ast::expr_call(@test_main_path_expr, - ~[@args_call_expr, @test_call_expr], - ast::NoSugar), - span: dummy_sp(), - }; - - return @test_main_call_expr; + let e = quote_expr!( + self::std::test::TestDescAndFn { + desc: self::std::test::TestDesc { + name: self::std::test::StaticTestName($name_expr), + ignore: $ignore_expr, + should_fail: $fail_expr + }, + testfn: $t_expr, + } + ); + e } // Local Variables: diff --git a/src/librustc/middle/check_const.rs b/src/librustc/middle/check_const.rs index 6cca576fa13..8890da1587d 100644 --- a/src/librustc/middle/check_const.rs +++ b/src/librustc/middle/check_const.rs @@ -127,15 +127,15 @@ pub fn check_expr(sess: Session, items without type parameters"); } match def_map.find(&e.id) { - Some(def_const(def_id)) | - Some(def_fn(def_id, _)) | - Some(def_variant(_, def_id)) | - Some(def_struct(def_id)) => { + Some(def_variant(_, _)) | + Some(def_struct(_)) => { } + + Some(def_const(def_id)) | + Some(def_fn(def_id, _)) => { if !ast_util::is_local(def_id) { sess.span_err( e.span, ~"paths in constants may only refer to \ - crate-local constants, functions, or \ - structs"); + crate-local constants or functions"); } } Some(def) => { diff --git a/src/libstd/stats.rs b/src/libstd/stats.rs index f0043803c4b..2048cb6c59f 100644 --- a/src/libstd/stats.rs +++ b/src/libstd/stats.rs @@ -52,7 +52,7 @@ impl &[f64] : Stats { fn median(self) -> f64 { assert self.len() != 0; - let tmp = vec::to_mut(vec::from_slice(self)); + let tmp = vec::cast_to_mut(vec::from_slice(self)); sort::tim_sort(tmp); if tmp.len() & 1 == 0 { let m = tmp.len() / 2; diff --git a/src/libstd/std.rc b/src/libstd/std.rc index 3b855b3453d..3c2baae6d57 100644 --- a/src/libstd/std.rc +++ b/src/libstd/std.rc @@ -115,6 +115,7 @@ pub mod serialize; #[doc(hidden)] // FIXME #3538 mod std { pub use serialize; + pub use test; } // Local Variables: diff --git a/src/libstd/test.rs b/src/libstd/test.rs index e83b759f901..f3e96826a8e 100644 --- a/src/libstd/test.rs +++ b/src/libstd/test.rs @@ -20,6 +20,8 @@ use sort; use term; use core::cmp::Eq; + +use core::to_str::ToStr; use core::either::Either; use core::either; use core::io::WriterUtil; @@ -43,13 +45,62 @@ extern mod rustrt { // paths; i.e. it should be a series of identifiers seperated by double // colons. This way if some test runner wants to arrange the tests // hierarchically it may. -pub type TestName = ~str; + +#[cfg(stage0)] +pub enum TestName { + // Stage0 doesn't understand sendable &static/str yet + StaticTestName(&static/[u8]), + DynTestName(~str) +} + +#[cfg(stage0)] +impl ToStr for TestName { + pure fn to_str(&self) -> ~str { + match self { + &StaticTestName(s) => str::from_bytes(s), + &DynTestName(s) => s.to_str() + } + } +} + +#[cfg(stage1)] +#[cfg(stage2)] +#[cfg(stage3)] +pub enum TestName { + StaticTestName(&static/str), + DynTestName(~str) +} + +#[cfg(stage1)] +#[cfg(stage2)] +#[cfg(stage3)] +impl ToStr for TestName { + pure fn to_str(&self) -> ~str { + match self { + &StaticTestName(s) => s.to_str(), + &DynTestName(s) => s.to_str() + } + } +} // 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 type TestFn = ~fn(); +pub enum TestFn { + StaticTestFn(extern fn()), + StaticBenchFn(extern fn(&mut BenchHarness)), + DynTestFn(~fn()), + DynBenchFn(~fn(&mut BenchHarness)) +} + +// Structure passed to BenchFns +pub struct BenchHarness { + iterations: u64, + ns_start: u64, + ns_end: u64, + bytes: u64 +} // The definition of a single test. A test runner will run a list of // these. @@ -65,7 +116,7 @@ pub struct TestDescAndFn { } // The default console test runner. It accepts the command line -// arguments and a vector of test_descs (generated at compile time). +// arguments and a vector of test_descs. pub fn test_main(args: &[~str], tests: ~[TestDescAndFn]) { let opts = match parse_opts(args) { @@ -75,10 +126,38 @@ pub fn test_main(args: &[~str], tests: ~[TestDescAndFn]) { if !run_tests_console(&opts, tests) { die!(~"Some tests failed"); } } +// 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 = do tests.map |t| { + match t.testfn { + StaticTestFn(f) => + TestDescAndFn { testfn: StaticTestFn(f), desc: copy t.desc }, + + StaticBenchFn(f) => + TestDescAndFn { testfn: StaticBenchFn(f), desc: copy t.desc }, + + _ => { + die! (~"non-static tests passed to test::test_main_static"); + } + } + }; + test_main(args, owned_tests) +} + pub struct TestOpts { filter: Option<~str>, run_ignored: bool, - logfile: Option<~str>, + run_tests: bool, + run_benchmarks: bool, + save_results: Option, + compare_results: Option, + logfile: Option } type OptRes = Either; @@ -86,7 +165,12 @@ type OptRes = Either; // Parses command line arguments into test options pub fn parse_opts(args: &[~str]) -> OptRes { let args_ = vec::tail(args); - let opts = ~[getopts::optflag(~"ignored"), getopts::optopt(~"logfile")]; + let opts = ~[getopts::optflag(~"ignored"), + getopts::optflag(~"test"), + getopts::optflag(~"bench"), + getopts::optopt(~"save"), + getopts::optopt(~"diff"), + getopts::optopt(~"logfile")]; let matches = match getopts::getopts(args_, opts) { Ok(move m) => m, @@ -99,19 +183,41 @@ pub fn parse_opts(args: &[~str]) -> OptRes { } else { option::None }; let run_ignored = getopts::opt_present(&matches, ~"ignored"); + let logfile = getopts::opt_maybe_str(&matches, ~"logfile"); + let logfile = logfile.map(|s| Path(*s)); + + let run_benchmarks = getopts::opt_present(&matches, ~"bench"); + let run_tests = ! run_benchmarks || + getopts::opt_present(&matches, ~"test"); + + let save_results = getopts::opt_maybe_str(&matches, ~"save"); + let save_results = save_results.map(|s| Path(*s)); + + let compare_results = getopts::opt_maybe_str(&matches, ~"diff"); + let compare_results = compare_results.map(|s| Path(*s)); let test_opts = TestOpts { filter: filter, run_ignored: run_ignored, - logfile: logfile, + run_tests: run_tests, + run_benchmarks: run_benchmarks, + save_results: save_results, + compare_results: compare_results, + logfile: logfile }; either::Left(test_opts) } #[deriving_eq] -pub enum TestResult { TrOk, TrFailed, TrIgnored, } +pub struct BenchSamples { + ns_iter_samples: ~[f64], + mb_s: uint +} + +#[deriving_eq] +pub enum TestResult { TrOk, TrFailed, TrIgnored, TrBench(BenchSamples) } struct ConsoleTestState { out: io::Writer, @@ -121,6 +227,7 @@ struct ConsoleTestState { mut passed: uint, mut failed: uint, mut ignored: uint, + mut benchmarked: uint, mut failures: ~[TestDesc] } @@ -137,7 +244,7 @@ pub fn run_tests_console(opts: &TestOpts, st.out.write_line(fmt!("\nrunning %u %s", st.total, noun)); } TeWait(ref test) => st.out.write_str( - fmt!("test %s ... ", test.name)), + fmt!("test %s ... ", test.name.to_str())), TeResult(copy test, result) => { match st.log_out { Some(f) => write_log(f, result, &test), @@ -160,14 +267,21 @@ pub fn run_tests_console(opts: &TestOpts, write_ignored(st.out, st.use_color); st.out.write_line(~""); } + TrBench(bs) => { + st.benchmarked += 1u; + write_bench(st.out, st.use_color); + st.out.write_line(fmt!(": %s", + fmt_bench_samples(&bs))); + } } } } } let log_out = match opts.logfile { - Some(ref path) => match io::file_writer(&Path(*path), - ~[io::Create, io::Truncate]) { + Some(ref path) => match io::file_writer(path, + ~[io::Create, + io::Truncate]) { result::Ok(w) => Some(w), result::Err(ref s) => { die!(fmt!("can't open output file: %s", *s)) @@ -176,20 +290,23 @@ pub fn run_tests_console(opts: &TestOpts, None => None }; - let st = - @ConsoleTestState{out: io::stdout(), - log_out: log_out, - use_color: use_color(), - mut total: 0, - mut passed: 0, - mut failed: 0, - mut ignored: 0, - mut failures: ~[]}; + let st = @ConsoleTestState { + out: io::stdout(), + log_out: log_out, + use_color: use_color(), + mut total: 0u, + mut passed: 0u, + mut failed: 0u, + mut ignored: 0u, + mut benchmarked: 0u, + mut failures: ~[] + }; run_tests(opts, tests, |x| callback(&x, st)); - assert (st.passed + st.failed + st.ignored == st.total); - let success = st.failed == 0; + assert (st.passed + st.failed + + st.ignored + st.benchmarked == st.total); + let success = st.failed == 0u; if !success { print_failures(st); @@ -199,19 +316,36 @@ pub fn run_tests_console(opts: &TestOpts, if success { // There's no parallelism at this point so it's safe to use color write_ok(st.out, true); - } else { write_failed(st.out, true); } - st.out.write_str(fmt!(". %u passed; %u failed; %u ignored\n\n", st.passed, - st.failed, st.ignored)); + } else { + write_failed(st.out, true); + } + st.out.write_str(fmt!(". %u passed; %u failed; %u ignored\n\n", + st.passed, st.failed, st.ignored)); return success; + fn fmt_bench_samples(bs: &BenchSamples) -> ~str { + use stats::Stats; + if bs.mb_s != 0 { + fmt!("%u ns/iter (+/- %u) = %u MB/s", + bs.ns_iter_samples.median() as uint, + 3 * (bs.ns_iter_samples.median_abs_dev() as uint), + bs.mb_s) + } else { + fmt!("%u ns/iter (+/- %u)", + bs.ns_iter_samples.median() as uint, + 3 * (bs.ns_iter_samples.median_abs_dev() as uint)) + } + } + fn write_log(out: io::Writer, result: TestResult, test: &TestDesc) { out.write_line(fmt!("%s %s", match result { TrOk => ~"ok", TrFailed => ~"failed", - TrIgnored => ~"ignored" - }, test.name)); + TrIgnored => ~"ignored", + TrBench(ref bs) => fmt_bench_samples(bs) + }, test.name.to_str())); } fn write_ok(out: io::Writer, use_color: bool) { @@ -226,6 +360,10 @@ pub fn run_tests_console(opts: &TestOpts, write_pretty(out, ~"ignored", term::color_yellow, use_color); } + fn write_bench(out: io::Writer, use_color: bool) { + write_pretty(out, ~"bench", term::color_cyan, use_color); + } + fn write_pretty(out: io::Writer, word: &str, color: u8, use_color: bool) { if use_color && term::color_supported() { term::fg(out, color); @@ -239,11 +377,10 @@ pub fn run_tests_console(opts: &TestOpts, fn print_failures(st: @ConsoleTestState) { st.out.write_line(~"\nfailures:"); - let failures = copy st.failures; - let failures = vec::map(failures, |test| test.name); - let failures = do sort::merge_sort(failures) |x, y| { str::le(*x, *y) }; + let failures = vec::cast_to_mut(st.failures.map(|t| t.name.to_str())); + sort::tim_sort(failures); for vec::each(failures) |name| { - st.out.write_line(fmt!(" %s", *name)); + st.out.write_line(fmt!(" %s", name.to_str())); } } @@ -253,26 +390,28 @@ fn should_sort_failures_before_printing_them() { let s = do io::with_str_writer |wr| { let test_a = TestDesc { - name: ~"a", + name: StaticTestName("a"), ignore: false, should_fail: false }; let test_b = TestDesc { - name: ~"b", + name: StaticTestName("b"), ignore: false, should_fail: false }; - let st = - @ConsoleTestState{out: wr, - log_out: option::None, - use_color: false, - mut total: 0, - mut passed: 0, - mut failed: 0, - mut ignored: 0, - mut failures: ~[move test_b, move test_a]}; + let st = @ConsoleTestState { + out: wr, + log_out: option::None, + use_color: false, + mut total: 0u, + mut passed: 0u, + mut failed: 0u, + mut ignored: 0u, + mut benchmarked: 0u, + mut failures: ~[move test_b, move test_a] + }; print_failures(st); }; @@ -300,6 +439,15 @@ fn run_tests(opts: &TestOpts, let filtered_descs = filtered_tests.map(|t| t.desc); callback(TeFiltered(filtered_descs)); + let mut (filtered_tests, + filtered_benchs) = + do vec::partition(filtered_tests) |e| { + match e.testfn { + StaticTestFn(_) | DynTestFn(_) => true, + StaticBenchFn(_) | DynBenchFn(_) => 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(); @@ -321,7 +469,7 @@ fn run_tests(opts: &TestOpts, // that hang forever. callback(TeWait(test.desc)); } - run_test(test, ch.clone()); + run_test(!opts.run_tests, test, ch.clone()); pending += 1; } @@ -332,6 +480,14 @@ fn run_tests(opts: &TestOpts, callback(TeResult(desc, result)); pending -= 1; } + + // All benchmarks run at the end, in serial. + do vec::consume(filtered_benchs) |_, b| { + callback(TeWait(copy b.desc)); + run_test(!opts.run_benchmarks, b, ch.clone()); + let (test, result) = p.recv(); + callback(TeResult(move test, result)); + } } // Windows tends to dislike being overloaded with threads. @@ -368,7 +524,7 @@ pub fn filter_tests( fn filter_fn(test: TestDescAndFn, filter_str: &str) -> Option { - if str::contains(test.desc.name, filter_str) { + if str::contains(test.desc.name.to_str(), filter_str) { return option::Some(test); } else { return option::None; } } @@ -391,13 +547,12 @@ pub fn filter_tests( None } }; - vec::filter_map(filtered, |x| filter(x)) }; // Sort the tests alphabetically pure fn lteq(t1: &TestDescAndFn, t2: &TestDescAndFn) -> bool { - str::le(t1.desc.name, t2.desc.name) + str::le(t1.desc.name.to_str(), t2.desc.name.to_str()) } sort::quick_sort(filtered, lteq); @@ -409,24 +564,47 @@ struct TestFuture { wait: fn@() -> TestResult, } -pub fn run_test(test: TestDescAndFn, monitor_ch: SharedChan) { +pub fn run_test(force_ignore: bool, + test: TestDescAndFn, + monitor_ch: SharedChan) { + let TestDescAndFn {desc, testfn} = test; - if desc.ignore { + if force_ignore || desc.ignore { monitor_ch.send((desc, TrIgnored)); return; } - let testfn_cell = ::cell::Cell(testfn); - do task::spawn { - let mut result_future = None; // task::future_result(builder); - task::task().unlinked().future_result(|+r| { - result_future = Some(move r); - }).spawn(testfn_cell.take()); - let task_result = option::unwrap(move result_future).recv(); - let test_result = calc_result(&desc, task_result == task::Success); - monitor_ch.send((desc, test_result)); - }; + fn run_test_inner(desc: TestDesc, + monitor_ch: SharedChan, + testfn: ~fn()) { + let testfn_cell = ::cell::Cell(testfn); + do task::spawn { + let mut result_future = None; // task::future_result(builder); + task::task().unlinked().future_result(|+r| { + result_future = Some(move r); + }).spawn(testfn_cell.take()); + let task_result = option::unwrap(move result_future).recv(); + let test_result = calc_result(&desc, + task_result == task::Success); + monitor_ch.send((desc, test_result)); + } + } + + match testfn { + DynBenchFn(benchfn) => { + let bs = ::test::bench::benchmark(benchfn); + monitor_ch.send((desc, TrBench(bs))); + return; + } + StaticBenchFn(benchfn) => { + let bs = ::test::bench::benchmark(benchfn); + monitor_ch.send((desc, TrBench(bs))); + return; + } + DynTestFn(f) => run_test_inner(desc, monitor_ch, f), + StaticTestFn(f) => run_test_inner(desc, monitor_ch, || f()) + } } fn calc_result(desc: &TestDesc, task_succeeded: bool) -> TestResult { @@ -439,10 +617,180 @@ fn calc_result(desc: &TestDesc, task_succeeded: bool) -> TestResult { } } +pub mod bench { + + use rand; + use u64; + use vec; + use time::precise_time_ns; + use test::{BenchHarness, BenchSamples}; + use stats::Stats; + use num; + use rand; + + pub impl BenchHarness { + + /// Callback for benchmark functions to run in their body. + pub fn iter(&mut self, inner:&fn()) { + self.ns_start = precise_time_ns(); + let k = self.iterations; + for u64::range(0, k) |_| { + inner(); + } + self.ns_end = precise_time_ns(); + } + + fn ns_elapsed(&mut self) -> u64 { + if self.ns_start == 0 || self.ns_end == 0 { + 0 + } else { + self.ns_end - self.ns_start + } + } + + fn ns_per_iter(&mut self) -> u64 { + if self.iterations == 0 { + 0 + } else { + self.ns_elapsed() / self.iterations + } + } + + fn bench_n(&mut self, n: u64, f: &fn(&mut BenchHarness)) { + self.iterations = n; + debug!("running benchmark for %u iterations", + n as uint); + f(self); + } + + // This is the Go benchmark algorithm. It produces a single + // datapoint and always tries to run for 1s. + pub fn go_bench(&mut self, f: &fn(&mut BenchHarness)) { + + // Rounds a number down to the nearest power of 10. + fn round_down_10(n: u64) -> u64 { + let mut n = n; + let mut res = 1; + while n > 10 { + n = n / 10; + res *= 10; + } + res + } + + // Rounds x up to a number of the form [1eX, 2eX, 5eX]. + fn round_up(n: u64) -> u64 { + let base = round_down_10(n); + if n < (2 * base) { + 2 * base + } else if n < (5 * base) { + 5 * base + } else { + 10 * base + } + } + + // Initial bench run to get ballpark figure. + let mut n = 1_u64; + self.bench_n(n, f); + + while n < 1_000_000_000 && + self.ns_elapsed() < 1_000_000_000 { + let last = n; + + // Try to estimate iter count for 1s falling back to 1bn + // iterations if first run took < 1ns. + if self.ns_per_iter() == 0 { + n = 1_000_000_000; + } else { + n = 1_000_000_000 / self.ns_per_iter(); + } + + n = u64::max(u64::min(n+n/2, 100*last), last+1); + n = round_up(n); + self.bench_n(n, f); + } + } + + // This is a more statistics-driven benchmark algorithm. + // It stops as quickly as 50ms, so long as the statistical + // properties are satisfactory. If those properties are + // not met, it may run as long as the Go algorithm. + pub fn auto_bench(&mut self, f: &fn(&mut BenchHarness)) -> ~[f64] { + + let rng = rand::Rng(); + let mut magnitude = 10; + let mut prev_madp = 0.0; + + loop { + + let n_samples = rng.gen_uint_range(50, 60); + let n_iter = rng.gen_uint_range(magnitude, + magnitude * 2); + + let samples = do vec::from_fn(n_samples) |_| { + self.bench_n(n_iter as u64, f); + self.ns_per_iter() as f64 + }; + + // Eliminate outliers + let med = samples.median(); + let mad = samples.median_abs_dev(); + let samples = do vec::filter(samples) |f| { + num::abs(*f - med) <= 3.0 * mad + }; + + debug!("%u samples, median %f, MAD=%f, %u survived filter", + n_samples, med as float, mad as float, + samples.len()); + + if samples.len() != 0 { + // If we have _any_ cluster of signal... + let curr_madp = samples.median_abs_dev_pct(); + if self.ns_elapsed() > 1_000_000 && + (curr_madp < 1.0 || + num::abs(curr_madp - prev_madp) < 0.1) { + return samples; + } + prev_madp = curr_madp; + + if n_iter > 20_000_000 || + self.ns_elapsed() > 20_000_000 { + return samples; + } + } + + magnitude *= 2; + } + } + } + + pub fn benchmark(f: &fn(&mut BenchHarness)) -> BenchSamples { + + let mut bs = BenchHarness { + iterations: 0, + ns_start: 0, + ns_end: 0, + bytes: 0 + }; + + let ns_iter_samples = bs.auto_bench(f); + + let iter_s = 1_000_000_000 / (ns_iter_samples.median() as u64); + let mb_s = (bs.bytes * iter_s) / 1_000_000; + + BenchSamples { + ns_iter_samples: ns_iter_samples, + mb_s: mb_s as uint + } + } +} + #[cfg(test)] mod tests { use test::{TrFailed, TrIgnored, TrOk, filter_tests, parse_opts, - TestDesc, TestDescAndFn}; + TestDesc, TestDescAndFn, + StaticTestName, DynTestName, DynTestFn}; use test::{TestOpts, run_test}; use core::either; @@ -455,15 +803,15 @@ mod tests { fn f() { die!(); } let desc = TestDescAndFn { desc: TestDesc { - name: ~"whatever", + name: StaticTestName("whatever"), ignore: true, should_fail: false }, - testfn: f, + testfn: DynTestFn(fn~() { f()}), }; let (p, ch) = stream(); let ch = SharedChan(ch); - run_test(desc, ch); + run_test(false, desc, ch); let (_, res) = p.recv(); assert res != TrOk; } @@ -473,15 +821,15 @@ mod tests { fn f() { } let desc = TestDescAndFn { desc: TestDesc { - name: ~"whatever", + name: StaticTestName("whatever"), ignore: true, should_fail: false }, - testfn: f, + testfn: DynTestFn(fn~() { f()}), }; let (p, ch) = stream(); let ch = SharedChan(ch); - run_test(desc, ch); + run_test(false, desc, ch); let (_, res) = p.recv(); assert res == TrIgnored; } @@ -492,15 +840,15 @@ mod tests { fn f() { die!(); } let desc = TestDescAndFn { desc: TestDesc { - name: ~"whatever", + name: StaticTestName("whatever"), ignore: false, should_fail: true }, - testfn: f, + testfn: DynTestFn(fn~() { f() }), }; let (p, ch) = stream(); let ch = SharedChan(ch); - run_test(desc, ch); + run_test(false, desc, ch); let (_, res) = p.recv(); assert res == TrOk; } @@ -510,15 +858,15 @@ mod tests { fn f() { } let desc = TestDescAndFn { desc: TestDesc { - name: ~"whatever", + name: StaticTestName("whatever"), ignore: false, should_fail: true }, - testfn: f, + testfn: DynTestFn(fn~() { f() }), }; let (p, ch) = stream(); let ch = SharedChan(ch); - run_test(desc, ch); + run_test(false, desc, ch); let (_, res) = p.recv(); assert res == TrFailed; } @@ -554,30 +902,34 @@ mod tests { filter: option::None, run_ignored: true, logfile: option::None, + run_tests: true, + run_benchmarks: false, + save_results: option::None, + compare_results: option::None }; let tests = ~[ TestDescAndFn { desc: TestDesc { - name: ~"1", + name: StaticTestName("1"), ignore: true, should_fail: false, }, - testfn: dummy, + testfn: DynTestFn(fn~() { }), }, TestDescAndFn { desc: TestDesc { - name: ~"2", + name: StaticTestName("2"), ignore: false, should_fail: false }, - testfn: dummy, + testfn: DynTestFn(fn~() { }), }, ]; let filtered = filter_tests(&opts, tests); assert (vec::len(filtered) == 1); - assert (filtered[0].desc.name == ~"1"); + assert (filtered[0].desc.name.to_str() == ~"1"); assert (filtered[0].desc.ignore == false); } @@ -587,6 +939,10 @@ mod tests { filter: option::None, run_ignored: false, logfile: option::None, + run_tests: true, + run_benchmarks: false, + save_results: option::None, + compare_results: option::None }; let names = @@ -603,10 +959,11 @@ mod tests { for vec::each(names) |name| { let test = TestDescAndFn { desc: TestDesc { - name: *name, ignore: false, + name: DynTestName(*name), + ignore: false, should_fail: false }, - testfn: testfn, + testfn: DynTestFn(copy testfn), }; tests.push(move test); } @@ -627,7 +984,7 @@ mod tests { for vec::each(pairs) |p| { match *p { - (ref a, ref b) => { assert (*a == b.desc.name); } + (ref a, ref b) => { assert (*a == b.desc.name.to_str()); } } } } diff --git a/src/test/run-pass/test-ignore-cfg.rs b/src/test/run-pass/test-ignore-cfg.rs index 1f4df7be1f3..6028d8c71d3 100644 --- a/src/test/run-pass/test-ignore-cfg.rs +++ b/src/test/run-pass/test-ignore-cfg.rs @@ -26,13 +26,13 @@ fn shouldnotignore() { #[test] fn checktests() { // Pull the tests out of the secreturn test module - let tests = __test::tests(); + let tests = __test::tests; assert vec::any( tests, - |t| t.desc.name == ~"shouldignore" && t.desc.ignore); + |t| t.desc.name.to_str() == ~"shouldignore" && t.desc.ignore); assert vec::any( tests, - |t| t.desc.name == ~"shouldnotignore" && !t.desc.ignore); + |t| t.desc.name.to_str() == ~"shouldnotignore" && !t.desc.ignore); } \ No newline at end of file