1use Mojo::Base -strict; 2 3BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 4 5use Test::More; 6use Mojo::IOLoop; 7use Scalar::Util 'refaddr'; 8 9subtest 'Resolved' => sub { 10 my $promise = Mojo::Promise->new; 11 my (@results, @errors); 12 $promise->then(sub { @results = @_ }, sub { @errors = @_ }); 13 $promise->resolve('hello', 'world'); 14 Mojo::IOLoop->one_tick; 15 is_deeply \@results, ['hello', 'world'], 'promise resolved'; 16 is_deeply \@errors, [], 'promise not rejected'; 17 18 $promise = Mojo::Promise->resolve('test'); 19 $promise->then(sub { @results = @_ }, sub { @errors = @_ }); 20 Mojo::IOLoop->one_tick; 21 is_deeply \@results, ['test'], 'promise resolved'; 22 is_deeply \@errors, [], 'promise not rejected'; 23}; 24 25subtest 'Already resolved' => sub { 26 my $promise = Mojo::Promise->new->resolve('early'); 27 my (@results, @errors); 28 $promise->then(sub { @results = @_ }, sub { @errors = @_ }); 29 Mojo::IOLoop->one_tick; 30 is_deeply \@results, ['early'], 'promise resolved'; 31 is_deeply \@errors, [], 'promise not rejected'; 32}; 33 34subtest 'Resolved with finally' => sub { 35 my $promise = Mojo::Promise->new; 36 my @results; 37 $promise->finally(sub { @results = ('finally'); 'fail' })->then(sub { push @results, @_ }); 38 $promise->resolve('hello', 'world'); 39 Mojo::IOLoop->one_tick; 40 is_deeply \@results, ['finally', 'hello', 'world'], 'promise settled'; 41}; 42 43subtest 'Rejected' => sub { 44 my $promise = Mojo::Promise->new; 45 my (@results, @errors); 46 $promise->then(sub { @results = @_ }, sub { @errors = @_ }); 47 $promise->reject('bye', 'world'); 48 Mojo::IOLoop->one_tick; 49 is_deeply \@results, [], 'promise not resolved'; 50 is_deeply \@errors, ['bye', 'world'], 'promise rejected'; 51 52 $promise = Mojo::Promise->reject('test'); 53 $promise->then(sub { @results = @_ }, sub { @errors = @_ }); 54 Mojo::IOLoop->one_tick; 55 is_deeply \@results, [], 'promise not resolved'; 56 is_deeply \@errors, ['test'], 'promise rejected'; 57}; 58 59subtest 'Rejected early' => sub { 60 my $promise = Mojo::Promise->new->reject('early'); 61 my (@results, @errors); 62 $promise->then(sub { @results = @_ }, sub { @errors = @_ }); 63 Mojo::IOLoop->one_tick; 64 is_deeply \@results, [], 'promise not resolved'; 65 is_deeply \@errors, ['early'], 'promise rejected'; 66}; 67 68subtest 'Rejected with finally' => sub { 69 my $promise = Mojo::Promise->new; 70 my @errors; 71 $promise->finally(sub { @errors = ('finally'); 'fail' })->then(undef, sub { push @errors, @_ }); 72 $promise->reject('bye', 'world'); 73 Mojo::IOLoop->one_tick; 74 is_deeply \@errors, ['finally', 'bye', 'world'], 'promise settled'; 75}; 76 77subtest 'Wrap' => sub { 78 my (@results, @errors); 79 my $promise = Mojo::Promise->new(sub { 80 my ($resolve, $reject) = @_; 81 Mojo::IOLoop->timer(0 => sub { $resolve->('resolved', '!') }); 82 }); 83 $promise->then(sub { @results = @_ }, sub { @errors = @_ })->wait; 84 is_deeply \@results, ['resolved', '!'], 'promise resolved'; 85 is_deeply \@errors, [], 'promise not rejected'; 86 87 (@results, @errors) = (); 88 $promise = Mojo::Promise->new(sub { 89 my ($resolve, $reject) = @_; 90 Mojo::IOLoop->timer(0 => sub { $reject->('rejected', '!') }); 91 }); 92 $promise->then(sub { @results = @_ }, sub { @errors = @_ })->wait; 93 is_deeply \@results, [], 'promise not resolved'; 94 is_deeply \@errors, ['rejected', '!'], 'promise rejected'; 95}; 96 97subtest 'No state change' => sub { 98 my $promise = Mojo::Promise->new; 99 my (@results, @errors); 100 $promise->then(sub { @results = @_ }, sub { @errors = @_ }); 101 $promise->resolve('pass')->reject('fail')->resolve('fail'); 102 Mojo::IOLoop->one_tick; 103 is_deeply \@results, ['pass'], 'promise resolved'; 104 is_deeply \@errors, [], 'promise not rejected'; 105}; 106 107subtest 'Resolved chained' => sub { 108 my $promise = Mojo::Promise->new; 109 my @results; 110 $promise->then(sub {"$_[0]:1"})->then(sub {"$_[0]:2"})->then(sub {"$_[0]:3"})->then(sub { push @results, "$_[0]:4" }); 111 $promise->resolve('test'); 112 Mojo::IOLoop->one_tick; 113 is_deeply \@results, ['test:1:2:3:4'], 'promises resolved'; 114}; 115 116subtest 'Rejected chained' => sub { 117 my $promise = Mojo::Promise->new; 118 my @errors; 119 $promise->then(undef, sub {"$_[0]:1"})->then(sub {"$_[0]:2"}, sub {"$_[0]:fail"})->then(sub {"$_[0]:3"}) 120 ->then(sub { push @errors, "$_[0]:4" }); 121 $promise->reject('tset'); 122 Mojo::IOLoop->one_tick; 123 is_deeply \@errors, ['tset:1:2:3:4'], 'promises rejected'; 124}; 125 126subtest 'Resolved nested' => sub { 127 my $promise = Mojo::Promise->new; 128 my $promise2 = Mojo::Promise->new; 129 my @results; 130 $promise->then(sub {$promise2})->then(sub { @results = @_ }); 131 $promise->resolve; 132 Mojo::IOLoop->one_tick; 133 is_deeply \@results, [], 'promise not resolved'; 134 135 $promise2->resolve('works too'); 136 Mojo::IOLoop->one_tick; 137 is_deeply \@results, ['works too'], 'promise resolved'; 138}; 139 140subtest 'Rejected nested' => sub { 141 my $promise = Mojo::Promise->new; 142 my $promise2 = Mojo::Promise->new; 143 my @errors; 144 $promise->then(undef, sub {$promise2})->then(undef, sub { @errors = @_ }); 145 $promise->reject; 146 Mojo::IOLoop->one_tick; 147 is_deeply \@errors, [], 'promise not resolved'; 148 149 $promise2->reject('hello world'); 150 Mojo::IOLoop->one_tick; 151 is_deeply \@errors, ['hello world'], 'promise rejected'; 152}; 153 154subtest 'Double finally' => sub { 155 my $promise = Mojo::Promise->new; 156 my @results; 157 $promise->finally(sub { push @results, 'finally1' })->finally(sub { push @results, 'finally2' }); 158 $promise->resolve('pass'); 159 Mojo::IOLoop->one_tick; 160 is_deeply \@results, ['finally1', 'finally2'], 'promise not resolved'; 161}; 162 163subtest 'Promise returned by finally' => sub { 164 my $loop = Mojo::IOLoop->new; 165 my $promise = Mojo::Promise->new->ioloop($loop); 166 my $promise2 = Mojo::Promise->new->ioloop($loop); 167 my @results; 168 my $promise3 = $promise->finally(sub { 169 $loop->next_tick(sub { $promise2->resolve }); 170 return $promise2; 171 })->finally(sub { @results = ('finally') }); 172 $promise->resolve('pass'); 173 $promise3->wait; 174 is_deeply \@results, ['finally'], 'promise already resolved'; 175}; 176 177subtest 'Promise returned by finally' => sub { 178 my $promise = Mojo::Promise->new; 179 my $promise2 = Mojo::Promise->new; 180 my @results; 181 my $promise3 = $promise->finally(sub { 182 Mojo::IOLoop->next_tick(sub { $promise2->resolve }); 183 return $promise2; 184 })->finally(sub { @results = ('finally') }); 185 $promise->resolve('pass'); 186 $promise3->wait; 187 is_deeply \@results, ['finally'], 'promise already resolved'; 188}; 189 190subtest 'Promise returned by finally (rejected)' => sub { 191 my $promise = Mojo::Promise->new; 192 my $promise2 = Mojo::Promise->new; 193 my (@results, @errors); 194 my $promise3 = $promise->finally(sub { 195 Mojo::IOLoop->next_tick(sub { $promise2->reject('works') }); 196 return $promise2; 197 })->then(sub { @results = @_ }, sub { @errors = @_ }); 198 $promise->resolve('failed'); 199 $promise3->wait; 200 is_deeply \@results, [], 'promises not resolved'; 201 is_deeply \@errors, ['works'], 'promises rejected'; 202}; 203 204subtest 'Exception in finally' => sub { 205 my $promise = Mojo::Promise->new; 206 my @results; 207 $promise->finally(sub { die "Test!\n" })->catch(sub { push @results, @_ }); 208 $promise->resolve('pass'); 209 Mojo::IOLoop->one_tick; 210 is_deeply \@results, ["Test!\n"], 'promise rejected'; 211}; 212 213subtest 'Clone' => sub { 214 my $loop = Mojo::IOLoop->new; 215 my $promise = Mojo::Promise->new->ioloop($loop)->resolve('failed'); 216 my $promise2 = $promise->clone; 217 my (@results, @errors); 218 $promise2->then(sub { @results = @_ }, sub { @errors = @_ }); 219 $promise2->resolve('success'); 220 is $loop, $promise2->ioloop, 'same loop'; 221 $loop->one_tick; 222 is_deeply \@results, ['success'], 'promise resolved'; 223 is_deeply \@errors, [], 'promise not rejected'; 224}; 225 226subtest 'Exception in chain' => sub { 227 my $promise = Mojo::Promise->new; 228 my (@results, @errors); 229 $promise->then(sub {@_})->then(sub {@_})->then(sub { die "test: $_[0]\n" })->then(sub { push @results, 'fail' }) 230 ->catch(sub { @errors = @_ }); 231 $promise->resolve('works'); 232 Mojo::IOLoop->one_tick; 233 is_deeply \@results, [], 'promises not resolved'; 234 is_deeply \@errors, ["test: works\n"], 'promises rejected'; 235}; 236 237subtest 'Race' => sub { 238 my $promise = Mojo::Promise->new->then(sub {@_}); 239 my $promise2 = Mojo::Promise->new->then(sub {@_}); 240 my $promise3 = Mojo::Promise->new->then(sub {@_}); 241 my @results; 242 Mojo::Promise->race($promise2, $promise, $promise3)->then(sub { @results = @_ }); 243 $promise2->resolve('second'); 244 $promise3->resolve('third'); 245 $promise->resolve('first'); 246 Mojo::IOLoop->one_tick; 247 is_deeply \@results, ['second'], 'promise resolved'; 248}; 249 250subtest 'Rejected race' => sub { 251 my $promise = Mojo::Promise->new->then(sub {@_}); 252 my $promise2 = Mojo::Promise->new->then(sub {@_}); 253 my $promise3 = Mojo::Promise->new->then(sub {@_}); 254 my (@results, @errors); 255 Mojo::Promise->race($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ }); 256 $promise2->reject('second'); 257 $promise3->resolve('third'); 258 $promise->resolve('first'); 259 Mojo::IOLoop->one_tick; 260 is_deeply \@results, [], 'promises not resolved'; 261 is_deeply \@errors, ['second'], 'promise rejected'; 262}; 263 264subtest 'Any' => sub { 265 my $promise = Mojo::Promise->new->then(sub {@_}); 266 my $promise2 = Mojo::Promise->new->then(sub {@_}); 267 my $promise3 = Mojo::Promise->new->then(sub {@_}); 268 my @results; 269 Mojo::Promise->any($promise2, $promise, $promise3)->then(sub { @results = @_ }); 270 $promise2->reject('second'); 271 $promise3->resolve('third'); 272 $promise->resolve('first'); 273 Mojo::IOLoop->one_tick; 274 is_deeply \@results, ['third'], 'promise resolved'; 275}; 276 277subtest 'Any (all rejections)' => sub { 278 my $promise = Mojo::Promise->new->then(sub {@_}); 279 my $promise2 = Mojo::Promise->new->then(sub {@_}); 280 my $promise3 = Mojo::Promise->new->then(sub {@_}); 281 my (@results, @errors); 282 Mojo::Promise->any($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ }); 283 $promise2->reject('second'); 284 $promise3->reject('third'); 285 $promise->reject('first'); 286 Mojo::IOLoop->one_tick; 287 is_deeply \@results, [], 'promises not resolved'; 288 is_deeply \@errors, [['first'], ['second'], ['third']], 'promises rejected'; 289}; 290 291subtest 'Timeout' => sub { 292 my (@errors, @results); 293 my $promise = Mojo::Promise->timeout(0.25 => 'Timeout1'); 294 my $promise2 = Mojo::Promise->new->timeout(0.025 => 'Timeout2'); 295 my $promise3 296 = Mojo::Promise->race($promise, $promise2)->then(sub { @results = @_ })->catch(sub { @errors = @_ })->wait; 297 is_deeply \@results, [], 'promises not resolved'; 298 is_deeply \@errors, ['Timeout2'], 'promise rejected'; 299}; 300 301subtest 'Timeout with default message' => sub { 302 my @errors; 303 Mojo::Promise->timeout(0.025)->catch(sub { @errors = @_ })->wait; 304 is_deeply \@errors, ['Promise timeout'], 'default timeout message'; 305}; 306 307subtest 'Timer without value' => sub { 308 my @results; 309 Mojo::Promise->timer(0.025)->then(sub { @results = (@_, 'works!') })->wait; 310 is_deeply \@results, ['works!'], 'default timer result'; 311}; 312 313subtest 'Timer with values' => sub { 314 my @results; 315 Mojo::Promise->new->timer(0, 'first', 'second')->then(sub { @results = (@_, 'works too!') })->wait; 316 is_deeply \@results, ['first', 'second', 'works too!'], 'timer result'; 317}; 318 319subtest 'All' => sub { 320 my $promise = Mojo::Promise->new->then(sub {@_}); 321 my $promise2 = Mojo::Promise->new->then(sub {@_}); 322 my $promise3 = Mojo::Promise->new->then(sub {@_}); 323 my @results; 324 Mojo::Promise->all($promise, $promise2, $promise3)->then(sub { @results = @_ }); 325 $promise2->resolve('second'); 326 $promise3->resolve('third'); 327 $promise->resolve('first'); 328 Mojo::IOLoop->one_tick; 329 is_deeply \@results, [['first'], ['second'], ['third']], 'promises resolved'; 330}; 331 332subtest 'Rejected all' => sub { 333 my $promise = Mojo::Promise->new->then(sub {@_}); 334 my $promise2 = Mojo::Promise->new->then(sub {@_}); 335 my $promise3 = Mojo::Promise->new->then(sub {@_}); 336 my (@results, @errors); 337 Mojo::Promise->all($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ }); 338 $promise2->resolve('second'); 339 $promise3->reject('third'); 340 $promise->resolve('first'); 341 Mojo::IOLoop->one_tick; 342 is_deeply \@results, [], 'promises not resolved'; 343 is_deeply \@errors, ['third'], 'promise rejected'; 344}; 345 346subtest 'All settled' => sub { 347 my $promise = Mojo::Promise->new->then(sub {@_}); 348 my $promise2 = Mojo::Promise->new->then(sub {@_}); 349 my $promise3 = Mojo::Promise->new->then(sub {@_}); 350 my @results; 351 Mojo::Promise->all_settled($promise, $promise2, $promise3)->then(sub { @results = @_ }); 352 $promise2->resolve('second'); 353 $promise3->resolve('third'); 354 $promise->resolve('first'); 355 Mojo::IOLoop->one_tick; 356 my $result = [ 357 {status => 'fulfilled', value => ['first']}, 358 {status => 'fulfilled', value => ['second']}, 359 {status => 'fulfilled', value => ['third']} 360 ]; 361 is_deeply \@results, $result, 'promise resolved'; 362}; 363 364subtest 'All settled (with rejection)' => sub { 365 my $promise = Mojo::Promise->new->then(sub {@_}); 366 my $promise2 = Mojo::Promise->new->then(sub {@_}); 367 my $promise3 = Mojo::Promise->new->then(sub {@_}); 368 my (@results, @errors); 369 Mojo::Promise->all_settled($promise, $promise2, $promise3)->then(sub { @results = @_ }, sub { @errors = @_ }); 370 $promise2->resolve('second'); 371 $promise3->reject('third'); 372 $promise->resolve('first'); 373 Mojo::IOLoop->one_tick; 374 is_deeply \@errors, [], 'promise not rejected'; 375 my $result = [ 376 {status => 'fulfilled', value => ['first']}, 377 {status => 'fulfilled', value => ['second']}, 378 {status => 'rejected', reason => ['third']} 379 ]; 380 is_deeply \@results, $result, 'promise resolved'; 381}; 382 383subtest 'Settle with promise' => sub { 384 my $promise = Mojo::Promise->new->resolve('works'); 385 my @results; 386 my $promise2 = Mojo::Promise->new->resolve($promise)->then(sub { push @results, 'first', @_; @_ }); 387 $promise2->then(sub { push @results, 'second', @_ }); 388 Mojo::IOLoop->one_tick; 389 is_deeply \@results, ['first', 'works', 'second', 'works'], 'promises resolved'; 390 391 $promise = Mojo::Promise->new->reject('works too'); 392 my @errors; 393 @results = (); 394 $promise2 = Mojo::Promise->new->reject($promise)->catch(sub { push @errors, 'first', @_; () }); 395 $promise2->then(sub { push @results, 'second', @_ }); 396 Mojo::IOLoop->one_tick; 397 is_deeply \@errors, ['first', $promise], 'promises rejected'; 398 is_deeply \@results, ['second'], 'promises resolved'; 399 $promise->catch(sub { }); 400}; 401 402subtest 'Promisify' => sub { 403 is ref Mojo::Promise->resolve('foo'), 'Mojo::Promise', 'right class'; 404 405 my $promise = Mojo::Promise->reject('foo'); 406 is ref $promise, 'Mojo::Promise', 'right class'; 407 my @errors; 408 $promise->catch(sub { push @errors, @_ })->wait; 409 is_deeply \@errors, ['foo'], 'promise rejected'; 410 411 $promise = Mojo::Promise->resolve('foo'); 412 is refaddr(Mojo::Promise->resolve($promise)), refaddr($promise), 'same object'; 413 414 $promise = Mojo::Promise->resolve('foo'); 415 isnt refaddr(Mojo::Promise->new->resolve($promise)), refaddr($promise), 'different object'; 416 417 $promise = Mojo::Promise->reject('foo'); 418 is refaddr(Mojo::Promise->resolve($promise)), refaddr($promise), 'same object'; 419 @errors = (); 420 $promise->catch(sub { push @errors, @_ })->wait; 421 is_deeply \@errors, ['foo'], 'promise rejected'; 422}; 423 424subtest 'Warnings' => sub { 425 my @warn; 426 local $SIG{__WARN__} = sub { push @warn, shift }; 427 Mojo::Promise->reject('one'); 428 like $warn[0], qr/Unhandled rejected promise: one/, 'unhandled'; 429 is $warn[1], undef, 'no more warnings'; 430 431 @warn = (); 432 Mojo::Promise->reject('two')->then(sub { })->wait; 433 like $warn[0], qr/Unhandled rejected promise: two/, 'unhandled'; 434 is $warn[1], undef, 'no more warnings'; 435 436 @warn = (); 437 Mojo::Promise->reject('three')->finally(sub { })->wait; 438 like $warn[0], qr/Unhandled rejected promise: three/, 'unhandled'; 439 is $warn[1], undef, 'no more warnings'; 440 441 @warn = (); 442 my $promise = Mojo::Promise->new; 443 $promise->reject('four'); 444 Mojo::IOLoop->one_tick; 445 is $warn[0], undef, 'no warnings'; 446 undef $promise; 447 like $warn[0], qr/Unhandled rejected promise: four/, 'unhandled'; 448 is $warn[1], undef, 'no more warnings'; 449}; 450 451subtest 'Warnings (multiple branches)' => sub { 452 my @warn; 453 local $SIG{__WARN__} = sub { push @warn, shift }; 454 my @errors; 455 my $promise = Mojo::Promise->new; 456 $promise->catch(sub { push @errors, @_ }); 457 $promise->reject('branches'); 458 $promise->wait; 459 is_deeply \@errors, ['branches'], 'promise rejected'; 460 is $warn[0], undef, 'no warnings'; 461 462 @errors = @warn = (); 463 $promise = Mojo::Promise->new; 464 $promise->catch(sub { push @errors, @_ }); 465 $promise->then(sub { }); 466 $promise->reject('branches2'); 467 $promise->wait; 468 is_deeply \@errors, ['branches2'], 'promise rejected'; 469 like $warn[0], qr/Unhandled rejected promise: branches2/, 'unhandled'; 470 is $warn[1], undef, 'no more warnings'; 471 472 @warn = (); 473 $promise = Mojo::Promise->new; 474 $promise->then(sub { })->then(sub { })->then(sub { }); 475 $promise->then(sub { }); 476 $promise->reject('branches3'); 477 $promise->wait; 478 like $warn[0], qr/Unhandled rejected promise: branches3/, 'unhandled'; 479 like $warn[1], qr/Unhandled rejected promise: branches3/, 'unhandled'; 480 is $warn[2], undef, 'no more warnings'; 481}; 482 483subtest 'Map' => sub { 484 my (@results, @errors, @started); 485 my $promise = Mojo::Promise->map(sub { push @started, $_; Mojo::Promise->resolve($_) }, 1 .. 5) 486 ->then(sub { @results = @_ }, sub { @errors = @_ }); 487 is_deeply \@started, [1, 2, 3, 4, 5], 'all started without concurrency'; 488 $promise->wait; 489 is_deeply \@results, [[1], [2], [3], [4], [5]], 'correct result'; 490 is_deeply \@errors, [], 'promise not rejected'; 491}; 492 493subtest 'Map (with concurrency limit)' => sub { 494 my $concurrent = 0; 495 my (@results, @errors); 496 Mojo::Promise->map( 497 {concurrency => 3}, 498 sub { 499 my $n = $_; 500 fail 'Concurrency too high' if ++$concurrent > 3; 501 Mojo::Promise->resolve->then(sub { 502 fail 'Concurrency too high' if $concurrent-- > 3; 503 $n; 504 }); 505 }, 506 1 .. 7 507 )->then(sub { @results = @_ }, sub { @errors = @_ })->wait; 508 is_deeply \@results, [[1], [2], [3], [4], [5], [6], [7]], 'correct result'; 509 is_deeply \@errors, [], 'promise not rejected'; 510}; 511 512subtest 'Map (with reject)' => sub { 513 my (@results, @errors, @started); 514 Mojo::Promise->map( 515 {concurrency => 3}, 516 sub { 517 my $n = $_; 518 push @started, $n; 519 Mojo::Promise->resolve->then(sub { Mojo::Promise->reject($n) }); 520 }, 521 1 .. 5 522 )->then(sub { @results = @_ }, sub { @errors = @_ })->wait; 523 is_deeply \@results, [], 'promise not resolved'; 524 is_deeply \@errors, [1], 'correct errors'; 525 is_deeply \@started, [1, 2, 3], 'only initial batch started'; 526}; 527 528subtest 'Map (custom event loop)' => sub { 529 my $ok; 530 my $loop = Mojo::IOLoop->new; 531 my $promise = Mojo::Promise->map(sub { Mojo::Promise->new->ioloop($loop)->resolve }, 1); 532 is $promise->ioloop, $loop, 'same loop'; 533 isnt $promise->ioloop, Mojo::IOLoop->singleton, 'not the singleton'; 534 $promise->then(sub { $ok = 1; $loop->stop }); 535 $loop->start; 536 ok $ok, 'loop completed'; 537}; 538 539subtest 'Wait for stopped loop' => sub { 540 my @results; 541 my $promise = Mojo::Promise->new; 542 Mojo::IOLoop->next_tick(sub { 543 Mojo::IOLoop->stop; 544 Mojo::IOLoop->timer(0.1 => sub { $promise->resolve('wait') }); 545 }); 546 $promise->then(sub { @results = @_ })->wait; 547 is_deeply \@results, ['wait'], 'promise resolved'; 548}; 549 550done_testing(); 551