### ----------------------------------------------------
### -- Test::Async
### -- Licenses: Artistic-2.0
### -- Authors: Vadim Belman <vrurg@cpan.org>
### -- File: t/060-subtest.rakutest
### ----------------------------------------------------
use v6;
BEGIN $*SCHEDULER = ThreadPoolScheduler.new: :max_threads(3000);

use lib $?FILE.IO.parent(1).add('lib/060-subtest/lib');
use Test::SubBundle;
use Test::Async <Base>;

# Be explicit about the mode of operation, no parallel, no randomization are allowed.
plan 19, :!parallel, :!random;

my @default-args = '-I' ~ $?FILE.IO.parent(2), '-MTest::Async';
my $job-count = test-suite.test-jobs;

# Catch the simplest errors here.
subtest "Simple" => {
    pass "simple";
}

is-run q:to/TEST-CODE/, "basic subtest",
           plan 1;
           subtest "simple" => {
               pass "subtest 1";
           };
           TEST-CODE
       :async,
       :compiler-args(@default-args),
       :exitcode(0),
       :err(''),
       :out(/^"1..1\n# Subtest: simple\n  ok 1 - subtest 1\n  1..1\nok 1 - simple\n"/);

is-run q:to/TEST-CODE/, "sequential",
           plan 2;
           subtest "simple 1" => {
               pass "subtest 1-1";
           };
           subtest "simple 2" => {
               plan 1;
               pass "subtest 2-1";
           };
           TEST-CODE
       :async,
       :compiler-args(@default-args),
       :exitcode(0),
       :err(''),
       :out(
            /
                ^"1..2\n# Subtest: simple 1\n  ok 1 - subtest 1-1\n  1..1\nok 1 - simple 1\n"
                ^^"# Subtest: simple 2\n  1..1\n  ok 1 - subtest 2-1\nok 2 - simple 2\n"
            /
       );

is-run q:to/TEST-CODE/, "hidden subtest",
           use Test::SubBundle;
           use Test::Async <Base>;
           plan 1;
           test-hidden-subtest;
           TEST-CODE
       :async,
       :compiler-args(@default-args[0], '-I' ~ $?FILE.IO.parent(1).add('lib/060-subtest/lib')),
       :exitcode(1),
       :err(''),
       :out(
            /
                ^"1..1\n  1..1\n  not ok 1 - must report test-hidden-subtest CallFrame\n"
                "  # You failed 1 test of 1\nnot ok 1 - hidden\n"
                "# Failed test 'hidden'\n# at " .*? " line 4"
            /
       );

is-run q:to/TEST-CODE/, "hidden from hidden subtest",
           use Test::SubBundle;
           use Test::Async <Base>;
           plan 1;
           test-hidden-in-hidden;
           TEST-CODE
       :async,
       :compiler-args(@default-args[0], '-I' ~ $?FILE.IO.parent(1).add('lib/060-subtest/lib')),
       :exitcode(1),
       :err(''),
       :out(
       /
       ^"1..1\n    1..1\n    not ok 1 - must report test-hidden-in-hidden CallFrame\n"
       "    # You failed 1 test of 1\n  not ok 1 - hidden\n"
       .*
       "# Failed test 'hidden in hidden'\n# at " .*? " line 4"
       /
       );

is-run q:to/TEST-CODE/, "a tool with anchor",
            use Test::SubBundle;
            use Test::Async <Base>;
            plan 1;
            test-tool-with-anchor
            TEST-CODE
       :async,
       :compiler-args(@default-args[0], '-I' ~ $?FILE.IO.parent(1).add('lib/060-subtest/lib')),
       :exitcode(1),
       :err(''),
       :out(
       /
       ^"1..1\n  1..1\n  not ok 1 - must report test-tool-with-anchor CallFrame\n"
       "  # You failed 1 test of 1\nnot ok 1 - hidden\n"
       "# Failed test 'hidden'\n# at " .*? " line 4"
       /
       );

is-run q:to/TEST-CODE/, "a wrappable anchoring tool",
            use Test::SubBundle;
            use Test::Async <Base>;
            plan 1;
            test-tool-anchoring
            TEST-CODE
       :async,
       :compiler-args(@default-args[0], '-I' ~ $?FILE.IO.parent(1).add('lib/060-subtest/lib')),
       :exitcode(1),
       :err(''),
       :out(
       /
       ^"1..1\n  1..1\n  not ok 1 - must report test-tool-anchoring CallFrame\n"
       "  # You failed 1 test of 1\nnot ok 1 - hidden\n"
       "# Failed test 'hidden'\n# at " .*? " line 4"
       /
       );

subtest "subtest topic" => {
    .plan: 1;
    .cmp-ok: $_, '===', test-suite, "topic is set to the test suite object";
}

# as of when this is written, isa-ok isn't implemented yet...
ok (my $completion = subtest "dummy" => { pass }) ~~ Promise, "subtest returns a completion Promise";
is $completion.status, Kept, "subtest completion promise is kept when subtest is done";

subtest "TODO in plan" => {
    plan 1, :todo("on subtest voids errors");
    flunk "this would be a failure without TODO";
}

todo "subtest fails";
subtest "TODO before subtest" => {
    flunk "this test fails but the subtest is TODO";
}

sub test-async($count, :%subtest-plan) {
    my $suite = test-suite;

    my atomicint $started = 0;
    my $starter = Promise.new;
    my @subtests-ready;
    my @subtests-complete;
    for ^$count -> $id {
        @subtests-ready.push: my $subtest-ready = Promise.new;
        @subtests-complete.push:
            subtest "job $id", |%subtest-plan, :hidden, {
                plan 4;
                # Sync with the parent suite.
                is $started, 0, "no concurrent subtests really started yet";
                $subtest-ready.keep($id);
                await $starter;
                ++⚛$started;
                pass "subtest $id started";

                # Test for our internal status
                my $suite = test-suite;
                # Take his subtest object.
                nok $suite.parallel, "a child suite is non-parallel by default";
                ok $suite.is-async, "but its async status is inherited";
            }
    }

    # Await for concurrent subtests to start and prepare.
    my Bool $all-ready;
    await Promise.anyof(
        Promise.in(30).then({ cas $all-ready, Bool, False }),
        Promise.allof(@subtests-ready).then({ cas $all-ready, Bool, True }),
    ) unless $suite.skip-message;

    ok $all-ready.defined, "internal test insurance: readiness flag is been set";
    ok $all-ready, "all subtests are ready and waiting";

    $starter.keep(True);

    # Await for subtests to complete.
    my Bool $all-completed;
    await Promise.anyof(
        Promise.in(30).then({ cas $all-completed, Bool, False }),
        Promise.allof(@subtests-complete).then({ cas $all-completed, Bool, True }),
    ) unless $suite.skip-message;

    ok $all-completed.defined, "internal test insurance: completion flag is been set";
    ok $all-completed, "all subtests completed";
    is $started, $count, "counter updated by all subtests";
}

subtest "Parallel subtests" => {
    # Test will fail if we try to start more than maximum allowed jobs because some subtests won't run until a slot
    # releases.

    my %plan-profile = skip-all => "must be 2 or more concurrent jobs allowed, $job-count now" if $job-count < 2;

    plan $job-count + 5,
         :parallel,
         |%plan-profile;

    test-async $job-count;
}

subtest "Force async" => {
    plan $job-count + 5, :!parallel, :!random;

    test-async $job-count, :subtest-plan{ :async, :instant };
}

subtest "Threading in a subtest" => {
    my $suite = test-suite;
    plan $job-count;

    for ^$job-count -> $id {
        $suite.start: {
            sleep .1.rand;
            pass "test $id";
        }
    }
}

subtest "Subtest returns" => {
    plan 8;
    my $sres = await subtest "passing subtest" => {
        plan 1;
        pass "just pass";
    };
    ok $sres, "successfull subtest returns a Promise kept with True";
    $sres = await subtest "no plan but passing" => {
        pass "pass 1";
        pass "pass 2";
    }
    ok $sres, "successfull subtest with no plan still returns a Promise kept with True";
    test-flunks;
    $sres = await subtest "flunking subtest" => {
        plan 1;
        flunk "just flunk";
    }
    nok $sres, "flunking subtest returns a Promise kept with False";
    test-flunks;
    $sres = await subtest "bad plan subtest" => {
        plan 2;
        pass "single pass";
    }
    nok $sres, "bad plan subtest returns a Promise kept with False";
}

# abort-testing must only affect it's immediate parent suite. This subtest must not affect other tests in this file.
subtest "Subtest aborts" => {
    plan 2;
    pass "one test passes";
    skip-rest "pretend of some kind of no-go condition";
    abort-testing;
    flunk "flunk must never be reached after abort-testing";
}

# We can't rely on the order test to determine if it was random. However low is the probability to get them ordered
# straight, it is no 0. Let's not write intentionally flapping test!
# What we can do though is conisder a side effect of the randomization: all subtests will be ran last when the test body
# execution is over.
is-run q:to/TEST-CODE/, "randomized",
           my $count = 3;
           plan $count + 1, :random;

           for ^$count -> $id {
               subtest "job $id" => {
                   plan 1;
                   pass "dummy $id";
               }
           }
           pass "this will preceede subtests";
           TEST-CODE
       :async,
       :compiler-args(@default-args),
       :exitcode(0),
       :err(''),
       :out(
            /
                ^"1..4\nok 1 - this will preceede subtests\n"
                ^^"# Subtest: job " \d \n
                ^^"  1..1\n  ok 1 - dummy " \d "\nok 2 - job " \d \n
                ^^"# Subtest: job " \d \n
                ^^"  1..1\n  ok 1 - dummy " \d "\nok 3 - job " \d \n
                ^^"# Subtest: job " \d \n
                ^^"  1..1\n  ok 1 - dummy " \d "\nok 4 - job " \d \n
            /
       );
done-testing;


