0
0
mirror of https://github.com/rust-lang/rust.git synced 2024-12-01 13:18:54 +01:00

Convert the test runners to typesafe spawn

Issue #1022
This commit is contained in:
Brian Anderson 2011-10-13 16:42:43 -07:00
parent 1abebf042a
commit 3b54dcfa79
5 changed files with 153 additions and 144 deletions

View File

@ -224,10 +224,19 @@ fn empty_fn_ty() -> ast::ty {
// The ast::ty of [std::test::test_desc]
fn mk_test_desc_vec_ty(cx: test_ctxt) -> @ast::ty {
let test_fn_ty: ast::ty = nospan(
ast::ty_path(
nospan({
global: false,
idents: ["std", "test", "default_test_fn"],
types: []
}),
cx.next_node_id()));
let test_desc_ty_path: ast::path =
nospan({global: false,
idents: ["std", "test", "test_desc"],
types: []});
types: [@test_fn_ty]});
let test_desc_ty: ast::ty =
nospan(ast::ty_path(test_desc_ty_path, cx.next_node_id()));
@ -273,8 +282,10 @@ fn mk_test_desc_rec(cx: test_ctxt, test: test) -> @ast::expr {
node: ast::expr_path(fn_path),
span: span};
let fn_wrapper_expr = mk_test_wrapper(cx, fn_expr, span);
let fn_field: ast::field =
nospan({mut: ast::imm, ident: "fn", expr: @fn_expr});
nospan({mut: ast::imm, ident: "fn", expr: fn_wrapper_expr});
let ignore_lit: ast::lit = nospan(ast::lit_bool(test.ignore));
@ -293,6 +304,51 @@ fn mk_test_desc_rec(cx: test_ctxt, test: test) -> @ast::expr {
ret @desc_rec;
}
// Produces a bare function that wraps the test function
// FIXME: This can go away once fn is the type of bare function
fn mk_test_wrapper(cx: test_ctxt,
fn_path_expr: ast::expr,
span: span) -> @ast::expr {
let call_expr: ast::expr = {
id: cx.next_node_id(),
node: ast::expr_call(@fn_path_expr, []),
span: span
};
let call_stmt: ast::stmt = nospan(
ast::stmt_expr(@call_expr, cx.next_node_id()));
let wrapper_decl: ast::fn_decl = {
inputs: [],
output: @nospan(ast::ty_nil),
purity: ast::impure_fn,
il: ast::il_normal,
cf: ast::return_val,
constraints: []
};
let wrapper_body: ast::blk = nospan({
stmts: [@call_stmt],
expr: option::none,
id: cx.next_node_id(),
rules: ast::default_blk
});
let wrapper_fn: ast::_fn = {
decl: wrapper_decl,
proto: ast::proto_bare,
body: wrapper_body
};
let wrapper_expr: ast::expr = {
id: cx.next_node_id(),
node: ast::expr_fn(wrapper_fn),
span: span
};
ret @wrapper_expr;
}
fn mk_main(cx: test_ctxt) -> @ast::item {
let args_mt: ast::mt = {ty: @nospan(ast::ty_str), mut: ast::imm};

View File

@ -1,3 +1,6 @@
// FIXME: The way this module sets up tests is a relic and more convoluted
// than it needs to be
import std::option;
import std::getopts;
import std::test;
@ -124,8 +127,10 @@ fn test_opts(config: config) -> test::test_opts {
run_ignored: config.run_ignored}
}
type tests_and_conv_fn =
{tests: [test::test_desc], to_task: fn(fn()) -> test::joinable};
type tests_and_conv_fn = {
tests: [test::test_desc<fn()>],
to_task: fn(fn()) -> test::joinable
};
fn make_tests(cx: cx) -> tests_and_conv_fn {
log #fmt["making tests from %s", cx.config.src_base];
@ -162,7 +167,7 @@ fn is_test(config: config, testfile: str) -> bool {
}
fn make_test(cx: cx, testfile: str, configport: port<[u8]>) ->
test::test_desc {
test::test_desc<fn()> {
{name: make_test_name(cx.config, testfile),
fn: make_test_closure(testfile, chan(configport)),
ignore: header::is_test_ignored(cx.config, testfile)}
@ -172,26 +177,8 @@ fn make_test_name(config: config, testfile: str) -> str {
#fmt["[%s] %s", mode_str(config.mode), testfile]
}
/*
So this is kind of crappy:
A test is just defined as a function, as you might expect, but tests have to
run in their own tasks. Unfortunately, if your test needs dynamic data then it
needs to be a closure, and transferring closures across tasks without
committing a host of memory management transgressions is just impossible.
To get around this, the standard test runner allows you the opportunity do
your own conversion from a test function to a task. It gives you your function
and you give it back a task.
So that's what we're going to do. Here's where it gets stupid. To get the
the data out of the test function we are going to run the test function,
which will do nothing but send the data for that test to a port we've set
up. Then we'll spawn that data into another task and return the task.
Really convoluted. Need to think up of a better definition for tests.
*/
fn make_test_closure(testfile: str, configchan: chan<[u8]>) -> test::test_fn {
fn make_test_closure(testfile: str,
configchan: chan<[u8]>) -> test::test_fn<fn()> {
bind send_config(testfile, configchan)
}
@ -199,67 +186,22 @@ fn send_config(testfile: str, configchan: chan<[u8]>) {
send(configchan, str::bytes(testfile));
}
/*
FIXME: Good god forgive me.
So actually shuttling structural data across tasks isn't possible at this
time, but we can send strings! Sadly, I need the whole config record, in the
test task so, instead of fixing the mechanism in the compiler I'm going to
break up the config record and pass everything individually to the spawned
function.
*/
fn closure_to_task(cx: cx, configport: port<[u8]>, testfn: fn()) ->
test::joinable {
testfn();
let testfile = recv(configport);
let compile_lib_path = cx.config.compile_lib_path;
let run_lib_path = cx.config.run_lib_path;
let rustc_path = cx.config.rustc_path;
let src_base = cx.config.src_base;
let build_base = cx.config.build_base;
let stage_id = cx.config.stage_id;
let mode = mode_str(cx.config.mode);
let run_ignored = cx.config.run_ignored;
let filter = opt_str(cx.config.filter);
let runtool = opt_str(cx.config.runtool);
let rustcflags = opt_str(cx.config.rustcflags);
let verbose = cx.config.verbose;
let chan = cx.procsrv.chan;
let testthunk =
bind run_test_task(compile_lib_path, run_lib_path, rustc_path,
src_base, build_base, stage_id, mode, run_ignored,
filter, runtool, rustcflags, verbose, chan,
testfile);
ret task::spawn_joinable(testthunk);
ret task::spawn_joinable2(
(cx.config, cx.procsrv.chan, testfile), run_test_task);
}
fn run_test_task(-compile_lib_path: str, -run_lib_path: str, -rustc_path: str,
-src_base: str, -build_base: str, -stage_id: str, -mode: str,
-run_ignored: bool, -opt_filter: str, -opt_runtool: str,
-opt_rustcflags: str, -verbose: bool,
-procsrv_chan: procsrv::reqchan, -testfile: [u8]) {
fn# run_test_task(args: (common::config, procsrv::reqchan, [u8])) {
let (config, procsrv_chan, testfile) = args;
test::configure_test_task();
let config =
{compile_lib_path: compile_lib_path,
run_lib_path: run_lib_path,
rustc_path: rustc_path,
src_base: src_base,
build_base: build_base,
stage_id: stage_id,
mode: str_mode(mode),
run_ignored: run_ignored,
filter: str_opt(opt_filter),
runtool: str_opt(opt_runtool),
rustcflags: str_opt(opt_rustcflags),
verbose: verbose};
let procsrv = procsrv::from_chan(procsrv_chan);
let cx = {config: config, procsrv: procsrv};
runtest::run(cx, testfile);

View File

@ -37,13 +37,14 @@ type response = {pid: int, infd: int, outfd: int, errfd: int};
fn mk() -> handle {
let setupport = port();
let task =
task::spawn_joinable(bind fn (setupchan: chan<chan<request>>) {
let reqport = port();
let reqchan = chan(reqport);
send(setupchan, reqchan);
worker(reqport);
}(chan(setupport)));
let task = task::spawn_joinable2(
chan(setupport),
fn# (setupchan: chan<chan<request>>) {
let reqport = port();
let reqchan = chan(reqport);
send(setupchan, reqchan);
worker(reqport);
});
ret {task: option::some(task), chan: recv(setupport)};
}

View File

@ -8,6 +8,7 @@ import task::task;
export test_name;
export test_fn;
export default_test_fn;
export test_desc;
export test_main;
export test_result;
@ -40,15 +41,21 @@ type test_name = str;
// 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.
type test_fn = fn();
type test_fn<@T> = T;
type default_test_fn = test_fn<fn#()>;
// The definition of a single test. A test runner will run a list of
// these.
type test_desc = {name: test_name, fn: test_fn, ignore: bool};
type test_desc<@T> = {
name: test_name,
fn: test_fn<T>,
ignore: bool
};
// The default console test runner. It accepts the command line
// arguments and a vector of test_descs (generated at compile time).
fn test_main(args: [str], tests: [test_desc]) {
fn test_main(args: [str], tests: [test_desc<default_test_fn>]) {
check (vec::is_not_empty(args));
let opts =
alt parse_opts(args) {
@ -93,15 +100,16 @@ type joinable = (task, comm::port<task::task_notification>);
// In cases where test functions are closures it is not ok to just dump them
// into a task and run them, so this transformation gives the caller a chance
// to create the test task.
type test_to_task = fn(fn()) -> joinable;
type test_to_task<@T> = fn(test_fn<T>) -> joinable;
// A simple console test runner
fn run_tests_console(opts: test_opts, tests: [test_desc]) -> bool {
fn run_tests_console(opts: test_opts,
tests: [test_desc<default_test_fn>]) -> bool {
run_tests_console_(opts, tests, default_test_to_task)
}
fn run_tests_console_(opts: test_opts, tests: [test_desc],
to_task: test_to_task) -> bool {
fn run_tests_console_<@T>(opts: test_opts, tests: [test_desc<T>],
to_task: test_to_task<T>) -> bool {
type test_state =
@{out: io::writer,
@ -110,9 +118,9 @@ fn run_tests_console_(opts: test_opts, tests: [test_desc],
mutable passed: uint,
mutable failed: uint,
mutable ignored: uint,
mutable failures: [test_desc]};
mutable failures: [test_desc<T>]};
fn callback(event: testevent, st: test_state) {
fn callback<@T>(event: testevent<T>, st: test_state) {
alt event {
te_filtered(filtered_tests) {
st.total = vec::len(filtered_tests);
@ -158,7 +166,7 @@ fn run_tests_console_(opts: test_opts, tests: [test_desc],
if !success {
st.out.write_line("\nfailures:");
for test: test_desc in st.failures {
for test: test_desc<T> in st.failures {
let testname = test.name; // Satisfy alias analysis
st.out.write_line(#fmt[" %s", testname]);
}
@ -199,14 +207,15 @@ fn run_tests_console_(opts: test_opts, tests: [test_desc],
fn use_color() -> bool { ret get_concurrency() == 1u; }
tag testevent {
te_filtered([test_desc]);
te_wait(test_desc);
te_result(test_desc, test_result);
tag testevent<@T> {
te_filtered([test_desc<T>]);
te_wait(test_desc<T>);
te_result(test_desc<T>, test_result);
}
fn run_tests(opts: test_opts, tests: [test_desc], to_task: test_to_task,
callback: fn(testevent)) {
fn run_tests<@T>(opts: test_opts, tests: [test_desc<T>],
to_task: test_to_task<T>,
callback: fn(testevent<T>)) {
let filtered_tests = filter_tests(opts, tests);
@ -239,54 +248,51 @@ fn run_tests(opts: test_opts, tests: [test_desc], to_task: test_to_task,
fn get_concurrency() -> uint { rustrt::sched_threads() }
fn filter_tests(opts: test_opts, tests: [test_desc]) -> [test_desc] {
fn filter_tests<@T>(opts: test_opts,
tests: [test_desc<T>]) -> [test_desc<T>] {
let filtered = tests;
// Remove tests that don't match the test filter
filtered =
if option::is_none(opts.filter) {
filtered
} else {
let filter_str =
alt opts.filter {
option::some(f) { f }
option::none. { "" }
};
let filter =
bind fn (test: test_desc, filter_str: str) ->
option::t<test_desc> {
if str::find(test.name, filter_str) >= 0 {
ret option::some(test);
} else { ret option::none; }
}(_, filter_str);
vec::filter_map(filter, filtered)
filtered = if option::is_none(opts.filter) {
filtered
} else {
let filter_str =
alt opts.filter {
option::some(f) { f }
option::none. { "" }
};
fn filter_fn<@T>(test: test_desc<T>, filter_str: str) ->
option::t<test_desc<T>> {
if str::find(test.name, filter_str) >= 0 {
ret option::some(test);
} else { ret option::none; }
}
let filter = bind filter_fn(_, filter_str);
vec::filter_map(filter, filtered)
};
// Maybe pull out the ignored test and unignore them
filtered =
if !opts.run_ignored {
filtered
} else {
let filter =
fn (test: test_desc) -> option::t<test_desc> {
if test.ignore {
ret option::some({name: test.name,
fn: test.fn,
ignore: false});
} else { ret option::none; }
};
vec::filter_map(filter, filtered)
filtered = if !opts.run_ignored {
filtered
} else {
fn filter<@T>(test: test_desc<T>) -> option::t<test_desc<T>> {
if test.ignore {
ret option::some({name: test.name,
fn: test.fn,
ignore: false});
} else { ret option::none; }
};
vec::filter_map(filter, filtered)
};
// Sort the tests alphabetically
filtered =
{
fn lteq(t1: test_desc, t2: test_desc) -> bool {
fn lteq<@T>(t1: test_desc<T>, t2: test_desc<T>) -> bool {
str::lteq(t1.name, t2.name)
}
sort::merge_sort(lteq, filtered)
@ -295,9 +301,10 @@ fn filter_tests(opts: test_opts, tests: [test_desc]) -> [test_desc] {
ret filtered;
}
type test_future = {test: test_desc, wait: fn() -> test_result};
type test_future<@T> = {test: test_desc<T>, wait: fn() -> test_result};
fn run_test(test: test_desc, to_task: test_to_task) -> test_future {
fn run_test<@T>(test: test_desc<T>,
to_task: test_to_task<T>) -> test_future<T> {
if !test.ignore {
let test_task = to_task(test.fn);
ret {test: test,
@ -313,9 +320,12 @@ fn run_test(test: test_desc, to_task: test_to_task) -> test_future {
// We need to run our tests in another task in order to trap test failures.
// This function only works with functions that don't contain closures.
fn default_test_to_task(f: fn()) -> joinable {
fn run_task(f: fn()) { configure_test_task(); f(); }
ret task::spawn_joinable(bind run_task(f));
fn default_test_to_task(&&f: default_test_fn) -> joinable {
fn# run_task(f: default_test_fn) {
configure_test_task();
f();
}
ret task::spawn_joinable2(f, run_task);
}
// Call from within a test task to make sure it's set up correctly

View File

@ -6,22 +6,22 @@ import std::vec;
#[test]
fn do_not_run_ignored_tests() {
let ran = @mutable false;
/*let ran = @mutable false;
let f = bind fn (ran: @mutable bool) { *ran = true; }(ran);
let desc = {name: "whatever", fn: f, ignore: true};
test::run_test(desc, test::default_test_to_task);
assert (*ran == false);
assert (*ran == false);*/
}
#[test]
fn ignored_tests_result_in_ignored() {
fn f() { }
/*fn f() { }
let desc = {name: "whatever", fn: f, ignore: true};
let res = test::run_test(desc, test::default_test_to_task).wait();
assert (res == test::tr_ignored);
assert (res == test::tr_ignored);*/
}
#[test]