summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/streams/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/streams/resources')
-rw-r--r--testing/web-platform/tests/streams/resources/recording-streams.js131
-rw-r--r--testing/web-platform/tests/streams/resources/rs-test-templates.js721
-rw-r--r--testing/web-platform/tests/streams/resources/rs-utils.js197
-rw-r--r--testing/web-platform/tests/streams/resources/test-utils.js27
4 files changed, 1076 insertions, 0 deletions
diff --git a/testing/web-platform/tests/streams/resources/recording-streams.js b/testing/web-platform/tests/streams/resources/recording-streams.js
new file mode 100644
index 0000000000..661fe512f5
--- /dev/null
+++ b/testing/web-platform/tests/streams/resources/recording-streams.js
@@ -0,0 +1,131 @@
+'use strict';
+
+self.recordingReadableStream = (extras = {}, strategy) => {
+ let controllerToCopyOver;
+ const stream = new ReadableStream({
+ type: extras.type,
+ start(controller) {
+ controllerToCopyOver = controller;
+
+ if (extras.start) {
+ return extras.start(controller);
+ }
+
+ return undefined;
+ },
+ pull(controller) {
+ stream.events.push('pull');
+
+ if (extras.pull) {
+ return extras.pull(controller);
+ }
+
+ return undefined;
+ },
+ cancel(reason) {
+ stream.events.push('cancel', reason);
+ stream.eventsWithoutPulls.push('cancel', reason);
+
+ if (extras.cancel) {
+ return extras.cancel(reason);
+ }
+
+ return undefined;
+ }
+ }, strategy);
+
+ stream.controller = controllerToCopyOver;
+ stream.events = [];
+ stream.eventsWithoutPulls = [];
+
+ return stream;
+};
+
+self.recordingWritableStream = (extras = {}, strategy) => {
+ let controllerToCopyOver;
+ const stream = new WritableStream({
+ start(controller) {
+ controllerToCopyOver = controller;
+
+ if (extras.start) {
+ return extras.start(controller);
+ }
+
+ return undefined;
+ },
+ write(chunk, controller) {
+ stream.events.push('write', chunk);
+
+ if (extras.write) {
+ return extras.write(chunk, controller);
+ }
+
+ return undefined;
+ },
+ close() {
+ stream.events.push('close');
+
+ if (extras.close) {
+ return extras.close();
+ }
+
+ return undefined;
+ },
+ abort(e) {
+ stream.events.push('abort', e);
+
+ if (extras.abort) {
+ return extras.abort(e);
+ }
+
+ return undefined;
+ }
+ }, strategy);
+
+ stream.controller = controllerToCopyOver;
+ stream.events = [];
+
+ return stream;
+};
+
+self.recordingTransformStream = (extras = {}, writableStrategy, readableStrategy) => {
+ let controllerToCopyOver;
+ const stream = new TransformStream({
+ start(controller) {
+ controllerToCopyOver = controller;
+
+ if (extras.start) {
+ return extras.start(controller);
+ }
+
+ return undefined;
+ },
+
+ transform(chunk, controller) {
+ stream.events.push('transform', chunk);
+
+ if (extras.transform) {
+ return extras.transform(chunk, controller);
+ }
+
+ controller.enqueue(chunk);
+
+ return undefined;
+ },
+
+ flush(controller) {
+ stream.events.push('flush');
+
+ if (extras.flush) {
+ return extras.flush(controller);
+ }
+
+ return undefined;
+ }
+ }, writableStrategy, readableStrategy);
+
+ stream.controller = controllerToCopyOver;
+ stream.events = [];
+
+ return stream;
+};
diff --git a/testing/web-platform/tests/streams/resources/rs-test-templates.js b/testing/web-platform/tests/streams/resources/rs-test-templates.js
new file mode 100644
index 0000000000..25751c477f
--- /dev/null
+++ b/testing/web-platform/tests/streams/resources/rs-test-templates.js
@@ -0,0 +1,721 @@
+'use strict';
+
+// These tests can be run against any readable stream produced by the web platform that meets the given descriptions.
+// For readable stream tests, the factory should return the stream. For reader tests, the factory should return a
+// { stream, reader } object. (You can use this to vary the time at which you acquire a reader.)
+
+self.templatedRSEmpty = (label, factory) => {
+ test(() => {}, 'Running templatedRSEmpty with ' + label);
+
+ test(() => {
+
+ const rs = factory();
+
+ assert_equals(typeof rs.locked, 'boolean', 'has a boolean locked getter');
+ assert_equals(typeof rs.cancel, 'function', 'has a cancel method');
+ assert_equals(typeof rs.getReader, 'function', 'has a getReader method');
+ assert_equals(typeof rs.pipeThrough, 'function', 'has a pipeThrough method');
+ assert_equals(typeof rs.pipeTo, 'function', 'has a pipeTo method');
+ assert_equals(typeof rs.tee, 'function', 'has a tee method');
+
+ }, label + ': instances have the correct methods and properties');
+
+ test(() => {
+ const rs = factory();
+
+ assert_throws_js(TypeError, () => rs.getReader({ mode: '' }), 'empty string mode should throw');
+ assert_throws_js(TypeError, () => rs.getReader({ mode: null }), 'null mode should throw');
+ assert_throws_js(TypeError, () => rs.getReader({ mode: 'asdf' }), 'asdf mode should throw');
+ assert_throws_js(TypeError, () => rs.getReader(5), '5 should throw');
+
+ // Should not throw
+ rs.getReader(null);
+
+ }, label + ': calling getReader with invalid arguments should throw appropriate errors');
+};
+
+self.templatedRSClosed = (label, factory) => {
+ test(() => {}, 'Running templatedRSClosed with ' + label);
+
+ promise_test(() => {
+
+ const rs = factory();
+ const cancelPromise1 = rs.cancel();
+ const cancelPromise2 = rs.cancel();
+
+ assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises');
+
+ return Promise.all([
+ cancelPromise1.then(v => assert_equals(v, undefined, 'first cancel() call should fulfill with undefined')),
+ cancelPromise2.then(v => assert_equals(v, undefined, 'second cancel() call should fulfill with undefined'))
+ ]);
+
+ }, label + ': cancel() should return a distinct fulfilled promise each time');
+
+ test(() => {
+
+ const rs = factory();
+ assert_false(rs.locked, 'locked getter should return false');
+
+ }, label + ': locked should be false');
+
+ test(() => {
+
+ const rs = factory();
+ rs.getReader(); // getReader() should not throw.
+
+ }, label + ': getReader() should be OK');
+
+ test(() => {
+
+ const rs = factory();
+
+ const reader = rs.getReader();
+ reader.releaseLock();
+
+ const reader2 = rs.getReader(); // Getting a second reader should not throw.
+ reader2.releaseLock();
+
+ rs.getReader(); // Getting a third reader should not throw.
+
+ }, label + ': should be able to acquire multiple readers if they are released in succession');
+
+ test(() => {
+
+ const rs = factory();
+
+ rs.getReader();
+
+ assert_throws_js(TypeError, () => rs.getReader(), 'getting a second reader should throw');
+ assert_throws_js(TypeError, () => rs.getReader(), 'getting a third reader should throw');
+
+ }, label + ': should not be able to acquire a second reader if we don\'t release the first one');
+};
+
+self.templatedRSErrored = (label, factory, error) => {
+ test(() => {}, 'Running templatedRSErrored with ' + label);
+
+ promise_test(t => {
+
+ const rs = factory();
+ const reader = rs.getReader();
+
+ return Promise.all([
+ promise_rejects_exactly(t, error, reader.closed),
+ promise_rejects_exactly(t, error, reader.read())
+ ]);
+
+ }, label + ': getReader() should return a reader that acts errored');
+
+ promise_test(t => {
+
+ const rs = factory();
+ const reader = rs.getReader();
+
+ return Promise.all([
+ promise_rejects_exactly(t, error, reader.read()),
+ promise_rejects_exactly(t, error, reader.read()),
+ promise_rejects_exactly(t, error, reader.closed)
+ ]);
+
+ }, label + ': read() twice should give the error each time');
+
+ test(() => {
+ const rs = factory();
+
+ assert_false(rs.locked, 'locked getter should return false');
+ }, label + ': locked should be false');
+};
+
+self.templatedRSErroredSyncOnly = (label, factory, error) => {
+ test(() => {}, 'Running templatedRSErroredSyncOnly with ' + label);
+
+ promise_test(t => {
+
+ const rs = factory();
+ rs.getReader().releaseLock();
+ const reader = rs.getReader(); // Calling getReader() twice does not throw (the stream is not locked).
+
+ return promise_rejects_exactly(t, error, reader.closed);
+
+ }, label + ': should be able to obtain a second reader, with the correct closed promise');
+
+ test(() => {
+
+ const rs = factory();
+ rs.getReader();
+
+ assert_throws_js(TypeError, () => rs.getReader(), 'getting a second reader should throw a TypeError');
+ assert_throws_js(TypeError, () => rs.getReader(), 'getting a third reader should throw a TypeError');
+
+ }, label + ': should not be able to obtain additional readers if we don\'t release the first lock');
+
+ promise_test(t => {
+
+ const rs = factory();
+ const cancelPromise1 = rs.cancel();
+ const cancelPromise2 = rs.cancel();
+
+ assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error, cancelPromise1),
+ promise_rejects_exactly(t, error, cancelPromise2)
+ ]);
+
+ }, label + ': cancel() should return a distinct rejected promise each time');
+
+ promise_test(t => {
+
+ const rs = factory();
+ const reader = rs.getReader();
+ const cancelPromise1 = reader.cancel();
+ const cancelPromise2 = reader.cancel();
+
+ assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error, cancelPromise1),
+ promise_rejects_exactly(t, error, cancelPromise2)
+ ]);
+
+ }, label + ': reader cancel() should return a distinct rejected promise each time');
+};
+
+self.templatedRSEmptyReader = (label, factory) => {
+ test(() => {}, 'Running templatedRSEmptyReader with ' + label);
+
+ test(() => {
+
+ const reader = factory().reader;
+
+ assert_true('closed' in reader, 'has a closed property');
+ assert_equals(typeof reader.closed.then, 'function', 'closed property is thenable');
+
+ assert_equals(typeof reader.cancel, 'function', 'has a cancel method');
+ assert_equals(typeof reader.read, 'function', 'has a read method');
+ assert_equals(typeof reader.releaseLock, 'function', 'has a releaseLock method');
+
+ }, label + ': instances have the correct methods and properties');
+
+ test(() => {
+
+ const stream = factory().stream;
+
+ assert_true(stream.locked, 'locked getter should return true');
+
+ }, label + ': locked should be true');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+
+ reader.read().then(
+ t.unreached_func('read() should not fulfill'),
+ t.unreached_func('read() should not reject')
+ );
+
+ return delay(500);
+
+ }, label + ': read() should never settle');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+
+ reader.read().then(
+ t.unreached_func('read() should not fulfill'),
+ t.unreached_func('read() should not reject')
+ );
+
+ reader.read().then(
+ t.unreached_func('read() should not fulfill'),
+ t.unreached_func('read() should not reject')
+ );
+
+ return delay(500);
+
+ }, label + ': two read()s should both never settle');
+
+ test(() => {
+
+ const reader = factory().reader;
+ assert_not_equals(reader.read(), reader.read(), 'the promises returned should be distinct');
+
+ }, label + ': read() should return distinct promises each time');
+
+ test(() => {
+
+ const stream = factory().stream;
+ assert_throws_js(TypeError, () => stream.getReader(), 'stream.getReader() should throw a TypeError');
+
+ }, label + ': getReader() again on the stream should fail');
+
+ promise_test(async t => {
+
+ const streamAndReader = factory();
+ const stream = streamAndReader.stream;
+ const reader = streamAndReader.reader;
+
+ const read1 = reader.read();
+ const read2 = reader.read();
+ const closed = reader.closed;
+
+ reader.releaseLock();
+
+ assert_false(stream.locked, 'the stream should be unlocked');
+
+ await Promise.all([
+ promise_rejects_js(t, TypeError, read1, 'first read should reject'),
+ promise_rejects_js(t, TypeError, read2, 'second read should reject'),
+ promise_rejects_js(t, TypeError, closed, 'closed should reject')
+ ]);
+
+ }, label + ': releasing the lock should reject all pending read requests');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+ reader.releaseLock();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, reader.read()),
+ promise_rejects_js(t, TypeError, reader.read())
+ ]);
+
+ }, label + ': releasing the lock should cause further read() calls to reject with a TypeError');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+
+ const closedBefore = reader.closed;
+ reader.releaseLock();
+ const closedAfter = reader.closed;
+
+ assert_equals(closedBefore, closedAfter, 'the closed promise should not change identity');
+
+ return promise_rejects_js(t, TypeError, closedBefore);
+
+ }, label + ': releasing the lock should cause closed calls to reject with a TypeError');
+
+ test(() => {
+
+ const streamAndReader = factory();
+ const stream = streamAndReader.stream;
+ const reader = streamAndReader.reader;
+
+ reader.releaseLock();
+ assert_false(stream.locked, 'locked getter should return false');
+
+ }, label + ': releasing the lock should cause locked to become false');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+ reader.cancel();
+
+ return reader.read().then(r => {
+ assert_object_equals(r, { value: undefined, done: true }, 'read()ing from the reader should give a done result');
+ });
+
+ }, label + ': canceling via the reader should cause the reader to act closed');
+
+ promise_test(t => {
+
+ const stream = factory().stream;
+ return promise_rejects_js(t, TypeError, stream.cancel());
+
+ }, label + ': canceling via the stream should fail');
+};
+
+self.templatedRSClosedReader = (label, factory) => {
+ test(() => {}, 'Running templatedRSClosedReader with ' + label);
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return reader.read().then(v => {
+ assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly');
+ });
+
+ }, label + ': read() should fulfill with { value: undefined, done: true }');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return Promise.all([
+ reader.read().then(v => {
+ assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly');
+ }),
+ reader.read().then(v => {
+ assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly');
+ })
+ ]);
+
+ }, label + ': read() multiple times should fulfill with { value: undefined, done: true }');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return reader.read().then(() => reader.read()).then(v => {
+ assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly');
+ });
+
+ }, label + ': read() should work when used within another read() fulfill callback');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return reader.closed.then(v => assert_equals(v, undefined, 'reader closed should fulfill with undefined'));
+
+ }, label + ': closed should fulfill with undefined');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+
+ const closedBefore = reader.closed;
+ reader.releaseLock();
+ const closedAfter = reader.closed;
+
+ assert_not_equals(closedBefore, closedAfter, 'the closed promise should change identity');
+
+ return Promise.all([
+ closedBefore.then(v => assert_equals(v, undefined, 'reader.closed acquired before release should fulfill')),
+ promise_rejects_js(t, TypeError, closedAfter)
+ ]);
+
+ }, label + ': releasing the lock should cause closed to reject and change identity');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+ const cancelPromise1 = reader.cancel();
+ const cancelPromise2 = reader.cancel();
+ const closedReaderPromise = reader.closed;
+
+ assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises');
+ assert_not_equals(cancelPromise1, closedReaderPromise, 'cancel() promise 1 should be distinct from reader.closed');
+ assert_not_equals(cancelPromise2, closedReaderPromise, 'cancel() promise 2 should be distinct from reader.closed');
+
+ return Promise.all([
+ cancelPromise1.then(v => assert_equals(v, undefined, 'first cancel() should fulfill with undefined')),
+ cancelPromise2.then(v => assert_equals(v, undefined, 'second cancel() should fulfill with undefined'))
+ ]);
+
+ }, label + ': cancel() should return a distinct fulfilled promise each time');
+};
+
+self.templatedRSErroredReader = (label, factory, error) => {
+ test(() => {}, 'Running templatedRSErroredReader with ' + label);
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+ return promise_rejects_exactly(t, error, reader.closed);
+
+ }, label + ': closed should reject with the error');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+ const closedBefore = reader.closed;
+
+ return promise_rejects_exactly(t, error, closedBefore).then(() => {
+ reader.releaseLock();
+
+ const closedAfter = reader.closed;
+ assert_not_equals(closedBefore, closedAfter, 'the closed promise should change identity');
+
+ return promise_rejects_js(t, TypeError, closedAfter);
+ });
+
+ }, label + ': releasing the lock should cause closed to reject and change identity');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+ return promise_rejects_exactly(t, error, reader.read());
+
+ }, label + ': read() should reject with the error');
+};
+
+self.templatedRSTwoChunksOpenReader = (label, factory, chunks) => {
+ test(() => {}, 'Running templatedRSTwoChunksOpenReader with ' + label);
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return Promise.all([
+ reader.read().then(r => {
+ assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct');
+ }),
+ reader.read().then(r => {
+ assert_object_equals(r, { value: chunks[1], done: false }, 'second result should be correct');
+ })
+ ]);
+
+ }, label + ': calling read() twice without waiting will eventually give both chunks (sequential)');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return reader.read().then(r => {
+ assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct');
+
+ return reader.read().then(r2 => {
+ assert_object_equals(r2, { value: chunks[1], done: false }, 'second result should be correct');
+ });
+ });
+
+ }, label + ': calling read() twice without waiting will eventually give both chunks (nested)');
+
+ test(() => {
+
+ const reader = factory().reader;
+ assert_not_equals(reader.read(), reader.read(), 'the promises returned should be distinct');
+
+ }, label + ': read() should return distinct promises each time');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ const promise1 = reader.closed.then(v => {
+ assert_equals(v, undefined, 'reader closed should fulfill with undefined');
+ });
+
+ const promise2 = reader.read().then(r => {
+ assert_object_equals(r, { value: chunks[0], done: false },
+ 'promise returned before cancellation should fulfill with a chunk');
+ });
+
+ reader.cancel();
+
+ const promise3 = reader.read().then(r => {
+ assert_object_equals(r, { value: undefined, done: true },
+ 'promise returned after cancellation should fulfill with an end-of-stream signal');
+ });
+
+ return Promise.all([promise1, promise2, promise3]);
+
+ }, label + ': cancel() after a read() should still give that single read result');
+};
+
+self.templatedRSTwoChunksClosedReader = function (label, factory, chunks) {
+ test(() => {}, 'Running templatedRSTwoChunksClosedReader with ' + label);
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return Promise.all([
+ reader.read().then(r => {
+ assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct');
+ }),
+ reader.read().then(r => {
+ assert_object_equals(r, { value: chunks[1], done: false }, 'second result should be correct');
+ }),
+ reader.read().then(r => {
+ assert_object_equals(r, { value: undefined, done: true }, 'third result should be correct');
+ })
+ ]);
+
+ }, label + ': third read(), without waiting, should give { value: undefined, done: true } (sequential)');
+
+ promise_test(() => {
+
+ const reader = factory().reader;
+
+ return reader.read().then(r => {
+ assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct');
+
+ return reader.read().then(r2 => {
+ assert_object_equals(r2, { value: chunks[1], done: false }, 'second result should be correct');
+
+ return reader.read().then(r3 => {
+ assert_object_equals(r3, { value: undefined, done: true }, 'third result should be correct');
+ });
+ });
+ });
+
+ }, label + ': third read(), without waiting, should give { value: undefined, done: true } (nested)');
+
+ promise_test(() => {
+
+ const streamAndReader = factory();
+ const stream = streamAndReader.stream;
+ const reader = streamAndReader.reader;
+
+ assert_true(stream.locked, 'stream should start locked');
+
+ const promise = reader.closed.then(v => {
+ assert_equals(v, undefined, 'reader closed should fulfill with undefined');
+ assert_true(stream.locked, 'stream should remain locked');
+ });
+
+ reader.read();
+ reader.read();
+
+ return promise;
+
+ }, label +
+ ': draining the stream via read() should cause the reader closed promise to fulfill, but locked stays true');
+
+ promise_test(() => {
+
+ const streamAndReader = factory();
+ const stream = streamAndReader.stream;
+ const reader = streamAndReader.reader;
+
+ const promise = reader.closed.then(() => {
+ assert_true(stream.locked, 'the stream should start locked');
+ reader.releaseLock(); // Releasing the lock after reader closed should not throw.
+ assert_false(stream.locked, 'the stream should end unlocked');
+ });
+
+ reader.read();
+ reader.read();
+
+ return promise;
+
+ }, label + ': releasing the lock after the stream is closed should cause locked to become false');
+
+ promise_test(t => {
+
+ const reader = factory().reader;
+
+ reader.releaseLock();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, reader.read()),
+ promise_rejects_js(t, TypeError, reader.read()),
+ promise_rejects_js(t, TypeError, reader.read())
+ ]);
+
+ }, label + ': releasing the lock should cause further read() calls to reject with a TypeError');
+
+ promise_test(() => {
+
+ const streamAndReader = factory();
+ const stream = streamAndReader.stream;
+ const reader = streamAndReader.reader;
+
+ const readerClosed = reader.closed;
+
+ assert_equals(reader.closed, readerClosed, 'accessing reader.closed twice in succession gives the same value');
+
+ const promise = reader.read().then(() => {
+ assert_equals(reader.closed, readerClosed, 'reader.closed is the same after read() fulfills');
+
+ reader.releaseLock();
+
+ assert_equals(reader.closed, readerClosed, 'reader.closed is the same after releasing the lock');
+
+ const newReader = stream.getReader();
+ return newReader.read();
+ });
+
+ assert_equals(reader.closed, readerClosed, 'reader.closed is the same after calling read()');
+
+ return promise;
+
+ }, label + ': reader\'s closed property always returns the same promise');
+};
+
+self.templatedRSTeeCancel = (label, factory) => {
+ test(() => {}, `Running templatedRSTeeCancel with ${label}`);
+
+ promise_test(async () => {
+
+ const reason1 = new Error('We\'re wanted men.');
+ const reason2 = new Error('I have the death sentence on twelve systems.');
+
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ const rs = factory({
+ cancel(reason) {
+ assert_array_equals(reason, [reason1, reason2],
+ 'the cancel reason should be an array containing those from the branches');
+ resolve();
+ }
+ });
+
+ const [branch1, branch2] = rs.tee();
+ await Promise.all([
+ branch1.cancel(reason1),
+ branch2.cancel(reason2),
+ promise
+ ]);
+
+ }, `${label}: canceling both branches should aggregate the cancel reasons into an array`);
+
+ promise_test(async () => {
+
+ const reason1 = new Error('This little one\'s not worth the effort.');
+ const reason2 = new Error('Come, let me get you something.');
+
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ const rs = factory({
+ cancel(reason) {
+ assert_array_equals(reason, [reason1, reason2],
+ 'the cancel reason should be an array containing those from the branches');
+ resolve();
+ }
+ });
+
+ const [branch1, branch2] = rs.tee();
+ await Promise.all([
+ branch2.cancel(reason2),
+ branch1.cancel(reason1),
+ promise
+ ]);
+
+ }, `${label}: canceling both branches in reverse order should aggregate the cancel reasons into an array`);
+
+ promise_test(async t => {
+
+ const theError = { name: 'I\'ll be careful.' };
+ const rs = factory({
+ cancel() {
+ throw theError;
+ }
+ });
+
+ const [branch1, branch2] = rs.tee();
+ await Promise.all([
+ promise_rejects_exactly(t, theError, branch1.cancel()),
+ promise_rejects_exactly(t, theError, branch2.cancel())
+ ]);
+
+ }, `${label}: failing to cancel the original stream should cause cancel() to reject on branches`);
+
+ promise_test(async t => {
+
+ const theError = { name: 'You just watch yourself!' };
+ let controller;
+ const stream = factory({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const [branch1, branch2] = stream.tee();
+ controller.error(theError);
+
+ await Promise.all([
+ promise_rejects_exactly(t, theError, branch1.cancel()),
+ promise_rejects_exactly(t, theError, branch2.cancel())
+ ]);
+
+ }, `${label}: erroring a teed stream should properly handle canceled branches`);
+
+};
diff --git a/testing/web-platform/tests/streams/resources/rs-utils.js b/testing/web-platform/tests/streams/resources/rs-utils.js
new file mode 100644
index 0000000000..f1a014275a
--- /dev/null
+++ b/testing/web-platform/tests/streams/resources/rs-utils.js
@@ -0,0 +1,197 @@
+'use strict';
+(function () {
+
+ class RandomPushSource {
+ constructor(toPush) {
+ this.pushed = 0;
+ this.toPush = toPush;
+ this.started = false;
+ this.paused = false;
+ this.closed = false;
+
+ this._intervalHandle = null;
+ }
+
+ readStart() {
+ if (this.closed) {
+ return;
+ }
+
+ if (!this.started) {
+ this._intervalHandle = setInterval(writeChunk, 2);
+ this.started = true;
+ }
+
+ if (this.paused) {
+ this._intervalHandle = setInterval(writeChunk, 2);
+ this.paused = false;
+ }
+
+ const source = this;
+ function writeChunk() {
+ if (source.paused) {
+ return;
+ }
+
+ source.pushed++;
+
+ if (source.toPush > 0 && source.pushed > source.toPush) {
+ if (source._intervalHandle) {
+ clearInterval(source._intervalHandle);
+ source._intervalHandle = undefined;
+ }
+ source.closed = true;
+ source.onend();
+ } else {
+ source.ondata(randomChunk(128));
+ }
+ }
+ }
+
+ readStop() {
+ if (this.paused) {
+ return;
+ }
+
+ if (this.started) {
+ this.paused = true;
+ clearInterval(this._intervalHandle);
+ this._intervalHandle = undefined;
+ } else {
+ throw new Error('Can\'t pause reading an unstarted source.');
+ }
+ }
+ }
+
+ function randomChunk(size) {
+ let chunk = '';
+
+ for (let i = 0; i < size; ++i) {
+ // Add a random character from the basic printable ASCII set.
+ chunk += String.fromCharCode(Math.round(Math.random() * 84) + 32);
+ }
+
+ return chunk;
+ }
+
+ function readableStreamToArray(readable, reader) {
+ if (reader === undefined) {
+ reader = readable.getReader();
+ }
+
+ const chunks = [];
+
+ return pump();
+
+ function pump() {
+ return reader.read().then(result => {
+ if (result.done) {
+ return chunks;
+ }
+
+ chunks.push(result.value);
+ return pump();
+ });
+ }
+ }
+
+ class SequentialPullSource {
+ constructor(limit, options) {
+ const async = options && options.async;
+
+ this.current = 0;
+ this.limit = limit;
+ this.opened = false;
+ this.closed = false;
+
+ this._exec = f => f();
+ if (async) {
+ this._exec = f => step_timeout(f, 0);
+ }
+ }
+
+ open(cb) {
+ this._exec(() => {
+ this.opened = true;
+ cb();
+ });
+ }
+
+ read(cb) {
+ this._exec(() => {
+ if (++this.current <= this.limit) {
+ cb(null, false, this.current);
+ } else {
+ cb(null, true, null);
+ }
+ });
+ }
+
+ close(cb) {
+ this._exec(() => {
+ this.closed = true;
+ cb();
+ });
+ }
+ }
+
+ function sequentialReadableStream(limit, options) {
+ const sequentialSource = new SequentialPullSource(limit, options);
+
+ const stream = new ReadableStream({
+ start() {
+ return new Promise((resolve, reject) => {
+ sequentialSource.open(err => {
+ if (err) {
+ reject(err);
+ }
+ resolve();
+ });
+ });
+ },
+
+ pull(c) {
+ return new Promise((resolve, reject) => {
+ sequentialSource.read((err, done, chunk) => {
+ if (err) {
+ reject(err);
+ } else if (done) {
+ sequentialSource.close(err2 => {
+ if (err2) {
+ reject(err2);
+ }
+ c.close();
+ resolve();
+ });
+ } else {
+ c.enqueue(chunk);
+ resolve();
+ }
+ });
+ });
+ }
+ });
+
+ stream.source = sequentialSource;
+
+ return stream;
+ }
+
+ function transferArrayBufferView(view) {
+ const noopByteStream = new ReadableStream({
+ type: 'bytes',
+ pull(c) {
+ c.byobRequest.respond(c.byobRequest.view.byteLength);
+ c.close();
+ }
+ });
+ const reader = noopByteStream.getReader({ mode: 'byob' });
+ return reader.read(view).then((result) => result.value);
+ }
+
+ self.RandomPushSource = RandomPushSource;
+ self.readableStreamToArray = readableStreamToArray;
+ self.sequentialReadableStream = sequentialReadableStream;
+ self.transferArrayBufferView = transferArrayBufferView;
+
+}());
diff --git a/testing/web-platform/tests/streams/resources/test-utils.js b/testing/web-platform/tests/streams/resources/test-utils.js
new file mode 100644
index 0000000000..a38f78027b
--- /dev/null
+++ b/testing/web-platform/tests/streams/resources/test-utils.js
@@ -0,0 +1,27 @@
+'use strict';
+
+self.delay = ms => new Promise(resolve => step_timeout(resolve, ms));
+
+// For tests which verify that the implementation doesn't do something it shouldn't, it's better not to use a
+// timeout. Instead, assume that any reasonable implementation is going to finish work after 2 times around the event
+// loop, and use flushAsyncEvents().then(() => assert_array_equals(...));
+// Some tests include promise resolutions which may mean the test code takes a couple of event loop visits itself. So go
+// around an extra 2 times to avoid complicating those tests.
+self.flushAsyncEvents = () => delay(0).then(() => delay(0)).then(() => delay(0)).then(() => delay(0));
+
+self.assert_typed_array_equals = (actual, expected, message) => {
+ const prefix = message === undefined ? '' : `${message} `;
+ assert_equals(typeof actual, 'object', `${prefix}type is object`);
+ assert_equals(actual.constructor, expected.constructor, `${prefix}constructor`);
+ assert_equals(actual.byteOffset, expected.byteOffset, `${prefix}byteOffset`);
+ assert_equals(actual.byteLength, expected.byteLength, `${prefix}byteLength`);
+ assert_equals(actual.buffer.byteLength, expected.buffer.byteLength, `${prefix}buffer.byteLength`);
+ assert_array_equals([...actual], [...expected], `${prefix}contents`);
+ assert_array_equals([...new Uint8Array(actual.buffer)], [...new Uint8Array(expected.buffer)], `${prefix}buffer contents`);
+};
+
+self.makePromiseAndResolveFunc = () => {
+ let resolve;
+ const promise = new Promise(r => { resolve = r; });
+ return [promise, resolve];
+};