about summary refs log tree commit diff
path: root/src/compiletest
diff options
context:
space:
mode:
authorBrian Anderson <banderson@mozilla.com>2011-10-02 17:28:59 -0700
committerBrian Anderson <banderson@mozilla.com>2011-10-02 17:28:59 -0700
commit3a6f3cf275061c03c8169ce50779efbda0faa845 (patch)
tree04496acc42c514c463c01f34684a575703145134 /src/compiletest
parentedb56507eee87e44625813a94a06b8e28dfb617e (diff)
downloadrust-3a6f3cf275061c03c8169ce50779efbda0faa845.tar.gz
rust-3a6f3cf275061c03c8169ce50779efbda0faa845.zip
Move compiletest to src/ and cleanup build rules
Diffstat (limited to 'src/compiletest')
-rw-r--r--src/compiletest/common.rs32
-rw-r--r--src/compiletest/compiletest.rc16
-rw-r--r--src/compiletest/compiletest.rs274
-rw-r--r--src/compiletest/header.rs117
-rw-r--r--src/compiletest/procsrv.rs183
-rw-r--r--src/compiletest/runtest.rs392
-rw-r--r--src/compiletest/util.rs39
7 files changed, 1053 insertions, 0 deletions
diff --git a/src/compiletest/common.rs b/src/compiletest/common.rs
new file mode 100644
index 00000000000..ab0415fd7c1
--- /dev/null
+++ b/src/compiletest/common.rs
@@ -0,0 +1,32 @@
+import std::option;
+
+tag mode { mode_compile_fail; mode_run_fail; mode_run_pass; mode_pretty; }
+
+type config =
+    // The library paths required for running the compiler
+    // The library paths required for running compiled programs
+    // The rustc executable
+    // The directory containing the tests to run
+    // The directory where programs should be built
+    // The name of the stage being built (stage1, etc)
+    // The test mode, compile-fail, run-fail, run-pass
+    // Run ignored tests
+    // Only run tests that match this filter
+    // A command line to prefix program execution with,
+    // for running under valgrind
+    // Flags to pass to the compiler
+    // Explain what's going on
+    {compile_lib_path: str,
+     run_lib_path: str,
+     rustc_path: str,
+     src_base: str,
+     build_base: str,
+     stage_id: str,
+     mode: mode,
+     run_ignored: bool,
+     filter: option::t<str>,
+     runtool: option::t<str>,
+     rustcflags: option::t<str>,
+     verbose: bool};
+
+type cx = {config: config, procsrv: procsrv::handle};
diff --git a/src/compiletest/compiletest.rc b/src/compiletest/compiletest.rc
new file mode 100644
index 00000000000..2f9ffe50f81
--- /dev/null
+++ b/src/compiletest/compiletest.rc
@@ -0,0 +1,16 @@
+use std;
+
+mod compiletest;
+mod procsrv;
+mod util;
+mod header;
+mod runtest;
+mod common;
+
+// Local Variables:
+// fill-column: 78;
+// indent-tabs-mode: nil
+// c-basic-offset: 4
+// buffer-file-coding-system: utf-8-unix
+// compile-command: "make -k -C $RBUILD 2>&1 | sed -e 's/\\/x\\//x:\\//g'";
+// End:
diff --git a/src/compiletest/compiletest.rs b/src/compiletest/compiletest.rs
new file mode 100644
index 00000000000..b75bb10bab3
--- /dev/null
+++ b/src/compiletest/compiletest.rs
@@ -0,0 +1,274 @@
+import std::option;
+import std::getopts;
+import std::test;
+import std::fs;
+import std::str;
+import std::vec;
+import std::task;
+
+import std::comm;
+import std::comm::port;
+import std::comm::chan;
+import std::comm::send;
+import std::comm::recv;
+
+import common::cx;
+import common::config;
+import common::mode_run_pass;
+import common::mode_run_fail;
+import common::mode_compile_fail;
+import common::mode_pretty;
+import common::mode;
+import util::logv;
+
+fn main(args: [str]) {
+    let config = parse_config(args);
+    log_config(config);
+    run_tests(config);
+}
+
+fn parse_config(args: [str]) -> config {
+    let opts =
+        [getopts::reqopt("compile-lib-path"), getopts::reqopt("run-lib-path"),
+         getopts::reqopt("rustc-path"), getopts::reqopt("src-base"),
+         getopts::reqopt("build-base"), getopts::reqopt("stage-id"),
+         getopts::reqopt("mode"), getopts::optflag("ignored"),
+         getopts::optopt("runtool"), getopts::optopt("rustcflags"),
+         getopts::optflag("verbose")];
+
+    check (vec::is_not_empty(args));
+    let args_ = vec::tail(args);
+    let match =
+        alt getopts::getopts(args_, opts) {
+          getopts::success(m) { m }
+          getopts::failure(f) { fail getopts::fail_str(f) }
+        };
+
+    ret {compile_lib_path: getopts::opt_str(match, "compile-lib-path"),
+         run_lib_path: getopts::opt_str(match, "run-lib-path"),
+         rustc_path: getopts::opt_str(match, "rustc-path"),
+         src_base: getopts::opt_str(match, "src-base"),
+         build_base: getopts::opt_str(match, "build-base"),
+         stage_id: getopts::opt_str(match, "stage-id"),
+         mode: str_mode(getopts::opt_str(match, "mode")),
+         run_ignored: getopts::opt_present(match, "ignored"),
+         filter:
+             if vec::len(match.free) > 0u {
+                 option::some(match.free[0])
+             } else { option::none },
+         runtool: getopts::opt_maybe_str(match, "runtool"),
+         rustcflags: getopts::opt_maybe_str(match, "rustcflags"),
+         verbose: getopts::opt_present(match, "verbose")};
+}
+
+fn log_config(config: config) {
+    let c = config;
+    logv(c, #fmt["configuration:"]);
+    logv(c, #fmt["compile_lib_path: %s", config.compile_lib_path]);
+    logv(c, #fmt["run_lib_path: %s", config.run_lib_path]);
+    logv(c, #fmt["rustc_path: %s", config.rustc_path]);
+    logv(c, #fmt["src_base: %s", config.src_base]);
+    logv(c, #fmt["build_base: %s", config.build_base]);
+    logv(c, #fmt["stage_id: %s", config.stage_id]);
+    logv(c, #fmt["mode: %s", mode_str(config.mode)]);
+    logv(c, #fmt["run_ignored: %b", config.run_ignored]);
+    logv(c, #fmt["filter: %s", opt_str(config.filter)]);
+    logv(c, #fmt["runtool: %s", opt_str(config.runtool)]);
+    logv(c, #fmt["rustcflags: %s", opt_str(config.rustcflags)]);
+    logv(c, #fmt["verbose: %b", config.verbose]);
+    logv(c, #fmt["\n"]);
+}
+
+fn opt_str(maybestr: option::t<str>) -> str {
+    alt maybestr { option::some(s) { s } option::none. { "(none)" } }
+}
+
+fn str_opt(maybestr: str) -> option::t<str> {
+    if maybestr != "(none)" { option::some(maybestr) } else { option::none }
+}
+
+fn str_mode(s: str) -> mode {
+    alt s {
+      "compile-fail" { mode_compile_fail }
+      "run-fail" { mode_run_fail }
+      "run-pass" { mode_run_pass }
+      "pretty" { mode_pretty }
+      _ { fail "invalid mode" }
+    }
+}
+
+fn mode_str(mode: mode) -> str {
+    alt mode {
+      mode_compile_fail. { "compile-fail" }
+      mode_run_fail. { "run-fail" }
+      mode_run_pass. { "run-pass" }
+      mode_pretty. { "pretty" }
+    }
+}
+
+fn run_tests(config: config) {
+    let opts = test_opts(config);
+    let cx = {config: config, procsrv: procsrv::mk()};
+    let tests = make_tests(cx);
+    let res = test::run_tests_console_(opts, tests.tests, tests.to_task);
+    procsrv::close(cx.procsrv);
+    if !res { fail "Some tests failed"; }
+}
+
+fn test_opts(config: config) -> test::test_opts {
+    {filter:
+         alt config.filter {
+           option::some(s) { option::some(s) }
+           option::none. { option::none }
+         },
+     run_ignored: config.run_ignored}
+}
+
+type tests_and_conv_fn =
+    {tests: [test::test_desc], 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];
+    let configport = port::<[u8]>();
+    let tests = [];
+    for file: str in fs::list_dir(cx.config.src_base) {
+        let file = file;
+        log #fmt["inspecting file %s", file];
+        if is_test(cx.config, file) {
+            tests += [make_test(cx, file, configport)];
+        }
+    }
+    ret {tests: tests, to_task: bind closure_to_task(cx, configport, _)};
+}
+
+fn is_test(config: config, testfile: str) -> bool {
+    // Pretty-printer does not work with .rc files yet
+    let valid_extensions =
+        alt config.mode { mode_pretty. { [".rs"] } _ { [".rc", ".rs"] } };
+    let invalid_prefixes = [".", "#", "~"];
+    let name = fs::basename(testfile);
+
+    let valid = false;
+
+    for ext in valid_extensions {
+        if str::ends_with(name, ext) { valid = true; }
+    }
+
+    for pre in invalid_prefixes {
+        if str::starts_with(name, pre) { valid = false; }
+    }
+
+    ret valid;
+}
+
+fn make_test(cx: cx, testfile: str, configport: port<[u8]>) ->
+   test::test_desc {
+    {name: make_test_name(cx.config, testfile),
+     fn: make_test_closure(testfile, chan(configport)),
+     ignore: header::is_test_ignored(cx.config, testfile)}
+}
+
+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 {
+    bind send_config(testfile, configchan)
+}
+
+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);
+}
+
+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]) {
+
+    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);
+}
+
+// Local Variables:
+// fill-column: 78;
+// indent-tabs-mode: nil
+// c-basic-offset: 4
+// buffer-file-coding-system: utf-8-unix
+// compile-command: "make -k -C $RBUILD 2>&1 | sed -e 's/\\/x\\//x:\\//g'";
+// End:
diff --git a/src/compiletest/header.rs b/src/compiletest/header.rs
new file mode 100644
index 00000000000..890818d4574
--- /dev/null
+++ b/src/compiletest/header.rs
@@ -0,0 +1,117 @@
+import std::option;
+import std::str;
+import std::io;
+import std::fs;
+
+import common::config;
+
+export test_props;
+export load_props;
+export is_test_ignored;
+
+type test_props = {
+    // Lines that should be expected, in order, on standard out
+    error_patterns: [str],
+    // Extra flags to pass to the compiler
+    compile_flags: option::t<str>,
+    // If present, the name of a file that this test should match when
+    // pretty-printed
+    pp_exact: 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;
+    let pp_exact = 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);
+        }
+
+        if option::is_none(pp_exact) {
+            pp_exact = parse_pp_exact(ln, testfile);
+        }
+    }
+    ret {
+        error_patterns: error_patterns,
+        compile_flags: compile_flags,
+        pp_exact: pp_exact
+    };
+}
+
+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-test");
+        found = found || parse_name_directive(ln, xfail_target());
+        if (config.mode == common::mode_pretty) {
+            found = found || parse_name_directive(ln, "xfail-pretty");
+        }
+    }
+    ret found;
+
+    fn xfail_target() -> str {
+        "xfail-" + std::os::target_os()
+    }
+}
+
+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_pp_exact(line: str, testfile: str) -> option::t<str> {
+    alt parse_name_value_directive(line, "pp-exact") {
+      option::some(s) { option::some(s) }
+      option::none. {
+        if parse_name_directive(line, "pp-exact") {
+            option::some(fs::basename(testfile))
+        } else {
+            option::none
+        }
+      }
+    }
+}
+
+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/compiletest/procsrv.rs b/src/compiletest/procsrv.rs
new file mode 100644
index 00000000000..6a40aaccaf4
--- /dev/null
+++ b/src/compiletest/procsrv.rs
@@ -0,0 +1,183 @@
+// 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::vec;
+import std::os;
+import std::run;
+import std::io;
+import std::str;
+import std::comm::chan;
+import std::comm::port;
+import std::comm::send;
+import std::comm::recv;
+
+export handle;
+export mk;
+export from_chan;
+export run;
+export close;
+export reqchan;
+
+type reqchan = chan<request>;
+
+type handle =
+    {task: option::t<(task::task, port<task::task_notification>)>,
+     chan: reqchan};
+
+tag request { exec([u8], [u8], [[u8]], chan<response>); stop; }
+
+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)));
+    ret {task: option::some(task), chan: recv(setupport)};
+}
+
+fn from_chan(ch: reqchan) -> handle { {task: option::none, chan: ch} }
+
+fn close(handle: handle) {
+    send(handle.chan, stop);
+    task::join(option::get(handle.task));
+}
+
+fn run(handle: handle, lib_path: str, prog: str, args: [str],
+       input: option::t<str>) -> {status: int, out: str, err: str} {
+    let p = port();
+    let ch = chan(p);
+    send(handle.chan,
+         exec(str::bytes(lib_path), str::bytes(prog), clone_vecstr(args),
+              ch));
+    let resp = recv(p);
+
+    writeclose(resp.infd, input);
+    let output = readclose(resp.outfd);
+    let errput = readclose(resp.errfd);
+    let status = run::waitpid(resp.pid);
+    ret {status: status, out: output, err: errput};
+}
+
+fn writeclose(fd: int, s: option::t<str>) {
+    if option::is_some(s) {
+        let writer = io::new_writer(io::fd_buf_writer(fd, option::none));
+        writer.write_str(option::get(s));
+    }
+
+    os::libc::close(fd);
+}
+
+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.
+    let execparms;
+
+    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 recv(p) {
+                  exec(lib_path, prog, args, respchan) {
+                    {lib_path: str::unsafe_from_bytes(lib_path),
+                     prog: str::unsafe_from_bytes(prog),
+                     args: clone_vecu8str(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, 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_out.out);
+        os::libc::close(pipe_err.out);
+        if pid == -1 {
+            os::libc::close(pipe_in.out);
+            os::libc::close(pipe_out.in);
+            os::libc::close(pipe_err.in);
+            fail;
+        }
+
+        send(execparms.respchan,
+             {pid: pid,
+              infd: pipe_in.out,
+              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_vecstr(v: [str]) -> [[u8]] {
+    let r = [];
+    for t: str in vec::slice(v, 0u, vec::len(v)) { r += [str::bytes(t)]; }
+    ret r;
+}
+
+fn clone_vecu8str(v: [[u8]]) -> [str] {
+    let r = [];
+    for t in vec::slice(v, 0u, vec::len(v)) {
+        r += [str::unsafe_from_bytes(t)];
+    }
+    ret r;
+}
diff --git a/src/compiletest/runtest.rs b/src/compiletest/runtest.rs
new file mode 100644
index 00000000000..de188f3927e
--- /dev/null
+++ b/src/compiletest/runtest.rs
@@ -0,0 +1,392 @@
+import std::io;
+import std::str;
+import std::option;
+import std::fs;
+import std::os;
+import std::vec;
+import std::test;
+
+import common::mode_run_pass;
+import common::mode_run_fail;
+import common::mode_compile_fail;
+import common::mode_pretty;
+import common::cx;
+import common::config;
+import header::load_props;
+import header::test_props;
+import util::logv;
+
+export run;
+
+fn run(cx: cx, -_testfile: [u8]) {
+    let testfile = str::unsafe_from_bytes(_testfile);
+    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); }
+      mode_pretty. { run_pretty_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_correct_failure_status(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, props, testfile);
+
+    // The value our Makefile configures valgrind to return on failure
+    const valgrind_err: int = 100;
+    if procres.status == valgrind_err {
+        fatal_procres("run-fail test isn't valgrind-clean!", procres);
+    }
+
+    check_correct_failure_status(procres);
+    check_error_patterns(props, testfile, procres);
+}
+
+fn check_correct_failure_status(procres: procres) {
+    // The value the rust runtime returns on failure
+    const rust_err: int = 101;
+    if procres.status != rust_err {
+        fatal_procres(
+            #fmt("failure produced the wrong error code: %d",
+                 procres.status),
+            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, props, testfile);
+
+
+    if procres.status != 0 { fatal_procres("test run failed!", procres); }
+}
+
+fn run_pretty_test(cx: cx, props: test_props, testfile: str) {
+    if option::is_some(props.pp_exact) {
+        logv(cx.config, "testing for exact pretty-printing");
+    } else { logv(cx.config, "testing for converging pretty-printing"); }
+
+    let rounds =
+        alt props.pp_exact { option::some(_) { 1 } option::none. { 2 } };
+
+    let srcs = [io::read_whole_file_str(testfile)];
+
+    let round = 0;
+    while round < rounds {
+        logv(cx.config, #fmt["pretty-printing round %d", round]);
+        let procres = print_source(cx, testfile, srcs[round]);
+
+        if procres.status != 0 {
+            fatal_procres(#fmt["pretty-printing failed in round %d", round],
+                          procres);
+        }
+
+        srcs += [procres.stdout];
+        round += 1;
+    }
+
+    let expected =
+        alt props.pp_exact {
+          option::some(file) {
+            let filepath = fs::connect(fs::dirname(testfile), file);
+            io::read_whole_file_str(filepath)
+          }
+          option::none. { srcs[vec::len(srcs) - 2u] }
+        };
+    let actual = srcs[vec::len(srcs) - 1u];
+
+    if option::is_some(props.pp_exact) {
+        // Now we have to care about line endings
+        let cr = "\r";
+        check (str::is_not_empty(cr));
+        actual = str::replace(actual, cr, "");
+        expected = str::replace(expected, cr, "");
+    }
+
+    compare_source(expected, actual);
+
+    // Finally, let's make sure it actually appears to remain valid code
+    let procres = typecheck_source(cx, testfile, actual);
+
+    if procres.status != 0 {
+        fatal_procres("pretty-printed source does not typecheck", procres);
+    }
+
+    ret;
+
+    fn print_source(cx: cx, testfile: str, src: str) -> procres {
+        compose_and_run(cx, testfile, make_pp_args,
+                        cx.config.compile_lib_path, option::some(src))
+    }
+
+    fn make_pp_args(config: config, _testfile: str) -> procargs {
+        let prog = config.rustc_path;
+        let args = ["-", "--pretty", "normal"];
+        ret {prog: prog, args: args};
+    }
+
+    fn compare_source(expected: str, actual: str) {
+        if expected != actual {
+            error("pretty-printed source does match expected source");
+            let msg =
+                #fmt["\n\
+expected:\n\
+------------------------------------------\n\
+%s\n\
+------------------------------------------\n\
+actual:\n\
+------------------------------------------\n\
+%s\n\
+------------------------------------------\n\
+\n",
+                     expected, actual];
+            io::stdout().write_str(msg);
+            fail;
+        }
+    }
+
+    fn typecheck_source(cx: cx, testfile: str, src: str) -> procres {
+        compose_and_run(cx, testfile, make_typecheck_args,
+                        cx.config.compile_lib_path, option::some(src))
+    }
+
+    fn make_typecheck_args(config: config, _testfile: str) -> procargs {
+        let prog = config.rustc_path;
+        let args = ["-", "--no-trans", "--lib"];
+        ret {prog: prog, args: args};
+    }
+}
+
+fn check_error_patterns(props: test_props, testfile: str, procres: procres) {
+    if vec::is_empty(props.error_patterns) {
+        fatal("no error pattern specified in " + testfile);
+    }
+
+    if procres.status == 0 {
+        fatal("process did not return an error status");
+    }
+
+    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 == vec::len(props.error_patterns) {
+                log "found all error patterns";
+                ret;
+            }
+            next_err_pat = props.error_patterns[next_err_idx];
+        }
+    }
+
+    let missing_patterns =
+        vec::slice(props.error_patterns, next_err_idx,
+                   vec::len(props.error_patterns));
+    if vec::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: [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, option::none)
+}
+
+fn exec_compiled_test(cx: cx, props: test_props, testfile: str) -> procres {
+    compose_and_run(cx, testfile, bind make_run_args(_, props, _),
+                    cx.config.run_lib_path, option::none)
+}
+
+fn compose_and_run(cx: cx, testfile: str,
+                   make_args: fn(config, str) -> procargs, lib_path: str,
+                   input: option::t<str>) -> procres {
+    let procargs = make_args(cx.config, testfile);
+    ret program_output(cx, testfile, lib_path, procargs.prog, procargs.args,
+                       input);
+}
+
+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)];
+    let rustcflags =
+        alt config.rustcflags {
+          option::some(s) { option::some(s) }
+          option::none. { option::none }
+        };
+    args += split_maybe_args(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, _props: test_props, testfile: str) ->
+   procargs {
+    let toolargs = {
+            // If we've got another tool to run under (valgrind),
+            // then split apart its command
+            let runtool =
+                alt config.runtool {
+                  option::some(s) { option::some(s) }
+                  option::none. { option::none }
+                };
+            split_maybe_args(runtool)
+        };
+
+    let args = toolargs + [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>) -> [str] {
+    fn rm_whitespace(v: [str]) -> [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: [str], input: option::t<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, input);
+    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: [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/compiletest/util.rs b/src/compiletest/util.rs
new file mode 100644
index 00000000000..aa6c7d5abdb
--- /dev/null
+++ b/src/compiletest/util.rs
@@ -0,0 +1,39 @@
+import std::option;
+import std::generic_os::getenv;
+import std::io;
+import std::str;
+
+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%s", path, path_div(), 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" }
+
+#[cfg(target_os = "linux")]
+#[cfg(target_os = "macos")]
+fn path_div() -> str { ":" }
+
+#[cfg(target_os = "win32")]
+fn path_div() -> str { ";" }
+
+fn logv(config: config, s: str) {
+    log s;
+    if config.verbose { io::stdout().write_line(s); }
+}