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