diff --git a/src/test/compiletest/common.rs b/src/test/compiletest/common.rs new file mode 100644 index 000000000000..d0fe2ebb1cf9 --- /dev/null +++ b/src/test/compiletest/common.rs @@ -0,0 +1,33 @@ +import std::option; + +tag mode { mode_compile_fail; mode_run_fail; mode_run_pass; } + +type config = { + // The library paths required for running the compiler + compile_lib_path: str, + // The library paths required for running compiled programs + run_lib_path: str, + // The rustc executable + rustc_path: str, + // The directory containing the tests to run + src_base: str, + // The directory where programs should be built + build_base: str, + // The name of the stage being built (stage1, etc) + stage_id: str, + // The test mode, compile-fail, run-fail, run-pass + mode: mode, + // Run ignored tests + run_ignored: bool, + // Only run tests that match this filter + filter: option::t[str], + // A command line to prefix program execution with, + // for running under valgrind + runtool: option::t[str], + // Flags to pass to the compiler + rustcflags: option::t[str], + // Explain what's going on + verbose: bool +}; + +type cx = {config: config, procsrv: procsrv::handle}; diff --git a/src/test/compiletest/compiletest.rc b/src/test/compiletest/compiletest.rc index b2c1f4662c24..2f9ffe50f81e 100644 --- a/src/test/compiletest/compiletest.rc +++ b/src/test/compiletest/compiletest.rc @@ -1,6 +1,11 @@ use std; mod compiletest; +mod procsrv; +mod util; +mod header; +mod runtest; +mod common; // Local Variables: // fill-column: 78; diff --git a/src/test/compiletest/compiletest.rs b/src/test/compiletest/compiletest.rs index 0bc7f392442e..9621d2b414c0 100644 --- a/src/test/compiletest/compiletest.rs +++ b/src/test/compiletest/compiletest.rs @@ -5,43 +5,15 @@ import std::fs; import std::str; import std::vec; import std::ivec; -import std::io; -import std::generic_os::setenv; -import std::generic_os::getenv; -import std::os; -import std::run; import std::task; -import std::unsafe; -tag mode { mode_compile_fail; mode_run_fail; mode_run_pass; } - -type config = { - // The library paths required for running the compiler - compile_lib_path: str, - // The library paths required for running compiled programs - run_lib_path: str, - // The rustc executable - rustc_path: str, - // The directory containing the tests to run - src_base: str, - // The directory where programs should be built - build_base: str, - // The name of the stage being built (stage1, etc) - stage_id: str, - // The test mode, compile-fail, run-fail, run-pass - mode: mode, - // Run ignored tests - run_ignored: bool, - // Only run tests that match this filter - filter: option::t[str], - // A command line to prefix program execution with, - // for running under valgrind - runtool: option::t[str], - // Flags to pass to the compiler - rustcflags: option::t[str], - // Explain what's going on - verbose: bool -}; +import common::cx; +import common::config; +import common::mode_run_pass; +import common::mode_run_fail; +import common::mode_compile_fail; +import common::mode; +import util::logv; fn main(args: vec[str]) { @@ -134,8 +106,6 @@ fn mode_str(mode: mode) -> str { } } -type cx = {config: config, procsrv: procsrv::handle}; - fn run_tests(config: &config) { let opts = test_opts(config); let cx = {config: config, procsrv: procsrv::mk()}; @@ -173,7 +143,7 @@ fn make_test(cx: &cx, testfile: &str, configport: &port[str]) -> test::test_desc { {name: testfile, fn: make_test_closure(testfile, chan(configport)), - ignore: header::is_test_ignored(cx.config, testfile)} + ignore: header::is_test_ignored(cx.config.stage_id, testfile)} } /* @@ -253,554 +223,6 @@ fn run_test_task(compile_lib_path: str, run_lib_path: str, rustc_path: str, runtest::run(cx, testfile); } - -fn make_cmdline(libpath: &str, prog: &str, args: &vec[str]) -> str { - #fmt("%s %s %s", lib_path_cmd_prefix(libpath), prog, - str::connect(args, " ")) -} - -// Build the LD_LIBRARY_PATH variable as it would be seen on the command line -// for diagnostic purposes -fn lib_path_cmd_prefix(path: &str) -> str { - #fmt("%s=\"%s\"", lib_path_env_var(), make_new_path(path)) -} - -fn make_new_path(path: &str) -> str { - - // Windows just uses PATH as the library search path, so we have to - // maintain the current value while adding our own - alt getenv(lib_path_env_var()) { - option::some(curr) { #fmt("%s:%s", path, curr) } - option::none. { path } - } -} - -#[cfg(target_os = "linux")] -fn lib_path_env_var() -> str { "LD_LIBRARY_PATH" } - -#[cfg(target_os = "macos")] -fn lib_path_env_var() -> str { "DYLD_LIBRARY_PATH" } - -#[cfg(target_os = "win32")] -fn lib_path_env_var() -> str { "PATH" } - -fn make_exe_name(config: &config, testfile: &str) -> str { - output_base_name(config, testfile) + os::exec_suffix() -} - -fn output_base_name(config: &config, testfile: &str) -> str { - let base = config.build_base; - let filename = - { - let parts = str::split(fs::basename(testfile), '.' as u8); - parts = vec::slice(parts, 0u, vec::len(parts) - 1u); - str::connect(parts, ".") - }; - #fmt("%s%s.%s", base, filename, config.stage_id) -} - -fn logv(config: &config, s: &str) { - log s; - if config.verbose { io::stdout().write_line(s); } -} - -fn clone_str(s: &str) -> str { - let new = s + ""; - // new should be a different pointer - let sptr: int = unsafe::reinterpret_cast(s); - let newptr: int = unsafe::reinterpret_cast(new); - assert sptr != newptr; - new -} - -fn clone_ivecstr(v: &str[]) -> str[] { - let r = ~[]; - for t: str in ivec::slice(v, 0u, ivec::len(v)) { - r += ~[clone_str(t)]; - } - ret r; -} - -mod header { - - export test_props; - export load_props; - export is_test_ignored; - - type test_props = {error_patterns: str[], compile_flags: option::t[str]}; - - // Load any test directives embedded in the file - fn load_props(testfile: &str) -> test_props { - let error_patterns = ~[]; - let compile_flags = option::none; - for each ln: str in iter_header(testfile) { - alt parse_error_pattern(ln) { - option::some(ep) { error_patterns += ~[ep]; } - option::none. { } - } - - - if option::is_none(compile_flags) { - compile_flags = parse_compile_flags(ln); - } - } - ret {error_patterns: error_patterns, compile_flags: compile_flags}; - } - - fn is_test_ignored(config: &config, testfile: &str) -> bool { - let found = false; - for each ln: str in iter_header(testfile) { - // FIXME: Can't return or break from iterator - found = found - || parse_name_directive(ln, "xfail-" + config.stage_id); - } - ret found; - } - - iter iter_header(testfile: &str) -> str { - let rdr = io::file_reader(testfile); - while !rdr.eof() { - let ln = rdr.read_line(); - - // Assume that any directives will be found before the first - // module or function. This doesn't seem to be an optimization - // with a warm page cache. Maybe with a cold one. - if str::starts_with(ln, "fn") || str::starts_with(ln, "mod") { - break; - } else { put ln; } - } - } - - fn parse_error_pattern(line: &str) -> option::t[str] { - parse_name_value_directive(line, "error-pattern") - } - - fn parse_compile_flags(line: &str) -> option::t[str] { - parse_name_value_directive(line, "compile-flags") - } - - fn parse_name_directive(line: &str, directive: &str) -> bool { - str::find(line, directive) >= 0 - } - - fn parse_name_value_directive(line: &str, - directive: &str) -> option::t[str] { - let keycolon = directive + ":"; - if str::find(line, keycolon) >= 0 { - let colon = str::find(line, keycolon) as uint; - let value = - str::slice(line, colon + str::byte_len(keycolon), - str::byte_len(line)); - log #fmt("%s: %s", directive, value); - option::some(value) - } else { option::none } - } -} - -mod runtest { - - import header::load_props; - import header::test_props; - - export run; - - fn run(cx: &cx, testfile: &str) { - test::configure_test_task(); - if (cx.config.verbose) { - // We're going to be dumping a lot of info. Start on a new line. - io::stdout().write_str("\n\n"); - } - log #fmt("running %s", testfile); - let props = load_props(testfile); - alt cx.config.mode { - mode_compile_fail. { run_cfail_test(cx, props, testfile); } - mode_run_fail. { run_rfail_test(cx, props, testfile); } - mode_run_pass. { run_rpass_test(cx, props, testfile); } - } - } - - fn run_cfail_test(cx: &cx, props: &test_props, testfile: &str) { - let procres = compile_test(cx, props, testfile); - - if procres.status == 0 { - fatal_procres("compile-fail test compiled successfully!", - procres); - } - - check_error_patterns(props, testfile, procres); - } - - fn run_rfail_test(cx: &cx, props: &test_props, testfile: &str) { - let procres = compile_test(cx, props, testfile); - - if procres.status != 0 { - fatal_procres("compilation failed!", procres); } - - procres = exec_compiled_test(cx, testfile); - - if procres.status == 0 { - fatal_procres("run-fail test didn't produce an error!", - procres); - } - - check_error_patterns(props, testfile, procres); - } - - fn run_rpass_test(cx: &cx, props: &test_props, testfile: &str) { - let procres = compile_test(cx, props, testfile); - - if procres.status != 0 { - fatal_procres("compilation failed!", procres); } - - procres = exec_compiled_test(cx, testfile); - - - if procres.status != 0 { fatal_procres("test run failed!", procres); } - } - - fn check_error_patterns(props: &test_props, testfile: &str, - procres: &procres) { - if ivec::is_empty(props.error_patterns) { - fatal("no error pattern specified in " + testfile); - } - - let next_err_idx = 0u; - let next_err_pat = props.error_patterns.(next_err_idx); - for line: str in str::split(procres.stdout, '\n' as u8) { - if str::find(line, next_err_pat) > 0 { - log #fmt("found error pattern %s", next_err_pat); - next_err_idx += 1u; - if next_err_idx == ivec::len(props.error_patterns) { - log "found all error patterns"; - ret; - } - next_err_pat = props.error_patterns.(next_err_idx); - } - } - - let missing_patterns = - ivec::slice(props.error_patterns, next_err_idx, - ivec::len(props.error_patterns)); - if ivec::len(missing_patterns) == 1u { - fatal_procres(#fmt("error pattern '%s' not found!", - missing_patterns.(0)), procres); - } else { - for pattern: str in missing_patterns { - error(#fmt("error pattern '%s' not found!", pattern)); - } - fatal_procres("multiple error patterns not found", procres); - } - } - - type procargs = {prog: str, args: vec[str]}; - - type procres = {status: int, stdout: str, stderr: str, cmdline: str}; - - fn compile_test(cx: &cx, props: &test_props, testfile: &str) -> procres { - compose_and_run(cx, testfile, bind make_compile_args(_, props, _), - cx.config.compile_lib_path) - } - - fn exec_compiled_test(cx: &cx, testfile: &str) -> procres { - compose_and_run(cx, testfile, make_run_args, cx.config.run_lib_path) - } - - fn compose_and_run(cx: &cx, testfile: &str, - make_args: fn(&config, &str) -> procargs , - lib_path: &str) -> procres { - let procargs = make_args(cx.config, testfile); - ret program_output(cx, testfile, lib_path, - procargs.prog, procargs.args); - } - - fn make_compile_args(config: &config, - props: &test_props, testfile: &str) -> - procargs { - let prog = config.rustc_path; - let args = [testfile, "-o", make_exe_name(config, testfile)]; - args += split_maybe_args(config.rustcflags); - args += split_maybe_args(props.compile_flags); - ret {prog: prog, args: args}; - } - - fn make_run_args(config: &config, testfile: &str) -> procargs { - // If we've got another tool to run under (valgrind), - // then split apart its command - let args = - split_maybe_args(config.runtool) - + [make_exe_name(config, testfile)]; - ret {prog: args.(0), args: vec::slice(args, 1u, vec::len(args))}; - } - - fn split_maybe_args(argstr: &option::t[str]) -> vec[str] { - fn rm_whitespace(v: vec[str]) -> vec[str] { - fn flt(s: &str) -> option::t[str] { - if !is_whitespace(s) { - option::some(s) - } else { - option::none - } - } - - // FIXME: This should be in std - fn is_whitespace(s: str) -> bool { - for c: u8 in s { - if c != (' ' as u8) { ret false; } - } - ret true; - } - vec::filter_map(flt, v) - } - - alt argstr { - option::some(s) { rm_whitespace(str::split(s, ' ' as u8)) } - option::none. { [] } - } - } - - fn program_output(cx: &cx, testfile: &str, lib_path: &str, prog: &str, - args: &vec[str]) -> procres { - let cmdline = - { - let cmdline = make_cmdline(lib_path, prog, args); - logv(cx.config, #fmt("executing %s", cmdline)); - cmdline - }; - let res = procsrv::run(cx.procsrv, lib_path, prog, args); - dump_output(cx.config, testfile, res.out, res.err); - ret {status: res.status, stdout: res.out, - stderr: res.err, cmdline: cmdline}; - } - - fn dump_output(config: &config, testfile: &str, - out: &str, err: &str) { - dump_output_file(config, testfile, out, "out"); - dump_output_file(config, testfile, err, "err"); - maybe_dump_to_stdout(config, out, err); - } - - #[cfg(target_os = "win32")] - #[cfg(target_os = "linux")] - fn dump_output_file(config: &config, testfile: &str, - out: &str, extension: &str) { - let outfile = make_out_name(config, testfile, extension); - let writer = io::file_writer(outfile, [io::create, io::truncate]); - writer.write_str(out); - } - - // FIXME (726): Can't use file_writer on mac - #[cfg(target_os = "macos")] - fn dump_output_file(config: &config, testfile: &str, - out: &str, extension: &str) { - } - - fn make_out_name(config: &config, testfile: &str, - extension: &str) -> str { - output_base_name(config, testfile) + "." + extension - } - - fn maybe_dump_to_stdout(config: &config, - out: &str, err: &str) { - if config.verbose { - let sep1 = #fmt("------%s------------------------------", - "stdout"); - let sep2 = #fmt("------%s------------------------------", - "stderr"); - let sep3 = "------------------------------------------"; - io::stdout().write_line(sep1); - io::stdout().write_line(out); - io::stdout().write_line(sep2); - io::stdout().write_line(err); - io::stdout().write_line(sep3); - } - } - - fn error(err: &str) { io::stdout().write_line(#fmt("\nerror: %s", err)); } - - fn fatal(err: &str) -> ! { error(err); fail; } - - fn fatal_procres(err: &str, procres: procres) -> ! { - let msg = - #fmt("\n\ - error: %s\n\ - command: %s\n\ - stdout:\n\ - ------------------------------------------\n\ - %s\n\ - ------------------------------------------\n\ - stderr:\n\ - ------------------------------------------\n\ - %s\n\ - ------------------------------------------\n\ - \n", - err, procres.cmdline, procres.stdout, procres.stderr); - io::stdout().write_str(msg); - fail; - } -} - -// So when running tests in parallel there's a potential race on environment -// variables if we let each task spawn its own children - between the time the -// environment is set and the process is spawned another task could spawn its -// child process. Because of that we have to use a complicated scheme with a -// dedicated server for spawning processes. -mod procsrv { - - export handle; - export mk; - export from_chan; - export clone; - export run; - export close; - export reqchan; - - type reqchan = chan[request]; - - type handle = {task: option::t[task], chan: reqchan}; - - tag request { exec(str, str, str[], chan[response]); stop; } - - type response = {pid: int, outfd: int, errfd: int}; - - fn mk() -> handle { - let setupport = port(); - let task = spawn fn(setupchan: chan[chan[request]]) { - let reqport = port(); - let reqchan = chan(reqport); - task::send(setupchan, task::clone_chan(reqchan)); - worker(reqport); - } (chan(setupport)); - ret {task: option::some(task), - chan: task::recv(setupport) - }; - } - - fn from_chan(ch: &reqchan) -> handle { {task: option::none, chan: ch} } - - fn clone(handle: &handle) -> handle { - - // Sharing tasks across tasks appears to be (yet another) recipe for - // disaster, so our handle clones will not get the task pointer. - {task: option::none, chan: task::clone_chan(handle.chan)} - } - - fn close(handle: &handle) { - task::send(handle.chan, stop); - task::join(option::get(handle.task)); - } - - fn run(handle: &handle, lib_path: &str, prog: &str, args: &vec[str]) -> - {status: int, out: str, err: str} { - let p = port[response](); - let ch = chan(p); - task::send(handle.chan, exec(lib_path, - prog, - clone_ivecstr(ivec::from_vec(args)), - task::clone_chan(ch))); - let resp = task::recv(p); - let output = readclose(resp.outfd); - let errput = readclose(resp.errfd); - let status = os::waitpid(resp.pid); - ret {status: status, out: output, err: errput}; - } - - fn readclose(fd: int) -> str { - // Copied from run::program_output - let file = os::fd_FILE(fd); - let reader = io::new_reader(io::FILE_buf_reader(file, option::none)); - let buf = ""; - while !reader.eof() { - let bytes = reader.read_bytes(4096u); - buf += str::unsafe_from_bytes(bytes); - } - os::libc::fclose(file); - ret buf; - } - - fn worker(p: port[request]) { - - // FIXME (787): If we declare this inside of the while loop and then - // break out of it before it's ever initialized (i.e. we don't run - // any tests), then the cleanups will puke, so we're initializing it - // here with defaults. - let execparms = { - lib_path: "", - prog: "", - args: ~[], - // This works because a NULL box is ignored during cleanup - respchan: unsafe::reinterpret_cast(0) - }; - - while true { - // FIXME: Sending strings across channels seems to still - // leave them refed on the sender's end, which causes problems if - // the receiver's poniters outlive the sender's. Here we clone - // everything and let the originals go out of scope before sending - // a response. - execparms = { - // FIXME (785): The 'discriminant' of an alt expression has - // the same scope as the alt expression itself, so we have to - // put the entire alt in another block to make sure the exec - // message goes out of scope. Seems like the scoping rules for - // the alt discriminant are wrong. - alt task::recv(p) { - exec(lib_path, prog, args, respchan) { - { - lib_path: clone_str(lib_path), - prog: clone_str(prog), - args: clone_ivecstr(args), - respchan: respchan - } - } - stop. { ret } - } - }; - - // This is copied from run::start_program - let pipe_in = os::pipe(); - let pipe_out = os::pipe(); - let pipe_err = os::pipe(); - let spawnproc = - bind run::spawn_process(execparms.prog, - ivec::to_vec(execparms.args), - pipe_in.in, - pipe_out.out, - pipe_err.out); - let pid = with_lib_path(execparms.lib_path, spawnproc); - os::libc::close(pipe_in.in); - os::libc::close(pipe_in.out); - os::libc::close(pipe_out.out); - os::libc::close(pipe_err.out); - if pid == -1 { - os::libc::close(pipe_out.in); - os::libc::close(pipe_err.in); - fail; - } - task::send(execparms.respchan, - {pid: pid, - outfd: pipe_out.in, - errfd: pipe_err.in}); - } - } - - fn with_lib_path[T](path: &str, f: fn() -> T ) -> T { - let maybe_oldpath = getenv(lib_path_env_var()); - append_lib_path(path); - let res = f(); - if option::is_some(maybe_oldpath) { - export_lib_path(option::get(maybe_oldpath)); - } else { - // FIXME: This should really be unset but we don't have that yet - export_lib_path(""); - } - ret res; - } - - fn append_lib_path(path: &str) { export_lib_path(make_new_path(path)); } - - fn export_lib_path(path: &str) { setenv(lib_path_env_var(), path); } -} - // Local Variables: // fill-column: 78; // indent-tabs-mode: nil diff --git a/src/test/compiletest/header.rs b/src/test/compiletest/header.rs new file mode 100644 index 000000000000..9961852f16e9 --- /dev/null +++ b/src/test/compiletest/header.rs @@ -0,0 +1,76 @@ +import std::option; +import std::str; +import std::io; + +export test_props; +export load_props; +export is_test_ignored; + +type test_props = {error_patterns: str[], compile_flags: option::t[str]}; + +// Load any test directives embedded in the file +fn load_props(testfile: &str) -> test_props { + let error_patterns = ~[]; + let compile_flags = option::none; + for each ln: str in iter_header(testfile) { + alt parse_error_pattern(ln) { + option::some(ep) { error_patterns += ~[ep]; } + option::none. { } + } + + + if option::is_none(compile_flags) { + compile_flags = parse_compile_flags(ln); + } + } + ret {error_patterns: error_patterns, compile_flags: compile_flags}; +} + +fn is_test_ignored(stage_id: &str, testfile: &str) -> bool { + let found = false; + for each ln: str in iter_header(testfile) { + // FIXME: Can't return or break from iterator + found = found + || parse_name_directive(ln, "xfail-" + stage_id); + } + ret found; +} + +iter iter_header(testfile: &str) -> str { + let rdr = io::file_reader(testfile); + while !rdr.eof() { + let ln = rdr.read_line(); + + // Assume that any directives will be found before the first + // module or function. This doesn't seem to be an optimization + // with a warm page cache. Maybe with a cold one. + if str::starts_with(ln, "fn") || str::starts_with(ln, "mod") { + break; + } else { put ln; } + } +} + +fn parse_error_pattern(line: &str) -> option::t[str] { + parse_name_value_directive(line, "error-pattern") +} + +fn parse_compile_flags(line: &str) -> option::t[str] { + parse_name_value_directive(line, "compile-flags") +} + +fn parse_name_directive(line: &str, directive: &str) -> bool { + str::find(line, directive) >= 0 +} + +fn parse_name_value_directive(line: &str, + directive: &str) -> option::t[str] { + let keycolon = directive + ":"; + if str::find(line, keycolon) >= 0 { + let colon = str::find(line, keycolon) as uint; + let value = + str::slice(line, colon + str::byte_len(keycolon), + str::byte_len(line)); + log #fmt("%s: %s", directive, value); + option::some(value) + } else { option::none } +} diff --git a/src/test/compiletest/procsrv.rs b/src/test/compiletest/procsrv.rs new file mode 100644 index 000000000000..0cc1e34a1f0f --- /dev/null +++ b/src/test/compiletest/procsrv.rs @@ -0,0 +1,186 @@ +// So when running tests in parallel there's a potential race on environment +// variables if we let each task spawn its own children - between the time the +// environment is set and the process is spawned another task could spawn its +// child process. Because of that we have to use a complicated scheme with a +// dedicated server for spawning processes. + +import std::option; +import std::task; +import std::generic_os::setenv; +import std::generic_os::getenv; +import std::ivec; +import std::os; +import std::run; +import std::unsafe; +import std::io; +import std::str; + +export handle; +export mk; +export from_chan; +export run; +export close; +export reqchan; + +type reqchan = chan[request]; + +type handle = {task: option::t[task], chan: reqchan}; + +tag request { exec(str, str, str[], chan[response]); stop; } + +type response = {pid: int, outfd: int, errfd: int}; + +fn mk() -> handle { + let setupport = port(); + let task = spawn fn(setupchan: chan[chan[request]]) { + let reqport = port(); + let reqchan = chan(reqport); + task::send(setupchan, task::clone_chan(reqchan)); + worker(reqport); + } (chan(setupport)); + ret {task: option::some(task), + chan: task::recv(setupport) + }; +} + +fn from_chan(ch: &reqchan) -> handle { {task: option::none, chan: ch} } + +fn clone(handle: &handle) -> handle { + + // Sharing tasks across tasks appears to be (yet another) recipe for + // disaster, so our handle clones will not get the task pointer. + {task: option::none, chan: task::clone_chan(handle.chan)} +} + +fn close(handle: &handle) { + task::send(handle.chan, stop); + task::join(option::get(handle.task)); +} + +fn run(handle: &handle, lib_path: &str, prog: &str, args: &vec[str]) -> +{status: int, out: str, err: str} { + let p = port[response](); + let ch = chan(p); + task::send(handle.chan, exec(lib_path, + prog, + clone_ivecstr(ivec::from_vec(args)), + task::clone_chan(ch))); + let resp = task::recv(p); + let output = readclose(resp.outfd); + let errput = readclose(resp.errfd); + let status = os::waitpid(resp.pid); + ret {status: status, out: output, err: errput}; +} + +fn readclose(fd: int) -> str { + // Copied from run::program_output + let file = os::fd_FILE(fd); + let reader = io::new_reader(io::FILE_buf_reader(file, option::none)); + let buf = ""; + while !reader.eof() { + let bytes = reader.read_bytes(4096u); + buf += str::unsafe_from_bytes(bytes); + } + os::libc::fclose(file); + ret buf; +} + +fn worker(p: port[request]) { + + // FIXME (787): If we declare this inside of the while loop and then + // break out of it before it's ever initialized (i.e. we don't run + // any tests), then the cleanups will puke, so we're initializing it + // here with defaults. + let execparms = { + lib_path: "", + prog: "", + args: ~[], + // This works because a NULL box is ignored during cleanup + respchan: unsafe::reinterpret_cast(0) + }; + + while true { + // FIXME: Sending strings across channels seems to still + // leave them refed on the sender's end, which causes problems if + // the receiver's poniters outlive the sender's. Here we clone + // everything and let the originals go out of scope before sending + // a response. + execparms = { + // FIXME (785): The 'discriminant' of an alt expression has + // the same scope as the alt expression itself, so we have to + // put the entire alt in another block to make sure the exec + // message goes out of scope. Seems like the scoping rules for + // the alt discriminant are wrong. + alt task::recv(p) { + exec(lib_path, prog, args, respchan) { + { + lib_path: clone_str(lib_path), + prog: clone_str(prog), + args: clone_ivecstr(args), + respchan: respchan + } + } + stop. { ret } + } + }; + + // This is copied from run::start_program + let pipe_in = os::pipe(); + let pipe_out = os::pipe(); + let pipe_err = os::pipe(); + let spawnproc = + bind run::spawn_process(execparms.prog, + ivec::to_vec(execparms.args), + pipe_in.in, + pipe_out.out, + pipe_err.out); + let pid = with_lib_path(execparms.lib_path, spawnproc); + os::libc::close(pipe_in.in); + os::libc::close(pipe_in.out); + os::libc::close(pipe_out.out); + os::libc::close(pipe_err.out); + if pid == -1 { + os::libc::close(pipe_out.in); + os::libc::close(pipe_err.in); + fail; + } + task::send(execparms.respchan, + {pid: pid, + outfd: pipe_out.in, + errfd: pipe_err.in}); + } +} + +fn with_lib_path[T](path: &str, f: fn() -> T ) -> T { + let maybe_oldpath = getenv(util::lib_path_env_var()); + append_lib_path(path); + let res = f(); + if option::is_some(maybe_oldpath) { + export_lib_path(option::get(maybe_oldpath)); + } else { + // FIXME: This should really be unset but we don't have that yet + export_lib_path(""); + } + ret res; +} + +fn append_lib_path(path: &str) { export_lib_path(util::make_new_path(path)); } + +fn export_lib_path(path: &str) { setenv(util::lib_path_env_var(), path); } + +fn clone_str(s: &str) -> str { + let new = s + ""; + // new should be a different pointer + let sptr: int = unsafe::reinterpret_cast(s); + let newptr: int = unsafe::reinterpret_cast(new); + assert sptr != newptr; + new +} + +fn clone_ivecstr(v: &str[]) -> str[] { + let r = ~[]; + for t: str in ivec::slice(v, 0u, ivec::len(v)) { + r += ~[clone_str(t)]; + } + ret r; +} diff --git a/src/test/compiletest/runtest.rs b/src/test/compiletest/runtest.rs new file mode 100644 index 000000000000..9355ed5620e0 --- /dev/null +++ b/src/test/compiletest/runtest.rs @@ -0,0 +1,279 @@ +import std::io; +import std::str; +import std::option; +import std::vec; +import std::fs; +import std::os; +import std::ivec; +import std::test; + +import common::mode_run_pass; +import common::mode_run_fail; +import common::mode_compile_fail; +import common::cx; +import common::config; +import header::load_props; +import header::test_props; +import util::logv; + +export run; + +fn run(cx: &cx, testfile: &str) { + test::configure_test_task(); + if (cx.config.verbose) { + // We're going to be dumping a lot of info. Start on a new line. + io::stdout().write_str("\n\n"); + } + log #fmt("running %s", testfile); + let props = load_props(testfile); + alt cx.config.mode { + mode_compile_fail. { run_cfail_test(cx, props, testfile); } + mode_run_fail. { run_rfail_test(cx, props, testfile); } + mode_run_pass. { run_rpass_test(cx, props, testfile); } + } +} + +fn run_cfail_test(cx: &cx, props: &test_props, testfile: &str) { + let procres = compile_test(cx, props, testfile); + + if procres.status == 0 { + fatal_procres("compile-fail test compiled successfully!", + procres); + } + + check_error_patterns(props, testfile, procres); +} + +fn run_rfail_test(cx: &cx, props: &test_props, testfile: &str) { + let procres = compile_test(cx, props, testfile); + + if procres.status != 0 { + fatal_procres("compilation failed!", procres); } + + procres = exec_compiled_test(cx, testfile); + + if procres.status == 0 { + fatal_procres("run-fail test didn't produce an error!", + procres); + } + + check_error_patterns(props, testfile, procres); +} + +fn run_rpass_test(cx: &cx, props: &test_props, testfile: &str) { + let procres = compile_test(cx, props, testfile); + + if procres.status != 0 { + fatal_procres("compilation failed!", procres); } + + procres = exec_compiled_test(cx, testfile); + + + if procres.status != 0 { fatal_procres("test run failed!", procres); } +} + +fn check_error_patterns(props: &test_props, testfile: &str, + procres: &procres) { + if ivec::is_empty(props.error_patterns) { + fatal("no error pattern specified in " + testfile); + } + + let next_err_idx = 0u; + let next_err_pat = props.error_patterns.(next_err_idx); + for line: str in str::split(procres.stdout, '\n' as u8) { + if str::find(line, next_err_pat) > 0 { + log #fmt("found error pattern %s", next_err_pat); + next_err_idx += 1u; + if next_err_idx == ivec::len(props.error_patterns) { + log "found all error patterns"; + ret; + } + next_err_pat = props.error_patterns.(next_err_idx); + } + } + + let missing_patterns = + ivec::slice(props.error_patterns, next_err_idx, + ivec::len(props.error_patterns)); + if ivec::len(missing_patterns) == 1u { + fatal_procres(#fmt("error pattern '%s' not found!", + missing_patterns.(0)), procres); + } else { + for pattern: str in missing_patterns { + error(#fmt("error pattern '%s' not found!", pattern)); + } + fatal_procres("multiple error patterns not found", procres); + } +} + +type procargs = {prog: str, args: vec[str]}; + +type procres = {status: int, stdout: str, stderr: str, cmdline: str}; + +fn compile_test(cx: &cx, props: &test_props, testfile: &str) -> procres { + compose_and_run(cx, testfile, bind make_compile_args(_, props, _), + cx.config.compile_lib_path) +} + +fn exec_compiled_test(cx: &cx, testfile: &str) -> procres { + compose_and_run(cx, testfile, make_run_args, cx.config.run_lib_path) +} + +fn compose_and_run(cx: &cx, testfile: &str, + make_args: fn(&config, &str) -> procargs , + lib_path: &str) -> procres { + let procargs = make_args(cx.config, testfile); + ret program_output(cx, testfile, lib_path, + procargs.prog, procargs.args); +} + +fn make_compile_args(config: &config, + props: &test_props, testfile: &str) -> + procargs { + let prog = config.rustc_path; + let args = [testfile, "-o", make_exe_name(config, testfile)]; + args += split_maybe_args(config.rustcflags); + args += split_maybe_args(props.compile_flags); + ret {prog: prog, args: args}; +} + +fn make_exe_name(config: &config, testfile: &str) -> str { + output_base_name(config, testfile) + os::exec_suffix() +} + +fn make_run_args(config: &config, testfile: &str) -> procargs { + // If we've got another tool to run under (valgrind), + // then split apart its command + let args = + split_maybe_args(config.runtool) + + [make_exe_name(config, testfile)]; + ret {prog: args.(0), args: vec::slice(args, 1u, vec::len(args))}; +} + +fn split_maybe_args(argstr: &option::t[str]) -> vec[str] { + fn rm_whitespace(v: vec[str]) -> vec[str] { + fn flt(s: &str) -> option::t[str] { + if !is_whitespace(s) { + option::some(s) + } else { + option::none + } + } + + // FIXME: This should be in std + fn is_whitespace(s: str) -> bool { + for c: u8 in s { + if c != (' ' as u8) { ret false; } + } + ret true; + } + vec::filter_map(flt, v) + } + + alt argstr { + option::some(s) { rm_whitespace(str::split(s, ' ' as u8)) } + option::none. { [] } + } +} + +fn program_output(cx: &cx, testfile: &str, lib_path: &str, prog: &str, + args: &vec[str]) -> procres { + let cmdline = + { + let cmdline = make_cmdline(lib_path, prog, args); + logv(cx.config, #fmt("executing %s", cmdline)); + cmdline + }; + let res = procsrv::run(cx.procsrv, lib_path, prog, args); + dump_output(cx.config, testfile, res.out, res.err); + ret {status: res.status, stdout: res.out, + stderr: res.err, cmdline: cmdline}; +} + +fn make_cmdline(libpath: &str, prog: &str, args: &vec[str]) -> str { + #fmt("%s %s %s", lib_path_cmd_prefix(libpath), prog, + str::connect(args, " ")) +} + +// Build the LD_LIBRARY_PATH variable as it would be seen on the command line +// for diagnostic purposes +fn lib_path_cmd_prefix(path: &str) -> str { + #fmt("%s=\"%s\"", util::lib_path_env_var(), util::make_new_path(path)) +} + +fn dump_output(config: &config, testfile: &str, + out: &str, err: &str) { + dump_output_file(config, testfile, out, "out"); + dump_output_file(config, testfile, err, "err"); + maybe_dump_to_stdout(config, out, err); +} + +#[cfg(target_os = "win32")] +#[cfg(target_os = "linux")] +fn dump_output_file(config: &config, testfile: &str, + out: &str, extension: &str) { + let outfile = make_out_name(config, testfile, extension); + let writer = io::file_writer(outfile, [io::create, io::truncate]); + writer.write_str(out); +} + +// FIXME (726): Can't use file_writer on mac +#[cfg(target_os = "macos")] +fn dump_output_file(config: &config, testfile: &str, + out: &str, extension: &str) { +} + +fn make_out_name(config: &config, testfile: &str, + extension: &str) -> str { + output_base_name(config, testfile) + "." + extension +} + +fn output_base_name(config: &config, testfile: &str) -> str { + let base = config.build_base; + let filename = + { + let parts = str::split(fs::basename(testfile), '.' as u8); + parts = vec::slice(parts, 0u, vec::len(parts) - 1u); + str::connect(parts, ".") + }; + #fmt("%s%s.%s", base, filename, config.stage_id) +} + +fn maybe_dump_to_stdout(config: &config, + out: &str, err: &str) { + if config.verbose { + let sep1 = #fmt("------%s------------------------------", + "stdout"); + let sep2 = #fmt("------%s------------------------------", + "stderr"); + let sep3 = "------------------------------------------"; + io::stdout().write_line(sep1); + io::stdout().write_line(out); + io::stdout().write_line(sep2); + io::stdout().write_line(err); + io::stdout().write_line(sep3); + } +} + +fn error(err: &str) { io::stdout().write_line(#fmt("\nerror: %s", err)); } + +fn fatal(err: &str) -> ! { error(err); fail; } + +fn fatal_procres(err: &str, procres: procres) -> ! { + let msg = + #fmt("\n\ +error: %s\n\ +command: %s\n\ +stdout:\n\ +------------------------------------------\n\ +%s\n\ +------------------------------------------\n\ +stderr:\n\ +------------------------------------------\n\ +%s\n\ +------------------------------------------\n\ +\n", + err, procres.cmdline, procres.stdout, procres.stderr); + io::stdout().write_str(msg); + fail; +} diff --git a/src/test/compiletest/util.rs b/src/test/compiletest/util.rs new file mode 100644 index 000000000000..5f971777c751 --- /dev/null +++ b/src/test/compiletest/util.rs @@ -0,0 +1,29 @@ +import std::option; +import std::generic_os::getenv; +import std::io; + +import common::config; + +fn make_new_path(path: &str) -> str { + + // Windows just uses PATH as the library search path, so we have to + // maintain the current value while adding our own + alt getenv(lib_path_env_var()) { + option::some(curr) { #fmt("%s:%s", path, curr) } + option::none. { path } + } +} + +#[cfg(target_os = "linux")] +fn lib_path_env_var() -> str { "LD_LIBRARY_PATH" } + +#[cfg(target_os = "macos")] +fn lib_path_env_var() -> str { "DYLD_LIBRARY_PATH" } + +#[cfg(target_os = "win32")] +fn lib_path_env_var() -> str { "PATH" } + +fn logv(config: &config, s: &str) { + log s; + if config.verbose { io::stdout().write_line(s); } +}