diff --git a/streams/byte-length-queuing-strategy.https.html b/streams/byte-length-queuing-strategy.https.html new file mode 100644 index 00000000000000..13f15acb9ffab6 --- /dev/null +++ b/streams/byte-length-queuing-strategy.https.html @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/streams/byte-length-queuing-strategy.js b/streams/byte-length-queuing-strategy.js new file mode 100644 index 00000000000000..d53b12bcb363c6 --- /dev/null +++ b/streams/byte-length-queuing-strategy.js @@ -0,0 +1,107 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('/resources/testharness.js'); +} + +test(() => { + + new ByteLengthQueuingStrategy({ highWaterMark: 4 }); + +}, 'Can construct a ByteLengthQueuingStrategy with a valid high water mark'); + +test(() => { + + for (let highWaterMark of [-Infinity, NaN, 'foo', {}, () => {}]) { + const strategy = new ByteLengthQueuingStrategy({ highWaterMark }); + assert_equals(strategy.highWaterMark, highWaterMark, `${highWaterMark} gets set correctly`); + } + +}, 'Can construct a ByteLengthQueuingStrategy with any value as its high water mark'); + +test(() => { + + const highWaterMark = 1; + const highWaterMarkObjectGetter = { + get highWaterMark() { return highWaterMark; } + }; + const error = new Error('wow!'); + const highWaterMarkObjectGetterThrowing = { + get highWaterMark() { throw error; } + }; + + assert_throws({ name: 'TypeError' }, () => new ByteLengthQueuingStrategy(), 'construction fails with undefined'); + assert_throws({ name: 'TypeError' }, () => new ByteLengthQueuingStrategy(null), 'construction fails with null'); + assert_throws({ name: 'Error' }, () => new ByteLengthQueuingStrategy(highWaterMarkObjectGetterThrowing), + 'construction fails with an object with a throwing highWaterMark getter'); + + // Should not fail: + new ByteLengthQueuingStrategy('potato'); + new ByteLengthQueuingStrategy({}); + new ByteLengthQueuingStrategy(highWaterMarkObjectGetter); + +}, 'ByteLengthQueuingStrategy constructor behaves as expected with strange arguments'); + +test(() => { + + const size = 1024; + const chunk = { byteLength: size }; + const chunkGetter = { + get byteLength() { return size; } + }; + const error = new Error('wow!'); + const chunkGetterThrowing = { + get byteLength() { throw error; } + }; + assert_throws({ name: 'TypeError' }, () => ByteLengthQueuingStrategy.prototype.size(), 'size fails with undefined'); + assert_throws({ name: 'TypeError' }, () => ByteLengthQueuingStrategy.prototype.size(null), 'size fails with null'); + assert_equals(ByteLengthQueuingStrategy.prototype.size('potato'), undefined, + 'size succeeds with undefined with a random non-object type'); + assert_equals(ByteLengthQueuingStrategy.prototype.size({}), undefined, + 'size succeeds with undefined with an object without hwm property'); + assert_equals(ByteLengthQueuingStrategy.prototype.size(chunk), size, + 'size succeeds with the right amount with an object with a hwm'); + assert_equals(ByteLengthQueuingStrategy.prototype.size(chunkGetter), size, + 'size succeeds with the right amount with an object with a hwm getter'); + assert_throws({ name: 'Error' }, () => ByteLengthQueuingStrategy.prototype.size(chunkGetterThrowing), + 'size fails with the error thrown by the getter'); + +}, 'ByteLengthQueuingStrategy size behaves as expected with strange arguments'); + +test(() => { + + const thisValue = null; + const returnValue = { 'returned from': 'byteLength getter' }; + const chunk = { + get byteLength() { return returnValue; } + }; + + assert_equals(ByteLengthQueuingStrategy.prototype.size.call(thisValue, chunk), returnValue); + +}, 'ByteLengthQueuingStrategy.prototype.size should work generically on its this and its arguments'); + +test(() => { + + const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 4 }); + + assert_object_equals(Object.getOwnPropertyDescriptor(strategy, 'highWaterMark'), + { value: 4, writable: true, enumerable: true, configurable: true }, + 'highWaterMark property should be a data property with the value passed the constructor'); + assert_equals(typeof strategy.size, 'function'); + +}, 'ByteLengthQueuingStrategy instances have the correct properties'); + +test(() => { + + const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 4 }); + assert_equals(strategy.highWaterMark, 4); + + strategy.highWaterMark = 10; + assert_equals(strategy.highWaterMark, 10); + + strategy.highWaterMark = 'banana'; + assert_equals(strategy.highWaterMark, 'banana'); + +}, 'ByteLengthQueuingStrategy\'s highWaterMark property can be set to anything'); + +done(); diff --git a/streams/count-queuing-strategy.https.html b/streams/count-queuing-strategy.https.html new file mode 100644 index 00000000000000..2afd1268865cc8 --- /dev/null +++ b/streams/count-queuing-strategy.https.html @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/streams/count-queuing-strategy.js b/streams/count-queuing-strategy.js new file mode 100644 index 00000000000000..02ca4c45843233 --- /dev/null +++ b/streams/count-queuing-strategy.js @@ -0,0 +1,106 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('/resources/testharness.js'); +} + +test(() => { + + new CountQueuingStrategy({ highWaterMark: 4 }); + +}, 'Can construct a CountQueuingStrategy with a valid high water mark'); + +test(() => { + + for (let highWaterMark of [-Infinity, NaN, 'foo', {}, () => {}]) { + const strategy = new CountQueuingStrategy({ highWaterMark }); + assert_equals(strategy.highWaterMark, highWaterMark, `${highWaterMark} gets set correctly`); + } + +}, 'Can construct a CountQueuingStrategy with any value as its high water mark'); + +test(() => { + + const highWaterMark = 1; + const highWaterMarkObjectGetter = { + get highWaterMark() { return highWaterMark; } + }; + const error = new Error('wow!'); + const highWaterMarkObjectGetterThrowing = { + get highWaterMark() { throw error; } + }; + + assert_throws({ name: 'TypeError' }, () => new CountQueuingStrategy(), 'construction fails with undefined'); + assert_throws({ name: 'TypeError' }, () => new CountQueuingStrategy(null), 'construction fails with null'); + assert_throws({ name: 'Error' }, () => new CountQueuingStrategy(highWaterMarkObjectGetterThrowing), + 'construction fails with an object with a throwing highWaterMark getter'); + + // Should not fail: + new CountQueuingStrategy('potato'); + new CountQueuingStrategy({}); + new CountQueuingStrategy(highWaterMarkObjectGetter); + +}, 'CountQueuingStrategy constructor behaves as expected with strange arguments'); + + +test(() => { + + const thisValue = null; + const chunk = { + get byteLength() { + throw new TypeError('shouldn\'t be called'); + } + }; + + assert_equals(CountQueuingStrategy.prototype.size.call(thisValue, chunk), 1); + +}, 'CountQueuingStrategy.prototype.size should work generically on its this and its arguments'); + +test(() => { + + const size = 1024; + const chunk = { byteLength: size }; + const chunkGetter = { + get byteLength() { return size; } + }; + const error = new Error('wow!'); + const chunkGetterThrowing = { + get byteLength() { throw error; } + }; + + assert_equals(CountQueuingStrategy.prototype.size(), 1, 'size returns 1 with undefined'); + assert_equals(CountQueuingStrategy.prototype.size(null), 1, 'size returns 1 with null'); + assert_equals(CountQueuingStrategy.prototype.size('potato'), 1, 'size returns 1 with non-object type'); + assert_equals(CountQueuingStrategy.prototype.size({}), 1, 'size returns 1 with empty object'); + assert_equals(CountQueuingStrategy.prototype.size(chunk), 1, 'size returns 1 with a chunk'); + assert_equals(CountQueuingStrategy.prototype.size(chunkGetter), 1, 'size returns 1 with chunk getter'); + assert_equals(CountQueuingStrategy.prototype.size(chunkGetterThrowing), 1, + 'size returns 1 with chunk getter that throws'); + +}, 'CountQueuingStrategy size behaves as expected with strange arguments'); + +test(() => { + + const strategy = new CountQueuingStrategy({ highWaterMark: 4 }); + + assert_object_equals(Object.getOwnPropertyDescriptor(strategy, 'highWaterMark'), + { value: 4, writable: true, enumerable: true, configurable: true }, + 'highWaterMark property should be a data property with the value passed the constructor'); + assert_equals(typeof strategy.size, 'function'); + +}, 'CountQueuingStrategy instances have the correct properties'); + +test(() => { + + const strategy = new CountQueuingStrategy({ highWaterMark: 4 }); + assert_equals(strategy.highWaterMark, 4); + + strategy.highWaterMark = 10; + assert_equals(strategy.highWaterMark, 10); + + strategy.highWaterMark = 'banana'; + assert_equals(strategy.highWaterMark, 'banana'); + +}, 'CountQueuingStrategy\'s highWaterMark property can be set to anything'); + +done(); diff --git a/streams/readable-streams/bad-strategies.https.html b/streams/readable-streams/bad-strategies.https.html new file mode 100644 index 00000000000000..4fab9313239b77 --- /dev/null +++ b/streams/readable-streams/bad-strategies.https.html @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/streams/readable-streams/bad-strategies.js b/streams/readable-streams/bad-strategies.js new file mode 100644 index 00000000000000..8fdc83c49102a7 --- /dev/null +++ b/streams/readable-streams/bad-strategies.js @@ -0,0 +1,176 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('/resources/testharness.js'); +} + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws(theError, () => { + new ReadableStream({}, { + get size() { + throw theError; + }, + highWaterMark: 5 + }); + }, 'construction should re-throw the error'); + +}, 'Readable stream: throwing strategy.size getter'); + +test(() => { + + const theError = new Error('a unique string'); + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + { + size() { + controller.error(theError); + throw theError; + }, + highWaterMark: 5 + } + ); + + assert_throws(theError, () => { + controller.enqueue('a'); + }, 'enqueue should re-throw the error'); + +}, 'Readable stream: strategy.size errors the stream and then throws'); + +test(() => { + + const theError = new Error('a unique string'); + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + { + size() { + controller.error(theError); + return Infinity; + }, + highWaterMark: 5 + } + ); + + try { + controller.enqueue('a'); + } catch (error) { + assert_equals(error.name, 'RangeError', 'enqueue should throw a RangeError'); + } + +}, 'Readable stream: strategy.size errors the stream and then returns Infinity'); + +promise_test(() => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream( + { + start(c) { + assert_throws(theError, () => c.enqueue('a'), 'enqueue should throw the error'); + } + }, + { + size() { + throw theError; + }, + highWaterMark: 5 + } + ); + + return rs.getReader().closed.catch(e => { + assert_equals(e, theError, 'closed should reject with the error'); + }); + +}, 'Readable stream: throwing strategy.size method'); + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws(theError, () => { + new ReadableStream({}, { + size() { + return 1; + }, + get highWaterMark() { + throw theError; + } + }); + }, 'construction should re-throw the error'); + +}, 'Readable stream: throwing strategy.highWaterMark getter'); + +test(() => { + + for (let highWaterMark of [-1, -Infinity]) { + assert_throws(new RangeError(), () => { + new ReadableStream({}, { + size() { + return 1; + }, + highWaterMark + }); + }, 'construction should throw a RangeError for ' + highWaterMark); + } + + for (let highWaterMark of [NaN, 'foo', {}]) { + assert_throws(new TypeError(), () => { + new ReadableStream({}, { + size() { + return 1; + }, + highWaterMark + }); + }, 'construction should throw a TypeError for ' + highWaterMark); + } + +}, 'Readable stream: invalid strategy.highWaterMark'); + +promise_test(() => { + + const promises = []; + for (let size of [NaN, -Infinity, Infinity, -1]) { + let theError; + const rs = new ReadableStream( + { + start(c) { + try { + c.enqueue('hi'); + assert_unreached('enqueue didn\'t throw'); + } catch (error) { + assert_equals(error.name, 'RangeError', 'enqueue should throw a RangeError for ' + size); + theError = error; + } + } + }, + { + size() { + return size; + }, + highWaterMark: 5 + } + ); + + promises.push(rs.getReader().closed.catch(e => { + assert_equals(e, theError, 'closed should reject with the error for ' + size); + })); + } + + return Promise.all(promises); + +}, 'Readable stream: invalid strategy.size return value'); + +done(); diff --git a/streams/readable-streams/bad-underlying-sources.https.html b/streams/readable-streams/bad-underlying-sources.https.html new file mode 100644 index 00000000000000..ecb11feab12c47 --- /dev/null +++ b/streams/readable-streams/bad-underlying-sources.https.html @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/streams/readable-streams/bad-underlying-sources.js b/streams/readable-streams/bad-underlying-sources.js new file mode 100644 index 00000000000000..f2fd0fdc9299d5 --- /dev/null +++ b/streams/readable-streams/bad-underlying-sources.js @@ -0,0 +1,383 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('/resources/testharness.js'); +} + + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws(theError, () => { + new ReadableStream({ + get start() { + throw theError; + } + }); + }, 'constructing the stream should re-throw the error'); + +}, 'Underlying source start: throwing getter'); + + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws(theError, () => { + new ReadableStream({ + start() { + throw theError; + } + }); + }, 'constructing the stream should re-throw the error'); + +}, 'Underlying source start: throwing method'); + + +promise_test(t => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream({ + get pull() { + throw theError; + } + }); + + return promise_rejects(t, theError, rs.getReader().closed); + +}, 'Underlying source: throwing pull getter (initial pull)'); + + +promise_test(t => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream({ + pull() { + throw theError; + } + }); + + return promise_rejects(t, theError, rs.getReader().closed); + +}, 'Underlying source: throwing pull method (initial pull)'); + + +promise_test(t => { + + const theError = new Error('a unique string'); + + let counter = 0; + const rs = new ReadableStream({ + get pull() { + ++counter; + if (counter === 1) { + return c => c.enqueue('a'); + } + + throw theError; + } + }); + const reader = rs.getReader(); + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'the chunk read should be correct'); + }), + promise_rejects(t, theError, reader.closed) + ]); + +}, 'Underlying source pull: throwing getter (second pull)'); + + +promise_test(t => { + + const theError = new Error('a unique string'); + + let counter = 0; + const rs = new ReadableStream({ + pull(c) { + ++counter; + if (counter === 1) { + c.enqueue('a'); + return; + } + + throw theError; + } + }); + const reader = rs.getReader(); + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'the chunk read should be correct'); + }), + promise_rejects(t, theError, reader.closed) + ]); + +}, 'Underlying source pull: throwing method (second pull)'); + +promise_test(t => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream({ + get cancel() { + throw theError; + } + }); + + return promise_rejects(t, theError, rs.cancel()); + +}, 'Underlying source cancel: throwing getter'); + +promise_test(t => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream({ + cancel() { + throw theError; + } + }); + + return promise_rejects(t, theError, rs.cancel()); + +}, 'Underlying source cancel: throwing method'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + rs.cancel(); + controller.enqueue('a'); // Calling enqueue after canceling should not throw anything. + + return rs.getReader().closed; + +}, 'Underlying source: calling enqueue on an empty canceled stream should not throw'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + controller = c; + } + }); + + rs.cancel(); + controller.enqueue('c'); // Calling enqueue after canceling should not throw anything. + + return rs.getReader().closed; + +}, 'Underlying source: calling enqueue on a non-empty canceled stream should not throw'); + +promise_test(() => { + + return new ReadableStream({ + start(c) { + c.close(); + assert_throws(new TypeError(), () => c.enqueue('a'), 'call to enqueue should throw a TypeError'); + } + }).getReader().closed; + +}, 'Underlying source: calling enqueue on a closed stream should throw'); + +promise_test(t => { + + const theError = new Error('boo'); + const closed = new ReadableStream({ + start(c) { + c.error(theError); + assert_throws(theError, () => c.enqueue('a'), 'call to enqueue should throw the error'); + } + }).getReader().closed; + + return promise_rejects(t, theError, closed); + +}, 'Underlying source: calling enqueue on an errored stream should throw'); + +promise_test(() => { + + return new ReadableStream({ + start(c) { + c.close(); + assert_throws(new TypeError(), () => c.close(), 'second call to close should throw a TypeError'); + } + }).getReader().closed; + +}, 'Underlying source: calling close twice on an empty stream should throw the second time'); + +promise_test(() => { + + let startCalled = false; + let readCalled = false; + const reader = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.close(); + assert_throws(new TypeError(), () => c.close(), 'second call to close should throw a TypeError'); + startCalled = true; + } + }).getReader(); + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'read() should read the enqueued chunk'); + readCalled = true; + }), + reader.closed.then(() => { + assert_true(startCalled); + assert_true(readCalled); + }) + ]); + +}, 'Underlying source: calling close twice on a non-empty stream should throw the second time'); + +promise_test(() => { + + let controller; + let startCalled = false; + const rs = new ReadableStream({ + start(c) { + controller = c; + startCalled = true; + } + }); + + rs.cancel(); + controller.close(); // Calling close after canceling should not throw anything. + + return rs.getReader().closed.then(() => { + assert_true(startCalled); + }); + +}, 'Underlying source: calling close on an empty canceled stream should not throw'); + +promise_test(() => { + + let controller; + let startCalled = false; + const rs = new ReadableStream({ + start(c) { + controller = c; + c.enqueue('a'); + startCalled = true; + } + }); + + rs.cancel(); + controller.close(); // Calling close after canceling should not throw anything. + + return rs.getReader().closed.then(() => { + assert_true(startCalled); + }); + +}, 'Underlying source: calling close on a non-empty canceled stream should not throw'); + +promise_test(() => { + + const theError = new Error('boo'); + let startCalled = false; + + const closed = new ReadableStream({ + start(c) { + c.error(theError); + assert_throws(new TypeError(), () => c.close(), 'call to close should throw a TypeError'); + startCalled = true; + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, theError, 'closed should reject with the error'); + }); + +}, 'Underlying source: calling close after error should throw'); + +promise_test(() => { + + const theError = new Error('boo'); + let startCalled = false; + + const closed = new ReadableStream({ + start(c) { + c.error(theError); + assert_throws(new TypeError(), () => c.error(), 'second call to error should throw a TypeError'); + startCalled = true; + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, theError, 'closed should reject with the error'); + }); + +}, 'Underlying source: calling error twice should throw the second time'); + +promise_test(() => { + + let startCalled = false; + + const closed = new ReadableStream({ + start(c) { + c.close(); + assert_throws(new TypeError(), () => c.error(), 'second call to error should throw a TypeError'); + startCalled = true; + } + }).getReader().closed; + + return closed.then(() => assert_true(startCalled)); + +}, 'Underlying source: calling error after close should throw'); + +promise_test(() => { + + let startCalled = false; + const firstError = new Error('1'); + const secondError = new Error('2'); + + const closed = new ReadableStream({ + start(c) { + c.error(firstError); + startCalled = true; + return Promise.reject(secondError); + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, firstError, 'closed should reject with the first error'); + }); + +}, 'Underlying source: calling error and returning a rejected promise from start should cause the stream to error ' + + 'with the first error'); + +promise_test(() => { + + let startCalled = false; + const firstError = new Error('1'); + const secondError = new Error('2'); + + const closed = new ReadableStream({ + pull(c) { + c.error(firstError); + startCalled = true; + return Promise.reject(secondError); + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, firstError, 'closed should reject with the first error'); + }); + +}, 'Underlying source: calling error and returning a rejected promise from pull should cause the stream to error ' + + 'with the first error'); + +done(); diff --git a/streams/readable-streams/brand-checks.https.html b/streams/readable-streams/brand-checks.https.html new file mode 100644 index 00000000000000..9dfed123d76db8 --- /dev/null +++ b/streams/readable-streams/brand-checks.https.html @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/streams/readable-streams/brand-checks.js b/streams/readable-streams/brand-checks.js new file mode 100644 index 00000000000000..32553cf92f52fd --- /dev/null +++ b/streams/readable-streams/brand-checks.js @@ -0,0 +1,151 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('../resources/test-utils.js'); + self.importScripts('/resources/testharness.js'); +} + +let ReadableStreamReader; +let ReadableStreamController; + +test(() => { + + // It's not exposed globally, but we test a few of its properties here. + ReadableStreamReader = (new ReadableStream()).getReader().constructor; + +}, 'Can get the ReadableStreamReader constructor indirectly'); + +test(() => { + + // It's not exposed globally, but we test a few of its properties here. + new ReadableStream({ + start(c) { + ReadableStreamController = c.constructor; + } + }); + +}, 'Can get the ReadableStreamController constructor indirectly'); + +function fakeReadableStream() { + return { + cancel() { return Promise.resolve(); }, + getReader() { return new ReadableStreamReader(new ReadableStream()); }, + pipeThrough(obj) { return obj.readable; }, + pipeTo() { return Promise.resolve(); }, + tee() { return [realReadableStream(), realReadableStream()]; } + }; +} + +function realReadableStream() { + return new ReadableStream(); +} + +function fakeReadableStreamReader() { + return { + get closed() { return Promise.resolve(); }, + cancel() { return Promise.resolve(); }, + read() { return Promise.resolve({ value: undefined, done: true }); }, + releaseLock() { return; } + }; +} + +function fakeReadableStreamController() { + return { + close() { }, + enqueue() { }, + error() { } + }; +} + +promise_test(t => { + + return methodRejects(t, ReadableStream.prototype, 'cancel', fakeReadableStream()); + +}, 'ReadableStream.prototype.cancel enforces a brand check'); + +test(() => { + + methodThrows(ReadableStream.prototype, 'getReader', fakeReadableStream()); + +}, 'ReadableStream.prototype.getReader enforces a brand check'); + +test(() => { + + methodThrows(ReadableStream.prototype, 'tee', fakeReadableStream()); + +}, 'ReadableStream.prototype.tee enforces a brand check'); + +test(() => { + + assert_throws(new TypeError(), () => new ReadableStreamReader(fakeReadableStream()), + 'Constructing a ReadableStreamReader should throw'); + +}, 'ReadableStreamReader enforces a brand check on its argument'); + +promise_test(t => { + + return Promise.all([ + getterRejects(t, ReadableStreamReader.prototype, 'closed', fakeReadableStreamReader()), + getterRejects(t, ReadableStreamReader.prototype, 'closed', realReadableStream()) + ]); + +}, 'ReadableStreamReader.prototype.closed enforces a brand check'); + +promise_test(t => { + + return Promise.all([ + methodRejects(t, ReadableStreamReader.prototype, 'cancel', fakeReadableStreamReader()), + methodRejects(t, ReadableStreamReader.prototype, 'cancel', realReadableStream()) + ]); + +}, 'ReadableStreamReader.prototype.cancel enforces a brand check'); + +promise_test(t => { + + return Promise.all([ + methodRejects(t, ReadableStreamReader.prototype, 'read', fakeReadableStreamReader()), + methodRejects(t, ReadableStreamReader.prototype, 'read', realReadableStream()) + ]); + +}, 'ReadableStreamReader.prototype.read enforces a brand check'); + +test(() => { + + methodThrows(ReadableStreamReader.prototype, 'releaseLock', fakeReadableStreamReader()); + methodThrows(ReadableStreamReader.prototype, 'releaseLock', realReadableStream()); + +}, 'ReadableStreamReader.prototype.releaseLock enforces a brand check'); + +test(() => { + + assert_throws(new TypeError(), () => new ReadableStreamController(fakeReadableStream()), + 'Constructing a ReadableStreamController should throw'); + +}, 'ReadableStreamController enforces a brand check on its argument'); + +test(() => { + + assert_throws(new TypeError(), () => new ReadableStreamController(realReadableStream()), + 'Constructing a ReadableStreamController should throw'); + +}, 'ReadableStreamController can\'t be given a fully-constructed ReadableStream'); + +test(() => { + + methodThrows(ReadableStreamController.prototype, 'close', fakeReadableStreamController()); + +}, 'ReadableStreamController.prototype.close enforces a brand check'); + +test(() => { + + methodThrows(ReadableStreamController.prototype, 'enqueue', fakeReadableStreamController()); + +}, 'ReadableStreamController.prototype.enqueue enforces a brand check'); + +test(() => { + + methodThrows(ReadableStreamController.prototype, 'error', fakeReadableStreamController()); + +}, 'ReadableStreamController.prototype.error enforces a brand check'); + +done(); diff --git a/streams/readable-streams/cancel.https.html b/streams/readable-streams/cancel.https.html new file mode 100644 index 00000000000000..f767429f170292 --- /dev/null +++ b/streams/readable-streams/cancel.https.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/streams/readable-streams/cancel.js b/streams/readable-streams/cancel.js new file mode 100644 index 00000000000000..f5014c2c94d271 --- /dev/null +++ b/streams/readable-streams/cancel.js @@ -0,0 +1,242 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('../resources/test-utils.js'); + self.importScripts('../resources/rs-utils.js'); + self.importScripts('/resources/testharness.js'); +} + +promise_test(() => { + + const randomSource = new RandomPushSource(); + + let cancellationFinished = false; + const rs = new ReadableStream({ + start(c) { + randomSource.ondata = c.enqueue.bind(c); + randomSource.onend = c.close.bind(c); + randomSource.onerror = c.error.bind(c); + }, + + pull() { + randomSource.readStart(); + }, + + cancel() { + randomSource.readStop(); + randomSource.onend(); + + return new Promise(resolve => { + setTimeout(() => { + cancellationFinished = true; + resolve(); + }, 1); + }); + } + }); + + const reader = rs.getReader(); + + // We call delay multiple times to avoid cancelling too early for the + // source to enqueue at least one chunk. + const cancel = delay(5).then(() => delay(5)).then(() => delay(5)).then(() => { + let cancelPromise = reader.cancel(); + assert_false(cancellationFinished, 'cancellation in source should happen later'); + return cancelPromise; + }) + + return readableStreamToArray(rs, reader).then(chunks => { + assert_greater_than(chunks.length, 0, 'at least one chunk should be read'); + for (let i = 0; i < chunks.length; i++) { + assert_equals(chunks[i].length, 128, 'chunk ' + i + ' should have 128 bytes'); + } + return cancel; + }).then(() => { + assert_true(cancellationFinished, 'it returns a promise that is fulfilled when the cancellation finishes'); + }); + +}, 'ReadableStream cancellation: integration test on an infinite stream derived from a random push source'); + +test(() => { + + let recordedReason; + const rs = new ReadableStream({ + cancel(reason) { + recordedReason = reason; + } + }); + + const passedReason = new Error('Sorry, it just wasn\'t meant to be.'); + rs.cancel(passedReason); + + assert_equals(recordedReason, passedReason, + 'the error passed to the underlying source\'s cancel method should equal the one passed to the stream\'s cancel'); + +}, 'ReadableStream cancellation: cancel(reason) should pass through the given reason to the underlying source'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.close(); + }, + cancel() { + assert_unreached('underlying source cancel() should not have been called'); + } + }); + + const reader = rs.getReader(); + + return rs.cancel().then(() => { + assert_unreached('cancel() should be rejected'); + }, e => { + assert_equals(e.name, 'TypeError', 'cancel() should be rejected with a TypeError'); + }).then(() => { + return reader.read(); + }).then(result => { + assert_object_equals(result, { value: 'a', done: false }, 'read() should still work after the attempted cancel'); + return reader.closed; + }); + +}, 'ReadableStream cancellation: cancel() on a locked stream should fail and not call the underlying source cancel'); + +promise_test(() => { + + let cancelReceived = false; + const cancelReason = new Error('I am tired of this stream, I prefer to cancel it'); + const rs = new ReadableStream({ + cancel(reason) { + cancelReceived = true; + assert_equals(reason, cancelReason, 'cancellation reason given to the underlying source should be equal to the one passed'); + } + }); + + return rs.cancel(cancelReason).then(() => { + assert_true(cancelReceived); + }); + +}, 'ReadableStream cancellation: should fulfill promise when cancel callback went fine'); + +promise_test(() => { + + const rs = new ReadableStream({ + cancel() { + return 'Hello'; + } + }); + + return rs.cancel().then(v => { + assert_equals(v, undefined, 'cancel() return value should be fulfilled with undefined'); + }); + +}, 'ReadableStream cancellation: returning a value from the underlying source\'s cancel should not affect the fulfillment value of the promise returned by the stream\'s cancel'); + +promise_test(() => { + + const thrownError = new Error('test'); + let cancelCalled = false; + + const rs = new ReadableStream({ + cancel() { + cancelCalled = true; + throw thrownError; + } + }); + + return rs.cancel('test').then(() => { + assert_unreached('cancel should reject'); + }, e => { + assert_true(cancelCalled); + assert_equals(e, thrownError); + }); + +}, 'ReadableStream cancellation: should reject promise when cancel callback raises an exception'); + +promise_test(() => { + + const cancelReason = new Error('test'); + + const rs = new ReadableStream({ + cancel(error) { + assert_equals(error, cancelReason); + return delay(1); + } + }); + + return rs.cancel(cancelReason); + +}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (1)'); + +promise_test(() => { + + let resolveSourceCancelPromise; + let sourceCancelPromiseHasFulfilled = false; + + const rs = new ReadableStream({ + cancel() { + const sourceCancelPromise = new Promise(resolve => resolveSourceCancelPromise = resolve); + + sourceCancelPromise.then(() => { + sourceCancelPromiseHasFulfilled = true; + }); + + return sourceCancelPromise; + } + }); + + setTimeout(() => resolveSourceCancelPromise('Hello'), 1); + + return rs.cancel().then(value => { + assert_true(sourceCancelPromiseHasFulfilled, 'cancel() return value should be fulfilled only after the promise returned by the underlying source\'s cancel'); + assert_equals(value, undefined, 'cancel() return value should be fulfilled with undefined'); + }); + +}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (2)'); + +promise_test(() => { + + let rejectSourceCancelPromise; + let sourceCancelPromiseHasRejected = false; + + const rs = new ReadableStream({ + cancel() { + const sourceCancelPromise = new Promise((resolve, reject) => rejectSourceCancelPromise = reject); + + sourceCancelPromise.catch(() => { + sourceCancelPromiseHasRejected = true; + }); + + return sourceCancelPromise; + } + }); + + const errorInCancel = new Error('Sorry, it just wasn\'t meant to be.'); + + setTimeout(() => rejectSourceCancelPromise(errorInCancel), 1); + + return rs.cancel().then(() => { + assert_unreached('cancel() return value should be rejected'); + }, r => { + assert_true(sourceCancelPromiseHasRejected, 'cancel() return value should be rejected only after the promise returned by the underlying source\'s cancel'); + assert_equals(r, errorInCancel, 'cancel() return value should be rejected with the underlying source\'s rejection reason'); + }); + +}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should reject when that one does'); + +promise_test(() => { + + const rs = new ReadableStream({ + start() { + return new Promise(() => {}); + }, + pull() { + assert_unreached('pull should not have been called'); + } + }); + + return Promise.all([rs.cancel(), rs.getReader().closed]); + +}, 'ReadableStream cancellation: cancelling before start finishes should prevent pull() from being called'); + +done(); diff --git a/streams/readable-streams/count-queuing-strategy-integration.https.html b/streams/readable-streams/count-queuing-strategy-integration.https.html new file mode 100644 index 00000000000000..bc38166c303588 --- /dev/null +++ b/streams/readable-streams/count-queuing-strategy-integration.https.html @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/streams/readable-streams/count-queuing-strategy-integration.js b/streams/readable-streams/count-queuing-strategy-integration.js new file mode 100644 index 00000000000000..fb9a1aa2ba631e --- /dev/null +++ b/streams/readable-streams/count-queuing-strategy-integration.js @@ -0,0 +1,213 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('/resources/testharness.js'); +} + +test(() => { + + new ReadableStream({}, new CountQueuingStrategy({ highWaterMark: 4 })); + +}, 'Can construct a readable stream with a valid CountQueuingStrategy'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + new CountQueuingStrategy({ highWaterMark: 0 }) + ); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 0, '0 reads, 0 enqueues: desiredSize should be 0'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, -1, '0 reads, 1 enqueue: desiredSize should be -1'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, -2, '0 reads, 2 enqueues: desiredSize should be -2'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, -3, '0 reads, 3 enqueues: desiredSize should be -3'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, -4, '0 reads, 4 enqueues: desiredSize should be -4'); + + return reader.read() + .then(result => { + assert_object_equals(result, { value: 'a', done: false }, + '1st read gives back the 1st chunk enqueued (queue now contains 3 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'b', done: false }, + '2nd read gives back the 2nd chunk enqueued (queue now contains 2 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'c', done: false }, + '3rd read gives back the 3rd chunk enqueued (queue now contains 1 chunk)'); + + assert_equals(controller.desiredSize, -1, '3 reads, 4 enqueues: desiredSize should be -1'); + controller.enqueue('e'); + assert_equals(controller.desiredSize, -2, '3 reads, 5 enqueues: desiredSize should be -2'); + + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'd', done: false }, + '4th read gives back the 4th chunk enqueued (queue now contains 1 chunks)'); + return reader.read(); + + }).then(result => { + assert_object_equals(result, { value: 'e', done: false }, + '5th read gives back the 5th chunk enqueued (queue now contains 0 chunks)'); + + assert_equals(controller.desiredSize, 0, '5 reads, 5 enqueues: desiredSize should be 0'); + controller.enqueue('f'); + assert_equals(controller.desiredSize, -1, '5 reads, 6 enqueues: desiredSize should be -1'); + controller.enqueue('g'); + assert_equals(controller.desiredSize, -2, '5 reads, 7 enqueues: desiredSize should be -2'); + }); + +}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 0)'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + new CountQueuingStrategy({ highWaterMark: 1 }) + ); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 1, '0 reads, 0 enqueues: desiredSize should be 1'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, 0, '0 reads, 1 enqueue: desiredSize should be 0'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, -1, '0 reads, 2 enqueues: desiredSize should be -1'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, -2, '0 reads, 3 enqueues: desiredSize should be -2'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, -3, '0 reads, 4 enqueues: desiredSize should be -3'); + + return reader.read() + .then(result => { + assert_object_equals(result, { value: 'a', done: false }, + '1st read gives back the 1st chunk enqueued (queue now contains 3 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'b', done: false }, + '2nd read gives back the 2nd chunk enqueued (queue now contains 2 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'c', done: false }, + '3rd read gives back the 3rd chunk enqueued (queue now contains 1 chunk)'); + + assert_equals(controller.desiredSize, 0, '3 reads, 4 enqueues: desiredSize should be 0'); + controller.enqueue('e'); + assert_equals(controller.desiredSize, -1, '3 reads, 5 enqueues: desiredSize should be -1'); + + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'd', done: false }, + '4th read gives back the 4th chunk enqueued (queue now contains 1 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'e', done: false }, + '5th read gives back the 5th chunk enqueued (queue now contains 0 chunks)'); + + assert_equals(controller.desiredSize, 1, '5 reads, 5 enqueues: desiredSize should be 1'); + controller.enqueue('f'); + assert_equals(controller.desiredSize, 0, '5 reads, 6 enqueues: desiredSize should be 0'); + controller.enqueue('g'); + assert_equals(controller.desiredSize, -1, '5 reads, 7 enqueues: desiredSize should be -1'); + }); + +}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 1)'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + new CountQueuingStrategy({ highWaterMark: 4 }) + ); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 4, '0 reads, 0 enqueues: desiredSize should be 4'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, 3, '0 reads, 1 enqueue: desiredSize should be 3'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, 2, '0 reads, 2 enqueues: desiredSize should be 2'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, 1, '0 reads, 3 enqueues: desiredSize should be 1'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, 0, '0 reads, 4 enqueues: desiredSize should be 0'); + controller.enqueue('e'); + assert_equals(controller.desiredSize, -1, '0 reads, 5 enqueues: desiredSize should be -1'); + controller.enqueue('f'); + assert_equals(controller.desiredSize, -2, '0 reads, 6 enqueues: desiredSize should be -2'); + + + reader.read() + .then(result => { + assert_object_equals(result, { value: 'a', done: false }, + '1st read gives back the 1st chunk enqueued (queue now contains 5 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'b', done: false }, + '2nd read gives back the 2nd chunk enqueued (queue now contains 4 chunks)'); + + assert_equals(controller.desiredSize, 0, '2 reads, 6 enqueues: desiredSize should be 0'); + controller.enqueue('g'); + assert_equals(controller.desiredSize, -1, '2 reads, 7 enqueues: desiredSize should be -1'); + + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'c', done: false }, + '3rd read gives back the 3rd chunk enqueued (queue now contains 4 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'd', done: false }, + '4th read gives back the 4th chunk enqueued (queue now contains 3 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'e', done: false }, + '5th read gives back the 5th chunk enqueued (queue now contains 2 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'f', done: false }, + '6th read gives back the 6th chunk enqueued (queue now contains 0 chunks)'); + + assert_equals(controller.desiredSize, 3, '6 reads, 7 enqueues: desiredSize should be 3'); + controller.enqueue('h'); + assert_equals(controller.desiredSize, 2, '6 reads, 8 enqueues: desiredSize should be 2'); + controller.enqueue('i'); + assert_equals(controller.desiredSize, 1, '6 reads, 9 enqueues: desiredSize should be 1'); + controller.enqueue('j'); + assert_equals(controller.desiredSize, 0, '6 reads, 10 enqueues: desiredSize should be 0'); + controller.enqueue('k'); + assert_equals(controller.desiredSize, -1, '6 reads, 11 enqueues: desiredSize should be -1'); + }); + +}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 4)'); + +done(); diff --git a/streams/readable-streams/garbage-collection.https.html b/streams/readable-streams/garbage-collection.https.html new file mode 100644 index 00000000000000..a38ed20534c206 --- /dev/null +++ b/streams/readable-streams/garbage-collection.https.html @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/streams/readable-streams/garbage-collection.js b/streams/readable-streams/garbage-collection.js new file mode 100644 index 00000000000000..fb00c946bbb52b --- /dev/null +++ b/streams/readable-streams/garbage-collection.js @@ -0,0 +1,75 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('../resources/test-utils.js'); + self.importScripts('/resources/testharness.js'); +} + +promise_test(() => { + + let controller; + new ReadableStream({ + start(c) { + controller = c; + } + }); + + garbageCollect(); + + return delay(50).then(() => { + controller.close(); + assert_throws(new TypeError(), () => controller.close(), 'close should throw a TypeError the second time'); + assert_throws(new TypeError(), () => controller.error(), 'error should throw a TypeError on a closed stream'); + }); + +}, 'ReadableStreamController methods should continue working properly when scripts lose their reference to the ' + + 'readable stream'); + +promise_test(() => { + + let controller; + + const closedPromise = new ReadableStream({ + start(c) { + controller = c; + } + }).getReader().closed; + + garbageCollect(); + + return delay(50).then(() => controller.close()).then(() => closedPromise); + +}, 'ReadableStream closed promise should fulfill even if the stream and reader JS references are lost'); + +promise_test(t => { + + const theError = new Error('boo'); + let controller; + + const closedPromise = new ReadableStream({ + start(c) { + controller = c; + } + }).getReader().closed; + + garbageCollect(); + + return delay(50).then(() => controller.error(theError)) + .then(() => promise_rejects(t, theError, closedPromise)); + +}, 'ReadableStream closed promise should reject even if stream and reader JS references are lost'); + +promise_test(() => { + + const rs = new ReadableStream({}); + + rs.getReader(); + + garbageCollect(); + + return delay(50).then(() => assert_throws(new TypeError(), () => rs.getReader(), + 'old reader should still be locking the stream even after garbage collection')); + +}, 'Garbage-collecting a ReadableStreamReader should not unlock its stream'); + +done(); diff --git a/streams/readable-streams/general.https.html b/streams/readable-streams/general.https.html new file mode 100644 index 00000000000000..f5241ffe8f072f --- /dev/null +++ b/streams/readable-streams/general.https.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/streams/readable-streams/general.js b/streams/readable-streams/general.js new file mode 100644 index 00000000000000..fe81bd54d3f520 --- /dev/null +++ b/streams/readable-streams/general.js @@ -0,0 +1,831 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('../resources/test-utils.js'); + self.importScripts('../resources/rs-utils.js'); + self.importScripts('/resources/testharness.js'); +} + +test(() => { + + new ReadableStream(); // ReadableStream constructed with no parameters + new ReadableStream({ }); // ReadableStream constructed with an empty object as parameter + new ReadableStream(undefined); // ReadableStream constructed with undefined as parameter + + let x; + new ReadableStream(x); // ReadableStream constructed with an undefined variable as parameter + +}, 'ReadableStream can be constructed with no errors'); + +test(() => { + + assert_throws(new TypeError(), () => new ReadableStream(null), 'constructor should throw when the source is null'); + +}, 'ReadableStream can\'t be constructed with garbage'); + +test(() => { + + const methods = ['cancel', 'constructor', 'getReader', 'pipeThrough', 'pipeTo', 'tee']; + const properties = methods.concat(['locked']).sort(); + + const rs = new ReadableStream(); + const proto = Object.getPrototypeOf(rs); + + assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties, 'should have all the correct methods'); + + for (let m of methods) { + const propDesc = Object.getOwnPropertyDescriptor(proto, m); + assert_false(propDesc.enumerable, 'method should be non-enumerable'); + assert_true(propDesc.configurable, 'method should be configurable'); + assert_true(propDesc.writable, 'method should be writable'); + assert_equals(typeof rs[m], 'function', 'method should be a function'); + } + + const lockedPropDesc = Object.getOwnPropertyDescriptor(proto, 'locked'); + assert_false(lockedPropDesc.enumerable, 'locked should be non-enumerable'); + assert_equals(lockedPropDesc.writable, undefined, 'locked should not be a data property'); + assert_equals(typeof lockedPropDesc.get, 'function', 'locked should have a getter'); + assert_equals(lockedPropDesc.set, undefined, 'locked should not have a setter'); + assert_true(lockedPropDesc.configurable, 'locked should be configurable'); + + assert_equals(rs.cancel.length, 1, 'cancel should have 1 parameter'); + assert_equals(rs.constructor.length, 0, 'constructor should have no parameters'); + assert_equals(rs.getReader.length, 0, 'getReader should have no parameters'); + assert_equals(rs.pipeThrough.length, 2, 'pipeThrough should have 2 parameters'); + assert_equals(rs.pipeTo.length, 1, 'pipeTo should have 1 parameter'); + assert_equals(rs.tee.length, 0, 'tee should have no parameters'); + +}, 'ReadableStream instances should have the correct list of properties'); + +test(() => { + + assert_throws(new TypeError(), () => { + new ReadableStream({ start: 'potato' }); + }, 'constructor should throw when start is not a function'); + +}, 'ReadableStream constructor should throw for non-function start arguments'); + +test(() => { + + new ReadableStream({ cancel: '2' }); + +}, 'ReadableStream constructor can get initial garbage as cancel argument'); + +test(() => { + + new ReadableStream({ pull: { } }); + +}, 'ReadableStream constructor can get initial garbage as pull argument'); + +test(() => { + + let startCalled = false; + + const source = { + start(controller) { + assert_equals(this, source, 'source is this during start'); + + const methods = ['close', 'enqueue', 'error', 'constructor']; + const properties = ['desiredSize'].concat(methods).sort(); + const proto = Object.getPrototypeOf(controller); + + assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties, + 'the controller should have the right properties'); + + for (let m of methods) { + const propDesc = Object.getOwnPropertyDescriptor(proto, m); + assert_equals(typeof controller[m], 'function', `should have a ${m} method`); + assert_false(propDesc.enumerable, m + ' should be non-enumerable'); + assert_true(propDesc.configurable, m + ' should be configurable'); + assert_true(propDesc.writable, m + ' should be writable'); + } + + const desiredSizePropDesc = Object.getOwnPropertyDescriptor(proto, 'desiredSize'); + assert_false(desiredSizePropDesc.enumerable, 'desiredSize should be non-enumerable'); + assert_equals(desiredSizePropDesc.writable, undefined, 'desiredSize should not be a data property'); + assert_equals(typeof desiredSizePropDesc.get, 'function', 'desiredSize should have a getter'); + assert_equals(desiredSizePropDesc.set, undefined, 'desiredSize should not have a setter'); + assert_true(desiredSizePropDesc.configurable, 'desiredSize should be configurable'); + + assert_equals(controller.close.length, 0, 'close should have no parameters'); + assert_equals(controller.constructor.length, 1, 'constructor should have 1 parameter'); + assert_equals(controller.enqueue.length, 1, 'enqueue should have 1 parameter'); + assert_equals(controller.error.length, 1, 'error should have 1 parameter'); + + startCalled = true; + } + }; + + new ReadableStream(source); + assert_true(startCalled); + +}, 'ReadableStream start should be called with the proper parameters'); + +test(() => { + + let startCalled = false; + const source = { + start(controller) { + const properties = ['close', 'constructor', 'desiredSize', 'enqueue', 'error']; + assert_array_equals(Object.getOwnPropertyNames(Object.getPrototypeOf(controller)).sort(), properties, + 'prototype should have the right properties'); + + controller.test = ''; + assert_array_equals(Object.getOwnPropertyNames(Object.getPrototypeOf(controller)).sort(), properties, + 'prototype should still have the right properties'); + assert_not_equals(Object.getOwnPropertyNames(controller).indexOf('test'), -1, + '"test" should be a property of the controller'); + + startCalled = true; + } + }; + + new ReadableStream(source); + assert_true(startCalled); + +}, 'ReadableStream start controller parameter should be extensible'); + +promise_test(() => { + + function SimpleStreamSource() {} + let resolve; + const promise = new Promise(r => resolve = r); + SimpleStreamSource.prototype = { + start: resolve + }; + + new ReadableStream(new SimpleStreamSource()); + return promise; + +}, 'ReadableStream should be able to call start method within prototype chain of its source'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + return delay(5).then(() => { + c.enqueue('a'); + c.close(); + }); + } + }); + + const reader = rs.getReader(); + return reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'value read should be the one enqueued'); + return reader.closed; + }); + +}, 'ReadableStream start should be able to return a promise'); + +promise_test(() => { + + const theError = new Error('rejected!'); + const rs = new ReadableStream({ + start() { + return delay(1).then(() => { throw theError; }); + } + }); + + return rs.getReader().closed.then(() => { + assert_unreached('closed promise should be rejected'); + }, e => { + assert_equals(e, theError, 'promise should be rejected with the same error'); + }); + +}, 'ReadableStream start should be able to return a promise and reject it'); + +promise_test(() => { + + const objects = [ + { potato: 'Give me more!' }, + 'test', + 1 + ]; + + const rs = new ReadableStream({ + start(c) { + for (let o of objects) { + c.enqueue(o); + } + c.close(); + } + }); + + const reader = rs.getReader(); + + return Promise.all([reader.read(), reader.read(), reader.read(), reader.closed]).then(r => { + assert_object_equals(r[0], { value: objects[0], done: false }, 'value read should be the one enqueued'); + assert_object_equals(r[1], { value: objects[1], done: false }, 'value read should be the one enqueued'); + assert_object_equals(r[2], { value: objects[2], done: false }, 'value read should be the one enqueued'); + }); + +}, 'ReadableStream should be able to enqueue different objects.'); + +promise_test(() => { + + const error = new Error('pull failure'); + const rs = new ReadableStream({ + pull() { + return Promise.reject(error); + } + }); + + const reader = rs.getReader(); + + let closed = false; + let read = false; + + return Promise.all([ + reader.closed.then(() => { + assert_unreached('closed should be rejected'); + }, e => { + closed = true; + assert_true(read); + assert_equals(e, error, 'closed should be rejected with the thrown error'); + }), + reader.read().then(() => { + assert_unreached('read() should be rejected'); + }, e => { + read = true; + assert_false(closed); + assert_equals(e, error, 'read() should be rejected with the thrown error'); + }) + ]); + +}, 'ReadableStream: if pull rejects, it should error the stream'); + +promise_test(() => { + + let pullCount = 0; + const startPromise = Promise.resolve(); + + new ReadableStream({ + start() { + return startPromise; + }, + pull() { + pullCount++; + } + }); + + return startPromise.then(() => { + assert_equals(pullCount, 1, 'pull should be called once start finishes'); + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should only call pull once upon starting the stream'); + +promise_test(() => { + + let pullCount = 0; + + const rs = new ReadableStream({ + pull(c) { + // Don't enqueue immediately after start. We want the stream to be empty when we call .read() on it. + if (pullCount > 0) { + c.enqueue(pullCount); + } + ++pullCount; + } + }); + + return delay(1).then(() => { + assert_equals(pullCount, 1, 'pull should be called once start finishes'); + + const reader = rs.getReader(); + const read = reader.read(); + assert_equals(pullCount, 2, 'pull should be called when read is called'); + return read; + }).then(result => { + assert_equals(pullCount, 3, 'pull should be called again in reaction to calling read'); + assert_object_equals(result, { value: 1, done: false }, 'the result read should be the one enqueued'); + }); + +}, 'ReadableStream: should call pull when trying to read from a started, empty stream'); + +promise_test(() => { + + let pullCount = 0; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + return startPromise; + }, + pull() { + pullCount++; + } + }); + + const read = rs.getReader().read(); + assert_equals(pullCount, 0, 'calling read() should not cause pull to be called yet'); + + return startPromise.then(() => { + assert_equals(pullCount, 1, 'pull should be called once start finishes'); + return read; + }).then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first read() should return first chunk'); + assert_equals(pullCount, 1, 'pull should not have been called again'); + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should only call pull once on a non-empty stream read from before start fulfills'); + +promise_test(() => { + + let pullCount = 0; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + return startPromise; + }, + pull() { + pullCount++; + } + }); + + return startPromise.then(() => { + assert_equals(pullCount, 0, 'pull should not be called once start finishes, since the queue is full'); + + const read = rs.getReader().read(); + assert_equals(pullCount, 1, 'calling read() should cause pull to be called immediately'); + return read; + }).then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first read() should return first chunk'); + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should only call pull once on a non-empty stream read from after start fulfills'); + +promise_test(() => { + + let pullCount = 0; + let controller; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start(c) { + controller = c; + return startPromise; + }, + pull() { + ++pullCount; + } + }); + + const reader = rs.getReader(); + return startPromise.then(() => { + assert_equals(pullCount, 1, 'pull should have been called once by the time the stream starts'); + + controller.enqueue('a'); + assert_equals(pullCount, 1, 'pull should not have been called again after enqueue'); + + return reader.read(); + }).then(() => { + assert_equals(pullCount, 2, 'pull should have been called again after read'); + + return delay(10); + }).then(() => { + assert_equals(pullCount, 2, 'pull should be called exactly twice'); + }); +}, 'ReadableStream: should call pull in reaction to read()ing the last chunk, if not draining'); + +promise_test(() => { + + let pullCount = 0; + let controller; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start(c) { + controller = c; + return startPromise; + }, + pull() { + ++pullCount; + } + }); + + const reader = rs.getReader(); + + return startPromise.then(() => { + assert_equals(pullCount, 1, 'pull should have been called once by the time the stream starts'); + + controller.enqueue('a'); + assert_equals(pullCount, 1, 'pull should not have been called again after enqueue'); + + controller.close(); + + return reader.read(); + }).then(() => { + assert_equals(pullCount, 1, 'pull should not have been called a second time after read'); + + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should not call pull() in reaction to read()ing the last chunk, if draining'); + +promise_test(() => { + + let resolve; + let returnedPromise; + let timesCalled = 0; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start() { + return startPromise; + }, + pull(c) { + c.enqueue(++timesCalled); + returnedPromise = new Promise(r => resolve = r); + return returnedPromise; + } + }); + const reader = rs.getReader(); + + return startPromise.then(() => { + return reader.read(); + }).then(result1 => { + assert_equals(timesCalled, 1, + 'pull should have been called once after start, but not yet have been called a second time'); + assert_object_equals(result1, { value: 1, done: false }, 'read() should fulfill with the enqueued value'); + + return delay(10); + }).then(() => { + assert_equals(timesCalled, 1, 'after 10 ms, pull should still only have been called once'); + + resolve(); + return returnedPromise; + }).then(() => { + assert_equals(timesCalled, 2, + 'after the promise returned by pull is fulfilled, pull should be called a second time'); + }); + +}, 'ReadableStream: should not call pull until the previous pull call\'s promise fulfills'); + +promise_test(() => { + + let timesCalled = 0; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream( + { + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.enqueue('c'); + return startPromise; + }, + pull() { + ++timesCalled; + } + }, + { + size() { + return 1; + }, + highWaterMark: Infinity + } + ); + const reader = rs.getReader(); + + return startPromise.then(() => { + return reader.read(); + }).then(result1 => { + assert_object_equals(result1, { value: 'a', done: false }, 'first chunk should be as expected'); + + return reader.read(); + }).then(result2 => { + assert_object_equals(result2, { value: 'b', done: false }, 'second chunk should be as expected'); + + return reader.read(); + }).then(result3 => { + assert_object_equals(result3, { value: 'c', done: false }, 'third chunk should be as expected'); + + return delay(10); + }).then(() => { + // Once for after start, and once for every read. + assert_equals(timesCalled, 4, 'pull() should be called exactly four times'); + }); + +}, 'ReadableStream: should pull after start, and after every read'); + +promise_test(() => { + + let timesCalled = 0; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.close(); + return startPromise; + }, + pull() { + ++timesCalled; + } + }); + + const reader = rs.getReader(); + return startPromise.then(() => { + assert_equals(timesCalled, 0, 'after start finishes, pull should not have been called'); + + return reader.read(); + }).then(() => { + assert_equals(timesCalled, 0, 'reading should not have triggered a pull call'); + + return reader.closed; + }).then(() => { + assert_equals(timesCalled, 0, 'stream should have closed with still no calls to pull'); + }); + +}, 'ReadableStream: should not call pull after start if the stream is now closed'); + +promise_test(() => { + + let timesCalled = 0; + let resolve; + const ready = new Promise(r => resolve = r); + + new ReadableStream( + { + start() {}, + pull(c) { + c.enqueue(++timesCalled); + + if (timesCalled === 4) { + resolve(); + } + } + }, + { + size() { + return 1; + }, + highWaterMark: 4 + } + ); + + return ready.then(() => { + // after start: size = 0, pull() + // after enqueue(1): size = 1, pull() + // after enqueue(2): size = 2, pull() + // after enqueue(3): size = 3, pull() + // after enqueue(4): size = 4, do not pull + assert_equals(timesCalled, 4, 'pull() should have been called four times'); + }); + +}, 'ReadableStream: should call pull after enqueueing from inside pull (with no read requests), if strategy allows'); + +promise_test(() => { + + let pullCalled = false; + + const rs = new ReadableStream({ + pull(c) { + pullCalled = true; + c.close(); + } + }); + + const reader = rs.getReader(); + return reader.closed.then(() => { + assert_true(pullCalled); + }); + +}, 'ReadableStream pull should be able to close a stream.'); + +test(() => { + + let startCalled = false; + + new ReadableStream({ + start(c) { + assert_equals(c.enqueue('a'), undefined, 'the first enqueue should return undefined'); + c.close(); + + assert_throws(new TypeError(), () => c.enqueue('b'), 'enqueue after close should throw a TypeError'); + startCalled = true; + } + }); + + assert_true(startCalled); + +}, 'ReadableStream: enqueue should throw when the stream is readable but draining'); + +test(() => { + + let startCalled = false; + + new ReadableStream({ + start(c) { + c.close(); + + assert_throws(new TypeError(), () => c.enqueue('a'), 'enqueue after close should throw a TypeError'); + startCalled = true; + } + }); + + assert_true(startCalled); + +}, 'ReadableStream: enqueue should throw when the stream is closed'); + +test(() => { + + let startCalled = false; + const expectedError = new Error('i am sad'); + + new ReadableStream({ + start(c) { + c.error(expectedError); + + assert_throws(expectedError, () => c.enqueue('a'), 'enqueue after error should throw that error'); + startCalled = true; + } + }); + + assert_true(startCalled); + +}, 'ReadableStream: enqueue should throw the stored error when the stream is errored'); + +promise_test(() => { + + let startCalled = 0; + let pullCalled = 0; + let cancelCalled = 0; + + /* eslint-disable no-use-before-define */ + class Source { + start(c) { + startCalled++; + assert_equals(this, theSource, 'start() should be called with the correct this'); + c.enqueue('a'); + } + + pull() { + pullCalled++; + assert_equals(this, theSource, 'pull() should be called with the correct this'); + } + + cancel() { + cancelCalled++; + assert_equals(this, theSource, 'cancel() should be called with the correct this'); + } + } + /* eslint-enable no-use-before-define */ + + const theSource = new Source(); + theSource.debugName = 'the source object passed to the constructor'; // makes test failures easier to diagnose + + const rs = new ReadableStream(theSource); + const reader = rs.getReader(); + + return reader.read().then(() => { + reader.releaseLock(); + rs.cancel(); + assert_equals(startCalled, 1); + assert_equals(pullCalled, 1); + assert_equals(cancelCalled, 1); + return rs.getReader().closed; + }); + +}, 'ReadableStream: should call underlying source methods as methods'); + +test(() => { + + let startCalled = false; + new ReadableStream({ + start(c) { + assert_equals(c.desiredSize, 1); + c.enqueue('a'); + assert_equals(c.desiredSize, 0); + c.enqueue('b'); + assert_equals(c.desiredSize, -1); + c.enqueue('c'); + assert_equals(c.desiredSize, -2); + c.enqueue('d'); + assert_equals(c.desiredSize, -3); + c.enqueue('e'); + startCalled = true; + } + }); + + assert_true(startCalled); + +}, 'ReadableStream strategies: the default strategy should give desiredSize of 1 to start, decreasing by 1 per enqueue'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 1, 'desiredSize should start at 1'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, 0, 'desiredSize should decrease to 0 after first enqueue'); + + return reader.read().then(result1 => { + assert_object_equals(result1, { value: 'a', done: false }, 'first chunk read should be correct'); + + assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the first read'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the second enqueue'); + + return reader.read(); + }).then(result2 => { + assert_object_equals(result2, { value: 'b', done: false }, 'second chunk read should be correct'); + + assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the second read'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the third enqueue'); + + return reader.read(); + }).then(result3 => { + assert_object_equals(result3, { value: 'c', done: false }, 'third chunk read should be correct'); + + assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the third read'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the fourth enqueue'); + }); + +}, 'ReadableStream strategies: the default strategy should continue giving desiredSize of 1 if the chunks are read immediately'); + +promise_test(t => { + + const randomSource = new RandomPushSource(8); + + const rs = new ReadableStream({ + start(c) { + assert_equals(typeof c, 'object', 'c should be an object in start'); + assert_equals(typeof c.enqueue, 'function', 'enqueue should be a function in start'); + assert_equals(typeof c.close, 'function', 'close should be a function in start'); + assert_equals(typeof c.error, 'function', 'error should be a function in start'); + + randomSource.ondata = t.step_func(chunk => { + if (!c.enqueue(chunk) <= 0) { + randomSource.readStop(); + } + }); + + randomSource.onend = c.close.bind(c); + randomSource.onerror = c.error.bind(c); + }, + + pull(c) { + assert_equals(typeof c, 'object', 'c should be an object in pull'); + assert_equals(typeof c.enqueue, 'function', 'enqueue should be a function in pull'); + assert_equals(typeof c.close, 'function', 'close should be a function in pull'); + + randomSource.readStart(); + } + }); + + return readableStreamToArray(rs).then(chunks => { + assert_equals(chunks.length, 8, '8 chunks should be read'); + for (let chunk of chunks) { + assert_equals(chunk.length, 128, 'chunk should have 128 bytes'); + } + }); + +}, 'ReadableStream integration test: adapting a random push source'); + +promise_test(() => { + + const rs = sequentialReadableStream(10); + + return readableStreamToArray(rs).then(chunks => { + assert_true(rs.source.closed, 'source should be closed after all chunks are read'); + assert_array_equals(chunks, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'the expected 10 chunks should be read'); + }); + +}, 'ReadableStream integration test: adapting a sync pull source'); + +promise_test(() => { + + const rs = sequentialReadableStream(10, { async: true }); + + return readableStreamToArray(rs).then(chunks => { + assert_true(rs.source.closed, 'source should be closed after all chunks are read'); + assert_array_equals(chunks, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'the expected 10 chunks should be read'); + }); + +}, 'ReadableStream integration test: adapting an async pull source'); + +done(); diff --git a/streams/readable-streams/pipe-through.https.html b/streams/readable-streams/pipe-through.https.html new file mode 100644 index 00000000000000..62d98d45af46f2 --- /dev/null +++ b/streams/readable-streams/pipe-through.https.html @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/streams/readable-streams/pipe-through.js b/streams/readable-streams/pipe-through.js new file mode 100644 index 00000000000000..4988928e78b18e --- /dev/null +++ b/streams/readable-streams/pipe-through.js @@ -0,0 +1,108 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('../resources/test-utils.js'); + self.importScripts('/resources/testharness.js'); +} + +test(() => { + + let pipeToArguments; + const thisValue = { + pipeTo() { + pipeToArguments = arguments; + } + }; + + const input = { readable: {}, writable: {} }; + const options = {}; + const result = ReadableStream.prototype.pipeThrough.call(thisValue, input, options); + + assert_array_equals(pipeToArguments, [input.writable, options], + 'correct arguments should be passed to thisValue.pipeTo'); + assert_equals(result, input.readable, 'return value should be the passed readable property'); + +}, 'ReadableStream.prototype.pipeThrough should work generically on its this and its arguments'); + +test(() => { + + const thisValue = { + pipeTo() { + assert_unreached('pipeTo should not be called'); + } + }; + + methodThrows(ReadableStream.prototype, 'pipeThrough', thisValue, [undefined, {}]); + methodThrows(ReadableStream.prototype, 'pipeThrough', thisValue, [null, {}]); + +}, 'ReadableStream.prototype.pipeThrough should throw when its first argument is not convertible to an object'); + +test(() => { + + const args = [{ readable: {}, writable: {} }, {}]; + + methodThrows(ReadableStream.prototype, 'pipeThrough', undefined, args); + methodThrows(ReadableStream.prototype, 'pipeThrough', null, args); + methodThrows(ReadableStream.prototype, 'pipeThrough', 1, args); + methodThrows(ReadableStream.prototype, 'pipeThrough', { pipeTo: 'test' }, args); + +}, 'ReadableStream.prototype.pipeThrough should throw when "this" has no pipeTo method'); + +test(() => { + const error = new Error('potato'); + + const throwingPipeTo = { + get pipeTo() { + throw error; + } + }; + assert_throws(error, + () => ReadableStream.prototype.pipeThrough.call(throwingPipeTo, { readable: { }, writable: { } }, {}), + 'pipeThrough should rethrow the error thrown by pipeTo'); + + const thisValue = { + pipeTo() { + assert_unreached('pipeTo should not be called'); + } + }; + + const throwingWritable = { + readable: {}, + get writable() { + throw error; + } + }; + assert_throws(error, + () => ReadableStream.prototype.pipeThrough.call(thisValue, throwingWritable, {}), + 'pipeThrough should rethrow the error thrown by the writable getter'); + + const throwingReadable = { + get readable() { + throw error; + }, + writable: {} + }; + assert_throws(error, + () => ReadableStream.prototype.pipeThrough.call(thisValue, throwingReadable, {}), + 'pipeThrough should rethrow the error thrown by the readable getter'); + +}, 'ReadableStream.prototype.pipeThrough should rethrow errors from accessing pipeTo, readable, or writable'); + +test(() => { + + let count = 0; + const thisValue = { + pipeTo() { + ++count; + } + }; + + ReadableStream.prototype.pipeThrough.call(thisValue, { readable: {}, writable: {} }); + ReadableStream.prototype.pipeThrough.call(thisValue, { readable: {} }, {}); + ReadableStream.prototype.pipeThrough.call(thisValue, { writable: {} }, {}); + + assert_equals(count, 3, 'pipeTo was called 3 times'); + +}, 'ReadableStream.prototype.pipeThrough should work with missing readable, writable, or options'); + +done(); diff --git a/streams/readable-streams/readable-stream-reader.https.html b/streams/readable-streams/readable-stream-reader.https.html new file mode 100644 index 00000000000000..df0db11d1bc4e8 --- /dev/null +++ b/streams/readable-streams/readable-stream-reader.https.html @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/streams/readable-streams/readable-stream-reader.js b/streams/readable-streams/readable-stream-reader.js new file mode 100644 index 00000000000000..d7d81cf2d34adc --- /dev/null +++ b/streams/readable-streams/readable-stream-reader.js @@ -0,0 +1,485 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('../resources/rs-utils.js'); + self.importScripts('/resources/testharness.js'); +} + +let ReadableStreamReader; + +test(() => { + + // It's not exposed globally, but we test a few of its properties here. + ReadableStreamReader = (new ReadableStream()).getReader().constructor; + +}, 'Can get the ReadableStreamReader constructor indirectly'); + +test(() => { + + assert_throws(new TypeError(), () => new ReadableStreamReader('potato')); + assert_throws(new TypeError(), () => new ReadableStreamReader({})); + assert_throws(new TypeError(), () => new ReadableStreamReader()); + +}, 'ReadableStreamReader constructor should get a ReadableStream object as argument'); + +test(() => { + + const methods = ['cancel', 'constructor', 'read', 'releaseLock']; + const properties = methods.concat(['closed']).sort(); + + const rsReader = new ReadableStreamReader(new ReadableStream()); + const proto = Object.getPrototypeOf(rsReader); + + assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties); + + for (let m of methods) { + const propDesc = Object.getOwnPropertyDescriptor(proto, m); + assert_equals(propDesc.enumerable, false, 'method should be non-enumerable'); + assert_equals(propDesc.configurable, true, 'method should be configurable'); + assert_equals(propDesc.writable, true, 'method should be writable'); + assert_equals(typeof rsReader[m], 'function', 'should have be a method'); + } + + const closedPropDesc = Object.getOwnPropertyDescriptor(proto, 'closed'); + assert_equals(closedPropDesc.enumerable, false, 'closed should be non-enumerable'); + assert_equals(closedPropDesc.configurable, true, 'closed should be configurable'); + assert_not_equals(closedPropDesc.get, undefined, 'closed should have a getter'); + assert_equals(closedPropDesc.set, undefined, 'closed should not have a setter'); + + assert_equals(rsReader.cancel.length, 1, 'cancel has 1 parameter'); + assert_not_equals(rsReader.closed, undefined, 'has a non-undefined closed property'); + assert_equals(typeof rsReader.closed.then, 'function', 'closed property is thenable'); + assert_equals(typeof rsReader.constructor, 'function', 'has a constructor method'); + assert_equals(rsReader.constructor.length, 1, 'constructor has 1 parameter'); + assert_equals(typeof rsReader.read, 'function', 'has a getReader method'); + assert_equals(rsReader.read.length, 0, 'read has no parameters'); + assert_equals(typeof rsReader.releaseLock, 'function', 'has a releaseLock method'); + assert_equals(rsReader.releaseLock.length, 0, 'releaseLock has no parameters'); + +}, 'ReadableStreamReader instances should have the correct list of properties'); + +test(() => { + + const rsReader = new ReadableStreamReader(new ReadableStream()); + assert_equals(rsReader.closed, rsReader.closed, 'closed should return the same promise'); + +}, 'ReadableStreamReader closed should always return the same promise object'); + +test(() => { + + const rs = new ReadableStream(); + new ReadableStreamReader(rs); // Constructing directly the first time should be fine. + assert_throws(new TypeError(), () => new ReadableStreamReader(rs), + 'constructing directly the second time should fail'); + +}, 'Constructing a ReadableStreamReader directly should fail if the stream is already locked (via direct ' + + 'construction)'); + +test(() => { + + const rs = new ReadableStream(); + new ReadableStreamReader(rs); // Constructing directly should be fine. + assert_throws(new TypeError(), () => rs.getReader(), 'getReader() should fail'); + +}, 'Getting a ReadableStreamReader via getReader should fail if the stream is already locked (via direct ' + + 'construction)'); + +test(() => { + + const rs = new ReadableStream(); + rs.getReader(); // getReader() should be fine. + assert_throws(new TypeError(), () => new ReadableStreamReader(rs), 'constructing directly should fail'); + +}, 'Constructing a ReadableStreamReader directly should fail if the stream is already locked (via getReader)'); + +test(() => { + + const rs = new ReadableStream(); + rs.getReader(); // getReader() should be fine. + assert_throws(new TypeError(), () => rs.getReader(), 'getReader() should fail'); + +}, 'Getting a ReadableStreamReader via getReader should fail if the stream is already locked (via getReader)'); + +test(() => { + + const rs = new ReadableStream({ + start(c) { + c.close(); + } + }); + + new ReadableStreamReader(rs); // Constructing directly should not throw. + +}, 'Constructing a ReadableStreamReader directly should be OK if the stream is closed'); + +test(() => { + + const theError = new Error('don\'t say i didn\'t warn ya'); + const rs = new ReadableStream({ + start(c) { + c.error(theError); + } + }); + + new ReadableStreamReader(rs); // Constructing directly should not throw. + +}, 'Constructing a ReadableStreamReader directly should be OK if the stream is errored'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + const reader = rs.getReader(); + + const promise = reader.read().then(result => { + assert_object_equals(result, { value: 'a', done: false }, 'read() should fulfill with the enqueued chunk'); + }); + + controller.enqueue('a'); + return promise; + +}, 'Reading from a reader for an empty stream will wait until a chunk is available'); + +promise_test(() => { + + let cancelCalled = false; + const passedReason = new Error('it wasn\'t the right time, sorry'); + const rs = new ReadableStream({ + cancel(reason) { + assert_true(rs.locked, 'the stream should still be locked'); + assert_throws(new TypeError(), () => rs.getReader(), 'should not be able to get another reader'); + assert_equals(reason, passedReason, 'the cancellation reason is passed through to the underlying source'); + cancelCalled = true; + } + }); + + const reader = rs.getReader(); + return reader.cancel(passedReason).then(() => assert_true(cancelCalled)); + +}, 'cancel() on a reader does not release the reader'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader = rs.getReader(); + const promise = reader.closed; + + controller.close(); + return promise; + +}, 'closed should be fulfilled after stream is closed (.closed access before acquiring)'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader1 = rs.getReader(); + + reader1.releaseLock(); + + const reader2 = rs.getReader(); + controller.close(); + + return Promise.all([ + promise_rejects(t, new TypeError(), reader1.closed), + reader2.closed + ]); + +}, 'closed should be rejected after reader releases its lock (multiple stream locks)'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const reader1 = rs.getReader(); + const promise1 = reader1.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'reading the first chunk from reader1 works'); + }); + reader1.releaseLock(); + + const reader2 = rs.getReader(); + const promise2 = reader2.read().then(r => { + assert_object_equals(r, { value: 'b', done: false }, 'reading the second chunk from reader2 works'); + }); + reader2.releaseLock(); + + return Promise.all([promise1, promise2]); + +}, 'Multiple readers can access the stream in sequence'); + +promise_test(() => { + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + } + }); + + const reader1 = rs.getReader(); + reader1.releaseLock(); + + const reader2 = rs.getReader(); + + // Should be a no-op + reader1.releaseLock(); + + return reader2.read().then(result => { + assert_object_equals(result, { value: 'a', done: false }, + 'read() should still work on reader2 even after reader1 is released'); + }); + +}, 'Cannot use an already-released reader to unlock a stream again'); + +promise_test(t => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + }, + cancel() { + assert_unreached('underlying source cancel should not be called'); + } + }); + + const reader = rs.getReader(); + reader.releaseLock(); + const cancelPromise = reader.cancel(); + + const reader2 = rs.getReader(); + const readPromise = reader2.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'a new reader should be able to read a chunk'); + }); + + return Promise.all([ + promise_rejects(t, new TypeError(), cancelPromise), + readPromise + ]); + +}, 'cancel() on a released reader is a no-op and does not pass through'); + +promise_test(t => { + + const promiseAsserts = []; + + let controller; + const theError = { name: 'unique error' }; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader1 = rs.getReader(); + + promiseAsserts.push( + promise_rejects(t, theError, reader1.closed), + promise_rejects(t, theError, reader1.read()) + ); + + assert_throws(new TypeError(), () => rs.getReader(), 'trying to get another reader before erroring should throw'); + + controller.error(theError); + + reader1.releaseLock(); + + const reader2 = rs.getReader(); + + promiseAsserts.push( + promise_rejects(t, theError, reader2.closed), + promise_rejects(t, theError, reader2.read()) + ); + + return Promise.all(promiseAsserts); + +}, 'Getting a second reader after erroring the stream and releasing the reader should succeed'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const promise = rs.getReader().closed.then( + t.unreached_func('closed promise should not be fulfilled when stream is errored'), + err => { + assert_equals(err, undefined, 'passed error should be undefined as it was'); + } + ); + + controller.error(); + return promise; + +}, 'ReadableStreamReader closed promise should be rejected with undefined if that is the error'); + + +promise_test(t => { + + const rs = new ReadableStream({ + start() { + return Promise.reject(); + } + }); + + return rs.getReader().read().then( + t.unreached_func('read promise should not be fulfilled when stream is errored'), + err => { + assert_equals(err, undefined, 'passed error should be undefined as it was'); + } + ); + +}, 'ReadableStreamReader: if start rejects with no parameter, it should error the stream with an undefined error'); + +promise_test(t => { + + const theError = { name: 'unique string' }; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const promise = promise_rejects(t, theError, rs.getReader().closed); + + controller.error(theError); + return promise; + +}, 'Erroring a ReadableStream after checking closed should reject ReadableStreamReader closed promise'); + +promise_test(t => { + + const theError = { name: 'unique string' }; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + controller.error(theError); + + // Let's call getReader twice for extra test coverage of this code path. + rs.getReader().releaseLock(); + + return promise_rejects(t, theError, rs.getReader().closed); + +}, 'Erroring a ReadableStream before checking closed should reject ReadableStreamReader closed promise'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + const reader = rs.getReader(); + + const promise = Promise.all([ + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (1)'); + }), + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (2)'); + }), + reader.closed + ]); + + controller.close(); + return promise; + +}, 'Reading twice on a stream that gets closed'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + controller.close(); + const reader = rs.getReader(); + + return Promise.all([ + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (1)'); + }), + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (2)'); + }), + reader.closed + ]); + +}, 'Reading twice on a closed stream'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const myError = { name: 'mashed potatoes' }; + controller.error(myError); + + const reader = rs.getReader(); + + return Promise.all([ + promise_rejects(t, myError, reader.read()), + promise_rejects(t, myError, reader.read()), + promise_rejects(t, myError, reader.closed) + ]); + +}, 'Reading twice on an errored stream'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const myError = { name: 'mashed potatoes' }; + const reader = rs.getReader(); + + const promise = Promise.all([ + promise_rejects(t, myError, reader.read()), + promise_rejects(t, myError, reader.read()), + promise_rejects(t, myError, reader.closed) + ]); + + controller.error(myError); + return promise; + +}, 'Reading twice on a stream that gets errored'); + +done(); diff --git a/streams/readable-streams/tee.https.html b/streams/readable-streams/tee.https.html new file mode 100644 index 00000000000000..02c847b81178ad --- /dev/null +++ b/streams/readable-streams/tee.https.html @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/streams/readable-streams/tee.js b/streams/readable-streams/tee.js new file mode 100644 index 00000000000000..485f2af7ab5ce4 --- /dev/null +++ b/streams/readable-streams/tee.js @@ -0,0 +1,254 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('../resources/rs-utils.js'); + self.importScripts('/resources/testharness.js'); +} + +test(() => { + + const rs = new ReadableStream(); + const result = rs.tee(); + + assert_true(Array.isArray(result), 'return value should be an array'); + assert_equals(result.length, 2, 'array should have length 2'); + assert_equals(result[0].constructor, ReadableStream, '0th element should be a ReadableStream'); + assert_equals(result[1].constructor, ReadableStream, '1st element should be a ReadableStream'); + +}, 'ReadableStream teeing: rs.tee() returns an array of two ReadableStreams'); + +promise_test(t => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const branch = rs.tee(); + const branch1 = branch[0]; + const branch2 = branch[1]; + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + reader2.closed.then(t.unreached_func('branch2 should not be closed')); + + return Promise.all([ + reader1.closed, + reader1.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch1 should be correct'); + }), + reader1.read().then(r => { + assert_object_equals(r, { value: 'b', done: false }, 'second chunk from branch1 should be correct'); + }), + reader1.read().then(r => { + assert_object_equals(r, { value: undefined, done: true }, 'third read() from branch1 should be done'); + }), + reader2.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch2 should be correct'); + }) + ]); + +}, 'ReadableStream teeing: should be able to read one branch to the end without affecting the other'); + +promise_test(() => { + + const theObject = { the: 'test object' }; + const rs = new ReadableStream({ + start(c) { + c.enqueue(theObject); + } + }); + + const branch = rs.tee(); + const branch1 = branch[0]; + const branch2 = branch[1]; + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + return Promise.all([reader1.read(), reader2.read()]).then(values => { + assert_object_equals(values[0], values[1], 'the values should be equal'); + }); + +}, 'ReadableStream teeing: values should be equal across each branch'); + +promise_test(t => { + + const theError = { name: 'boo!' }; + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + }, + pull() { + throw theError; + } + }); + + const branches = rs.tee(); + const reader1 = branches[0].getReader(); + const reader2 = branches[1].getReader(); + + reader1.label = 'reader1'; + reader2.label = 'reader2'; + + return Promise.all([ + promise_rejects(t, theError, reader1.closed), + promise_rejects(t, theError, reader2.closed), + reader1.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'should be able to read the first chunk in branch1'); + }), + reader1.read().then(r => { + assert_object_equals(r, { value: 'b', done: false }, 'should be able to read the second chunk in branch1'); + + return promise_rejects(t, theError, reader2.read()); + }) + .then(() => promise_rejects(t, theError, reader1.read())) + ]); + +}, 'ReadableStream teeing: errors in the source should propagate to both branches'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const branches = rs.tee(); + const branch1 = branches[0]; + const branch2 = branches[1]; + branch1.cancel(); + + return Promise.all([ + readableStreamToArray(branch1).then(chunks => { + assert_array_equals(chunks, [], 'branch1 should have no chunks'); + }), + readableStreamToArray(branch2).then(chunks => { + assert_array_equals(chunks, ['a', 'b'], 'branch2 should have two chunks'); + }) + ]); + +}, 'ReadableStream teeing: canceling branch1 should not impact branch2'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const branches = rs.tee(); + const branch1 = branches[0]; + const branch2 = branches[1]; + branch2.cancel(); + + return Promise.all([ + readableStreamToArray(branch1).then(chunks => { + assert_array_equals(chunks, ['a', 'b'], 'branch1 should have two chunks'); + }), + readableStreamToArray(branch2).then(chunks => { + assert_array_equals(chunks, [], 'branch2 should have no chunks'); + }) + ]); + +}, 'ReadableStream teeing: canceling branch2 should not impact branch2'); + +promise_test(() => { + + 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 = new ReadableStream({ + cancel(reason) { + assert_array_equals(reason, [reason1, reason2], + 'the cancel reason should be an array containing those from the branches'); + resolve(); + } + }); + + const branch = rs.tee(); + const branch1 = branch[0]; + const branch2 = branch[1]; + branch1.cancel(reason1); + branch2.cancel(reason2); + + return promise; + +}, 'ReadableStream teeing: canceling both branches should aggregate the cancel reasons into an array'); + +promise_test(t => { + + const theError = { name: 'I\'ll be careful.' }; + const rs = new ReadableStream({ + cancel() { + throw theError; + } + }); + + const branch = rs.tee(); + const branch1 = branch[0]; + const branch2 = branch[1]; + + return Promise.all([ + promise_rejects(t, theError, branch1.cancel()), + promise_rejects(t, theError, branch2.cancel()) + ]); + +}, 'ReadableStream teeing: failing to cancel the original stream should cause cancel() to reject on branches'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const branches = rs.tee(); + const reader1 = branches[0].getReader(); + const reader2 = branches[1].getReader(); + + const promise = Promise.all([reader1.closed, reader2.closed]); + + controller.close(); + return promise; + +}, 'ReadableStream teeing: closing the original should immediately close the branches'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const branches = rs.tee(); + const reader1 = branches[0].getReader(); + const reader2 = branches[1].getReader(); + + const theError = { name: 'boo!' }; + const promise = Promise.all([ + promise_rejects(t, theError, reader1.closed), + promise_rejects(t, theError, reader2.closed) + ]); + + controller.error(theError); + return promise; + +}, 'ReadableStream teeing: erroring the original should immediately error the branches'); + +done(); diff --git a/streams/readable-streams/templated.https.html b/streams/readable-streams/templated.https.html new file mode 100644 index 00000000000000..95c6a6acaefb5d --- /dev/null +++ b/streams/readable-streams/templated.https.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/streams/readable-streams/templated.js b/streams/readable-streams/templated.js new file mode 100644 index 00000000000000..6db0429994d453 --- /dev/null +++ b/streams/readable-streams/templated.js @@ -0,0 +1,148 @@ +'use strict'; + +if (self.importScripts) { + self.importScripts('/resources/testharness.js'); + self.importScripts('../resources/test-utils.js'); + self.importScripts('../resources/rs-test-templates.js'); +} + +// Run the readable stream test templates against readable streams created directly using the constructor + +const theError = { name: 'boo!' }; +const chunks = ['a', 'b']; + +templatedRSEmpty('ReadableStream (empty)', () => { + return new ReadableStream(); +}); + +templatedRSEmptyReader('ReadableStream (empty) reader', () => { + return streamAndDefaultReader(new ReadableStream()); +}); + +templatedRSClosed('ReadableStream (closed via call in start)', () => { + return new ReadableStream({ + start(c) { + c.close(); + } + }); +}); + +templatedRSClosedReader('ReadableStream reader (closed before getting reader)', () => { + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + } + }); + controller.close(); + const result = streamAndDefaultReader(stream); + return result; +}); + +templatedRSClosedReader('ReadableStream reader (closed after getting reader)', () => { + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + } + }); + const result = streamAndDefaultReader(stream); + controller.close(); + return result; +}); + +templatedRSClosed('ReadableStream (closed via cancel)', () => { + const stream = new ReadableStream(); + stream.cancel(); + return stream; +}); + +templatedRSClosedReader('ReadableStream reader (closed via cancel after getting reader)', () => { + const stream = new ReadableStream(); + const result = streamAndDefaultReader(stream); + result.reader.cancel(); + return result; +}); + +templatedRSErrored('ReadableStream (errored via call in start)', () => { + return new ReadableStream({ + start(c) { + c.error(theError); + } + }); +}, theError); + +templatedRSErroredSyncOnly('ReadableStream (errored via call in start)', () => { + return new ReadableStream({ + start(c) { + c.error(theError); + } + }); +}, theError); + +templatedRSErrored('ReadableStream (errored via returning a rejected promise in start)', () => { + return new ReadableStream({ + start() { + return Promise.reject(theError); + } + }); +}, theError); + +templatedRSErroredReader('ReadableStream (errored via returning a rejected promise in start) reader', () => { + return streamAndDefaultReader(new ReadableStream({ + start() { + return Promise.reject(theError); + } + })); +}, theError); + +templatedRSErroredReader('ReadableStream reader (errored before getting reader)', () => { + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + } + }); + controller.error(theError); + return streamAndDefaultReader(stream); +}, theError); + +templatedRSErroredReader('ReadableStream reader (errored after getting reader)', () => { + let controller; + const result = streamAndDefaultReader(new ReadableStream({ + start(c) { + controller = c; + } + })); + controller.error(theError); + return result; +}, theError); + +templatedRSTwoChunksOpenReader('ReadableStream (two chunks enqueued, still open) reader', () => { + return streamAndDefaultReader(new ReadableStream({ + start(c) { + c.enqueue(chunks[0]); + c.enqueue(chunks[1]); + } + })); +}, chunks); + +templatedRSTwoChunksClosedReader('ReadableStream (two chunks enqueued, then closed) reader', () => { + let doClose; + const stream = new ReadableStream({ + start(c) { + c.enqueue(chunks[0]); + c.enqueue(chunks[1]); + doClose = c.close.bind(c); + } + }); + const result = streamAndDefaultReader(stream); + doClose(); + return result; +}, chunks); + +function streamAndDefaultReader(stream) { + return { stream, reader: stream.getReader() }; +} + +done(); diff --git a/streams/resources/rs-test-templates.js b/streams/resources/rs-test-templates.js new file mode 100644 index 00000000000000..852b565d9042e7 --- /dev/null +++ b/streams/resources/rs-test-templates.js @@ -0,0 +1,624 @@ +'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'); + + }, 'instances have the correct methods and properties'); +}; + +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')) + ]); + + }, 'cancel() should return a distinct fulfilled promise each time'); + + test(() => { + + const rs = factory(); + assert_false(rs.locked, 'locked getter should return false'); + + }, 'locked should be false'); + + test(() => { + + const rs = factory(); + rs.getReader(); // getReader() should not throw. + + }, '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. + + }, 'should be able to acquire multiple readers if they are released in succession'); + + test(() => { + + const rs = factory(); + + rs.getReader(); + + assert_throws(new TypeError(), () => rs.getReader(), 'getting a second reader should throw'); + assert_throws(new TypeError(), () => rs.getReader(), 'getting a third reader should throw'); + + }, '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(t, error, reader.closed), + promise_rejects(t, error, reader.read()) + ]); + + }, 'getReader() should return a reader that acts errored'); + + promise_test(t => { + + const rs = factory(); + const reader = rs.getReader(); + + return Promise.all([ + promise_rejects(t, error, reader.read()), + promise_rejects(t, error, reader.read()), + promise_rejects(t, error, reader.closed) + ]); + + }, 'read() twice should give the error each time'); + + test(() => { + const rs = factory(); + + assert_false(rs.locked, 'locked getter should return false'); + }, '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(t, error, reader.closed); + + }, 'should be able to obtain a second reader, with the correct closed promise'); + + test(() => { + + const rs = factory(); + rs.getReader(); + + assert_throws(new TypeError(), () => rs.getReader(), 'getting a second reader should throw a TypeError'); + assert_throws(new TypeError(), () => rs.getReader(), 'getting a third reader should throw a TypeError'); + + }, '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(t, error, cancelPromise1), + promise_rejects(t, error, cancelPromise2) + ]); + + }, '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(t, error, cancelPromise1), + promise_rejects(t, error, cancelPromise2) + ]); + + }, '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'); + + }, 'instances have the correct methods and properties'); + + test(() => { + + const stream = factory().stream; + + assert_true(stream.locked, 'locked getter should return true'); + + }, '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); + + }, '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); + + }, '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'); + + }, 'read() should return distinct promises each time'); + + test(() => { + + const stream = factory().stream; + assert_throws(new TypeError(), () => stream.getReader(), 'stream.getReader() should throw a TypeError'); + + }, 'getReader() again on the stream should fail'); + + promise_test(t => { + + const streamAndReader = factory(); + const stream = streamAndReader.stream; + const reader = streamAndReader.reader; + + reader.read().then( + t.unreached_func('first read() should not fulfill'), + t.unreached_func('first read() should not reject') + ); + + reader.read().then( + t.unreached_func('second read() should not fulfill'), + t.unreached_func('second read() should not reject') + ); + + reader.closed.then( + t.unreached_func('closed should not fulfill'), + t.unreached_func('closed should not reject') + ); + + assert_throws(new TypeError(), () => reader.releaseLock(), 'releaseLock should throw a TypeError'); + + assert_true(stream.locked, 'the stream should still be locked'); + + return delay(500); + + }, 'releasing the lock with pending read requests should throw but the read requests should stay pending'); + + promise_test(t => { + + const reader = factory().reader; + reader.releaseLock(); + + return Promise.all([ + promise_rejects(t, new TypeError(), reader.read()), + promise_rejects(t, new TypeError(), reader.read()) + ]); + + }, '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(t, new TypeError(), closedBefore); + + }, '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'); + + }, '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'); + }); + + }, 'canceling via the reader should cause the reader to act closed'); + + promise_test(t => { + + const stream = factory().stream; + return promise_rejects(t, new TypeError(), stream.cancel()); + + }, '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'); + }); + + }, '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'); + }) + ]); + + }, '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'); + }); + + }, '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')); + + }, '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(t, new TypeError(), closedAfter) + ]); + + }, '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')) + ]); + + }, '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(t, error, reader.closed); + + }, 'closed should reject with the error'); + + promise_test(t => { + + const reader = factory().reader; + const closedBefore = reader.closed; + + return promise_rejects(t, error, closedBefore).then(() => { + reader.releaseLock(); + + const closedAfter = reader.closed; + assert_not_equals(closedBefore, closedAfter, 'the closed promise should change identity'); + + return promise_rejects(t, new TypeError(), closedAfter); + }); + + }, 'releasing the lock should cause closed to reject and change identity'); + + promise_test(t => { + + const reader = factory().reader; + return promise_rejects(t, error, reader.read()); + + }, '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'); + }) + ]); + + }, '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'); + }); + }); + + }, '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'); + + }, '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]); + + }, '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'); + }) + ]); + + }, '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'); + }); + }); + }); + + }, '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; + + }, '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; + + }, '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(t, new TypeError(), reader.read()), + promise_rejects(t, new TypeError(), reader.read()), + promise_rejects(t, new TypeError(), reader.read()) + ]); + + }, '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; + + }, 'reader\'s closed property always returns the same promise'); +}; diff --git a/streams/resources/rs-utils.js b/streams/resources/rs-utils.js new file mode 100644 index 00000000000000..0f3222e23cab7a --- /dev/null +++ b/streams/resources/rs-utils.js @@ -0,0 +1,185 @@ +'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 => setTimeout(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; + } + + + self.RandomPushSource = RandomPushSource; + self.readableStreamToArray = readableStreamToArray; + self.sequentialReadableStream = sequentialReadableStream; + +}()); diff --git a/streams/resources/test-initializer.js b/streams/resources/test-initializer.js new file mode 100644 index 00000000000000..d6ed4a7aab541e --- /dev/null +++ b/streams/resources/test-initializer.js @@ -0,0 +1,14 @@ +'use strict'; + +function worker_test(file) { + fetch_tests_from_worker(new Worker(file)); + if (typeof SharedWorker === 'function') { + fetch_tests_from_worker(new SharedWorker(file)); + } else { + test(() => { + assert_unreached('SharedWorker is unavailable'); + }, 'Load ' + file + ' with SharedWorker'); + } + service_worker_test(file); +} + diff --git a/streams/resources/test-utils.js b/streams/resources/test-utils.js new file mode 100644 index 00000000000000..cc47898a608e93 --- /dev/null +++ b/streams/resources/test-utils.js @@ -0,0 +1,43 @@ +'use strict'; + +self.getterRejects = (t, obj, getterName, target) => { + const getter = Object.getOwnPropertyDescriptor(obj, getterName).get; + + return promise_rejects(t, new TypeError(), getter.call(target)); +}; + +self.methodRejects = (t, obj, methodName, target) => { + const method = obj[methodName]; + + return promise_rejects(t, new TypeError(), method.call(target)); +}; + +self.getterThrows = (obj, getterName, target) => { + const getter = Object.getOwnPropertyDescriptor(obj, getterName).get; + + assert_throws(new TypeError(), () => getter.call(target), getterName + ' should throw a TypeError'); +}; + +self.methodThrows = (obj, methodName, target, args) => { + const method = obj[methodName]; + + assert_throws(new TypeError(), () => method.apply(target, args), methodName + ' should throw a TypeError'); +}; + +self.garbageCollect = () => { + if (self.gc) { + // Use --expose_gc for V8 (and Node.js) + // Exposed in SpiderMonkey shell as well + self.gc(); + } else if (self.GCController) { + // Present in some WebKit development environments + GCController.collect(); + } else { + /* eslint-disable no-console */ + console.warn('Tests are running without the ability to do manual garbage collection. They will still work, but ' + + 'coverage will be suboptimal.'); + /* eslint-enable no-console */ + } +}; + +self.delay = ms => new Promise(resolve => step_timeout(resolve, ms));