toolkit/modules/tests/xpcshell/test_Promise.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:f3db090ab692
1 /* Any copyright is dedicated to the Public Domain.
2 * http://creativecommons.org/publicdomain/zero/1.0/ */
3 "use strict";
4
5 Components.utils.import("resource://gre/modules/Promise.jsm");
6 Components.utils.import("resource://gre/modules/Services.jsm");
7 Components.utils.import("resource://gre/modules/Task.jsm");
8
9 // Deactivate the standard xpcshell observer, as it turns uncaught
10 // rejections into failures, which we don't want here.
11 Promise.Debugging.clearUncaughtErrorObservers();
12
13 ////////////////////////////////////////////////////////////////////////////////
14 //// Test runner
15
16 let run_promise_tests = function run_promise_tests(tests, cb) {
17 let loop = function loop(index) {
18 if (index >= tests.length) {
19 if (cb) {
20 cb.call();
21 }
22 return;
23 }
24 do_print("Launching test " + (index + 1) + "/" + tests.length);
25 let test = tests[index];
26 // Execute from an empty stack
27 let next = function next() {
28 do_print("Test " + (index + 1) + "/" + tests.length + " complete");
29 do_execute_soon(function() {
30 loop(index + 1);
31 });
32 };
33 let result = test();
34 result.then(next, next);
35 };
36 return loop(0);
37 };
38
39 let make_promise_test = function(test) {
40 return function runtest() {
41 do_print("Test starting: " + test.name);
42 try {
43 let result = test();
44 if (result && "promise" in result) {
45 result = result.promise;
46 }
47 if (!result || !("then" in result)) {
48 let exn;
49 try {
50 do_throw("Test " + test.name + " did not return a promise: " + result);
51 } catch (x) {
52 exn = x;
53 }
54 return Promise.reject(exn);
55 }
56 // The test returns a promise
57 result = result.then(
58 // Test complete
59 function onResolve() {
60 do_print("Test complete: " + test.name);
61 },
62 // The test failed with an unexpected error
63 function onReject(err) {
64 let detail;
65 if (err && typeof err == "object" && "stack" in err) {
66 detail = err.stack;
67 } else {
68 detail = "(no stack)";
69 }
70 do_throw("Test " + test.name + " rejected with the following reason: "
71 + err + detail);
72 });
73 return result;
74 } catch (x) {
75 // The test failed because of an error outside of a promise
76 do_throw("Error in body of test " + test.name + ": " + x + " at " + x.stack);
77 return Promise.reject();
78 }
79 };
80 };
81
82 ////////////////////////////////////////////////////////////////////////////////
83 //// Tests
84
85 let tests = [];
86
87 // Utility function to observe an failures in a promise
88 // This function is useful if the promise itself is
89 // not returned.
90 let observe_failures = function observe_failures(promise) {
91 promise.catch(function onReject(reason) {
92 test.do_throw("Observed failure in test " + test + ": " + reason);
93 });
94 };
95
96 // Test that all observers are notified
97 tests.push(make_promise_test(
98 function notification(test) {
99 // The size of the test
100 const SIZE = 10;
101 const RESULT = "this is an arbitrary value";
102
103 // Number of observers that yet need to be notified
104 let expected = SIZE;
105
106 // |true| once an observer has been notified
107 let notified = [];
108
109 // The promise observed
110 let source = Promise.defer();
111 let result = Promise.defer();
112
113 let install_observer = function install_observer(i) {
114 observe_failures(source.promise.then(
115 function onSuccess(value) {
116 do_check_true(!notified[i], "Ensuring that observer is notified at most once");
117 notified[i] = true;
118
119 do_check_eq(value, RESULT, "Ensuring that the observed value is correct");
120 if (--expected == 0) {
121 result.resolve();
122 }
123 }));
124 };
125
126 // Install a number of observers before resolving
127 let i;
128 for (i = 0; i < SIZE/2; ++i) {
129 install_observer(i);
130 }
131
132 source.resolve(RESULT);
133
134 // Install remaining observers
135 for(;i < SIZE; ++i) {
136 install_observer(i);
137 }
138
139 return result;
140 }));
141
142 // Test that observers get the correct "this" value in strict mode.
143 tests.push(
144 make_promise_test(function handlers_this_value(test) {
145 return Promise.resolve().then(
146 function onResolve() {
147 // Since this file is in strict mode, the correct value is "undefined".
148 do_check_eq(this, undefined);
149 throw "reject";
150 }
151 ).then(
152 null,
153 function onReject() {
154 // Since this file is in strict mode, the correct value is "undefined".
155 do_check_eq(this, undefined);
156 }
157 );
158 }));
159
160 // Test that observers registered on a pending promise are notified in order.
161 tests.push(
162 make_promise_test(function then_returns_before_callbacks(test) {
163 let deferred = Promise.defer();
164 let promise = deferred.promise;
165
166 let order = 0;
167
168 promise.then(
169 function onResolve() {
170 do_check_eq(order, 0);
171 order++;
172 }
173 );
174
175 promise.then(
176 function onResolve() {
177 do_check_eq(order, 1);
178 order++;
179 }
180 );
181
182 let newPromise = promise.then(
183 function onResolve() {
184 do_check_eq(order, 2);
185 }
186 );
187
188 deferred.resolve();
189
190 // This test finishes after the last handler succeeds.
191 return newPromise;
192 }));
193
194 // Test that observers registered on a resolved promise are notified in order.
195 tests.push(
196 make_promise_test(function then_returns_before_callbacks(test) {
197 let promise = Promise.resolve();
198
199 let order = 0;
200
201 promise.then(
202 function onResolve() {
203 do_check_eq(order, 0);
204 order++;
205 }
206 );
207
208 promise.then(
209 function onResolve() {
210 do_check_eq(order, 1);
211 order++;
212 }
213 );
214
215 // This test finishes after the last handler succeeds.
216 return promise.then(
217 function onResolve() {
218 do_check_eq(order, 2);
219 }
220 );
221 }));
222
223 // Test that all observers are notified at most once, even if source
224 // is resolved/rejected several times
225 tests.push(make_promise_test(
226 function notification_once(test) {
227 // The size of the test
228 const SIZE = 10;
229 const RESULT = "this is an arbitrary value";
230
231 // Number of observers that yet need to be notified
232 let expected = SIZE;
233
234 // |true| once an observer has been notified
235 let notified = [];
236
237 // The promise observed
238 let observed = Promise.defer();
239 let result = Promise.defer();
240
241 let install_observer = function install_observer(i) {
242 observe_failures(observed.promise.then(
243 function onSuccess(value) {
244 do_check_true(!notified[i], "Ensuring that observer is notified at most once");
245 notified[i] = true;
246
247 do_check_eq(value, RESULT, "Ensuring that the observed value is correct");
248 if (--expected == 0) {
249 result.resolve();
250 }
251 }));
252 };
253
254 // Install a number of observers before resolving
255 let i;
256 for (i = 0; i < SIZE/2; ++i) {
257 install_observer(i);
258 }
259
260 observed.resolve(RESULT);
261
262 // Install remaining observers
263 for(;i < SIZE; ++i) {
264 install_observer(i);
265 }
266
267 // Resolve some more
268 for (i = 0; i < 10; ++i) {
269 observed.resolve(RESULT);
270 observed.reject();
271 }
272
273 return result;
274 }));
275
276 // Test that throwing an exception from a onResolve listener
277 // does not prevent other observers from receiving the notification
278 // of success.
279 tests.push(
280 make_promise_test(function exceptions_do_not_stop_notifications(test) {
281 let source = Promise.defer();
282
283 let exception_thrown = false;
284 let exception_content = new Error("Boom!");
285
286 let observer_1 = source.promise.then(
287 function onResolve() {
288 exception_thrown = true;
289 throw exception_content;
290 });
291
292 let observer_2 = source.promise.then(
293 function onResolve() {
294 do_check_true(exception_thrown, "Second observer called after first observer has thrown");
295 }
296 );
297
298 let result = observer_1.then(
299 function onResolve() {
300 do_throw("observer_1 should not have resolved");
301 },
302 function onReject(reason) {
303 do_check_true(reason == exception_content, "Obtained correct rejection");
304 }
305 );
306
307 source.resolve();
308 return result;
309 }
310 ));
311
312 // Test that, once a promise is resolved, further resolve/reject
313 // are ignored.
314 tests.push(
315 make_promise_test(function subsequent_resolves_are_ignored(test) {
316 let deferred = Promise.defer();
317 deferred.resolve(1);
318 deferred.resolve(2);
319 deferred.reject(3);
320
321 let result = deferred.promise.then(
322 function onResolve(value) {
323 do_check_eq(value, 1, "Resolution chose the first value");
324 },
325 function onReject(reason) {
326 do_throw("Obtained a rejection while the promise was already resolved");
327 }
328 );
329
330 return result;
331 }));
332
333 // Test that, once a promise is rejected, further resolve/reject
334 // are ignored.
335 tests.push(
336 make_promise_test(function subsequent_rejects_are_ignored(test) {
337 let deferred = Promise.defer();
338 deferred.reject(1);
339 deferred.reject(2);
340 deferred.resolve(3);
341
342 let result = deferred.promise.then(
343 function onResolve() {
344 do_throw("Obtained a resolution while the promise was already rejected");
345 },
346 function onReject(reason) {
347 do_check_eq(reason, 1, "Rejection chose the first value");
348 }
349 );
350
351 return result;
352 }));
353
354 // Test that returning normally from a rejection recovers from the error
355 // and that listeners are informed of a success.
356 tests.push(
357 make_promise_test(function recovery(test) {
358 let boom = new Error("Boom!");
359 let deferred = Promise.defer();
360 const RESULT = "An arbitrary value";
361
362 let promise = deferred.promise.then(
363 function onResolve() {
364 do_throw("A rejected promise should not resolve");
365 },
366 function onReject(reason) {
367 do_check_true(reason == boom, "Promise was rejected with the correct error");
368 return RESULT;
369 }
370 );
371
372 promise = promise.then(
373 function onResolve(value) {
374 do_check_eq(value, RESULT, "Promise was recovered with the correct value");
375 }
376 );
377
378 deferred.reject(boom);
379 return promise;
380 }));
381
382 // Test that returning a resolved promise from a onReject causes a resolution
383 // (recovering from the error) and that returning a rejected promise
384 // from a onResolve listener causes a rejection (raising an error).
385 tests.push(
386 make_promise_test(function recovery_with_promise(test) {
387 let boom = new Error("Arbitrary error");
388 let deferred = Promise.defer();
389 const RESULT = "An arbitrary value";
390 const boom2 = new Error("Another arbitrary error");
391
392 // return a resolved promise from a onReject listener
393 let promise = deferred.promise.then(
394 function onResolve() {
395 do_throw("A rejected promise should not resolve");
396 },
397 function onReject(reason) {
398 do_check_true(reason == boom, "Promise was rejected with the correct error");
399 return Promise.resolve(RESULT);
400 }
401 );
402
403 // return a rejected promise from a onResolve listener
404 promise = promise.then(
405 function onResolve(value) {
406 do_check_eq(value, RESULT, "Promise was recovered with the correct value");
407 return Promise.reject(boom2);
408 }
409 );
410
411 promise = promise.catch(
412 function onReject(reason) {
413 do_check_eq(reason, boom2, "Rejection was propagated with the correct " +
414 "reason, through a promise");
415 }
416 );
417
418 deferred.reject(boom);
419 return promise;
420 }));
421
422 // Test that we can resolve with promises of promises
423 tests.push(
424 make_promise_test(function test_propagation(test) {
425 const RESULT = "Yet another arbitrary value";
426 let d1 = Promise.defer();
427 let d2 = Promise.defer();
428 let d3 = Promise.defer();
429
430 d3.resolve(d2.promise);
431 d2.resolve(d1.promise);
432 d1.resolve(RESULT);
433
434 return d3.promise.then(
435 function onSuccess(value) {
436 do_check_eq(value, RESULT, "Resolution with a promise eventually yielded "
437 + " the correct result");
438 }
439 );
440 }));
441
442 // Test sequences of |then| and |catch|
443 tests.push(
444 make_promise_test(function test_chaining(test) {
445 let error_1 = new Error("Error 1");
446 let error_2 = new Error("Error 2");
447 let result_1 = "First result";
448 let result_2 = "Second result";
449 let result_3 = "Third result";
450
451 let source = Promise.defer();
452
453 let promise = source.promise.then().then();
454
455 source.resolve(result_1);
456
457 // Check that result_1 is correctly propagated
458 promise = promise.then(
459 function onSuccess(result) {
460 do_check_eq(result, result_1, "Result was propagated correctly through " +
461 " several applications of |then|");
462 return result_2;
463 }
464 );
465
466 // Check that returning from the promise produces a resolution
467 promise = promise.catch(
468 function onReject() {
469 do_throw("Incorrect rejection");
470 }
471 );
472
473 // ... and that the check did not alter the value
474 promise = promise.then(
475 function onResolve(value) {
476 do_check_eq(value, result_2, "Result was propagated correctly once again");
477 }
478 );
479
480 // Now the same kind of tests for rejections
481 promise = promise.then(
482 function onResolve() {
483 throw error_1;
484 }
485 );
486
487 promise = promise.then(
488 function onResolve() {
489 do_throw("Incorrect resolution: the exception should have caused a rejection");
490 }
491 );
492
493 promise = promise.catch(
494 function onReject(reason) {
495 do_check_true(reason == error_1, "Reason was propagated correctly");
496 throw error_2;
497 }
498 );
499
500 promise = promise.catch(
501 function onReject(reason) {
502 do_check_true(reason == error_2, "Throwing an error altered the reason " +
503 "as expected");
504 return result_3;
505 }
506 );
507
508 promise = promise.then(
509 function onResolve(result) {
510 do_check_eq(result, result_3, "Error was correctly recovered");
511 }
512 );
513
514 return promise;
515 }));
516
517 // Test that resolving with a rejected promise actually rejects
518 tests.push(
519 make_promise_test(function resolve_to_rejected(test) {
520 let source = Promise.defer();
521 let error = new Error("Boom");
522
523 let promise = source.promise.then(
524 function onResolve() {
525 do_throw("Incorrect call to onResolve listener");
526 },
527 function onReject(reason) {
528 do_check_eq(reason, error, "Rejection lead to the expected reason");
529 }
530 );
531
532 source.resolve(Promise.reject(error));
533
534 return promise;
535 }));
536
537 // Test that Promise.resolve resolves as expected
538 tests.push(
539 make_promise_test(function test_resolve(test) {
540 const RESULT = "arbitrary value";
541 let p1 = Promise.resolve(RESULT);
542 let p2 = Promise.resolve(p1);
543 do_check_eq(p1, p2, "Promise.resolve used on a promise just returns the promise");
544
545 return p1.then(
546 function onResolve(result) {
547 do_check_eq(result, RESULT, "Promise.resolve propagated the correct result");
548 }
549 );
550 }));
551
552 // Test that Promise.resolve throws when its argument is an async function.
553 tests.push(
554 make_promise_test(function test_promise_resolve_throws_with_async_function(test) {
555 Assert.throws(() => Promise.resolve(Task.async(function* () {})),
556 /Cannot resolve a promise with an async function/);
557 return Promise.resolve();
558 }));
559
560 // Test that the code after "then" is always executed before the callbacks
561 tests.push(
562 make_promise_test(function then_returns_before_callbacks(test) {
563 let promise = Promise.resolve();
564
565 let thenExecuted = false;
566
567 promise = promise.then(
568 function onResolve() {
569 thenExecuted = true;
570 }
571 );
572
573 do_check_false(thenExecuted);
574
575 return promise;
576 }));
577
578 // Test that chaining promises does not generate long stack traces
579 tests.push(
580 make_promise_test(function chaining_short_stack(test) {
581 let source = Promise.defer();
582 let promise = source.promise;
583
584 const NUM_ITERATIONS = 100;
585
586 for (let i = 0; i < NUM_ITERATIONS; i++) {
587 promise = promise.then(
588 function onResolve(result) {
589 return result + ".";
590 }
591 );
592 }
593
594 promise = promise.then(
595 function onResolve(result) {
596 // Check that the execution went as expected.
597 let expectedString = new Array(1 + NUM_ITERATIONS).join(".");
598 do_check_true(result == expectedString);
599
600 // Check that we didn't generate one or more stack frames per iteration.
601 let stackFrameCount = 0;
602 let stackFrame = Components.stack;
603 while (stackFrame) {
604 stackFrameCount++;
605 stackFrame = stackFrame.caller;
606 }
607
608 do_check_true(stackFrameCount < NUM_ITERATIONS);
609 }
610 );
611
612 source.resolve("");
613
614 return promise;
615 }));
616
617 // Test that the values of the promise return by Promise.all() are kept in the
618 // given order even if the given promises are resolved in arbitrary order
619 tests.push(
620 make_promise_test(function all_resolve(test) {
621 let d1 = Promise.defer();
622 let d2 = Promise.defer();
623 let d3 = Promise.defer();
624
625 d3.resolve(4);
626 d2.resolve(2);
627 do_execute_soon(() => d1.resolve(1));
628
629 let promises = [d1.promise, d2.promise, 3, d3.promise];
630
631 return Promise.all(promises).then(
632 function onResolve([val1, val2, val3, val4]) {
633 do_check_eq(val1, 1);
634 do_check_eq(val2, 2);
635 do_check_eq(val3, 3);
636 do_check_eq(val4, 4);
637 }
638 );
639 }));
640
641 // Test that rejecting one of the promises passed to Promise.all()
642 // rejects the promise return by Promise.all()
643 tests.push(
644 make_promise_test(function all_reject(test) {
645 let error = new Error("Boom");
646
647 let d1 = Promise.defer();
648 let d2 = Promise.defer();
649 let d3 = Promise.defer();
650
651 d3.resolve(3);
652 d2.resolve(2);
653 do_execute_soon(() => d1.reject(error));
654
655 let promises = [d1.promise, d2.promise, d3.promise];
656
657 return Promise.all(promises).then(
658 function onResolve() {
659 do_throw("Incorrect call to onResolve listener");
660 },
661 function onReject(reason) {
662 do_check_eq(reason, error, "Rejection lead to the expected reason");
663 }
664 );
665 }));
666
667 // Test that passing only values (not promises) to Promise.all()
668 // forwards them all as resolution values.
669 tests.push(
670 make_promise_test(function all_resolve_no_promises(test) {
671 try {
672 Promise.all(null);
673 do_check_true(false, "all() should only accept iterables");
674 } catch (e) {
675 do_check_true(true, "all() fails when first the arg is not an iterable");
676 }
677
678 let p1 = Promise.all([]).then(
679 function onResolve(val) {
680 do_check_true(Array.isArray(val) && val.length == 0);
681 }
682 );
683
684 let p2 = Promise.all([1, 2, 3]).then(
685 function onResolve([val1, val2, val3]) {
686 do_check_eq(val1, 1);
687 do_check_eq(val2, 2);
688 do_check_eq(val3, 3);
689 }
690 );
691
692 return Promise.all([p1, p2]);
693 }));
694
695 // Test that Promise.all() handles non-array iterables
696 tests.push(
697 make_promise_test(function all_iterable(test) {
698 function* iterable() {
699 yield 1;
700 yield 2;
701 yield 3;
702 }
703
704 return Promise.all(iterable()).then(
705 function onResolve([val1, val2, val3]) {
706 do_check_eq(val1, 1);
707 do_check_eq(val2, 2);
708 do_check_eq(val3, 3);
709 },
710 function onReject() {
711 do_throw("all() unexpectedly rejected");
712 }
713 );
714 }));
715
716 // Test that throwing from the iterable passed to Promise.all() rejects the
717 // promise returned by Promise.all()
718 tests.push(
719 make_promise_test(function all_iterable_throws(test) {
720 function* iterable() {
721 throw 1;
722 }
723
724 return Promise.all(iterable()).then(
725 function onResolve() {
726 do_throw("all() unexpectedly resolved");
727 },
728 function onReject(reason) {
729 do_check_eq(reason, 1, "all() rejects when the iterator throws");
730 }
731 );
732 }));
733
734 // Test that Promise.race() resolves with the first available resolution value
735 tests.push(
736 make_promise_test(function race_resolve(test) {
737 let p1 = Promise.resolve(1);
738 let p2 = Promise.resolve().then(() => 2);
739
740 return Promise.race([p1, p2]).then(
741 function onResolve(value) {
742 do_check_eq(value, 1);
743 }
744 );
745 }));
746
747 // Test that passing only values (not promises) to Promise.race() works
748 tests.push(
749 make_promise_test(function race_resolve_no_promises(test) {
750 try {
751 Promise.race(null);
752 do_check_true(false, "race() should only accept iterables");
753 } catch (e) {
754 do_check_true(true, "race() fails when first the arg is not an iterable");
755 }
756
757 return Promise.race([1, 2, 3]).then(
758 function onResolve(value) {
759 do_check_eq(value, 1);
760 }
761 );
762 }));
763
764 // Test that Promise.race() never resolves when passed an empty iterable
765 tests.push(
766 make_promise_test(function race_resolve_never(test) {
767 return new Promise(resolve => {
768 Promise.race([]).then(
769 function onResolve() {
770 do_throw("race() unexpectedly resolved");
771 },
772 function onReject() {
773 do_throw("race() unexpectedly rejected");
774 }
775 );
776
777 // Approximate "never" so we don't have to solve the halting problem.
778 do_timeout(200, resolve);
779 });
780 }));
781
782 // Test that Promise.race() handles non-array iterables.
783 tests.push(
784 make_promise_test(function race_iterable(test) {
785 function* iterable() {
786 yield 1;
787 yield 2;
788 yield 3;
789 }
790
791 return Promise.race(iterable()).then(
792 function onResolve(value) {
793 do_check_eq(value, 1);
794 },
795 function onReject() {
796 do_throw("race() unexpectedly rejected");
797 }
798 );
799 }));
800
801 // Test that throwing from the iterable passed to Promise.race() rejects the
802 // promise returned by Promise.race()
803 tests.push(
804 make_promise_test(function race_iterable_throws(test) {
805 function* iterable() {
806 throw 1;
807 }
808
809 return Promise.race(iterable()).then(
810 function onResolve() {
811 do_throw("race() unexpectedly resolved");
812 },
813 function onReject(reason) {
814 do_check_eq(reason, 1, "race() rejects when the iterator throws");
815 }
816 );
817 }));
818
819 // Test that rejecting one of the promises passed to Promise.race() rejects the
820 // promise returned by Promise.race()
821 tests.push(
822 make_promise_test(function race_reject(test) {
823 let p1 = Promise.reject(1);
824 let p2 = Promise.resolve(2);
825 let p3 = Promise.resolve(3);
826
827 return Promise.race([p1, p2, p3]).then(
828 function onResolve() {
829 do_throw("race() unexpectedly resolved");
830 },
831 function onReject(reason) {
832 do_check_eq(reason, 1, "race() rejects when given a rejected promise");
833 }
834 );
835 }));
836
837 // Test behavior of the Promise constructor.
838 tests.push(
839 make_promise_test(function test_constructor(test) {
840 try {
841 new Promise(null);
842 do_check_true(false, "Constructor should fail when not passed a function");
843 } catch (e) {
844 do_check_true(true, "Constructor fails when not passed a function");
845 }
846
847 let executorRan = false;
848 let promise = new Promise(
849 function executor(resolve, reject) {
850 executorRan = true;
851 do_check_eq(this, undefined);
852 do_check_eq(typeof resolve, "function",
853 "resolve function should be passed to the executor");
854 do_check_eq(typeof reject, "function",
855 "reject function should be passed to the executor");
856 }
857 );
858 do_check_instanceof(promise, Promise);
859 do_check_true(executorRan, "Executor should execute synchronously");
860
861 // resolve a promise from the executor
862 let resolvePromise = new Promise(
863 function executor(resolve) {
864 resolve(1);
865 }
866 ).then(
867 function onResolve(value) {
868 do_check_eq(value, 1, "Executor resolved with correct value");
869 },
870 function onReject() {
871 do_throw("Executor unexpectedly rejected");
872 }
873 );
874
875 // reject a promise from the executor
876 let rejectPromise = new Promise(
877 function executor(_, reject) {
878 reject(1);
879 }
880 ).then(
881 function onResolve() {
882 do_throw("Executor unexpectedly resolved");
883 },
884 function onReject(reason) {
885 do_check_eq(reason, 1, "Executor rejected with correct value");
886 }
887 );
888
889 // throw from the executor, causing a rejection
890 let throwPromise = new Promise(
891 function executor() {
892 throw 1;
893 }
894 ).then(
895 function onResolve() {
896 do_throw("Throwing inside an executor should not resolve the promise");
897 },
898 function onReject(reason) {
899 do_check_eq(reason, 1, "Executor rejected with correct value");
900 }
901 );
902
903 return Promise.all([resolvePromise, rejectPromise, throwPromise]);
904 }));
905
906 // Test deadlock in Promise.jsm with nested event loops
907 // The scenario being tested is:
908 // promise_1.then({
909 // do some work that will asynchronously signal done
910 // start an event loop waiting for the done signal
911 // }
912 // where the async work uses resolution of a second promise to
913 // trigger the "done" signal. While this would likely work in a
914 // naive implementation, our constant-stack implementation needs
915 // a special case to avoid deadlock. Note that this test is
916 // sensitive to the implementation-dependent order in which then()
917 // clauses for two different promises are executed, so it is
918 // possible for other implementations to pass this test and still
919 // have similar deadlocks.
920 tests.push(
921 make_promise_test(function promise_nested_eventloop_deadlock(test) {
922 // Set up a (long enough to be noticeable) timeout to
923 // exit the nested event loop and throw if the test run is hung
924 let shouldExitNestedEventLoop = false;
925
926 function event_loop() {
927 let thr = Services.tm.mainThread;
928 while(!shouldExitNestedEventLoop) {
929 thr.processNextEvent(true);
930 }
931 }
932
933 // I wish there was a way to cancel xpcshell do_timeout()s
934 do_timeout(2000, () => {
935 if (!shouldExitNestedEventLoop) {
936 shouldExitNestedEventLoop = true;
937 do_throw("Test timed out");
938 }
939 });
940
941 let promise1 = Promise.resolve(1);
942 let promise2 = Promise.resolve(2);
943
944 do_print("Setting wait for first promise");
945 promise1.then(value => {
946 do_print("Starting event loop");
947 event_loop();
948 }, null);
949
950 do_print("Setting wait for second promise");
951 return promise2.catch(error => {return 3;})
952 .then(
953 count => {
954 shouldExitNestedEventLoop = true;
955 });
956 }));
957
958 function wait_for_uncaught(aMustAppear, aTimeout = undefined) {
959 let remaining = new Set();
960 for (let k of aMustAppear) {
961 remaining.add(k);
962 }
963 let deferred = Promise.defer();
964 let print = do_print;
965 let execute_soon = do_execute_soon;
966 let observer = function({message, stack}) {
967 let data = message + stack;
968 print("Observing " + message + ", looking for " + aMustAppear.join(", "));
969 for (let expected of remaining) {
970 if (data.indexOf(expected) != -1) {
971 print("I found " + expected);
972 remaining.delete(expected);
973 }
974 if (remaining.size == 0 && observer) {
975 Promise.Debugging.removeUncaughtErrorObserver(observer);
976 observer = null;
977 deferred.resolve();
978 }
979 }
980 };
981 Promise.Debugging.addUncaughtErrorObserver(observer);
982 if (aTimeout) {
983 do_timeout(aTimeout, function timeout() {
984 if (observer) {
985 Promise.Debugging.removeUncaughtErrorObserver(observer);
986 observer = null;
987 }
988 deferred.reject(new Error("Timeout"));
989 });
990 }
991 return deferred.promise;
992 }
993
994 // Test that uncaught errors are reported as uncaught
995 (function() {
996 let make_string_rejection = function make_string_rejection() {
997 let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
998 let string = "This is an uncaught rejection " + salt;
999 // Our error is not Error-like nor an nsIException, so the stack will
1000 // include the closure doing the actual rejection.
1001 return {mustFind: ["test_rejection_closure", string], error: string};
1002 };
1003 let make_num_rejection = function make_num_rejection() {
1004 let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
1005 // Our error is not Error-like nor an nsIException, so the stack will
1006 // include the closure doing the actual rejection.
1007 return {mustFind: ["test_rejection_closure", salt], error: salt};
1008 };
1009 let make_undefined_rejection = function make_undefined_rejection() {
1010 // Our error is not Error-like nor an nsIException, so the stack will
1011 // include the closure doing the actual rejection.
1012 return {mustFind: ["test_rejection_closure"], error: undefined};
1013 };
1014 let make_error_rejection = function make_error_rejection() {
1015 let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
1016 let error = new Error("This is an uncaught error " + salt);
1017 return {
1018 mustFind: [error.message, error.fileName, error.lineNumber, error.stack],
1019 error: error
1020 };
1021 };
1022 let make_exception_rejection = function make_exception_rejection() {
1023 let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
1024 let exn = new Components.Exception("This is an uncaught exception " + salt,
1025 Components.results.NS_ERROR_NOT_AVAILABLE);
1026 return {
1027 mustFind: [exn.message, exn.filename, exn.lineNumber, exn.location.toString()],
1028 error: exn
1029 };
1030 };
1031 for (let make_rejection of [make_string_rejection,
1032 make_num_rejection,
1033 make_undefined_rejection,
1034 make_error_rejection,
1035 make_exception_rejection]) {
1036 let {mustFind, error} = make_rejection();
1037 let name = make_rejection.name;
1038 tests.push(make_promise_test(function test_uncaught_is_reported() {
1039 do_print("Testing with rejection " + name);
1040 let promise = wait_for_uncaught(mustFind);
1041 (function test_rejection_closure() {
1042 // For the moment, we cannot be absolutely certain that a value is
1043 // garbage-collected, even if it is not referenced anymore, due to
1044 // the conservative stack-scanning algorithm.
1045 //
1046 // To be _almost_ certain that a value will be garbage-collected, we
1047 // 1. isolate that value in an anonymous closure;
1048 // 2. allocate 100 values instead of 1 (gc-ing a single value from
1049 // these is sufficient for the test);
1050 // 3. place everything in a loop, as the JIT typically reuses memory;
1051 // 4. call all the GC methods we can.
1052 //
1053 // Unfortunately, we might still have intermittent failures,
1054 // materialized as timeouts.
1055 //
1056 for (let i = 0; i < 100; ++i) {
1057 Promise.reject(error);
1058 }
1059 })();
1060 do_print("Posted all rejections");
1061 Components.utils.forceGC();
1062 Components.utils.forceCC();
1063 Components.utils.forceShrinkingGC();
1064 return promise;
1065 }));
1066 }
1067 })();
1068
1069
1070 // Test that caught errors are not reported as uncaught
1071 tests.push(
1072 make_promise_test(function test_caught_is_not_reported() {
1073 let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
1074 let promise = wait_for_uncaught([salt], 500);
1075 (function() {
1076 let uncaught = Promise.reject("This error, on the other hand, is caught " + salt);
1077 uncaught.catch(function() { /* ignore rejection */});
1078 uncaught = null;
1079 })();
1080 // Isolate this in a function to increase likelihood that the gc will
1081 // realise that |uncaught| has remained uncaught.
1082 Components.utils.forceGC();
1083
1084 return promise.then(function onSuccess() {
1085 throw new Error("This error was caught and should not have been reported");
1086 }, function onError() {
1087 do_print("The caught error was not reported, all is fine");
1088 }
1089 );
1090 }));
1091
1092 function run_test()
1093 {
1094 do_test_pending();
1095 run_promise_tests(tests, do_test_finished);
1096 }

mercurial