Mojolicious/t/mojo/promise.t
use Mojo::Base -strict;
BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' }
use Test::More;
use Mojo::IOLoop;
use Scalar::Util 'refaddr';
subtest 'Resolved' => sub {
my $promise = Mojo::Promise->new;
my (@results, @errors);
$promise->then(sub { @results = @_ }, sub { @errors = @_ });
$promise->resolve('hello', 'world');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['hello', 'world'], 'promise resolved';
is_deeply \@errors, [], 'promise not rejected';
$promise = Mojo::Promise->resolve('test');
$promise->then(sub { @results = @_ }, sub { @errors = @_ });
Mojo::IOLoop->one_tick;
is_deeply \@results, ['test'], 'promise resolved';
is_deeply \@errors, [], 'promise not rejected';
};
subtest 'Already resolved' => sub {
my $promise = Mojo::Promise->new->resolve('early');
my (@results, @errors);
$promise->then(sub { @results = @_ }, sub { @errors = @_ });
Mojo::IOLoop->one_tick;
is_deeply \@results, ['early'], 'promise resolved';
is_deeply \@errors, [], 'promise not rejected';
};
subtest 'Resolved with finally' => sub {
my $promise = Mojo::Promise->new;
my @results;
$promise->finally(sub { @results = ('finally'); 'fail' })->then(sub { push @results, @_ });
$promise->resolve('hello', 'world');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['finally', 'hello', 'world'], 'promise settled';
};
subtest 'Rejected' => sub {
my $promise = Mojo::Promise->new;
my (@results, @errors);
$promise->then(sub { @results = @_ }, sub { @errors = @_ });
$promise->reject('bye', 'world');
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promise not resolved';
is_deeply \@errors, ['bye', 'world'], 'promise rejected';
$promise = Mojo::Promise->reject('test');
$promise->then(sub { @results = @_ }, sub { @errors = @_ });
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promise not resolved';
is_deeply \@errors, ['test'], 'promise rejected';
};
subtest 'Rejected early' => sub {
my $promise = Mojo::Promise->new->reject('early');
my (@results, @errors);
$promise->then(sub { @results = @_ }, sub { @errors = @_ });
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promise not resolved';
is_deeply \@errors, ['early'], 'promise rejected';
};
subtest 'Rejected with finally' => sub {
my $promise = Mojo::Promise->new;
my @errors;
$promise->finally(sub { @errors = ('finally'); 'fail' })->then(undef, sub { push @errors, @_ });
$promise->reject('bye', 'world');
Mojo::IOLoop->one_tick;
is_deeply \@errors, ['finally', 'bye', 'world'], 'promise settled';
};
subtest 'Wrap' => sub {
my (@results, @errors);
my $promise = Mojo::Promise->new(sub {
my ($resolve, $reject) = @_;
Mojo::IOLoop->timer(0 => sub { $resolve->('resolved', '!') });
});
$promise->then(sub { @results = @_ }, sub { @errors = @_ })->wait;
is_deeply \@results, ['resolved', '!'], 'promise resolved';
is_deeply \@errors, [], 'promise not rejected';
(@results, @errors) = ();
$promise = Mojo::Promise->new(sub {
my ($resolve, $reject) = @_;
Mojo::IOLoop->timer(0 => sub { $reject->('rejected', '!') });
});
$promise->then(sub { @results = @_ }, sub { @errors = @_ })->wait;
is_deeply \@results, [], 'promise not resolved';
is_deeply \@errors, ['rejected', '!'], 'promise rejected';
};
subtest 'No state change' => sub {
my $promise = Mojo::Promise->new;
my (@results, @errors);
$promise->then(sub { @results = @_ }, sub { @errors = @_ });
$promise->resolve('pass')->reject('fail')->resolve('fail');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['pass'], 'promise resolved';
is_deeply \@errors, [], 'promise not rejected';
};
subtest 'Resolved chained' => sub {
my $promise = Mojo::Promise->new;
my @results;
$promise->then(sub {"$_[0]:1"})->then(sub {"$_[0]:2"})->then(sub {"$_[0]:3"})->then(sub { push @results, "$_[0]:4" });
$promise->resolve('test');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['test:1:2:3:4'], 'promises resolved';
};
subtest 'Rejected chained' => sub {
my $promise = Mojo::Promise->new;
my @errors;
$promise->then(undef, sub {"$_[0]:1"})->then(sub {"$_[0]:2"}, sub {"$_[0]:fail"})->then(sub {"$_[0]:3"})
->then(sub { push @errors, "$_[0]:4" });
$promise->reject('tset');
Mojo::IOLoop->one_tick;
is_deeply \@errors, ['tset:1:2:3:4'], 'promises rejected';
};
subtest 'Resolved nested' => sub {
my $promise = Mojo::Promise->new;
my $promise2 = Mojo::Promise->new;
my @results;
$promise->then(sub {$promise2})->then(sub { @results = @_ });
$promise->resolve;
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promise not resolved';
$promise2->resolve('works too');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['works too'], 'promise resolved';
};
subtest 'Rejected nested' => sub {
my $promise = Mojo::Promise->new;
my $promise2 = Mojo::Promise->new;
my @errors;
$promise->then(undef, sub {$promise2})->then(undef, sub { @errors = @_ });
$promise->reject;
Mojo::IOLoop->one_tick;
is_deeply \@errors, [], 'promise not resolved';
$promise2->reject('hello world');
Mojo::IOLoop->one_tick;
is_deeply \@errors, ['hello world'], 'promise rejected';
};
subtest 'Double finally' => sub {
my $promise = Mojo::Promise->new;
my @results;
$promise->finally(sub { push @results, 'finally1' })->finally(sub { push @results, 'finally2' });
$promise->resolve('pass');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['finally1', 'finally2'], 'promise not resolved';
};
subtest 'Promise returned by finally' => sub {
my $loop = Mojo::IOLoop->new;
my $promise = Mojo::Promise->new->ioloop($loop);
my $promise2 = Mojo::Promise->new->ioloop($loop);
my @results;
my $promise3 = $promise->finally(sub {
$loop->next_tick(sub { $promise2->resolve });
return $promise2;
})->finally(sub { @results = ('finally') });
$promise->resolve('pass');
$promise3->wait;
is_deeply \@results, ['finally'], 'promise already resolved';
};
subtest 'Promise returned by finally' => sub {
my $promise = Mojo::Promise->new;
my $promise2 = Mojo::Promise->new;
my @results;
my $promise3 = $promise->finally(sub {
Mojo::IOLoop->next_tick(sub { $promise2->resolve });
return $promise2;
})->finally(sub { @results = ('finally') });
$promise->resolve('pass');
$promise3->wait;
is_deeply \@results, ['finally'], 'promise already resolved';
};
subtest 'Promise returned by finally (rejected)' => sub {
my $promise = Mojo::Promise->new;
my $promise2 = Mojo::Promise->new;
my (@results, @errors);
my $promise3 = $promise->finally(sub {
Mojo::IOLoop->next_tick(sub { $promise2->reject('works') });
return $promise2;
})->then(sub { @results = @_ }, sub { @errors = @_ });
$promise->resolve('failed');
$promise3->wait;
is_deeply \@results, [], 'promises not resolved';
is_deeply \@errors, ['works'], 'promises rejected';
};
subtest 'Exception in finally' => sub {
my $promise = Mojo::Promise->new;
my @results;
$promise->finally(sub { die "Test!\n" })->catch(sub { push @results, @_ });
$promise->resolve('pass');
Mojo::IOLoop->one_tick;
is_deeply \@results, ["Test!\n"], 'promise rejected';
};
subtest 'Clone' => sub {
my $loop = Mojo::IOLoop->new;
my $promise = Mojo::Promise->new->ioloop($loop)->resolve('failed');
my $promise2 = $promise->clone;
my (@results, @errors);
$promise2->then(sub { @results = @_ }, sub { @errors = @_ });
$promise2->resolve('success');
is $loop, $promise2->ioloop, 'same loop';
$loop->one_tick;
is_deeply \@results, ['success'], 'promise resolved';
is_deeply \@errors, [], 'promise not rejected';
};
subtest 'Exception in chain' => sub {
my $promise = Mojo::Promise->new;
my (@results, @errors);
$promise->then(sub {@_})->then(sub {@_})->then(sub { die "test: $_[0]\n" })->then(sub { push @results, 'fail' })
->catch(sub { @errors = @_ });
$promise->resolve('works');
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promises not resolved';
is_deeply \@errors, ["test: works\n"], 'promises rejected';
};
subtest 'Race' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my @results;
Mojo::Promise->race($promise2, $promise, $promise3)->then(sub { @results = @_ });
$promise2->resolve('second');
$promise3->resolve('third');
$promise->resolve('first');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['second'], 'promise resolved';
};
subtest 'Rejected race' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my (@results, @errors);
Mojo::Promise->race($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ });
$promise2->reject('second');
$promise3->resolve('third');
$promise->resolve('first');
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promises not resolved';
is_deeply \@errors, ['second'], 'promise rejected';
};
subtest 'Any' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my @results;
Mojo::Promise->any($promise2, $promise, $promise3)->then(sub { @results = @_ });
$promise2->reject('second');
$promise3->resolve('third');
$promise->resolve('first');
Mojo::IOLoop->one_tick;
is_deeply \@results, ['third'], 'promise resolved';
};
subtest 'Any (all rejections)' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my (@results, @errors);
Mojo::Promise->any($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ });
$promise2->reject('second');
$promise3->reject('third');
$promise->reject('first');
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promises not resolved';
is_deeply \@errors, [['first'], ['second'], ['third']], 'promises rejected';
};
subtest 'Timeout' => sub {
my (@errors, @results);
my $promise = Mojo::Promise->timeout(0.25 => 'Timeout1');
my $promise2 = Mojo::Promise->new->timeout(0.025 => 'Timeout2');
my $promise3
= Mojo::Promise->race($promise, $promise2)->then(sub { @results = @_ })->catch(sub { @errors = @_ })->wait;
is_deeply \@results, [], 'promises not resolved';
is_deeply \@errors, ['Timeout2'], 'promise rejected';
};
subtest 'Timeout with default message' => sub {
my @errors;
Mojo::Promise->timeout(0.025)->catch(sub { @errors = @_ })->wait;
is_deeply \@errors, ['Promise timeout'], 'default timeout message';
};
subtest 'Timer without value' => sub {
my @results;
Mojo::Promise->timer(0.025)->then(sub { @results = (@_, 'works!') })->wait;
is_deeply \@results, ['works!'], 'default timer result';
};
subtest 'Timer with values' => sub {
my @results;
Mojo::Promise->new->timer(0, 'first', 'second')->then(sub { @results = (@_, 'works too!') })->wait;
is_deeply \@results, ['first', 'second', 'works too!'], 'timer result';
};
subtest 'All' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my @results;
Mojo::Promise->all($promise, $promise2, $promise3)->then(sub { @results = @_ });
$promise2->resolve('second');
$promise3->resolve('third');
$promise->resolve('first');
Mojo::IOLoop->one_tick;
is_deeply \@results, [['first'], ['second'], ['third']], 'promises resolved';
};
subtest 'Rejected all' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my (@results, @errors);
Mojo::Promise->all($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ });
$promise2->resolve('second');
$promise3->reject('third');
$promise->resolve('first');
Mojo::IOLoop->one_tick;
is_deeply \@results, [], 'promises not resolved';
is_deeply \@errors, ['third'], 'promise rejected';
};
subtest 'All settled' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my @results;
Mojo::Promise->all_settled($promise, $promise2, $promise3)->then(sub { @results = @_ });
$promise2->resolve('second');
$promise3->resolve('third');
$promise->resolve('first');
Mojo::IOLoop->one_tick;
my $result = [
{status => 'fulfilled', value => ['first']},
{status => 'fulfilled', value => ['second']},
{status => 'fulfilled', value => ['third']}
];
is_deeply \@results, $result, 'promise resolved';
};
subtest 'All settled (with rejection)' => sub {
my $promise = Mojo::Promise->new->then(sub {@_});
my $promise2 = Mojo::Promise->new->then(sub {@_});
my $promise3 = Mojo::Promise->new->then(sub {@_});
my (@results, @errors);
Mojo::Promise->all_settled($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ });
$promise2->resolve('second');
$promise3->reject('third');
$promise->resolve('first');
Mojo::IOLoop->one_tick;
is_deeply \@errors, [], 'promise not rejected';
my $result = [
{status => 'fulfilled', value => ['first']},
{status => 'fulfilled', value => ['second']},
{status => 'rejected', reason => ['third']}
];
is_deeply \@results, $result, 'promise resolved';
};
subtest 'Settle with promise' => sub {
my $promise = Mojo::Promise->new->resolve('works');
my @results;
my $promise2 = Mojo::Promise->new->resolve($promise)->then(sub { push @results, 'first', @_; @_ });
$promise2->then(sub { push @results, 'second', @_ });
Mojo::IOLoop->one_tick;
is_deeply \@results, ['first', 'works', 'second', 'works'], 'promises resolved';
$promise = Mojo::Promise->new->reject('works too');
my @errors;
@results = ();
$promise2 = Mojo::Promise->new->reject($promise)->catch(sub { push @errors, 'first', @_; () });
$promise2->then(sub { push @results, 'second', @_ });
Mojo::IOLoop->one_tick;
is_deeply \@errors, ['first', $promise], 'promises rejected';
is_deeply \@results, ['second'], 'promises resolved';
$promise->catch(sub { });
};
subtest 'Promisify' => sub {
is ref Mojo::Promise->resolve('foo'), 'Mojo::Promise', 'right class';
my $promise = Mojo::Promise->reject('foo');
is ref $promise, 'Mojo::Promise', 'right class';
my @errors;
$promise->catch(sub { push @errors, @_ })->wait;
is_deeply \@errors, ['foo'], 'promise rejected';
$promise = Mojo::Promise->resolve('foo');
is refaddr(Mojo::Promise->resolve($promise)), refaddr($promise), 'same object';
$promise = Mojo::Promise->resolve('foo');
isnt refaddr(Mojo::Promise->new->resolve($promise)), refaddr($promise), 'different object';
$promise = Mojo::Promise->reject('foo');
is refaddr(Mojo::Promise->resolve($promise)), refaddr($promise), 'same object';
@errors = ();
$promise->catch(sub { push @errors, @_ })->wait;
is_deeply \@errors, ['foo'], 'promise rejected';
};
subtest 'Warnings' => sub {
my @warn;
local $SIG{__WARN__} = sub { push @warn, shift };
Mojo::Promise->reject('one');
like $warn[0], qr/Unhandled rejected promise: one/, 'unhandled';
is $warn[1], undef, 'no more warnings';
@warn = ();
Mojo::Promise->reject('two')->then(sub { })->wait;
like $warn[0], qr/Unhandled rejected promise: two/, 'unhandled';
is $warn[1], undef, 'no more warnings';
@warn = ();
Mojo::Promise->reject('three')->finally(sub { })->wait;
like $warn[0], qr/Unhandled rejected promise: three/, 'unhandled';
is $warn[1], undef, 'no more warnings';
@warn = ();
my $promise = Mojo::Promise->new;
$promise->reject('four');
Mojo::IOLoop->one_tick;
is $warn[0], undef, 'no warnings';
undef $promise;
like $warn[0], qr/Unhandled rejected promise: four/, 'unhandled';
is $warn[1], undef, 'no more warnings';
};
subtest 'Warnings (multiple branches)' => sub {
my @warn;
local $SIG{__WARN__} = sub { push @warn, shift };
my @errors;
my $promise = Mojo::Promise->new;
$promise->catch(sub { push @errors, @_ });
$promise->reject('branches');
$promise->wait;
is_deeply \@errors, ['branches'], 'promise rejected';
is $warn[0], undef, 'no warnings';
@errors = @warn = ();
$promise = Mojo::Promise->new;
$promise->catch(sub { push @errors, @_ });
$promise->then(sub { });
$promise->reject('branches2');
$promise->wait;
is_deeply \@errors, ['branches2'], 'promise rejected';
like $warn[0], qr/Unhandled rejected promise: branches2/, 'unhandled';
is $warn[1], undef, 'no more warnings';
@warn = ();
$promise = Mojo::Promise->new;
$promise->then(sub { })->then(sub { })->then(sub { });
$promise->then(sub { });
$promise->reject('branches3');
$promise->wait;
like $warn[0], qr/Unhandled rejected promise: branches3/, 'unhandled';
like $warn[1], qr/Unhandled rejected promise: branches3/, 'unhandled';
is $warn[2], undef, 'no more warnings';
};
subtest 'Map' => sub {
my (@results, @errors, @started);
my $promise = Mojo::Promise->map(sub { push @started, $_; Mojo::Promise->resolve($_) }, 1 .. 5, undef, 0)
->then(sub { @results = @_ }, sub { @errors = @_ });
is_deeply \@started, [1, 2, 3, 4, 5, undef, 0], 'all started without concurrency';
$promise->wait;
is_deeply \@results, [[1], [2], [3], [4], [5], [undef], [0]], 'correct result';
is_deeply \@errors, [], 'promise not rejected';
};
subtest 'Map (with concurrency limit)' => sub {
my $concurrent = 0;
my (@results, @errors);
Mojo::Promise->map(
{concurrency => 3},
sub {
my $n = $_;
fail 'Concurrency too high' if ++$concurrent > 3;
Mojo::Promise->resolve->then(sub {
fail 'Concurrency too high' if $concurrent-- > 3;
$n;
});
},
1 .. 7,
undef,
0,
)->then(sub { @results = @_ }, sub { @errors = @_ })->wait;
is_deeply \@results, [[1], [2], [3], [4], [5], [6], [7], [undef], [0]], 'correct result';
is_deeply \@errors, [], 'promise not rejected';
};
subtest 'Map (with reject)' => sub {
my (@results, @errors, @started);
Mojo::Promise->map(
{concurrency => 3},
sub {
my $n = $_;
push @started, $n;
Mojo::Promise->resolve->then(sub { Mojo::Promise->reject($n) });
},
1 .. 5
)->then(sub { @results = @_ }, sub { @errors = @_ })->wait;
is_deeply \@results, [], 'promise not resolved';
is_deeply \@errors, [1], 'correct errors';
is_deeply \@started, [1, 2, 3], 'only initial batch started';
};
subtest 'Map (custom event loop)' => sub {
my $ok;
my $loop = Mojo::IOLoop->new;
my $promise = Mojo::Promise->map(sub { Mojo::Promise->new->ioloop($loop)->resolve }, 1);
is $promise->ioloop, $loop, 'same loop';
isnt $promise->ioloop, Mojo::IOLoop->singleton, 'not the singleton';
$promise->then(sub { $ok = 1; $loop->stop });
$loop->start;
ok $ok, 'loop completed';
};
subtest 'Wait for stopped loop' => sub {
my @results;
my $promise = Mojo::Promise->new;
Mojo::IOLoop->next_tick(sub {
Mojo::IOLoop->stop;
Mojo::IOLoop->timer(0.1 => sub { $promise->resolve('wait') });
});
$promise->then(sub { @results = @_ })->wait;
is_deeply \@results, ['wait'], 'promise resolved';
};
done_testing();