[Streams API] pipe-to writable stream tests
authorcalvaris@igalia.com <calvaris@igalia.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 14 Sep 2015 08:47:39 +0000 (08:47 +0000)
committercalvaris@igalia.com <calvaris@igalia.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 14 Sep 2015 08:47:39 +0000 (08:47 +0000)
https://bugs.webkit.org/show_bug.cgi?id=148296

Reviewed by Darin Adler.

* streams/reference-implementation/pipe-to-expected.txt: Added.
* streams/reference-implementation/pipe-to.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@189696 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/streams/reference-implementation/pipe-to-expected.txt [new file with mode: 0644]
LayoutTests/streams/reference-implementation/pipe-to.html [new file with mode: 0644]

index 1cab64a..7d42456 100644 (file)
@@ -1,5 +1,15 @@
 2015-09-14  Xabier Rodriguez Calvar  <calvaris@igalia.com>
 
+        [Streams API] pipe-to writable stream tests
+        https://bugs.webkit.org/show_bug.cgi?id=148296
+
+        Reviewed by Darin Adler.
+
+        * streams/reference-implementation/pipe-to-expected.txt: Added.
+        * streams/reference-implementation/pipe-to.html: Added.
+
+2015-09-14  Xabier Rodriguez Calvar  <calvaris@igalia.com>
+
         [Streams API] Add readable stream templated tests for writable streams
         https://bugs.webkit.org/show_bug.cgi?id=148304
 
diff --git a/LayoutTests/streams/reference-implementation/pipe-to-expected.txt b/LayoutTests/streams/reference-implementation/pipe-to-expected.txt
new file mode 100644 (file)
index 0000000..d489bc5
--- /dev/null
@@ -0,0 +1,26 @@
+
+FAIL Piping from a ReadableStream from which lots of data are readable synchronously Can't find variable: WritableStream
+FAIL Piping from a ReadableStream in readable state to a WritableStream in closing state Can't find variable: WritableStream
+FAIL Piping from a ReadableStream in readable state to a WritableStream in errored state Can't find variable: WritableStream
+FAIL Piping from a ReadableStream in the readable state which becomes closed after pipeTo call to a WritableStream in the writable state Can't find variable: WritableStream
+FAIL Piping from a ReadableStream in the readable state which becomes errored after pipeTo call to a WritableStream in the writable state Can't find variable: WritableStream
+FAIL Piping from an empty ReadableStream which becomes non-empty after pipeTo call to a WritableStream in the writable state Can't find variable: WritableStream
+FAIL Piping from an empty ReadableStream which becomes errored after pipeTo call to a WritableStream in the writable state Can't find variable: WritableStream
+FAIL Piping from an empty ReadableStream to a WritableStream in the writable state which becomes errored after a pipeTo call Can't find variable: WritableStream
+FAIL Piping from a non-empty ReadableStream to a WritableStream in the waiting state which becomes writable after a pipeTo call Can't find variable: WritableStream
+FAIL Piping from a non-empty ReadableStream to a WritableStream in waiting state which becomes errored after a pipeTo call Can't find variable: WritableStream
+FAIL Piping from a non-empty ReadableStream which becomes errored after pipeTo call to a WritableStream in the waiting state Can't find variable: WritableStream
+FAIL Piping from a non-empty ReadableStream to a WritableStream in the waiting state where both become ready after a pipeTo Can't find variable: WritableStream
+FAIL Piping from an empty ReadableStream to a WritableStream in the waiting state which becomes writable after a pipeTo call Can't find variable: WritableStream
+FAIL Piping from an empty ReadableStream which becomes closed after a pipeTo call to a WritableStream in the waiting state whose writes never complete Can't find variable: WritableStream
+FAIL Piping from an empty ReadableStream which becomes errored after a pipeTo call to a WritableStream in the waiting state Can't find variable: WritableStream
+FAIL Piping to a duck-typed asynchronous "writable stream" works pipeTo is not implemented
+FAIL Piping to a stream that has been aborted passes through the error as the cancellation reason Can't find variable: WritableStream
+FAIL Piping to a stream and then aborting it passes through the error as the cancellation reason Can't find variable: WritableStream
+FAIL Piping to a stream that has been closed propagates a TypeError cancellation reason backward Can't find variable: WritableStream
+FAIL Piping to a stream and then closing it propagates a TypeError cancellation reason backward Can't find variable: WritableStream
+FAIL Piping to a stream that errors on write should pass through the error as the cancellation reason Can't find variable: WritableStream
+FAIL Piping to a stream that errors on write should not pass through the error if the stream is already closed Can't find variable: WritableStream
+FAIL Piping to a stream that errors soon after writing should pass through the error as the cancellation reason Can't find variable: WritableStream
+FAIL Piping to a writable stream that does not consume the writes fast enough exerts backpressure on the source Can't find variable: WritableStream
+
diff --git a/LayoutTests/streams/reference-implementation/pipe-to.html b/LayoutTests/streams/reference-implementation/pipe-to.html
new file mode 100644 (file)
index 0000000..d8d1dc6
--- /dev/null
@@ -0,0 +1,1054 @@
+<!DOCTYPE html>
+<script src='../../resources/testharness.js'></script>
+<script src='../../resources/testharnessreport.js'></script>
+<script src='resources/streams-utils.js'></script>
+<script>
+var test1 = async_test('Piping from a ReadableStream from which lots of data are readable synchronously');
+test1.step(function() {
+    var events = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            for (var i = 0; i < 1000; ++i) {
+                c.enqueue(i);
+            }
+            c.close();
+        }
+    });
+
+    var ws = new WritableStream({}, new CountQueuingStrategy({ highWaterMark: 1000 }));
+
+    assert_equals(ws.state, 'writable', 'writable stream state should start out writable');
+
+    var pipeFinished = false;
+    rs.pipeTo(ws).then(
+        test1.step_func(function() {
+            pipeFinished = true;
+            rs.getReader().closed.then(test1.step_func(function() {
+                assert_equals(++events, 2);
+            }));
+            assert_equals(ws.state, 'closed', 'writable stream state should be closed after pipe finishes');
+            assert_equals(++events, 1);
+        })
+    ).catch(test1.step_func(function(e) { assert_unreached(e); }));
+
+    setTimeout(test1.step_func(function() {
+        assert_true(pipeFinished, 'pipe should have finished before a setTimeout(,0) since it should only be microtasks');
+        assert_equals(++events, 3);
+        test1.done();
+    }), 0);
+});
+
+var test2 = async_test('Piping from a ReadableStream in readable state to a WritableStream in closing state');
+test2.step(function() {
+    var events = 0;
+    var cancelReason;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('Hello');
+        },
+        cancel: function(reason) {
+            assert_throws(new TypeError(), function() { throw reason; }, 'underlying source cancel should have been called with a TypeError');
+            assert_equals(++events, 1);
+            cancelReason = reason;
+        }
+    });
+
+    var ws = new WritableStream({
+        write: function() {
+            assert_unreached('Unexpected write call');
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+
+    ws.close();
+    assert_equals(ws.state, 'closing', 'writable stream should be closing immediately after closing it');
+
+    rs.pipeTo(ws).then(
+        test2.step_func(function() { assert_unreached('promise returned by pipeTo should not fulfill'); }),
+        test2.step_func(function(r) {
+            assert_equals(r, cancelReason, 'the pipeTo promise should reject with the same error as the underlying source cancel was called with');
+            assert_equals(++events, 2);
+            rs.getReader().closed.then(test2.step_func(function() {
+                assert_equals(++events, 3);
+                test2.done();
+            }));
+        })
+    ).catch(test2.step_func(function(e) { assert_unreached(e); }));
+});
+
+var test3 = async_test('Piping from a ReadableStream in readable state to a WritableStream in errored state');
+test3.step(function() {
+    var pullCount = 0;
+    var cancelCalled = false;
+    var passedError = new Error('horrible things');
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('Hello');
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function(reason) {
+            assert_false(cancelCalled, 'cancel must not be called more than once');
+            cancelCalled = true;
+
+            assert_equals(reason, passedError);
+        }
+    });
+
+    var writeCalled = false;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            assert_false(writeCalled, 'write must not be called more than once');
+            writeCalled = true;
+
+            assert_equals(chunk, 'Hello');
+
+            return Promise.reject(passedError);
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+
+    startPromise.then(test3.step_func(function() {
+        ws.write('Hello');
+        assert_true(writeCalled, 'write must be called');
+
+        ws.ready.then(test3.step_func(function() {
+            assert_equals(ws.state, 'errored', 'as a result of rejected promise, ws must be in errored state');
+
+            rs.pipeTo(ws).catch(test3.step_func(function(e) {
+                assert_equals(e, passedError, 'pipeTo promise should be rejected with the error');
+                assert_true(cancelCalled, 'cancel should have been called');
+                test3.done();
+            }));
+        }));
+    }));
+});
+
+var test4 = async_test('Piping from a ReadableStream in the readable state which becomes closed after pipeTo call to a WritableStream in the writable state');
+test4.step(function() {
+    var closeReadableStream;
+    var pullCount = 0;
+    var events = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('Hello');
+            closeReadableStream = c.close.bind(c);
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var writeCalled = false;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            if (!writeCalled) {
+                assert_equals(chunk, 'Hello', 'chunk written to writable stream should be the one enqueued into the readable stream');
+                writeCalled = true;
+                assert_equals(++events, 2);
+            } else {
+                assert_unreached('Unexpected extra write call');
+            }
+        },
+        close: function() {
+            assert_equals(pullCount, 1, 'underlying source pull should have been called once');
+            assert_equals(++events, 3);
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+
+    startPromise.then(test4.step_func(function() {
+        rs.pipeTo(ws).then(test4.step_func(function() {
+            assert_equals(ws.state, 'closed', 'writable stream should be closed after pipeTo completes');
+            assert_equals(++events, 4);
+            test4.done();
+        }));
+
+        assert_equals(ws.state, 'writable', 'writable stream should still be writable immediately after pipeTo');
+        assert_equals(++events, 1);
+
+        closeReadableStream();
+    }));
+});
+
+var test5 = async_test('Piping from a ReadableStream in the readable state which becomes errored after pipeTo call to a WritableStream in the writable state');
+test5.step(function() {
+    var errorReadableStream;
+    var pullCount = 0;
+    var events = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('Hello');
+            errorReadableStream = c.error.bind(c);
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var passedError = new Error('horrible things');
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            assert_unreached('Unexpected extra write call');
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function(reason) {
+            assert_equals(reason, passedError, 'underlying sink abort should receive the error from the readable stream');
+            assert_equals(pullCount, 1, 'underlying source pull should have been called once');
+            assert_equals(++events, 2);
+        }
+    });
+
+    startPromise.then(test5.step_func(function() {
+        rs.pipeTo(ws).catch(test5.step_func(function(e) {
+            assert_equals(e, passedError, 'pipeTo should be rejected with the passed error');
+            assert_equals(ws.state, 'errored', 'writable stream should be errored after pipeTo completes');
+            assert_equals(++events, 3);
+            test5.done();
+        }));
+
+        assert_equals(ws.state, 'writable', 'writable stream should still be writable immediately after pipeTo');
+        assert_equals(++events, 1);
+
+        errorReadableStream(passedError);
+    }));
+});
+
+var test6 = async_test('Piping from an empty ReadableStream which becomes non-empty after pipeTo call to a WritableStream in the writable state');
+test6.step(function() {
+    var controller;
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            controller = c;
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var ws = new WritableStream({
+        write: function(chunk) {
+            assert_equals(chunk, 'Hello', 'underlying sink write should be called with the single chunk');
+            assert_equals(pullCount, 1, 'pull should have been called once');
+            test6.done();
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function(reason) {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+
+    rs.pipeTo(ws).then(test6.step_func(function() { assert_unreached('pipeTo promise should not fulfill'); }));
+    assert_equals(ws.state, 'writable', 'writable stream should start in writable state');
+
+    controller.enqueue('Hello');
+});
+
+var test7 = async_test('Piping from an empty ReadableStream which becomes errored after pipeTo call to a WritableStream in the writable state');
+test7.step(function() {
+    var errorReadableStream;
+    var abortCalled = false;
+    var rs = new ReadableStream({
+        start: function(c) {
+            errorReadableStream = c.error.bind(c);
+        },
+        pull: function() {
+            assert_unreached('Unexpected pull call');
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var passedError = new Error('horrible things');
+    var ws = new WritableStream({
+        write: function() {
+            assert_unreached('Unexpected write call');
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function(reason) {
+            assert_equals(reason, passedError, 'underlying sink abort should receive the error from the readable stream');
+            abortCalled = true;
+        }
+    });
+
+    rs.pipeTo(ws).catch(test7.step_func(function(e) {
+        assert_equals(e, passedError, 'pipeTo should reject with the passed error');
+        assert_true(abortCalled);
+        test7.done();
+    }));
+    assert_equals(ws.state, 'writable', 'writable stream should start out writable');
+    errorReadableStream(passedError);
+});
+
+var test8 = async_test('Piping from an empty ReadableStream to a WritableStream in the writable state which becomes errored after a pipeTo call');
+test8.step(function() {
+    var cancelCalled = false;
+
+    var theError = new Error('cancel with me!');
+
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function(reason) {
+            assert_equals(reason, theError, 'underlying source cancellation reason should be the writable stream error');
+            assert_equals(pullCount, 2, 'pull should have been called twice by cancel-time');
+            cancelCalled = true;
+        }
+    });
+
+    var errorWritableStream;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function(error) {
+            errorWritableStream = error;
+            return startPromise;
+        },
+        write: function(chunk) {
+            assert_unreached('Unexpected write call');
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+
+    startPromise.then(test8.step_func(function() {
+        assert_equals(ws.state, 'writable', 'ws should start writable');
+
+        rs.pipeTo(ws).catch(test8.step_func(function(e) {
+            assert_equals(e, theError, 'pipeTo should reject with the passed error');
+            assert_true(cancelCalled);
+            test8.done();
+        }));
+        assert_equals(ws.state, 'writable', 'ws should be writable after pipe');
+
+        errorWritableStream(theError);
+        assert_equals(ws.state, 'errored', 'ws should be errored after erroring it');
+    }));
+});
+
+var test9 = async_test('Piping from a non-empty ReadableStream to a WritableStream in the waiting state which becomes writable after a pipeTo call');
+test9.step(function() {
+    var pullCount = 0;
+    var readyFulfilled = false;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('World');
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var resolveWritePromise;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            if (!resolveWritePromise) {
+                assert_equals(chunk, 'Hello', 'the first chunk to arrive in write should be the first chunk written');
+                return new Promise(test9.step_func(function(resolve) { resolveWritePromise = resolve; }));
+            } else {
+                assert_equals(chunk, 'World', 'the second chunk to arrive in write should be from the readable stream');
+                assert_equals(pullCount, 1, 'the readable stream\'s pull should have been called once');
+                assert_true(readyFulfilled);
+                test9.done();
+            }
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+    ws.write('Hello');
+
+    startPromise.then(test9.step_func(function() {
+        assert_equals(ws.state, 'waiting', 'writable stream state should start waiting');
+
+        rs.pipeTo(ws);
+        assert_equals(ws.state, 'waiting', 'writable stream state should still be waiting immediately after piping');
+
+        resolveWritePromise();
+        ws.ready.then(test9.step_func(function() {
+            assert_equals(ws.state, 'writable', 'writable stream should eventually become writable (when ready fulfills)');
+            readyFulfilled = true;
+        })).catch(test9.step_func(function(e) { assert_unreached(e); }));
+    }));
+});
+
+var test10 = async_test('Piping from a non-empty ReadableStream to a WritableStream in waiting state which becomes errored after a pipeTo call');
+test10.step(function() {
+    var writeCalled = false;
+
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('World');
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_true(writeCalled);
+            assert_equals(pullCount, 1);
+            test10.done();
+        }
+    });
+
+    var errorWritableStream;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function(error) {
+            errorWritableStream = error;
+            return startPromise;
+        },
+        write: function(chunk) {
+            assert_false(writeCalled);
+            assert_equals(chunk, 'Hello');
+            writeCalled = true;
+            return new Promise(test10.step_func(function() { }));
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+    ws.write('Hello');
+
+    startPromise.then(test10.step_func(function() {
+        assert_equals(ws.state, 'waiting');
+
+        rs.pipeTo(ws);
+        assert_equals(ws.state, 'waiting');
+
+        errorWritableStream();
+        assert_equals(ws.state, 'errored');
+    }));
+});
+
+var test11 = async_test('Piping from a non-empty ReadableStream which becomes errored after pipeTo call to a WritableStream in the waiting state');
+test11.step(function() {
+    var errorReadableStream;
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('World');
+            errorReadableStream = c.error.bind(c);
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var writeCalled = false;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            assert_false(writeCalled);
+            writeCalled = true;
+
+            assert_equals(chunk, 'Hello');
+
+            return new Promise(test11.step_func(function() { }));
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function() {
+            test11.done();
+        }
+    });
+    ws.write('Hello');
+
+    startPromise.then(test11.step_func(function() {
+        assert_equals(ws.state, 'waiting');
+        assert_equals(pullCount, 0);
+
+        rs.pipeTo(ws);
+        assert_equals(ws.state, 'waiting');
+
+        errorReadableStream();
+    }));
+});
+
+var test12 = async_test('Piping from a non-empty ReadableStream to a WritableStream in the waiting state where both become ready after a pipeTo');
+test12.step(function() {
+    var controller;
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            controller = c;
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var writeCount = 0;
+    var resolveWritePromise;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            ++writeCount;
+
+            if (writeCount === 1) {
+                assert_equals(chunk, 'Hello', 'first chunk written should equal the one passed to ws.write');
+                return new Promise(test12.step_func(function(resolve) { resolveWritePromise = resolve; }));
+            }
+            if (writeCount === 2) {
+                assert_equals(chunk, 'Goodbye', 'second chunk written should be from the source readable stream');
+                test12.done();
+            }
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function(reason) {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+    ws.write('Hello');
+
+    startPromise.then(test12.step_func(function() {
+        assert_equals(writeCount, 1, 'exactly one write should have happened');
+        assert_equals(ws.state, 'waiting', 'writable stream should be waiting');
+
+        assert_equals(pullCount, 1, 'pull should have been called only once');
+        rs.pipeTo(ws);
+
+        controller.enqueue('Goodbye');
+
+        // Check that nothing happens before calling resolveWritePromise(), and then call resolveWritePromise()
+        // to check that pipeTo is woken up.
+        assert_equals(pullCount, 1, 'after the pipeTo and enqueue, pull still should have been called only once');
+        resolveWritePromise();
+    }));
+});
+
+var test13 = async_test('Piping from an empty ReadableStream to a WritableStream in the waiting state which becomes writable after a pipeTo call');
+test13.step(function() {
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var resolveWritePromise;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            assert_true(!resolveWritePromise);
+            assert_equals(chunk, 'Hello');
+            return new Promise(test13.step_func(function(resolve) { resolveWritePromise = resolve; }));
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+    ws.write('Hello');
+
+    startPromise.then(test13.step_func(function() {
+        assert_equals(ws.state, 'waiting');
+
+        rs.pipeTo(ws);
+        assert_equals(ws.state, 'waiting');
+        assert_equals(pullCount, 1);
+
+        resolveWritePromise();
+        setTimeout(test13.step_func(function() {
+            assert_equals(pullCount, 2);
+
+            test13.done();
+        }), 500);
+    }));
+});
+
+var test14 = async_test('Piping from an empty ReadableStream which becomes closed after a pipeTo call to a WritableStream in the waiting state whose writes never complete');
+test14.step(function() {
+    var closeReadableStream;
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            closeReadableStream = c.close.bind(c);
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var writeCalled = false;
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            if (!writeCalled) {
+                assert_equals(chunk, 'Hello', 'the chunk should be written to the writable stream');
+                writeCalled = true;
+                closeReadableStream();
+            } else {
+                assert_unreached('Unexpected extra write call');
+            }
+            return new Promise(test14.step_func(function() { }));
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function() {
+            assert_unreached('Unexpected abort call');
+        }
+    });
+    ws.write('Hello');
+
+    startPromise.then(test14.step_func(function() {
+        assert_equals(ws.state, 'waiting', 'the writable stream should be in the waiting state after starting');
+
+        rs.pipeTo(ws);
+
+        setTimeout(test14.step_func(function() {
+            assert_equals(ws.state, 'waiting', 'the writable stream should still be waiting since the write never completed');
+            assert_equals(pullCount, 1, 'pull should have been called only once');
+            assert_true(writeCalled);
+            test14.done();
+        }), 500);
+    }));
+});
+
+var test15 = async_test('Piping from an empty ReadableStream which becomes errored after a pipeTo call to a WritableStream in the waiting state');
+test15.step(function() {
+    var errorReadableStream;
+    var pullCount = 0;
+    var rs = new ReadableStream({
+        start: function(c) {
+            errorReadableStream = c.error.bind(c);
+        },
+        pull: function() {
+            ++pullCount;
+        },
+        cancel: function() {
+            assert_unreached('Unexpected cancel call');
+        }
+    });
+
+    var writeCalled = false;
+    var passedError = new Error('horrible things');
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            if (!writeCalled) {
+                assert_equals(chunk, 'Hello');
+                writeCalled = true;
+            } else {
+                assert_unreached('Unexpected extra write call');
+            }
+            return new Promise(test15.step_func(function() { }));
+        },
+        close: function() {
+            assert_unreached('Unexpected close call');
+        },
+        abort: function(reason) {
+            assert_equals(reason, passedError);
+            assert_true(writeCalled);
+            assert_equals(pullCount, 1);
+            test15.done();
+        }
+    });
+    ws.write('Hello');
+
+    startPromise.then(test15.step_func(function() {
+        assert_equals(ws.state, 'waiting');
+
+        rs.pipeTo(ws);
+
+        errorReadableStream(passedError);
+    }));
+});
+
+var test16 = async_test('Piping to a duck-typed asynchronous "writable stream" works');
+test16.step(function() {
+    var rs = sequentialReadableStream(5, { async: true });
+
+    var chunksWritten = [];
+    var dest = {
+        state: 'writable',
+        write: function(chunk) {
+            chunksWritten.push(chunk);
+            return Promise.resolve();
+        },
+        get ready() {
+            return Promise.resolve();
+        },
+        close: function() {
+            assert_array_equals(chunksWritten, [1, 2, 3, 4, 5]);
+            test16.done();
+            return Promise.resolve();
+        },
+        abort: function() {
+            assert_unreached('Should not call abort');
+        },
+        closed: new Promise(test16.step_func(function() { }))
+    };
+
+    rs.pipeTo(dest);
+});
+
+var test17 = async_test('Piping to a stream that has been aborted passes through the error as the cancellation reason');
+test17.step(function() {
+    var recordedReason;
+    var rs = new ReadableStream({
+        cancel: function(reason) {
+            recordedReason = reason;
+        }
+    });
+
+    var ws = new WritableStream();
+    var passedReason = new Error('I don\'t like you.');
+    ws.abort(passedReason);
+
+    rs.pipeTo(ws).catch(test17.step_func(function(e) {
+        assert_equals(e, passedReason, 'pipeTo rejection reason should be the cancellation reason');
+        assert_equals(recordedReason, passedReason, 'the recorded cancellation reason must be the passed abort reason');
+        test17.done();
+    }));
+});
+
+var test18 = async_test('Piping to a stream and then aborting it passes through the error as the cancellation reason');
+test18.step(function() {
+    var recordedReason;
+    var rs = new ReadableStream({
+        cancel: function(reason) {
+            recordedReason = reason;
+        }
+    });
+
+    var ws = new WritableStream();
+    var passedReason = new Error('I don\'t like you.');
+
+    var pipeToPromise = rs.pipeTo(ws);
+    ws.abort(passedReason);
+
+    pipeToPromise.catch(test18.step_func(function(e) {
+        assert_equals(e, passedReason, 'pipeTo rejection reason should be the abortion reason');
+        assert_equals(recordedReason, passedReason, 'the recorded cancellation reason must be the passed abort reason');
+        test18.done();
+    }));
+});
+
+var test19 = async_test('Piping to a stream that has been closed propagates a TypeError cancellation reason backward');
+test19.step(function() {
+    var recordedReason;
+    var rs = new ReadableStream({
+        cancel: function(reason) {
+            recordedReason = reason;
+        }
+    });
+
+    var ws = new WritableStream();
+    ws.close();
+
+    rs.pipeTo(ws).catch(test19.step_func(function(e) {
+        assert_throws(new TypeError(), function() { throw e; }, 'the rejection reason for the pipeTo promise should be a TypeError');
+        assert_throws(new TypeError(), function() { throw recordedReason; }, 'the recorded cancellation reason should be a TypeError');
+        test19.done();
+    }));
+});
+
+var test20 = async_test('Piping to a stream and then closing it propagates a TypeError cancellation reason backward');
+test20.step(function() {
+    var recordedReason;
+    var rs = new ReadableStream({
+        cancel: function(reason) {
+            recordedReason = reason;
+        }
+    });
+
+    var ws = new WritableStream();
+
+    var pipeToPromise = rs.pipeTo(ws);
+    ws.close();
+
+    pipeToPromise.catch(test20.step_func(function(e) {
+        assert_throws(new TypeError(), function() { throw e; }, 'the rejection reason for the pipeTo promise should be a TypeError');
+        assert_throws(new TypeError(), function() { throw recordedReason; }, 'the recorded cancellation reason should be a TypeError');
+        test20.done();
+    }));
+});
+
+var test21 = async_test('Piping to a stream that errors on write should pass through the error as the cancellation reason');
+test21.step(function() {
+    var recordedReason;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('a');
+            c.enqueue('b');
+            c.enqueue('c');
+        },
+        cancel: function(reason) {
+            assert_equals(reason, passedError, 'the recorded cancellation reason must be the passed error');
+            test21.done();
+        }
+    });
+
+    var written = 0;
+    var passedError = new Error('I don\'t like you.');
+    var ws = new WritableStream({
+        write: function(chunk) {
+            return new Promise(test21.step_func(function(resolve, reject) {
+                if (++written > 1) {
+                    reject(passedError);
+                } else {
+                    resolve();
+                }
+            }));
+        }
+    });
+
+    rs.pipeTo(ws);
+});
+
+var test22 = async_test('Piping to a stream that errors on write should not pass through the error if the stream is already closed');
+test22.step(function() {
+    var cancelCalled = false;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('a');
+            c.enqueue('b');
+            c.enqueue('c');
+            c.close();
+        },
+        cancel: function() {
+            cancelCalled = true;
+        }
+    });
+
+    var written = 0;
+    var passedError = new Error('I don\'t like you.');
+    var ws = new WritableStream({
+        write: function(chunk) {
+            return new Promise(test22.step_func(function(resolve, reject) {
+                if (++written > 1) {
+                    reject(passedError);
+                } else {
+                    resolve();
+                }
+            }));
+        }
+    });
+
+    rs.pipeTo(ws).then(
+        test22.step_func(function() { assert_unreached('pipeTo should not fulfill'); }),
+        test22.step_func(function(r) {
+            assert_equals(r, passedError, 'pipeTo should reject with the same error as the write');
+            assert_equals(cancelCalled, false, 'cancel should not have been called');
+            test22.done();
+        })
+    );
+});
+
+var test23 = async_test('Piping to a stream that errors soon after writing should pass through the error as the cancellation reason');
+test23.step(function() {
+    var recordedReason;
+    var rs = new ReadableStream({
+        start: function(c) {
+            c.enqueue('a');
+            c.enqueue('b');
+            c.enqueue('c');
+        },
+        cancel: function(reason) {
+            assert_equals(reason, passedError, 'the recorded cancellation reason must be the passed error');
+            test23.done();
+        }
+    });
+
+    var written = 0;
+    var passedError = new Error('I don\'t like you.');
+    var ws = new WritableStream({
+        write: function(chunk) {
+            return new Promise(test23.step_func(function(resolve, reject) {
+                if (++written > 1) {
+                    setTimeout(test23.step_func(function() { reject(passedError); }), 200);
+                } else {
+                    resolve();
+                }
+            }));
+        }
+    });
+
+    rs.pipeTo(ws);
+});
+
+var test24 = async_test('Piping to a writable stream that does not consume the writes fast enough exerts backpressure on the source');
+test24.step(function() {
+    var desiredSizes = [];
+    var rs = new ReadableStream({
+        start: function(c) {
+            setTimeout(test24.step_func(function() { enqueue('a'); }), 200);
+            setTimeout(test24.step_func(function() { enqueue('b'); }), 400);
+            setTimeout(test24.step_func(function() { enqueue('c'); }), 600);
+            setTimeout(test24.step_func(function() { enqueue('d'); }), 800);
+            setTimeout(test24.step_func(function() { c.close(); }), 1000);
+
+            function enqueue(chunk) {
+                c.enqueue(chunk);
+                desiredSizes.push(c.desiredSize);
+            }
+        }
+    });
+
+    var chunksGivenToWrite = [];
+    var chunksFinishedWriting = [];
+    var startPromise = Promise.resolve();
+    var ws = new WritableStream({
+        start: function() {
+            return startPromise;
+        },
+        write: function(chunk) {
+            chunksGivenToWrite.push(chunk);
+            return new Promise(test24.step_func(function(resolve) {
+                setTimeout(test24.step_func(function() {
+                    chunksFinishedWriting.push(chunk);
+                    resolve();
+                }), 700);
+            }));
+        }
+    });
+
+    startPromise.then(test24.step_func(function() {
+        rs.pipeTo(ws).then(test24.step_func(function() {
+            assert_array_equals(desiredSizes, [1, 1, 0, -1], 'backpressure was correctly exerted at the source');
+            assert_array_equals(chunksFinishedWriting, ['a', 'b', 'c', 'd'], 'all chunks were written');
+            test24.done();
+        }));
+
+        assert_equals(ws.state, 'writable', 'at t = 0 ms, ws should be writable');
+
+        setTimeout(test24.step_func(function() {
+            assert_equals(ws.state, 'waiting', 'at t = 250 ms, ws should be waiting');
+            assert_array_equals(chunksGivenToWrite, ['a'], 'at t = 250 ms, ws.write should have been called with one chunk');
+            assert_array_equals(chunksFinishedWriting, [], 'at t = 250 ms, no chunks should have finished writing');
+
+            // When 'a' (the very first chunk) was enqueued, it was immediately used to fulfill the outstanding read request
+            // promise, leaving room in the queue
+            assert_array_equals(desiredSizes, [1], 'at t = 250 ms, the one enqueued chunk in rs did not cause backpressure');
+        }), 250);
+
+        setTimeout(test24.step_func(function() {
+            assert_equals(ws.state, 'waiting', 'at t = 450 ms, ws should be waiting');
+            assert_array_equals(chunksGivenToWrite, ['a'], 'at t = 450 ms, ws.write should have been called with one chunk');
+            assert_array_equals(chunksFinishedWriting, [], 'at t = 450 ms, no chunks should have finished writing');
+
+            // When 'b' was enqueued at 200 ms, the queue was also empty, since immediately after enqueuing 'a' at
+            // t = 100 ms, it was dequeued in order to fulfill the read() call that was made at time t = 0.
+            assert_array_equals(desiredSizes, [1, 1], 'at t = 450 ms, the two enqueued chunks in rs did not cause backpressure');
+        }), 450);
+
+        setTimeout(test24.step_func(function() {
+            assert_equals(ws.state, 'waiting', 'at t = 650 ms, ws should be waiting');
+            assert_array_equals(chunksGivenToWrite, ['a'], 'at t = 650 ms, ws.write should have been called with one chunk');
+            assert_array_equals(chunksFinishedWriting, [], 'at t = 650 ms, no chunks should have finished writing');
+
+            // When 'c' was enqueued at 300 ms, the queue was again empty, since at time t = 200 ms when 'b' was enqueued,
+            // it was immediately dequeued in order to fulfill the second read() call that was made at time t = 0.
+            // However, this time there was no pending read request to whisk it away, so after the enqueue desired size is 0.
+            assert_array_equals(desiredSizes, [1, 1, 0], 'at t = 650 ms, the three enqueued chunks in rs did not cause backpressure');
+        }), 650);
+
+        setTimeout(test24.step_func(function() {
+            assert_equals(ws.state, 'waiting', 'at t = 850 ms, ws should be waiting');
+            assert_array_equals(chunksGivenToWrite, ['a'], 'at t = 850 ms, ws.write should have been called with one chunk');
+            assert_array_equals(chunksFinishedWriting, [], 'at t = 850 ms, no chunks should have finished writing');
+
+            // When 'd' was enqueued at 400 ms, the queue was *not* empty. 'c' was still in it, since the write() of 'b' will
+            // not finish until t = 100 ms + 350 ms = 450 ms. Thus backpressure should have been exerted.
+            assert_array_equals(desiredSizes, [1, 1, 0, -1], 'at t = 850 ms, the fourth enqueued chunks in rs did cause backpressure');
+        }), 850);
+
+        setTimeout(test24.step_func(function() {
+            assert_equals(ws.state, 'waiting', 'at t = 950 ms, ws should be waiting');
+            assert_array_equals(chunksGivenToWrite, ['a', 'b'], 'at t = 950 ms, ws.write should have been called with two chunks');
+            assert_array_equals(chunksFinishedWriting, ['a'], 'at t = 950 ms, one chunk should have finished writing');
+        }), 950);
+    }));
+});
+</script>