use MONKEY-GUTS;          # Allow NQP ops.

unit module Test;
# Copyright (C) 2007 - 2022 The Perl Foundation.

# settable from outside
my int $raku_test_times =
  ?(%*ENV<RAKU_TEST_TIMES> // %*ENV<PERL6_TEST_TIMES>);
my int $die_on_fail =
  ?(%*ENV<RAKU_TEST_DIE_ON_FAIL> // %*ENV<PERL6_TEST_DIE_ON_FAIL>);

# global state
my @vars;
my $indents = "";

# variables to keep track of our tests
my $subtest_callable_type;
my int $num_of_tests_run;
my int $num_of_tests_failed;
my int $todo_upto_test_num;
my $todo_reason;
my $num_of_tests_planned;
my int $no_plan;
my int $time_before;
my int $time_after;
my int $subtest_level;
my $subtest_todo_reason;
my int $pseudo_fails; # number of untodoed failed tests inside a todoed subtest

# Output should always go to real stdout/stderr, not to any dynamic overrides.
my $output;
my $failure_output;
my $todo_output;

## If done-testing hasn't been run when we hit our END block, we need to know
## so that it can be run. This allows compatibility with old tests that use
## plans and don't call done-testing.
my int $done_testing_has_been_run = 0;

# make sure we have initializations
_init_vars();

sub _init_io {
    nqp::setbuffersizefh(nqp::getstdout(), 0);
    nqp::setbuffersizefh(nqp::getstderr(), 0);
    $output         = $PROCESS::OUT;
    $failure_output = $PROCESS::ERR;
    $todo_output    = $PROCESS::OUT;
}

sub MONKEY-SEE-NO-EVAL() is export { 1 }

## test functions

our sub output is rw {
    $output
}

our sub failure_output is rw {
    $failure_output
}

our sub todo_output is rw {
    $todo_output
}

multi sub trait_mod:<is>(Routine:D $r, :$test-assertion!) is export {
    $r.^mixin( role is-test-assertion {
        method is-test-assertion(--> True) { }
    }) if $test-assertion;
}

proto sub plan ($?, Cool :$skip-all) {*}

my class X::SubtestsSkipped is Exception {}

multi sub plan (Cool:D :skip-all($reason)!) {
    _init_io() unless $output;
    $output.say: $indents ~ "1..0 # Skipped: $reason";

    exit unless $subtest_level; # just exit if we're not in a subtest

    # but if we are, adjust the vars, so the output matches up with zero
    # planned tests, all passsing. Also, check the subtest's Callable is
    # something we can actually return from.

    $subtest_callable_type === Sub|Method
        or die "Must give `subtest` a (Sub) or a (Method) to be able to use "
            ~ "`skip-all` plan inside, but you gave a "
            ~ $subtest_callable_type.gist;

    $done_testing_has_been_run = 1;
    $num_of_tests_failed = $num_of_tests_planned = $num_of_tests_run = 0;
    X::SubtestsSkipped.new.throw
}

# "plan 'no_plan';" is now "plan *;"
# It is also the default if nobody calls plan at all
multi sub plan($number_of_tests) is export {
    _init_io() unless $output;

    my str $str-message;

    if $number_of_tests ~~ ::Whatever {
        $no_plan = 1;
    }
    else {
        $num_of_tests_planned = $number_of_tests;
        $no_plan = 0;

        $str-message ~= $indents ~ '1..' ~ $number_of_tests;
    }
    # Get two successive timestamps to say how long it takes to read the
    # clock, and to let the first test timing work just like the rest.
    # These readings should be made with the expression now.to-posix[0],
    # but its execution time when tried in the following two lines is a
    # lot slower than the non portable nqp::time_n.
    $time_before = nqp::time;
    $time_after  = nqp::time;
    $str-message ~= "\n$indents# between two timestamps " ~ ceiling(($time_after-$time_before)*1_000_000) ~ ' microseconds'
        if nqp::iseq_i($raku_test_times,1);

    $output.say: $str-message;

    # Take one more reading to serve as the begin time of the first test
    $time_before = nqp::time;
}

multi sub pass($desc = '') is export {
    $time_after = nqp::time;
    my $ok = proclaim(1, $desc);
    $time_before = nqp::time;
    $ok;
}

multi sub ok(Mu $cond, $desc = '') is export {
    $time_after = nqp::time;
    my $ok = proclaim($cond, $desc);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub nok(Mu $cond, $desc = '') is export {
    $time_after = nqp::time;
    my $ok = proclaim(!$cond, $desc);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub is(Mu $got, Mu:U $expected, $desc = '') is export {
    $time_after = nqp::time;
    my $ok;
    if $got.defined { # also hack to deal with Failures
        $ok = proclaim(False, $desc);
        _diag "expected: ($expected.^name())\n"
            ~ "     got: '$got'";
    }
    else {
        # infix:<===> can't handle Mu's
        my $test = nqp::eqaddr($expected.WHAT, Mu)
            ?? nqp::eqaddr($got.WHAT, Mu)
                !! nqp::eqaddr($got.WHAT, Mu)
                    ?? False
                    !! $got === $expected;

        $ok = proclaim($test, $desc);
        if !$test {
            _diag "expected: ($expected.^name())\n"
                ~ "     got: ($got.^name())";
        }
    }
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub is(Mu $got, Mu:D $expected, $desc = '') is export {
    $time_after = nqp::time;
    my $ok;
    if $got.defined { # also hack to deal with Failures
        # infix:<eq> can't handle Mu's
        my $test = nqp::eqaddr($expected.WHAT, Mu)
            ?? nqp::eqaddr($got.WHAT, Mu)
                !! nqp::eqaddr($got.WHAT, Mu)
                    ?? False
                    !! $got eq $expected;
        $ok = proclaim($test, $desc);
        if !$test {
            if try    $got     .Str.subst(/\s/, '', :g)
                   eq $expected.Str.subst(/\s/, '', :g)
            {
                # only white space differs, so better show it to the user
                _diag "expected: $expected.raku()\n"
                    ~ "     got: $got.raku()";
            }
            else {
                 try { # if the type support Stringification
                      # note: we can't use ^can('Str') as Buf error in its Str method itself
                    _diag "expected: '$expected'\n"
                    ~ "     got: '$got'";
                    True;
                } or do {
                    _diag "expected: $expected.raku()\n"
                    ~ "     got: $got.raku()";
                }
            }
        }
    }
    else {
        $ok = proclaim(False, $desc);
        _diag "expected: '$expected'\n"
            ~ "     got: ($got.^name())";
    }
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub isnt(Mu $got, Mu:U $expected, $desc = '') is export {
    $time_after = nqp::time;
    my $ok;
    if $got.defined { # also hack to deal with Failures
        $ok = proclaim(True, $desc);
    }
    else {
        my $test = $got !=== $expected;
        $ok = proclaim($test, $desc);
        if !$test {
            _diag "expected: anything except '$expected.raku()'\n"
                ~ "     got: '$got.raku()'";
        }
    }
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub isnt(Mu $got, Mu:D $expected, $desc = '') is export {
    $time_after = nqp::time;
    my $ok;
    if $got.defined { # also hack to deal with Failures
        my $test = $got ne $expected;
        $ok = proclaim($test, $desc);
        if !$test {
            _diag "expected: anything except '$expected.raku()'\n"
                ~ "     got: '$got.raku()'";
        }
    }
    else {
        $ok = proclaim(True, $desc);
    }
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub cmp-ok(Mu $got is raw, $op, Mu $expected is raw, $desc = '') is export {
    $time_after = nqp::time;
    $got.defined if nqp::istype($got, Failure); # Hack to deal with Failures
    my $ok;

    # the three labeled &CALLERS below are as follows:
    #  #1 handles ops that don't have '<' or '>'
    #  #2 handles ops that don't have '«' or '»'
    #  #3 handles all the rest by escaping '<' and '>' with backslashes.
    #     Note: #3 doesn't eliminate #1, as #3 fails with '<' operator
    my $matcher = nqp::istype($op, Callable) ?? $op
        !! &CALLER::LEXICAL::("infix:<$op>") #1
            // &CALLER::LEXICAL::("infix:«$op»") #2
            // &CALLER::LEXICAL::("infix:<$op.subst(/<?before <[<>]>>/, "\\", :g)>"); #3

    if $matcher {
        $ok = proclaim($matcher($got,$expected), $desc);
        if !$ok {
            my $expected-desc = stringify $expected;
            my      $got-desc = stringify $got;
            _diag "expected: $expected-desc\n"
                ~ " matcher: '" ~ ($matcher.?name || $matcher.^name) ~ "'\n"
                ~ "     got: $got-desc";
        }
    }
    else {
        $ok = proclaim(False, $desc);
        _diag "Could not use '$op.raku()' as a comparator." ~ (
            ' If you are trying to use a meta operator, pass it as a '
            ~ "Callable instead of a string: \&[$op]"
            unless nqp::istype($op, Callable)
        );
    }
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

sub bail-out ($desc?) is export {
    _init_io() unless $output;
    $output.put: join ' ', 'Bail out!', ($desc if $desc);
    $done_testing_has_been_run = 1;
    exit 255;
}

# We're picking and choosing which tolerance to use here, to make it easier
# to test numbers close to zero, yet maintain relative tolerance elsewhere.
# For example, relative tolerance works equally well with regular and huge,
# but once we go down to zero, things break down: is-approx sin(τ), 0; would
# fail, because the computed relative tolerance is 1. For such cases, absolute
# tolerance is better suited, so we DWIM in the no-tol version of the sub.
multi sub is-approx(Numeric $got, Numeric $expected, $desc = '') is export {
    $expected.abs < 1e-6
        ?? is-approx-calculate($got, $expected, 1e-5, Nil, $desc) # abs-tol
        !! is-approx-calculate($got, $expected, Nil, 1e-6, $desc) # rel-tol
}

multi sub is-approx(
    Numeric $got, Numeric $expected, Numeric $abs-tol, $desc = ''
) is export {
    is-approx-calculate($got, $expected, $abs-tol, Nil, $desc);
}

multi sub is-approx(
    Numeric $got, Numeric $expected, $desc = '', Numeric :$rel-tol is required
) is export {
    is-approx-calculate($got, $expected, Nil, $rel-tol, $desc);
}

multi sub is-approx(
    Numeric $got, Numeric $expected, $desc = '', Numeric :$abs-tol is required
) is export {
    is-approx-calculate($got, $expected, $abs-tol, Nil, $desc);
}

multi sub is-approx(
    Numeric $got, Numeric $expected, $desc = '',
    Numeric :$rel-tol is required, Numeric :$abs-tol is required
) is export {
    is-approx-calculate($got, $expected, $abs-tol, $rel-tol, $desc);
}

sub is-approx-calculate (
    $got,
    $expected,
    $abs-tol where { !.defined or $_ >= 0 },
    $rel-tol where { !.defined or $_ >= 0 },
    $desc,
) {
    $time_after = nqp::time;

    my Bool    ($abs-tol-ok, $rel-tol-ok) = True, True;
    my Numeric ($abs-tol-got, $rel-tol-got);
    if $abs-tol.defined {
        $abs-tol-got = abs($got - $expected);
        $abs-tol-ok = $abs-tol-got <= $abs-tol;
    }
    if $rel-tol.defined {
        if max($got.abs, $expected.abs) -> $max {
            $rel-tol-got = abs($got - $expected) / $max;
            $rel-tol-ok = $rel-tol-got <= $rel-tol;
        }
        else {
            # if $max is zero, then both $got and $expected are zero
            # and so our relative difference is also zero
            $rel-tol-got = 0;
        }
    }

    my $ok = proclaim($abs-tol-ok && $rel-tol-ok, $desc);
    unless $ok {
            _diag "    expected approximately: $expected\n"
                ~ "                       got: $got";

        unless $abs-tol-ok {
            _diag "maximum absolute tolerance: $abs-tol\n"
                ~ "actual absolute difference: $abs-tol-got";
        }
        unless $rel-tol-ok {
            _diag "maximum relative tolerance: $rel-tol\n"
                ~ "actual relative difference: $rel-tol-got";
        }
    }

    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub todo($reason, $count = 1) is export {
    $time_after = nqp::time;
    $todo_upto_test_num = $num_of_tests_run + $count;
    # Adding a space to not backslash the # TODO
    $todo_reason = " # TODO $reason";
    $time_before = nqp::time;
}

proto  sub skip(|) is export {*}
multi sub skip() {
    $time_after = nqp::time;
    proclaim(1, '', "# SKIP");
    $time_before = nqp::time;
}
multi sub skip($reason, $count = 1) {
    $time_after = nqp::time;
    die "skip() was passed a non-integer number of tests.  Did you get the arguments backwards or use a non-integer number?" if $count !~~ Int;
    my $i = 1;
    while $i <= $count { proclaim(1, $reason, "# SKIP "); $i = $i + 1; }
    $time_before = nqp::time;
}

sub skip-rest($reason = '<unknown>') is export {
    $time_after = nqp::time;
    die "A plan is required in order to use skip-rest"
      if nqp::iseq_i($no_plan,1);
    skip($reason, $num_of_tests_planned - $num_of_tests_run);
    $time_before = nqp::time;
}

multi sub subtest(Pair $what)            is export { subtest($what.value,$what.key) }
multi sub subtest($desc, &subtests)      is export { subtest(&subtests,$desc)       }
multi sub subtest(&subtests, $desc = '') is export {
    _diag "Subtest" ~ ($desc ?? ": " ~ $desc !! ''), :force-informative;
    my $parent_todo = $todo_reason || $subtest_todo_reason;
    _push_vars();
    _init_vars();
    $subtest_todo_reason   = $parent_todo;
    $subtest_callable_type = &subtests.WHAT;
    $indents ~= "    ";
    $subtest_level++;
    {
        subtests();
        CATCH {
            when X::SubtestsSkipped {
                # Subtests all skipped
            }
        }
    }
    $subtest_level--;
    done-testing() if nqp::iseq_i($done_testing_has_been_run,0);
    my $status = $num_of_tests_failed == 0
        && $num_of_tests_planned == $num_of_tests_run
        && $pseudo_fails == 0;
    _pop_vars;
    $indents .= chop(4);
    proclaim($status,$desc) or ($die_on_fail and die-on-fail);
}

sub diag($message) is export {
    # Always send user-triggered diagnostics to STDERR. This prevents
    # cases of confusion of where diag() has to send its ouput when
    # we are in the middle of TODO tests
    _diag $message, :force-stderr;
}

sub _diag($message, :$force-stderr, :$force-informative) {
    _init_io() unless $output;
    my $is_todo = !$force-stderr && !$force-informative
        && ($subtest_todo_reason || $num_of_tests_run <= $todo_upto_test_num);
    my $out     = $is_todo || $force-informative ?? $todo_output !! $failure_output;

    $time_after = nqp::time;
    my $str-message = nqp::join(
        "\n$indents# ", nqp::split("\n", "$indents# $message")
    );
    $out.say: $str-message;
    $time_before = nqp::time;
}

# In earlier Perls, this is spelled "sub fail"
multi sub flunk($reason = '') is export {
    $time_after = nqp::time;
    my $ok = proclaim(0, $reason);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub isa-ok(
    Mu $var, Mu $type, $desc = "The object is-a '$type.raku()'"
) is export {
    $time_after = nqp::time;
    my $ok = ($type ~~ Str:D
                ?? proclaim($var.isa($type), $desc)
                !! proclaim(nqp::istype($var, $type.WHAT), $desc))
        or _diag('Actual type: ' ~ $var.^name);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub does-ok(
    Mu $var, Mu $type, $desc = "The object does role '$type.raku()'"
) is export {
    $time_after = nqp::time;
    my $ok = proclaim($var.does($type), $desc)
        or _diag([~] 'Type: ',  $var.^name, " doesn't do role ", $type.raku);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub can-ok(
    Mu $var, Str $meth,
    $desc = ($var.defined ?? "An object of type '" !! "The type '")
        ~ "$var.WHAT.raku()' can do the method '$meth'"
) is export {
    $time_after = nqp::time;
    my $ok = proclaim($var.^can($meth), $desc);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub like(
    Str() $got, Regex:D $expected,
    $desc = "text matches $expected.raku()"
) is export {
    $time_after = nqp::time;
    my $ok := proclaim $got ~~ $expected, $desc
        or _diag "expected a match with: $expected.raku()\n"
               ~ "                  got: $got.raku()";
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub unlike(
    Str() $got, Regex:D $expected,
    $desc = "text does not match $expected.raku()"
) is export {
    $time_after = nqp::time;
    my $ok := proclaim !($got ~~ $expected), $desc
        or _diag "expected no match with: $expected.raku()\n"
               ~ "                   got: $got.raku()";
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub use-ok(Str $code, $desc = "$code module can be use-d ok") is export {
    $time_after = nqp::time;
    try {
        EVAL ( "use $code" );
    }
    my $ok = proclaim((not defined $!), $desc) or _diag($!);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub dies-ok(Callable $code, $reason = '') is export {
    $time_after = nqp::time;
    my $death = 1;
    try {
        $code();
        $death = 0;
    }
    my $ok = proclaim( $death, $reason );
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub lives-ok(Callable $code, $reason = '') is export {
    $time_after = nqp::time;
    try {
        $code();
    }
    my $ok = proclaim((not defined $!), $reason) or _diag($!);
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub eval-dies-ok(Str $code, $reason = '') is export {
    $time_after = nqp::time;
    my $ee = eval_exception($code);
    my $ok = proclaim( $ee.defined, $reason );
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

multi sub eval-lives-ok(Str $code, $reason = '') is export {
    $time_after = nqp::time;
    my $ee = eval_exception($code);
    my $ok = proclaim((not defined $ee), $reason)
        or _diag("Error: $ee");
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

######################################################################
# The fact that is-deeply converts Seq args to Lists is actually a bug
# that ended up being too-much-pain-for-little-gain to fix. Using Seqs
# breaks ~65 tests in 6.c-errata and likely breaks a lot of module
# tests as well. So... for the foreseeable future we decided to leave it
# as is. If a user really wants to ensure Seq comparison, there's always
# `cmp-ok` with `eqv` op.
# https://irclogs.raku.org/perl6-dev/2017-05-04.html#15:38-0001
######################################################################
multi sub is-deeply(Seq:D $got, Seq:D $expected, $reason = '') is export {
    is-deeply $got.cache, $expected.cache, $reason;
}
multi sub is-deeply(Seq:D $got, Mu $expected, $reason = '') is export {
    is-deeply $got.cache, $expected, $reason;
}
multi sub is-deeply(Mu $got, Seq:D $expected, $reason = '') is export {
    is-deeply $got, $expected.cache, $reason;
}
multi sub is-deeply(Mu $got, Mu $expected, $reason = '') is export {
    $time_after = nqp::time;
    my $test = _is_deeply( $got, $expected );
    my $ok = proclaim($test, $reason);
    if !$test {
        my $got_perl      = try { $got.raku };
        my $expected_perl = try { $expected.raku };
        if $got_perl.defined && $expected_perl.defined {
            _diag "expected: $expected_perl\n"
                ~ "     got: $got_perl";
        }
    }
    $time_before = nqp::time;
    $ok or ($die_on_fail and die-on-fail) or $ok;
}

sub throws-like($code, $ex_type, $reason?, *%matcher) is export {
    for %matcher.kv -> $k, $v {
        if $v.DEFINITE && $v ~~ Bool {
            X::Match::Bool.new(:type(".$k")).throw;
        }
    }
    my $caller-context = $*THROWS-LIKE-CONTEXT // CALLER::; # Don't guess our caller context, know it!
    subtest {
        plan 2 + %matcher.keys.elems;
        my $msg;
        if $code ~~ Callable {
            $msg = 'code dies';
            $code()
        } else {
            $msg = "'$code' died";
            EVAL $code, context => $caller-context;
        }
        flunk $msg;
        skip 'Code did not die, can not check exception', 1 + %matcher.elems;
        CATCH {
            default {
                pass $msg;
                my $type_ok = $_ ~~ $ex_type;
                if !$type_ok && $ex_type ~~ X::Comp && $_ ~~ X::Comp::Group {
                    # Compile time exceptions may be found in a group if the compiler reports
                    # more than one problem.
                    with $_.sorrows.first($ex_type) -> $ex {
                        $_ := $ex;
                        $type_ok = True;
                    }
                }
                ok $type_ok , "right exception type ($ex_type.^name())";
                if $type_ok {
                    for %matcher.kv -> $k, $v {
                        my $got is default(Nil) = $_."$k"();
                        my $ok = $got ~~ $v,;
                        ok $ok, ".$k matches $v.gist()";
                        unless $ok {
                            _diag "Expected: " ~ ($v ~~ Str ?? $v !! $v.raku)
                              ~ "\nGot:      $got";
                        }
                    }
                } else {
                    _diag "Expected: $ex_type.^name()\n"
                        ~ "Got:      $_.^name()\n"
                        ~ "Exception message: $_.message()";
                    skip 'wrong exception type', %matcher.elems;
                }
            }
        }
    }, $reason // "did we throws-like $ex_type.^name()?";
}

sub fails-like (
    \test where Callable:D|Str:D, $ex-type, $reason?, *%matcher
) is export {
    for %matcher.kv -> $k, $v {
        if $v.DEFINITE && $v ~~ Bool {
            X::Match::Bool.new(:type(".$k")).throw;
        }
    }
    my $*THROWS-LIKE-CONTEXT = CALLER::;
    subtest sub {
        plan 2;
        CATCH { default {
            with "expected code to fail but it threw {.^name} instead" {
                .&flunk;
                .&skip;
                return False;
            }
        }}
        my $res = test ~~ Callable ?? test.() !! test.EVAL;
        isa-ok $res, Failure, 'code returned a Failure';
        throws-like { $res.sink }, $ex-type,
            'Failure threw when sunk', |%matcher,
    }, $reason // "did we fails-like $ex-type.^name()?"
}

sub _is_deeply(Mu $got, Mu $expected) {
    $got eqv $expected;
}


## 'private' subs

sub die-on-fail {
    if !$todo_reason && !$subtest_level && nqp::iseq_i($die_on_fail,1) {
        _diag 'Test failed. Stopping test suite, because the '
          ~ (%*ENV<RAKU_TEST_DIE_ON_FAIL> ?? 'RAKU' !! 'PERL6')
          ~ "_TEST_DIE_ON_FAIL\n"
          ~ 'environmental variable is set to a true value.';
        exit 255;
    }

    $todo_reason = '' if $todo_upto_test_num == $num_of_tests_run;

    False;
}

sub eval_exception($code) {
    try {
        EVAL ($code);
    }
    $!;
}

# Stringifies values passed to &cmp-ok.
sub stringify(Mu $obj is raw --> Str:D) {
    (try $obj.raku if nqp::can($obj, 'raku'))
        // ($obj.gist if nqp::can($obj, 'gist'))
        // ($obj.HOW.name($obj) if nqp::can($obj.HOW, 'name'))
        // '?'
}

# Take $cond as Mu so we don't thread with Junctions:
sub proclaim(
  Bool(Mu) $cond,
  $desc is copy,
  $unescaped-prefix = ''
) is hidden-from-backtrace {
    _init_io() unless $output;
    # exclude the time spent in proclaim from the test time
    $num_of_tests_run = $num_of_tests_run + 1;

    my $tap = $indents;
    unless $cond {
        $tap ~= "not ";

        $num_of_tests_failed = $num_of_tests_failed + 1
            unless $num_of_tests_run <= $todo_upto_test_num;

        $pseudo_fails = $pseudo_fails + 1 if $subtest_todo_reason;
    }

    # TAP parsers do not like '#' in the description, they'd miss the '# TODO'
    # So, adding a ' \' before it.
    $desc = $desc
    ??  nqp::join("\n$indents# ", # prefix newlines with `#`
            nqp::split("\n",
                nqp::join(' \\#', # escape `#`
                    nqp::split('#', $desc.Str))))
    !! '';

    $tap ~= $todo_reason && $num_of_tests_run <= $todo_upto_test_num
        ?? "ok $num_of_tests_run - $unescaped-prefix$desc$todo_reason"
        !! (! $cond && $subtest_todo_reason)
            ?? "ok $num_of_tests_run - $unescaped-prefix$desc$subtest_todo_reason"
            !! "ok $num_of_tests_run - $unescaped-prefix$desc";

    $tap ~= ("\n$indents# t=" ~ ceiling(($time_after - $time_before)*1_000_000))
        if nqp::iseq_i($raku_test_times,1);

    $output.say: $tap;

    unless $cond {
        my $caller;
        # sub proclaim is not called directly, so 2 is minimum level
        my int $level = 1;

        repeat {
            $caller = callframe(++$level);
        } while $?FILE.ends-with($caller.file);

        # initial level for reporting
        my $tester = $caller;

        repeat {
            ($caller = callframe($level++)) or last;
            my \code := $caller.code;
            $tester = callframe($level)  # the next one should be reported
                if nqp::can(code,'is-test-assertion');  # must use nqp
        } until !$caller.file || $caller.file.ends-with('.nqp');

        # the final place we want to report from
        _diag $desc
          ?? "Failed test '$desc'\nat $tester.file() line $tester.line()"
          !! "Failed test at $tester.file() line $tester.line()";
    }

    # must clear this between tests
    $todo_reason = ''
        if $todo_upto_test_num == $num_of_tests_run
            and nqp::iseq_i($die_on_fail,0);

    $cond
}

sub done-testing(-->Bool:D) is export {
    my $return = True;
    _init_io() unless $output;
    $done_testing_has_been_run = 1;

    if nqp::iseq_i($no_plan,1) {
        $num_of_tests_planned = $num_of_tests_run;
        $output.say: $indents ~ "1..$num_of_tests_planned";
    }

    if ($num_of_tests_planned or $num_of_tests_run)
        && ($num_of_tests_planned != $num_of_tests_run) {
        _diag("You planned $num_of_tests_planned test"
            ~ ($num_of_tests_planned == 1 ?? '' !! 's')
            ~ ", but ran $num_of_tests_run");
        $return = False;
    }
    if $num_of_tests_failed && ! $subtest_todo_reason {
        _diag("You failed $num_of_tests_failed test"
            ~ ($num_of_tests_failed == 1 ?? '' !! 's')
            ~ " of $num_of_tests_run");
        $return = False;
    }
    $return;
}

sub _init_vars {
    $subtest_callable_type = Mu;
    $num_of_tests_run     = 0;
    $num_of_tests_failed  = 0;
    $todo_upto_test_num   = 0;
    $todo_reason          = '';
    $num_of_tests_planned = Any;
    $no_plan              = 1;
    $time_before          = 0;
    $time_after           = 0;
    $done_testing_has_been_run = 0;
    $pseudo_fails         = 0;
    $subtest_todo_reason  = '';
}

sub _push_vars {
    @vars.push: item [
      $subtest_callable_type,
      $num_of_tests_run,
      $num_of_tests_failed,
      $todo_upto_test_num,
      $todo_reason,
      $num_of_tests_planned,
      $no_plan,
      $time_before,
      $time_after,
      $done_testing_has_been_run,
      $pseudo_fails,
      $subtest_todo_reason,
    ];
}

sub _pop_vars {
    (
      $subtest_callable_type,
      $num_of_tests_run,
      $num_of_tests_failed,
      $todo_upto_test_num,
      $todo_reason,
      $num_of_tests_planned,
      $no_plan,
      $time_before,
      $time_after,
      $done_testing_has_been_run,
      $pseudo_fails,
      $subtest_todo_reason,
    ) = @(@vars.pop);
}

END {
    ## In planned mode, people don't necessarily expect to have to call done-testing
    ## So call it for them if they didn't
    done-testing
      if nqp::iseq_i($done_testing_has_been_run,0)
      && nqp::iseq_i($no_plan,0);

    .?close unless $_ === $*OUT || $_ === $*ERR
      for $output, $failure_output, $todo_output;

    exit($num_of_tests_failed min 254) if $num_of_tests_failed > 0;

    exit(255)
      if nqp::iseq_i($no_plan,0)
      && ($num_of_tests_planned or $num_of_tests_run)
      &&  $num_of_tests_planned != $num_of_tests_run;
}

=begin pod

=head1 NAME

Test - Rakudo Testing Library

=head1 SYNOPSIS

  use Test;

=head1 DESCRIPTION

Please check the section Language/testing of the doc repository.
If you have 'p6doc' installed, you can do 'p6doc Language/testing'.

You can also check the documentation about testing in Raku online on L<https://docs.raku.org/language/testing>.

=end pod

# vim: expandtab shiftwidth=4
