#[doc(hidden)];

// Support code for rustc's built in test runner generator. Currently,
// none of this is meant for users. It is intended to support the
// simplest interface possible for representing and running tests
// while providing a base that other test frameworks may build off of.

#[forbid(deprecated_mode)];

use core::cmp::Eq;
use either::Either;
use result::{Ok, Err};
use io::WriterUtil;
use libc::size_t;
use task::TaskBuilder;
use comm = core::comm;

#[abi = "cdecl"]
extern mod rustrt {
    #[legacy_exports];
    fn rust_sched_threads() -> libc::size_t;
}

// The name of a test. By convention this follows the rules for rust
// 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;

// 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~();

// The definition of a single test. A test runner will run a list of
// these.
pub type TestDesc = {
    name: TestName,
    testfn: TestFn,
    ignore: bool,
    should_fail: bool
};

// The default console test runner. It accepts the command line
// arguments and a vector of test_descs (generated at compile time).
pub fn test_main(args: &[~str], tests: &[TestDesc]) {
    let opts =
        match parse_opts(args) {
          either::Left(move o) => o,
          either::Right(move m) => fail m
        };
    if !run_tests_console(&opts, tests) { fail ~"Some tests failed"; }
}

pub type TestOpts = {filter: Option<~str>, run_ignored: bool,
                  logfile: Option<~str>};

type OptRes = Either<TestOpts, ~str>;

// Parses command line arguments into test options
fn parse_opts(args: &[~str]) -> OptRes {
    let args_ = vec::tail(args);
    let opts = ~[getopts::optflag(~"ignored"), getopts::optopt(~"logfile")];
    let matches =
        match getopts::getopts(args_, opts) {
          Ok(move m) => m,
          Err(move f) => return either::Right(getopts::fail_str(f))
        };

    let filter =
        if vec::len(matches.free) > 0u {
            option::Some(matches.free[0])
        } else { option::None };

    let run_ignored = getopts::opt_present(matches, ~"ignored");
    let logfile = getopts::opt_maybe_str(matches, ~"logfile");

    let test_opts = {filter: filter, run_ignored: run_ignored,
                     logfile: logfile};

    return either::Left(test_opts);
}

pub enum TestResult { TrOk, TrFailed, TrIgnored, }

impl TestResult : Eq {
    pure fn eq(other: &TestResult) -> bool {
        (self as uint) == ((*other) as uint)
    }
    pure fn ne(other: &TestResult) -> bool { !self.eq(other) }
}

type ConsoleTestState =
    @{out: io::Writer,
      log_out: Option<io::Writer>,
      use_color: bool,
      mut total: uint,
      mut passed: uint,
      mut failed: uint,
      mut ignored: uint,
      mut failures: ~[TestDesc]};

// A simple console test runner
pub fn run_tests_console(opts: &TestOpts,
                     tests: &[TestDesc]) -> bool {

    fn callback(event: &TestEvent, st: ConsoleTestState) {
        debug!("callback(event=%?)", event);
        match *event {
          TeFiltered(ref filtered_tests) => {
            st.total = filtered_tests.len();
            let noun = if st.total != 1u { ~"tests" } else { ~"test" };
            st.out.write_line(fmt!("\nrunning %u %s", st.total, noun));
          }
          TeWait(ref test) => st.out.write_str(
              fmt!("test %s ... ", test.name)),
          TeResult(copy test, result) => {
            match st.log_out {
                Some(f) => write_log(f, result, &test),
                None => ()
            }
            match result {
              TrOk => {
                st.passed += 1u;
                write_ok(st.out, st.use_color);
                st.out.write_line(~"");
              }
              TrFailed => {
                st.failed += 1u;
                write_failed(st.out, st.use_color);
                st.out.write_line(~"");
                st.failures.push(test);
              }
              TrIgnored => {
                st.ignored += 1u;
                write_ignored(st.out, st.use_color);
                st.out.write_line(~"");
              }
            }
          }
        }
    }

    let log_out = match opts.logfile {
        Some(ref path) => match io::file_writer(&Path(*path),
                                            ~[io::Create, io::Truncate]) {
          result::Ok(w) => Some(w),
          result::Err(ref s) => {
              fail(fmt!("can't open output file: %s", *s))
          }
        },
        None => None
    };

    let st =
        @{out: io::stdout(),
          log_out: log_out,
          use_color: use_color(),
          mut total: 0u,
          mut passed: 0u,
          mut failed: 0u,
          mut ignored: 0u,
          mut failures: ~[]};

    run_tests(opts, tests, |x| callback(&x, st));

    assert (st.passed + st.failed + st.ignored == st.total);
    let success = st.failed == 0u;

    if !success {
        print_failures(st);
    }

    st.out.write_str(fmt!("\nresult: "));
    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));

    return success;

    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));
    }

    fn write_ok(out: io::Writer, use_color: bool) {
        write_pretty(out, ~"ok", term::color_green, use_color);
    }

    fn write_failed(out: io::Writer, use_color: bool) {
        write_pretty(out, ~"FAILED", term::color_red, use_color);
    }

    fn write_ignored(out: io::Writer, use_color: bool) {
        write_pretty(out, ~"ignored", term::color_yellow, 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);
        }
        out.write_str(word);
        if use_color && term::color_supported() {
            term::reset(out);
        }
    }
}

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 = sort::merge_sort(|x, y| str::le(*x, *y), failures);
    for vec::each(failures) |name| {
        st.out.write_line(fmt!("    %s", *name));
    }
}

#[test]
fn should_sort_failures_before_printing_them() {
    let s = do io::with_str_writer |wr| {
        let test_a = {
            name: ~"a",
            testfn: fn~() { },
            ignore: false,
            should_fail: false
        };

        let test_b = {
            name: ~"b",
            testfn: fn~() { },
            ignore: false,
            should_fail: false
        };

        let st =
            @{out: wr,
              log_out: option::None,
              use_color: false,
              mut total: 0u,
              mut passed: 0u,
              mut failed: 0u,
              mut ignored: 0u,
              mut failures: ~[test_b, test_a]};

        print_failures(st);
    };

    let apos = str::find_str(s, ~"a").get();
    let bpos = str::find_str(s, ~"b").get();
    assert apos < bpos;
}

fn use_color() -> bool { return get_concurrency() == 1u; }

enum TestEvent {
    TeFiltered(~[TestDesc]),
    TeWait(TestDesc),
    TeResult(TestDesc, TestResult),
}

type MonitorMsg = (TestDesc, TestResult);

fn run_tests(opts: &TestOpts, tests: &[TestDesc],
             callback: fn@(e: TestEvent)) {

    let mut filtered_tests = filter_tests(opts, tests);
    callback(TeFiltered(copy filtered_tests));

    // It's tempting to just spawn all the tests at once, but since we have
    // many tests that run in other processes we would be making a big mess.
    let concurrency = get_concurrency();
    debug!("using %u test tasks", concurrency);

    let total = vec::len(filtered_tests);
    let mut run_idx = 0;
    let mut wait_idx = 0;
    let mut done_idx = 0;

    let p = core::comm::Port();
    let ch = core::comm::Chan(&p);

    while done_idx < total {
        while wait_idx < concurrency && run_idx < total {
            let test = copy filtered_tests[run_idx];
            if concurrency == 1 {
                // We are doing one test at a time so we can print the name
                // of the test before we run it. Useful for debugging tests
                // that hang forever.
                callback(TeWait(copy test));
            }
            run_test(move test, ch);
            wait_idx += 1;
            run_idx += 1;
        }

        let (test, result) = core::comm::recv(p);
        if concurrency != 1 {
            callback(TeWait(copy test));
        }
        callback(TeResult(move test, result));
        wait_idx -= 1;
        done_idx += 1;
    }
}

// Windows tends to dislike being overloaded with threads.
#[cfg(windows)]
const sched_overcommit : uint = 1u;

#[cfg(unix)]
const sched_overcommit : uint = 4u;

fn get_concurrency() -> uint {
    let threads = rustrt::rust_sched_threads() as uint;
    if threads == 1u { 1u }
    else { threads * sched_overcommit }
}

#[allow(non_implicitly_copyable_typarams)]
fn filter_tests(opts: &TestOpts,
                tests: &[TestDesc]) -> ~[TestDesc] {
    let mut filtered = vec::slice(tests, 0, tests.len());

    // Remove tests that don't match the test filter
    filtered = if opts.filter.is_none() {
        move filtered
    } else {
        let filter_str =
            match opts.filter {
          option::Some(copy f) => f,
          option::None => ~""
        };

        fn filter_fn(test: &TestDesc, filter_str: &str) ->
            Option<TestDesc> {
            if str::contains(test.name, filter_str) {
                return option::Some(copy *test);
            } else { return option::None; }
        }

        vec::filter_map(filtered, |x| filter_fn(x, filter_str))
    };

    // Maybe pull out the ignored test and unignore them
    filtered = if !opts.run_ignored {
        move filtered
    } else {
        fn filter(test: &TestDesc) -> Option<TestDesc> {
            if test.ignore {
                return option::Some({name: test.name,
                                  testfn: copy test.testfn,
                                  ignore: false,
                                  should_fail: test.should_fail});
            } else { return option::None; }
        };

        vec::filter_map(filtered, |x| filter(x))
    };

    // Sort the tests alphabetically
    filtered = {
        pure fn lteq(t1: &TestDesc, t2: &TestDesc) -> bool {
            str::le(t1.name, t2.name)
        }
        sort::merge_sort(lteq, filtered)
    };

    move filtered
}

type TestFuture = {test: TestDesc, wait: fn@() -> TestResult};

fn run_test(test: TestDesc, monitor_ch: comm::Chan<MonitorMsg>) {
    if test.ignore {
        core::comm::send(monitor_ch, (copy test, TrIgnored));
        return;
    }

    do task::spawn |move test| {
        let testfn = copy test.testfn;
        let mut result_future = None; // task::future_result(builder);
        task::task().unlinked().future_result(|+r| {
            result_future = Some(move r);
        }).spawn(move testfn);
        let task_result = future::get(&option::unwrap(move result_future));
        let test_result = calc_result(&test, task_result == task::Success);
        comm::send(monitor_ch, (copy test, test_result));
    };
}

fn calc_result(test: &TestDesc, task_succeeded: bool) -> TestResult {
    if task_succeeded {
        if test.should_fail { TrFailed }
        else { TrOk }
    } else {
        if test.should_fail { TrOk }
        else { TrFailed }
    }
}

#[cfg(test)]
mod tests {
    #[legacy_exports];

    #[test]
    fn do_not_run_ignored_tests() {
        fn f() { fail; }
        let desc = {
            name: ~"whatever",
            testfn: f,
            ignore: true,
            should_fail: false
        };
        let p = core::comm::Port();
        let ch = core::comm::Chan(&p);
        run_test(desc, ch);
        let (_, res) = core::comm::recv(p);
        assert res != TrOk;
    }

    #[test]
    fn ignored_tests_result_in_ignored() {
        fn f() { }
        let desc = {
            name: ~"whatever",
            testfn: f,
            ignore: true,
            should_fail: false
        };
        let p = core::comm::Port();
        let ch = core::comm::Chan(&p);
        run_test(desc, ch);
        let (_, res) = core::comm::recv(p);
        assert res == TrIgnored;
    }

    #[test]
    #[ignore(cfg(windows))]
    fn test_should_fail() {
        fn f() { fail; }
        let desc = {
            name: ~"whatever",
            testfn: f,
            ignore: false,
            should_fail: true
        };
        let p = core::comm::Port();
        let ch = core::comm::Chan(&p);
        run_test(desc, ch);
        let (_, res) = core::comm::recv(p);
        assert res == TrOk;
    }

    #[test]
    fn test_should_fail_but_succeeds() {
        fn f() { }
        let desc = {
            name: ~"whatever",
            testfn: f,
            ignore: false,
            should_fail: true
        };
        let p = core::comm::Port();
        let ch = core::comm::Chan(&p);
        run_test(desc, ch);
        let (_, res) = core::comm::recv(p);
        assert res == TrFailed;
    }

    #[test]
    fn first_free_arg_should_be_a_filter() {
        let args = ~[~"progname", ~"filter"];
        let opts = match parse_opts(args) {
          either::Left(copy o) => o,
          _ => fail ~"Malformed arg in first_free_arg_should_be_a_filter"
        };
        assert ~"filter" == opts.filter.get();
    }

    #[test]
    fn parse_ignored_flag() {
        let args = ~[~"progname", ~"filter", ~"--ignored"];
        let opts = match parse_opts(args) {
          either::Left(copy o) => o,
          _ => fail ~"Malformed arg in parse_ignored_flag"
        };
        assert (opts.run_ignored);
    }

    #[test]
    fn filter_for_ignored_option() {
        // When we run ignored tests the test filter should filter out all the
        // unignored tests and flip the ignore flag on the rest to false

        let opts = {filter: option::None, run_ignored: true,
            logfile: option::None};
        let tests =
            ~[{name: ~"1", testfn: fn~() { },
               ignore: true, should_fail: false},
             {name: ~"2", testfn: fn~() { },
              ignore: false, should_fail: false}];
        let filtered = filter_tests(&opts, tests);

        assert (vec::len(filtered) == 1u);
        assert (filtered[0].name == ~"1");
        assert (filtered[0].ignore == false);
    }

    #[test]
    fn sort_tests() {
        let opts = {filter: option::None, run_ignored: false,
            logfile: option::None};

        let names =
            ~[~"sha1::test", ~"int::test_to_str", ~"int::test_pow",
             ~"test::do_not_run_ignored_tests",
             ~"test::ignored_tests_result_in_ignored",
             ~"test::first_free_arg_should_be_a_filter",
             ~"test::parse_ignored_flag", ~"test::filter_for_ignored_option",
             ~"test::sort_tests"];
        let tests =
        {
            let testfn = fn~() { };
            let mut tests = ~[];
            for vec::each(names) |name| {
                let test = {name: *name, testfn: copy testfn, ignore: false,
                            should_fail: false};
                tests.push(test);
            }
            tests
        };
        let filtered = filter_tests(&opts, tests);

        let expected =
            ~[~"int::test_pow", ~"int::test_to_str", ~"sha1::test",
              ~"test::do_not_run_ignored_tests",
              ~"test::filter_for_ignored_option",
              ~"test::first_free_arg_should_be_a_filter",
              ~"test::ignored_tests_result_in_ignored",
              ~"test::parse_ignored_flag",
              ~"test::sort_tests"];

        let pairs = vec::zip(expected, filtered);

        for vec::each(pairs) |p| {
            match *p {
                (ref a, ref b) => { assert (*a == b.name); }
            }
        }
    }
}


// Local Variables:
// mode: rust;
// fill-column: 78;
// indent-tabs-mode: nil
// c-basic-offset: 4
// buffer-file-coding-system: utf-8-unix
// End:
