Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,732 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/rs-utils.js
// META: script=../resources/test-utils.js
// META: script=../resources/recording-streams.js
'use strict';
const error1 = new Error('error1');
function assert_iter_result(iterResult, value, done, message) {
const prefix = message === undefined ? '' : `${message} `;
assert_equals(typeof iterResult, 'object', `${prefix}type is object`);
assert_equals(Object.getPrototypeOf(iterResult), Object.prototype, `${prefix}[[Prototype]]`);
assert_array_equals(Object.getOwnPropertyNames(iterResult).sort(), ['done', 'value'], `${prefix}property names`);
assert_equals(iterResult.value, value, `${prefix}value`);
assert_equals(iterResult.done, done, `${prefix}done`);
}
test(() => {
const s = new ReadableStream();
const it = s.values();
const proto = Object.getPrototypeOf(it);
const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype);
assert_equals(Object.getPrototypeOf(proto), AsyncIteratorPrototype, 'prototype should extend AsyncIteratorPrototype');
const methods = ['next', 'return'].sort();
assert_array_equals(Object.getOwnPropertyNames(proto).sort(), methods, 'should have all the correct methods');
for (const m of methods) {
const propDesc = Object.getOwnPropertyDescriptor(proto, m);
assert_true(propDesc.enumerable, 'method should be enumerable');
assert_true(propDesc.configurable, 'method should be configurable');
assert_true(propDesc.writable, 'method should be writable');
assert_equals(typeof it[m], 'function', 'method should be a function');
assert_equals(it[m].name, m, 'method should have the correct name');
}
assert_equals(it.next.length, 0, 'next should have no parameters');
assert_equals(it.return.length, 1, 'return should have 1 parameter');
assert_equals(typeof it.throw, 'undefined', 'throw should not exist');
}, 'Async iterator instances should have the correct list of properties');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.enqueue(1);
c.enqueue(2);
c.enqueue(3);
c.close();
}
});
const chunks = [];
for await (const chunk of s) {
chunks.push(chunk);
}
assert_array_equals(chunks, [1, 2, 3]);
}, 'Async-iterating a push source');
promise_test(async () => {
let i = 1;
const s = new ReadableStream({
pull(c) {
c.enqueue(i);
if (i >= 3) {
c.close();
}
i += 1;
}
});
const chunks = [];
for await (const chunk of s) {
chunks.push(chunk);
}
assert_array_equals(chunks, [1, 2, 3]);
}, 'Async-iterating a pull source');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.enqueue(undefined);
c.enqueue(undefined);
c.enqueue(undefined);
c.close();
}
});
const chunks = [];
for await (const chunk of s) {
chunks.push(chunk);
}
assert_array_equals(chunks, [undefined, undefined, undefined]);
}, 'Async-iterating a push source with undefined values');
promise_test(async () => {
let i = 1;
const s = new ReadableStream({
pull(c) {
c.enqueue(undefined);
if (i >= 3) {
c.close();
}
i += 1;
}
});
const chunks = [];
for await (const chunk of s) {
chunks.push(chunk);
}
assert_array_equals(chunks, [undefined, undefined, undefined]);
}, 'Async-iterating a pull source with undefined values');
promise_test(async () => {
let i = 1;
const s = recordingReadableStream({
pull(c) {
c.enqueue(i);
if (i >= 3) {
c.close();
}
i += 1;
},
}, new CountQueuingStrategy({ highWaterMark: 0 }));
const it = s.values();
assert_array_equals(s.events, []);
const read1 = await it.next();
assert_iter_result(read1, 1, false);
assert_array_equals(s.events, ['pull']);
const read2 = await it.next();
assert_iter_result(read2, 2, false);
assert_array_equals(s.events, ['pull', 'pull']);
const read3 = await it.next();
assert_iter_result(read3, 3, false);
assert_array_equals(s.events, ['pull', 'pull', 'pull']);
const read4 = await it.next();
assert_iter_result(read4, undefined, true);
assert_array_equals(s.events, ['pull', 'pull', 'pull']);
}, 'Async-iterating a pull source manually');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.error('e');
},
});
try {
for await (const chunk of s) {}
assert_unreached();
} catch (e) {
assert_equals(e, 'e');
}
}, 'Async-iterating an errored stream throws');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.close();
}
});
for await (const chunk of s) {
assert_unreached();
}
}, 'Async-iterating a closed stream never executes the loop body, but works fine');
promise_test(async () => {
const s = new ReadableStream();
const loop = async () => {
for await (const chunk of s) {
assert_unreached();
}
assert_unreached();
};
await Promise.race([
loop(),
flushAsyncEvents()
]);
}, 'Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.enqueue(1);
c.enqueue(2);
c.enqueue(3);
c.close();
},
});
const reader = s.getReader();
const readResult = await reader.read();
assert_iter_result(readResult, 1, false);
reader.releaseLock();
const chunks = [];
for await (const chunk of s) {
chunks.push(chunk);
}
assert_array_equals(chunks, [2, 3]);
}, 'Async-iterating a partially consumed stream');
for (const type of ['throw', 'break', 'return']) {
for (const preventCancel of [false, true]) {
promise_test(async () => {
const s = recordingReadableStream({
start(c) {
c.enqueue(0);
}
});
// use a separate function for the loop body so return does not stop the test
const loop = async () => {
for await (const c of s.values({ preventCancel })) {
if (type === 'throw') {
throw new Error();
} else if (type === 'break') {
break;
} else if (type === 'return') {
return;
}
}
};
try {
await loop();
} catch (e) {}
if (preventCancel) {
assert_array_equals(s.events, ['pull'], `cancel() should not be called`);
} else {
assert_array_equals(s.events, ['pull', 'cancel', undefined], `cancel() should be called`);
}
}, `Cancellation behavior when ${type}ing inside loop body; preventCancel = ${preventCancel}`);
}
}
for (const preventCancel of [false, true]) {
promise_test(async () => {
const s = recordingReadableStream({
start(c) {
c.enqueue(0);
}
});
const it = s.values({ preventCancel });
await it.return();
if (preventCancel) {
assert_array_equals(s.events, [], `cancel() should not be called`);
} else {
assert_array_equals(s.events, ['cancel', undefined], `cancel() should be called`);
}
}, `Cancellation behavior when manually calling return(); preventCancel = ${preventCancel}`);
}
promise_test(async t => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
if (timesPulled === 0) {
c.enqueue(0);
++timesPulled;
} else {
c.error(error1);
}
}
});
const it = s[Symbol.asyncIterator]();
const iterResult1 = await it.next();
assert_iter_result(iterResult1, 0, false, '1st next()');
await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
}, 'next() rejects if the stream errors');
promise_test(async () => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
if (timesPulled === 0) {
c.enqueue(0);
++timesPulled;
} else {
c.error(error1);
}
}
});
const it = s[Symbol.asyncIterator]();
const iterResult = await it.return('return value');
assert_iter_result(iterResult, 'return value', true);
}, 'return() does not rejects if the stream has not errored yet');
promise_test(async t => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
// Do not error in start() because doing so would prevent acquiring a reader/async iterator.
c.error(error1);
}
});
const it = s[Symbol.asyncIterator]();
await flushAsyncEvents();
await promise_rejects_exactly(t, error1, it.return('return value'));
}, 'return() rejects if the stream has errored');
promise_test(async t => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
if (timesPulled === 0) {
c.enqueue(0);
++timesPulled;
} else {
c.error(error1);
}
}
});
const it = s[Symbol.asyncIterator]();
const iterResult1 = await it.next();
assert_iter_result(iterResult1, 0, false, '1st next()');
await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
const iterResult3 = await it.next();
assert_iter_result(iterResult3, undefined, true, '3rd next()');
}, 'next() that succeeds; next() that reports an error; next()');
promise_test(async () => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
if (timesPulled === 0) {
c.enqueue(0);
++timesPulled;
} else {
c.error(error1);
}
}
});
const it = s[Symbol.asyncIterator]();
const iterResults = await Promise.allSettled([it.next(), it.next(), it.next()]);
assert_equals(iterResults[0].status, 'fulfilled', '1st next() promise status');
assert_iter_result(iterResults[0].value, 0, false, '1st next()');
assert_equals(iterResults[1].status, 'rejected', '2nd next() promise status');
assert_equals(iterResults[1].reason, error1, '2nd next() rejection reason');
assert_equals(iterResults[2].status, 'fulfilled', '3rd next() promise status');
assert_iter_result(iterResults[2].value, undefined, true, '3rd next()');
}, 'next() that succeeds; next() that reports an error(); next() [no awaiting]');
promise_test(async t => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
if (timesPulled === 0) {
c.enqueue(0);
++timesPulled;
} else {
c.error(error1);
}
}
});
const it = s[Symbol.asyncIterator]();
const iterResult1 = await it.next();
assert_iter_result(iterResult1, 0, false, '1st next()');
await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
const iterResult3 = await it.return('return value');
assert_iter_result(iterResult3, 'return value', true, 'return()');
}, 'next() that succeeds; next() that reports an error(); return()');
promise_test(async () => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
if (timesPulled === 0) {
c.enqueue(0);
++timesPulled;
} else {
c.error(error1);
}
}
});
const it = s[Symbol.asyncIterator]();
const iterResults = await Promise.allSettled([it.next(), it.next(), it.return('return value')]);
assert_equals(iterResults[0].status, 'fulfilled', '1st next() promise status');
assert_iter_result(iterResults[0].value, 0, false, '1st next()');
assert_equals(iterResults[1].status, 'rejected', '2nd next() promise status');
assert_equals(iterResults[1].reason, error1, '2nd next() rejection reason');
assert_equals(iterResults[2].status, 'fulfilled', 'return() promise status');
assert_iter_result(iterResults[2].value, 'return value', true, 'return()');
}, 'next() that succeeds; next() that reports an error(); return() [no awaiting]');
promise_test(async () => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
c.enqueue(timesPulled);
++timesPulled;
}
});
const it = s[Symbol.asyncIterator]();
const iterResult1 = await it.next();
assert_iter_result(iterResult1, 0, false, 'next()');
const iterResult2 = await it.return('return value');
assert_iter_result(iterResult2, 'return value', true, 'return()');
assert_equals(timesPulled, 2);
}, 'next() that succeeds; return()');
promise_test(async () => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
c.enqueue(timesPulled);
++timesPulled;
}
});
const it = s[Symbol.asyncIterator]();
const iterResults = await Promise.allSettled([it.next(), it.return('return value')]);
assert_equals(iterResults[0].status, 'fulfilled', 'next() promise status');
assert_iter_result(iterResults[0].value, 0, false, 'next()');
assert_equals(iterResults[1].status, 'fulfilled', 'return() promise status');
assert_iter_result(iterResults[1].value, 'return value', true, 'return()');
assert_equals(timesPulled, 2);
}, 'next() that succeeds; return() [no awaiting]');
promise_test(async () => {
const rs = new ReadableStream();
const it = rs.values();
const iterResult1 = await it.return('return value');
assert_iter_result(iterResult1, 'return value', true, 'return()');
const iterResult2 = await it.next();
assert_iter_result(iterResult2, undefined, true, 'next()');
}, 'return(); next()');
promise_test(async () => {
const rs = new ReadableStream();
const it = rs.values();
const resolveOrder = [];
const iterResults = await Promise.allSettled([
it.return('return value').then(result => {
resolveOrder.push('return');
return result;
}),
it.next().then(result => {
resolveOrder.push('next');
return result;
})
]);
assert_equals(iterResults[0].status, 'fulfilled', 'return() promise status');
assert_iter_result(iterResults[0].value, 'return value', true, 'return()');
assert_equals(iterResults[1].status, 'fulfilled', 'next() promise status');
assert_iter_result(iterResults[1].value, undefined, true, 'next()');
assert_array_equals(resolveOrder, ['return', 'next'], 'next() resolves after return()');
}, 'return(); next() [no awaiting]');
promise_test(async () => {
let resolveCancelPromise;
const rs = recordingReadableStream({
cancel(reason) {
return new Promise(r => resolveCancelPromise = r);
}
});
const it = rs.values();
let returnResolved = false;
const returnPromise = it.return('return value').then(result => {
returnResolved = true;
return result;
});
await flushAsyncEvents();
assert_false(returnResolved, 'return() should not resolve while cancel() promise is pending');
resolveCancelPromise();
const iterResult1 = await returnPromise;
assert_iter_result(iterResult1, 'return value', true, 'return()');
const iterResult2 = await it.next();
assert_iter_result(iterResult2, undefined, true, 'next()');
}, 'return(); next() with delayed cancel()');
promise_test(async () => {
let resolveCancelPromise;
const rs = recordingReadableStream({
cancel(reason) {
return new Promise(r => resolveCancelPromise = r);
}
});
const it = rs.values();
const resolveOrder = [];
const returnPromise = it.return('return value').then(result => {
resolveOrder.push('return');
return result;
});
const nextPromise = it.next().then(result => {
resolveOrder.push('next');
return result;
});
assert_array_equals(rs.events, ['cancel', 'return value'], 'return() should call cancel()');
assert_array_equals(resolveOrder, [], 'return() should not resolve before cancel() resolves');
resolveCancelPromise();
const iterResult1 = await returnPromise;
assert_iter_result(iterResult1, 'return value', true, 'return() should resolve with original reason');
const iterResult2 = await nextPromise;
assert_iter_result(iterResult2, undefined, true, 'next() should resolve with done result');
assert_array_equals(rs.events, ['cancel', 'return value'], 'no pull() after cancel()');
assert_array_equals(resolveOrder, ['return', 'next'], 'next() should resolve after return() resolves');
}, 'return(); next() with delayed cancel() [no awaiting]');
promise_test(async () => {
const rs = new ReadableStream();
const it = rs.values();
const iterResult1 = await it.return('return value 1');
assert_iter_result(iterResult1, 'return value 1', true, '1st return()');
const iterResult2 = await it.return('return value 2');
assert_iter_result(iterResult2, 'return value 2', true, '1st return()');
}, 'return(); return()');
promise_test(async () => {
const rs = new ReadableStream();
const it = rs.values();
const resolveOrder = [];
const iterResults = await Promise.allSettled([
it.return('return value 1').then(result => {
resolveOrder.push('return 1');
return result;
}),
it.return('return value 2').then(result => {
resolveOrder.push('return 2');
return result;
})
]);
assert_equals(iterResults[0].status, 'fulfilled', '1st return() promise status');
assert_iter_result(iterResults[0].value, 'return value 1', true, '1st return()');
assert_equals(iterResults[1].status, 'fulfilled', '2nd return() promise status');
assert_iter_result(iterResults[1].value, 'return value 2', true, '1st return()');
assert_array_equals(resolveOrder, ['return 1', 'return 2'], '2nd return() resolves after 1st return()');
}, 'return(); return() [no awaiting]');
test(() => {
const s = new ReadableStream({
start(c) {
c.enqueue(0);
c.close();
},
});
s.values();
assert_throws_js(TypeError, () => s.values(), 'values() should throw');
}, 'values() throws if there\'s already a lock');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.enqueue(1);
c.enqueue(2);
c.enqueue(3);
c.close();
}
});
const chunks = [];
for await (const chunk of s) {
chunks.push(chunk);
}
assert_array_equals(chunks, [1, 2, 3]);
const reader = s.getReader();
await reader.closed;
}, 'Acquiring a reader after exhaustively async-iterating a stream');
promise_test(async t => {
let timesPulled = 0;
const s = new ReadableStream({
pull(c) {
if (timesPulled === 0) {
c.enqueue(0);
++timesPulled;
} else {
c.error(error1);
}
}
});
const it = s[Symbol.asyncIterator]({ preventCancel: true });
const iterResult1 = await it.next();
assert_iter_result(iterResult1, 0, false, '1st next()');
await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
const iterResult2 = await it.return('return value');
assert_iter_result(iterResult2, 'return value', true, 'return()');
// i.e. it should not reject with a generic "this stream is locked" TypeError.
const reader = s.getReader();
await promise_rejects_exactly(t, error1, reader.closed, 'closed on the new reader should reject with the error');
}, 'Acquiring a reader after return()ing from a stream that errors');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.enqueue(1);
c.enqueue(2);
c.enqueue(3);
c.close();
},
});
// read the first two chunks, then cancel
const chunks = [];
for await (const chunk of s) {
chunks.push(chunk);
if (chunk >= 2) {
break;
}
}
assert_array_equals(chunks, [1, 2]);
const reader = s.getReader();
await reader.closed;
}, 'Acquiring a reader after partially async-iterating a stream');
promise_test(async () => {
const s = new ReadableStream({
start(c) {
c.enqueue(1);
c.enqueue(2);
c.enqueue(3);
c.close();
},
});
// read the first two chunks, then release lock
const chunks = [];
for await (const chunk of s.values({preventCancel: true})) {
chunks.push(chunk);
if (chunk >= 2) {
break;
}
}
assert_array_equals(chunks, [1, 2]);
const reader = s.getReader();
const readResult = await reader.read();
assert_iter_result(readResult, 3, false);
await reader.closed;
}, 'Acquiring a reader and reading the remaining chunks after partially async-iterating a stream with preventCancel = true');
for (const preventCancel of [false, true]) {
test(() => {
const rs = new ReadableStream();
rs.values({ preventCancel }).return();
// The test passes if this line doesn't throw.
rs.getReader();
}, `return() should unlock the stream synchronously when preventCancel = ${preventCancel}`);
}
promise_test(async () => {
const rs = new ReadableStream({
async start(c) {
c.enqueue('a');
c.enqueue('b');
c.enqueue('c');
await flushAsyncEvents();
// At this point, the async iterator has a read request in the stream's queue for its pending next() promise.
// Closing the stream now causes two things to happen *synchronously*:
// 1. ReadableStreamClose resolves reader.[[closedPromise]] with undefined.
// 2. ReadableStreamClose calls the read request's close steps, which calls ReadableStreamReaderGenericRelease,
// which replaces reader.[[closedPromise]] with a rejected promise.
c.close();
}
});
const chunks = [];
for await (const chunk of rs) {
chunks.push(chunk);
}
assert_array_equals(chunks, ['a', 'b', 'c']);
}, 'close() while next() is pending');

View File

@ -0,0 +1,198 @@
// META: global=window,worker,shadowrealm
'use strict';
test(() => {
const theError = new Error('a unique string');
assert_throws_exactly(theError, () => {
new ReadableStream({}, {
get size() {
throw theError;
},
highWaterMark: 5
});
}, 'construction should re-throw the error');
}, 'Readable stream: throwing strategy.size getter');
promise_test(t => {
const controllerError = { name: 'controller error' };
const thrownError = { name: 'thrown error' };
let controller;
const rs = new ReadableStream(
{
start(c) {
controller = c;
}
},
{
size() {
controller.error(controllerError);
throw thrownError;
},
highWaterMark: 5
}
);
assert_throws_exactly(thrownError, () => controller.enqueue('a'), 'enqueue should re-throw the error');
return promise_rejects_exactly(t, controllerError, rs.getReader().closed);
}, 'Readable stream: strategy.size errors the stream and then throws');
promise_test(t => {
const theError = { name: 'my error' };
let controller;
const rs = new ReadableStream(
{
start(c) {
controller = c;
}
},
{
size() {
controller.error(theError);
return Infinity;
},
highWaterMark: 5
}
);
assert_throws_js(RangeError, () => controller.enqueue('a'), 'enqueue should throw a RangeError');
return promise_rejects_exactly(t, theError, rs.getReader().closed, 'closed should reject with the error');
}, '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_exactly(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_exactly(theError, () => {
new ReadableStream({}, {
size() {
return 1;
},
get highWaterMark() {
throw theError;
}
});
}, 'construction should re-throw the error');
}, 'Readable stream: throwing strategy.highWaterMark getter');
test(() => {
for (const highWaterMark of [-1, -Infinity, NaN, 'foo', {}]) {
assert_throws_js(RangeError, () => {
new ReadableStream({}, {
size() {
return 1;
},
highWaterMark
});
}, 'construction should throw a RangeError for ' + highWaterMark);
}
}, 'Readable stream: invalid strategy.highWaterMark');
promise_test(() => {
const promises = [];
for (const 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.then(() => {
assert_unreached('closed didn\'t throw');
}, e => {
assert_equals(e, theError, 'closed should reject with the error for ' + size);
}));
}
return Promise.all(promises);
}, 'Readable stream: invalid strategy.size return value');
promise_test(() => {
const promises = [];
for (const size of [NaN, -Infinity, Infinity, -1]) {
let theError;
const rs = new ReadableStream(
{
pull(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.then(() => {
assert_unreached('closed didn\'t throw');
}, e => {
assert_equals(e, theError, 'closed should reject with the error for ' + size);
}));
}
return Promise.all(promises);
}, 'Readable stream: invalid strategy.size return value when pulling');

View File

@ -0,0 +1,400 @@
// META: global=window,worker,shadowrealm
'use strict';
test(() => {
const theError = new Error('a unique string');
assert_throws_exactly(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_exactly(theError, () => {
new ReadableStream({
start() {
throw theError;
}
});
}, 'constructing the stream should re-throw the error');
}, 'Underlying source start: throwing method');
test(() => {
const theError = new Error('a unique string');
assert_throws_exactly(theError, () => new ReadableStream({
get pull() {
throw theError;
}
}), 'constructor should throw');
}, '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_exactly(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 first chunk read should be correct');
}),
reader.read().then(r => {
assert_object_equals(r, { value: 'a', done: false }, 'the second chunk read should be correct');
assert_equals(counter, 1, 'counter should be 1');
})
]);
}, 'Underlying source pull: throwing getter (second pull does not result in a second get)');
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_exactly(t, theError, reader.closed)
]);
}, 'Underlying source pull: throwing method (second pull)');
test(() => {
const theError = new Error('a unique string');
assert_throws_exactly(theError, () => new ReadableStream({
get cancel() {
throw theError;
}
}), 'constructor should throw');
}, '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_exactly(t, theError, rs.cancel());
}, 'Underlying source cancel: throwing method');
promise_test(() => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
rs.cancel();
assert_throws_js(TypeError, () => controller.enqueue('a'), 'Calling enqueue after canceling should throw');
return rs.getReader().closed;
}, 'Underlying source: calling enqueue on an empty canceled stream should throw');
promise_test(() => {
let controller;
const rs = new ReadableStream({
start(c) {
c.enqueue('a');
c.enqueue('b');
controller = c;
}
});
rs.cancel();
assert_throws_js(TypeError, () => controller.enqueue('c'), 'Calling enqueue after canceling should throw');
return rs.getReader().closed;
}, 'Underlying source: calling enqueue on a non-empty canceled stream should throw');
promise_test(() => {
return new ReadableStream({
start(c) {
c.close();
assert_throws_js(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_js(TypeError, () => c.enqueue('a'), 'call to enqueue should throw the error');
}
}).getReader().closed;
return promise_rejects_exactly(t, theError, closed);
}, 'Underlying source: calling enqueue on an errored stream should throw');
promise_test(() => {
return new ReadableStream({
start(c) {
c.close();
assert_throws_js(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_js(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();
assert_throws_js(TypeError, () => controller.close(), 'Calling close after canceling should throw');
return rs.getReader().closed.then(() => {
assert_true(startCalled);
});
}, 'Underlying source: calling close on an empty canceled stream should throw');
promise_test(() => {
let controller;
let startCalled = false;
const rs = new ReadableStream({
start(c) {
controller = c;
c.enqueue('a');
startCalled = true;
}
});
rs.cancel();
assert_throws_js(TypeError, () => controller.close(), 'Calling close after canceling should throw');
return rs.getReader().closed.then(() => {
assert_true(startCalled);
});
}, 'Underlying source: calling close on a non-empty canceled stream should throw');
promise_test(() => {
const theError = new Error('boo');
let startCalled = false;
const closed = new ReadableStream({
start(c) {
c.error(theError);
assert_throws_js(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);
c.error();
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 not throw');
promise_test(() => {
let startCalled = false;
const closed = new ReadableStream({
start(c) {
c.close();
c.error();
startCalled = true;
}
}).getReader().closed;
return closed.then(() => assert_true(startCalled));
}, 'Underlying source: calling error after close should not 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');
const error1 = { name: 'error1' };
promise_test(t => {
let pullShouldThrow = false;
const rs = new ReadableStream({
pull(controller) {
if (pullShouldThrow) {
throw error1;
}
controller.enqueue(0);
}
}, new CountQueuingStrategy({highWaterMark: 1}));
const reader = rs.getReader();
return Promise.resolve().then(() => {
pullShouldThrow = true;
return Promise.all([
reader.read(),
promise_rejects_exactly(t, error1, reader.closed, '.closed promise should reject')
]);
});
}, 'read should not error if it dequeues and pull() throws');

View File

@ -0,0 +1,261 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
// META: script=../resources/rs-utils.js
'use strict';
promise_test(t => {
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();
return new Promise(resolve => {
t.step_timeout(() => {
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(() => {
const 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(t => {
let resolveSourceCancelPromise;
let sourceCancelPromiseHasFulfilled = false;
const rs = new ReadableStream({
cancel() {
const sourceCancelPromise = new Promise(resolve => resolveSourceCancelPromise = resolve);
sourceCancelPromise.then(() => {
sourceCancelPromiseHasFulfilled = true;
});
return sourceCancelPromise;
}
});
t.step_timeout(() => 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(t => {
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.');
t.step_timeout(() => 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');
promise_test(async () => {
const events = [];
const pendingPromise = new Promise(() => {});
const rs = new ReadableStream({
pull() {
events.push('pull');
return pendingPromise;
},
cancel() {
events.push('cancel');
}
});
const reader = rs.getReader();
reader.read().catch(() => {}); // No await.
await delay(0);
await Promise.all([reader.cancel(), reader.closed]);
assert_array_equals(events, ['pull', 'cancel'], 'cancel should have been called');
}, 'ReadableStream cancellation: underlyingSource.cancel() should called, even with pending pull');

View File

@ -0,0 +1,17 @@
// META: global=window,worker,shadowrealm
'use strict';
const error1 = new Error('error1');
error1.name = 'error1';
const error2 = new Error('error2');
error2.name = 'error2';
test(() => {
const underlyingSource = { get start() { throw error1; } };
const queuingStrategy = { highWaterMark: 0, get size() { throw error2; } };
// underlyingSource is converted in prose in the method body, whereas queuingStrategy is done at the IDL layer.
// So the queuingStrategy exception should be encountered first.
assert_throws_exactly(error2, () => new ReadableStream(underlyingSource, queuingStrategy));
}, 'underlyingSource argument should be converted after queuingStrategy argument');

View File

@ -0,0 +1,208 @@
// META: global=window,worker,shadowrealm
'use strict';
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');
return 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)');

View File

@ -0,0 +1,18 @@
<html class="test-wait">
<meta charset="utf-8">
<script type="module">
let a = window.open()
try {
let dir = await a.navigator.storage.getDirectory()
let hdl = await dir.getFileHandle("7399d8cf-9ff9-494d-89eb-d3045f229c27", {"create": true})
let map = new Map([[]])
let b = ReadableStream.from(map)
let c = await hdl.createWritable({ })
await b.pipeTo(c, { }).catch(() => {
// Error expected as we are not piping the right form of chunk to FileHandle
})
} finally {
document.documentElement.classList.remove("test-wait")
a.close()
}
</script>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html class="test-wait">
<meta charset="utf-8">
<script>
var c = new Worker("/streams/readable-streams/crashtests/strategy-worker.js");
c.onmessage = () => {
c.terminate();
document.documentElement.classList.remove("test-wait");
}
</script>

View File

@ -0,0 +1,4 @@
var b = new CountQueuingStrategy({ highWaterMark: 3 });
importScripts("empty.js");
postMessage("done");

View File

@ -0,0 +1,13 @@
// This is a repro for a crash bug that existed in Blink. See
// https://crbug.com/1290014. If there's no crash then the test passed.
test(t => {
const iframeTag = document.createElement('iframe');
document.body.appendChild(iframeTag);
const readableStream = new ReadableStream();
const reader = new iframeTag.contentWindow.ReadableStreamDefaultReader(readableStream);
iframeTag.remove();
reader.cancel();
reader.read();
}, 'should not crash on reading from stream cancelled in destroyed realm');

View File

@ -0,0 +1,539 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/rs-utils.js
'use strict';
test(() => {
assert_throws_js(TypeError, () => new ReadableStreamDefaultReader('potato'));
assert_throws_js(TypeError, () => new ReadableStreamDefaultReader({}));
assert_throws_js(TypeError, () => new ReadableStreamDefaultReader());
}, 'ReadableStreamDefaultReader constructor should get a ReadableStream object as argument');
test(() => {
const rsReader = new ReadableStreamDefaultReader(new ReadableStream());
assert_equals(rsReader.closed, rsReader.closed, 'closed should return the same promise');
}, 'ReadableStreamDefaultReader closed should always return the same promise object');
test(() => {
const rs = new ReadableStream();
new ReadableStreamDefaultReader(rs); // Constructing directly the first time should be fine.
assert_throws_js(TypeError, () => new ReadableStreamDefaultReader(rs),
'constructing directly the second time should fail');
}, 'Constructing a ReadableStreamDefaultReader directly should fail if the stream is already locked (via direct ' +
'construction)');
test(() => {
const rs = new ReadableStream();
new ReadableStreamDefaultReader(rs); // Constructing directly should be fine.
assert_throws_js(TypeError, () => rs.getReader(), 'getReader() should fail');
}, 'Getting a ReadableStreamDefaultReader 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_js(TypeError, () => new ReadableStreamDefaultReader(rs), 'constructing directly should fail');
}, 'Constructing a ReadableStreamDefaultReader directly should fail if the stream is already locked (via getReader)');
test(() => {
const rs = new ReadableStream();
rs.getReader(); // getReader() should be fine.
assert_throws_js(TypeError, () => rs.getReader(), 'getReader() should fail');
}, 'Getting a ReadableStreamDefaultReader via getReader should fail if the stream is already locked (via getReader)');
test(() => {
const rs = new ReadableStream({
start(c) {
c.close();
}
});
new ReadableStreamDefaultReader(rs); // Constructing directly should not throw.
}, 'Constructing a ReadableStreamDefaultReader 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 ReadableStreamDefaultReader(rs); // Constructing directly should not throw.
}, 'Constructing a ReadableStreamDefaultReader 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_js(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_js(t, TypeError, reader1.closed),
reader2.closed
]);
}, 'closed should be rejected after reader releases its lock (multiple stream locks)');
promise_test(t => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const reader = rs.getReader();
const promise1 = reader.closed;
controller.close();
reader.releaseLock();
const promise2 = reader.closed;
assert_not_equals(promise1, promise2, '.closed should be replaced');
return Promise.all([
promise1,
promise_rejects_js(t, TypeError, promise2, '.closed after releasing lock'),
]);
}, 'closed is replaced when stream closes and reader releases its lock');
promise_test(t => {
const theError = { name: 'unique error' };
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const reader = rs.getReader();
const promise1 = reader.closed;
controller.error(theError);
reader.releaseLock();
const promise2 = reader.closed;
assert_not_equals(promise1, promise2, '.closed should be replaced');
return Promise.all([
promise_rejects_exactly(t, theError, promise1, '.closed before releasing lock'),
promise_rejects_js(t, TypeError, promise2, '.closed after releasing lock')
]);
}, 'closed is replaced when stream errors and reader releases its lock');
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_js(t, 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_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader1.read())
);
assert_throws_js(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_exactly(t, theError, reader2.closed),
promise_rejects_exactly(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;
}, 'ReadableStreamDefaultReader 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');
}
);
}, 'ReadableStreamDefaultReader: 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_exactly(t, theError, rs.getReader().closed);
controller.error(theError);
return promise;
}, 'Erroring a ReadableStream after checking closed should reject ReadableStreamDefaultReader 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_exactly(t, theError, rs.getReader().closed);
}, 'Erroring a ReadableStream before checking closed should reject ReadableStreamDefaultReader 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_exactly(t, myError, reader.read()),
promise_rejects_exactly(t, myError, reader.read()),
promise_rejects_exactly(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_exactly(t, myError, reader.read()),
promise_rejects_exactly(t, myError, reader.read()),
promise_rejects_exactly(t, myError, reader.closed)
]);
controller.error(myError);
return promise;
}, 'Reading twice on a stream that gets errored');
test(() => {
const rs = new ReadableStream();
let toStringCalled = false;
const mode = {
toString() {
toStringCalled = true;
return '';
}
};
assert_throws_js(TypeError, () => rs.getReader({ mode }), 'getReader() should throw');
assert_true(toStringCalled, 'toString() should be called');
}, 'getReader() should call ToString() on mode');
promise_test(() => {
const rs = new ReadableStream({
pull(controller) {
controller.close();
}
});
const reader = rs.getReader();
return reader.read().then(() => {
// The test passes if releaseLock() does not throw.
reader.releaseLock();
});
}, 'controller.close() should clear the list of pending read requests');
promise_test(t => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const reader1 = rs.getReader();
const promise1 = promise_rejects_js(t, TypeError, reader1.read(), 'read() from reader1 should reject when reader1 is released');
reader1.releaseLock();
controller.enqueue('a');
const reader2 = rs.getReader();
const promise2 = reader2.read().then(r => {
assert_object_equals(r, { value: 'a', done: false }, 'read() from reader2 should resolve with enqueued chunk');
})
reader2.releaseLock();
return Promise.all([promise1, promise2]);
}, 'Second reader can read chunks after first reader was released with pending read requests');

View File

@ -0,0 +1,116 @@
// META: global=window,worker,shadowrealm
'use strict';
// Due to the limitations of floating-point precision, the calculation of desiredSize sometimes gives different answers
// than adding up the items in the queue would. It is important that implementations give the same result in these edge
// cases so that developers do not come to depend on non-standard behaviour. See
// https://github.com/whatwg/streams/issues/582 and linked issues for further discussion.
promise_test(() => {
const { reader, controller } = setupTestStream();
controller.enqueue(2);
assert_equals(controller.desiredSize, 0 - 2, 'desiredSize must be -2 after enqueueing such a chunk');
controller.enqueue(Number.MAX_SAFE_INTEGER);
assert_equals(controller.desiredSize, 0 - Number.MAX_SAFE_INTEGER - 2,
'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
return reader.read().then(() => {
assert_equals(controller.desiredSize, 0 - Number.MAX_SAFE_INTEGER - 2 + 2,
'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
return reader.read();
}).then(() => {
assert_equals(controller.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative');
});
}, 'Floating point arithmetic must manifest near NUMBER.MAX_SAFE_INTEGER (total ends up positive)');
promise_test(() => {
const { reader, controller } = setupTestStream();
controller.enqueue(1e-16);
assert_equals(controller.desiredSize, 0 - 1e-16, 'desiredSize must be -1e16 after enqueueing such a chunk');
controller.enqueue(1);
assert_equals(controller.desiredSize, 0 - 1e-16 - 1,
'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
return reader.read().then(() => {
assert_equals(controller.desiredSize, 0 - 1e-16 - 1 + 1e-16,
'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
return reader.read();
}).then(() => {
assert_equals(controller.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative');
});
}, 'Floating point arithmetic must manifest near 0 (total ends up positive, but clamped)');
promise_test(() => {
const { reader, controller } = setupTestStream();
controller.enqueue(1e-16);
assert_equals(controller.desiredSize, 0 - 1e-16, 'desiredSize must be -2e16 after enqueueing such a chunk');
controller.enqueue(1);
assert_equals(controller.desiredSize, 0 - 1e-16 - 1,
'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
controller.enqueue(2e-16);
assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16,
'desiredSize must be calculated using double-precision floating-point arithmetic (adding a third chunk)');
return reader.read().then(() => {
assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16,
'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
return reader.read();
}).then(() => {
assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1,
'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a second chunk)');
return reader.read();
}).then(() => {
assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1 + 2e-16,
'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a third chunk)');
});
}, 'Floating point arithmetic must manifest near 0 (total ends up positive, and not clamped)');
promise_test(() => {
const { reader, controller } = setupTestStream();
controller.enqueue(2e-16);
assert_equals(controller.desiredSize, 0 - 2e-16, 'desiredSize must be -2e16 after enqueueing such a chunk');
controller.enqueue(1);
assert_equals(controller.desiredSize, 0 - 2e-16 - 1,
'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
return reader.read().then(() => {
assert_equals(controller.desiredSize, 0 - 2e-16 - 1 + 2e-16,
'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
return reader.read();
}).then(() => {
assert_equals(controller.desiredSize, 0,
'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a second chunk)');
});
}, 'Floating point arithmetic must manifest near 0 (total ends up zero)');
function setupTestStream() {
const strategy = {
size(x) {
return x;
},
highWaterMark: 0
};
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, strategy);
return { reader: rs.getReader(), controller };
}

View File

@ -0,0 +1,492 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
'use strict';
const iterableFactories = [
['an array of values', () => {
return ['a', 'b'];
}],
['an array of promises', () => {
return [
Promise.resolve('a'),
Promise.resolve('b')
];
}],
['an array iterator', () => {
return ['a', 'b'][Symbol.iterator]();
}],
['a string', () => {
// This iterates over the code points of the string.
return 'ab';
}],
['a Set', () => {
return new Set(['a', 'b']);
}],
['a Set iterator', () => {
return new Set(['a', 'b'])[Symbol.iterator]();
}],
['a sync generator', () => {
function* syncGenerator() {
yield 'a';
yield 'b';
}
return syncGenerator();
}],
['an async generator', () => {
async function* asyncGenerator() {
yield 'a';
yield 'b';
}
return asyncGenerator();
}],
['a sync iterable of values', () => {
const chunks = ['a', 'b'];
const iterator = {
next() {
return {
done: chunks.length === 0,
value: chunks.shift()
};
}
};
const iterable = {
[Symbol.iterator]: () => iterator
};
return iterable;
}],
['a sync iterable of promises', () => {
const chunks = ['a', 'b'];
const iterator = {
next() {
return chunks.length === 0 ? { done: true } : {
done: false,
value: Promise.resolve(chunks.shift())
};
}
};
const iterable = {
[Symbol.iterator]: () => iterator
};
return iterable;
}],
['an async iterable', () => {
const chunks = ['a', 'b'];
const asyncIterator = {
next() {
return Promise.resolve({
done: chunks.length === 0,
value: chunks.shift()
})
}
};
const asyncIterable = {
[Symbol.asyncIterator]: () => asyncIterator
};
return asyncIterable;
}],
['a ReadableStream', () => {
return new ReadableStream({
start(c) {
c.enqueue('a');
c.enqueue('b');
c.close();
}
});
}],
['a ReadableStream async iterator', () => {
return new ReadableStream({
start(c) {
c.enqueue('a');
c.enqueue('b');
c.close();
}
})[Symbol.asyncIterator]();
}]
];
for (const [label, factory] of iterableFactories) {
promise_test(async () => {
const iterable = factory();
const rs = ReadableStream.from(iterable);
assert_equals(rs.constructor, ReadableStream, 'from() should return a ReadableStream');
const reader = rs.getReader();
assert_object_equals(await reader.read(), { value: 'a', done: false }, 'first read should be correct');
assert_object_equals(await reader.read(), { value: 'b', done: false }, 'second read should be correct');
assert_object_equals(await reader.read(), { value: undefined, done: true }, 'third read should be done');
await reader.closed;
}, `ReadableStream.from accepts ${label}`);
}
const badIterables = [
['null', null],
['undefined', undefined],
['0', 0],
['NaN', NaN],
['true', true],
['{}', {}],
['Object.create(null)', Object.create(null)],
['a function', () => 42],
['a symbol', Symbol()],
['an object with a non-callable @@iterator method', {
[Symbol.iterator]: 42
}],
['an object with a non-callable @@asyncIterator method', {
[Symbol.asyncIterator]: 42
}],
];
for (const [label, iterable] of badIterables) {
test(() => {
assert_throws_js(TypeError, () => ReadableStream.from(iterable), 'from() should throw a TypeError')
}, `ReadableStream.from throws on invalid iterables; specifically ${label}`);
}
test(() => {
const theError = new Error('a unique string');
const iterable = {
[Symbol.iterator]() {
throw theError;
}
};
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from re-throws errors from calling the @@iterator method`);
test(() => {
const theError = new Error('a unique string');
const iterable = {
[Symbol.asyncIterator]() {
throw theError;
}
};
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from re-throws errors from calling the @@asyncIterator method`);
test(t => {
const theError = new Error('a unique string');
const iterable = {
[Symbol.iterator]: t.unreached_func('@@iterator should not be called'),
[Symbol.asyncIterator]() {
throw theError;
}
};
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from ignores @@iterator if @@asyncIterator exists`);
test(() => {
const theError = new Error('a unique string');
const iterable = {
[Symbol.asyncIterator]: null,
[Symbol.iterator]() {
throw theError
}
};
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from ignores a null @@asyncIterator`);
promise_test(async () => {
const iterable = {
async next() {
return { value: undefined, done: true };
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
const reader = rs.getReader();
const read = await reader.read();
assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
await reader.closed;
}, `ReadableStream.from accepts an empty iterable`);
promise_test(async t => {
const theError = new Error('a unique string');
const iterable = {
async next() {
throw theError;
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
const reader = rs.getReader();
await Promise.all([
promise_rejects_exactly(t, theError, reader.read()),
promise_rejects_exactly(t, theError, reader.closed)
]);
}, `ReadableStream.from: stream errors when next() rejects`);
promise_test(async t => {
const iterable = {
next() {
return new Promise(() => {});
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
const reader = rs.getReader();
await Promise.race([
reader.read().then(t.unreached_func('read() should not resolve'), t.unreached_func('read() should not reject')),
reader.closed.then(t.unreached_func('closed should not resolve'), t.unreached_func('closed should not reject')),
flushAsyncEvents()
]);
}, 'ReadableStream.from: stream stalls when next() never settles');
promise_test(async () => {
let nextCalls = 0;
let nextArgs;
const iterable = {
async next(...args) {
nextCalls += 1;
nextArgs = args;
return { value: 'a', done: false };
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
const reader = rs.getReader();
await flushAsyncEvents();
assert_equals(nextCalls, 0, 'next() should not be called yet');
const read = await reader.read();
assert_object_equals(read, { value: 'a', done: false }, 'first read should be correct');
assert_equals(nextCalls, 1, 'next() should be called after first read()');
assert_array_equals(nextArgs, [], 'next() should be called with no arguments');
}, `ReadableStream.from: calls next() after first read()`);
promise_test(async t => {
const theError = new Error('a unique string');
let returnCalls = 0;
let returnArgs;
let resolveReturn;
const iterable = {
next: t.unreached_func('next() should not be called'),
throw: t.unreached_func('throw() should not be called'),
async return(...args) {
returnCalls += 1;
returnArgs = args;
await new Promise(r => resolveReturn = r);
return { done: true };
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
const reader = rs.getReader();
assert_equals(returnCalls, 0, 'return() should not be called yet');
let cancelResolved = false;
const cancelPromise = reader.cancel(theError).then(() => {
cancelResolved = true;
});
await flushAsyncEvents();
assert_equals(returnCalls, 1, 'return() should be called');
assert_array_equals(returnArgs, [theError], 'return() should be called with cancel reason');
assert_false(cancelResolved, 'cancel() should not resolve while promise from return() is pending');
resolveReturn();
await Promise.all([
cancelPromise,
reader.closed
]);
}, `ReadableStream.from: cancelling the returned stream calls and awaits return()`);
promise_test(async t => {
let nextCalls = 0;
let returnCalls = 0;
const iterable = {
async next() {
nextCalls += 1;
return { value: undefined, done: true };
},
throw: t.unreached_func('throw() should not be called'),
async return() {
returnCalls += 1;
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
const reader = rs.getReader();
const read = await reader.read();
assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
assert_equals(nextCalls, 1, 'next() should be called once');
await reader.closed;
assert_equals(returnCalls, 0, 'return() should not be called');
}, `ReadableStream.from: return() is not called when iterator completes normally`);
promise_test(async t => {
const theError = new Error('a unique string');
const iterable = {
next: t.unreached_func('next() should not be called'),
throw: t.unreached_func('throw() should not be called'),
async return() {
return 42;
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
const reader = rs.getReader();
await promise_rejects_js(t, TypeError, reader.cancel(theError), 'cancel() should reject with a TypeError');
await reader.closed;
}, `ReadableStream.from: cancel() rejects when return() fulfills with a non-object`);
promise_test(async () => {
let nextCalls = 0;
let reader;
let values = ['a', 'b', 'c'];
const iterable = {
async next() {
nextCalls += 1;
if (nextCalls === 1) {
reader.read();
}
return { value: values.shift(), done: false };
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
reader = rs.getReader();
const read1 = await reader.read();
assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct');
await flushAsyncEvents();
assert_equals(nextCalls, 2, 'next() should be called two times');
const read2 = await reader.read();
assert_object_equals(read2, { value: 'c', done: false }, 'second read should be correct');
assert_equals(nextCalls, 3, 'next() should be called three times');
}, `ReadableStream.from: reader.read() inside next()`);
promise_test(async () => {
let nextCalls = 0;
let returnCalls = 0;
let reader;
const iterable = {
async next() {
nextCalls++;
await reader.cancel();
assert_equals(returnCalls, 1, 'return() should be called once');
return { value: 'something else', done: false };
},
async return() {
returnCalls++;
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
reader = rs.getReader();
const read = await reader.read();
assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
assert_equals(nextCalls, 1, 'next() should be called once');
await reader.closed;
}, `ReadableStream.from: reader.cancel() inside next()`);
promise_test(async t => {
let returnCalls = 0;
let reader;
const iterable = {
next: t.unreached_func('next() should not be called'),
async return() {
returnCalls++;
await reader.cancel();
return { done: true };
},
[Symbol.asyncIterator]: () => iterable
};
const rs = ReadableStream.from(iterable);
reader = rs.getReader();
await reader.cancel();
assert_equals(returnCalls, 1, 'return() should be called once');
await reader.closed;
}, `ReadableStream.from: reader.cancel() inside return()`);
promise_test(async t => {
let array = ['a', 'b'];
const rs = ReadableStream.from(array);
const reader = rs.getReader();
const read1 = await reader.read();
assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct');
const read2 = await reader.read();
assert_object_equals(read2, { value: 'b', done: false }, 'second read should be correct');
array.push('c');
const read3 = await reader.read();
assert_object_equals(read3, { value: 'c', done: false }, 'third read after push() should be correct');
const read4 = await reader.read();
assert_object_equals(read4, { value: undefined, done: true }, 'fourth read should be done');
await reader.closed;
}, `ReadableStream.from(array), push() to array while reading`);

View File

@ -0,0 +1,71 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
// META: script=/common/gc.js
'use strict';
promise_test(async () => {
let controller;
new ReadableStream({
start(c) {
controller = c;
}
});
await garbageCollect();
return delay(50).then(() => {
controller.close();
assert_throws_js(TypeError, () => controller.close(), 'close should throw a TypeError the second time');
controller.error();
});
}, 'ReadableStreamController methods should continue working properly when scripts lose their reference to the ' +
'readable stream');
promise_test(async () => {
let controller;
const closedPromise = new ReadableStream({
start(c) {
controller = c;
}
}).getReader().closed;
await 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(async t => {
const theError = new Error('boo');
let controller;
const closedPromise = new ReadableStream({
start(c) {
controller = c;
}
}).getReader().closed;
await garbageCollect();
return delay(50).then(() => controller.error(theError))
.then(() => promise_rejects_exactly(t, theError, closedPromise));
}, 'ReadableStream closed promise should reject even if stream and reader JS references are lost');
promise_test(async () => {
const rs = new ReadableStream({});
rs.getReader();
await garbageCollect();
return delay(50).then(() => assert_throws_js(TypeError, () => rs.getReader(),
'old reader should still be locking the stream even after garbage collection'));
}, 'Garbage-collecting a ReadableStreamDefaultReader should not unlock its stream');

View File

@ -0,0 +1,840 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
// META: script=../resources/rs-utils.js
'use strict';
const error1 = new Error('error1');
error1.name = 'error1';
test(() => {
new ReadableStream(); // ReadableStream constructed with no parameters
new ReadableStream({ }); // ReadableStream constructed with an empty object as parameter
new ReadableStream({ type: undefined }); // ReadableStream constructed with undefined type
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_js(TypeError, () => new ReadableStream(null), 'constructor should throw when the source is null');
}, 'ReadableStream can\'t be constructed with garbage');
test(() => {
assert_throws_js(TypeError, () => new ReadableStream({ type: null }),
'constructor should throw when the type is null');
assert_throws_js(TypeError, () => new ReadableStream({ type: '' }),
'constructor should throw when the type is empty string');
assert_throws_js(TypeError, () => new ReadableStream({ type: 'asdf' }),
'constructor should throw when the type is asdf');
assert_throws_exactly(
error1,
() => new ReadableStream({ type: { get toString() { throw error1; } } }),
'constructor should throw when ToString() throws'
);
assert_throws_exactly(
error1,
() => new ReadableStream({ type: { toString() { throw error1; } } }),
'constructor should throw when ToString() throws'
);
}, 'ReadableStream can\'t be constructed with an invalid type');
test(() => {
assert_throws_js(TypeError, () => {
new ReadableStream({ start: 'potato' });
}, 'constructor should throw when start is not a function');
}, 'ReadableStream constructor should throw for non-function start arguments');
test(() => {
assert_throws_js(TypeError, () => new ReadableStream({ cancel: '2' }), 'constructor should throw');
}, 'ReadableStream constructor will not tolerate initial garbage as cancel argument');
test(() => {
assert_throws_js(TypeError, () => new ReadableStream({ pull: { } }), 'constructor should throw');
}, 'ReadableStream constructor will not tolerate initial garbage as pull argument');
test(() => {
let startCalled = false;
const source = {
start() {
assert_equals(this, source, 'source is this during start');
startCalled = true;
}
};
new ReadableStream(source);
assert_true(startCalled);
}, 'ReadableStream start should be called with the proper thisArg');
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');
test(() => {
(new ReadableStream()).getReader(undefined);
(new ReadableStream()).getReader({});
(new ReadableStream()).getReader({ mode: undefined, notmode: 'ignored' });
assert_throws_js(TypeError, () => (new ReadableStream()).getReader({ mode: 'potato' }));
}, 'default ReadableStream getReader() should only accept mode:undefined');
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 (const 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_false(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_true(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;
new ReadableStream({
pull() {
pullCount++;
}
});
return flushAsyncEvents().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 flushAsyncEvents().then(() => {
assert_equals(pullCount, 1, 'pull should be called once start finishes');
}).then(() => {
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 rs = new ReadableStream({
start(c) {
c.enqueue('a');
},
pull() {
pullCount++;
}
});
const read = rs.getReader().read();
assert_equals(pullCount, 0, 'calling read() should not cause pull to be called yet');
return flushAsyncEvents().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');
},
pull() {
pullCount++;
}
});
return flushAsyncEvents().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 rs = new ReadableStream({
start(c) {
controller = c;
},
pull() {
++pullCount;
}
});
const reader = rs.getReader();
return flushAsyncEvents().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 rs = new ReadableStream({
start(c) {
controller = c;
},
pull() {
++pullCount;
}
});
const reader = rs.getReader();
return flushAsyncEvents().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 rs = new ReadableStream({
pull(c) {
c.enqueue(++timesCalled);
returnedPromise = new Promise(r => resolve = r);
return returnedPromise;
}
});
const reader = rs.getReader();
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 rs = new ReadableStream(
{
start(c) {
c.enqueue('a');
c.enqueue('b');
c.enqueue('c');
},
pull() {
++timesCalled;
}
},
{
size() {
return 1;
},
highWaterMark: Infinity
}
);
const reader = rs.getReader();
return flushAsyncEvents().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.');
promise_test(t => {
const controllerError = { name: 'controller error' };
const rs = new ReadableStream({
pull(c) {
c.error(controllerError);
}
});
return promise_rejects_exactly(t, controllerError, rs.getReader().closed);
}, 'ReadableStream pull should be able to error a stream.');
promise_test(t => {
const controllerError = { name: 'controller error' };
const thrownError = { name: 'thrown error' };
const rs = new ReadableStream({
pull(c) {
c.error(controllerError);
throw thrownError;
}
});
return promise_rejects_exactly(t, controllerError, rs.getReader().closed);
}, 'ReadableStream pull should be able to error a stream and throw.');
test(() => {
let startCalled = false;
new ReadableStream({
start(c) {
assert_equals(c.enqueue('a'), undefined, 'the first enqueue should return undefined');
c.close();
assert_throws_js(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_js(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');
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(() => {
new ReadableStream({
start(c) {
assert_equals(c.desiredSize, 10, 'desiredSize must start at highWaterMark');
c.close();
assert_equals(c.desiredSize, 0, 'after closing, desiredSize must be 0');
}
}, {
highWaterMark: 10
});
}, 'ReadableStream: desiredSize when closed');
test(() => {
new ReadableStream({
start(c) {
assert_equals(c.desiredSize, 10, 'desiredSize must start at highWaterMark');
c.error();
assert_equals(c.desiredSize, null, 'after erroring, desiredSize must be null');
}
}, {
highWaterMark: 10
});
}, 'ReadableStream: desiredSize when errored');
test(() => {
class Subclass extends ReadableStream {
extraFunction() {
return true;
}
}
assert_equals(
Object.getPrototypeOf(Subclass.prototype), ReadableStream.prototype,
'Subclass.prototype\'s prototype should be ReadableStream.prototype');
assert_equals(Object.getPrototypeOf(Subclass), ReadableStream,
'Subclass\'s prototype should be ReadableStream');
const sub = new Subclass();
assert_true(sub instanceof ReadableStream,
'Subclass object should be an instance of ReadableStream');
assert_true(sub instanceof Subclass,
'Subclass object should be an instance of Subclass');
const lockedGetter = Object.getOwnPropertyDescriptor(
ReadableStream.prototype, 'locked').get;
assert_equals(lockedGetter.call(sub), sub.locked,
'Subclass object should pass brand check');
assert_true(sub.extraFunction(),
'extraFunction() should be present on Subclass object');
}, 'Subclassing ReadableStream should work');
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 (const 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');

View File

@ -0,0 +1,162 @@
<!doctype html>
<meta charset="utf-8">
<title>Ensure Stream objects are created in expected globals. </title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body></body>
<script>
// These tests are loosely derived from Gecko's readable-stream-globals.js,
// which is a test case designed around the JS Streams implementation.
//
// Unlike in JS Streams, where function calls switch realms and change
// the resulting global of the resulting objects, in WebIDL streams,
// the global of an object is (currently underspecified, but) intended
// to be the "Relevant Global" of the 'this' object.
//
// See:
// https://html.spec.whatwg.org/multipage/webappapis.html#relevant
// https://github.com/whatwg/streams/issues/1213
"use strict"
const iframe = document.createElement("iframe")
document.body.append(iframe)
const otherGlobal = iframe.contentWindow;
const OtherReadableStream = otherGlobal.ReadableStream
const OtherReadableStreamDefaultReader = otherGlobal.ReadableStreamDefaultReader;
const OtherReadableStreamDefaultController = otherGlobal.ReadableStreamDefaultController;
promise_test(async () => {
// Controllers
let controller;
let otherController;
// Get Stream Prototypes and controllers.
let streamController;
let stream = new ReadableStream({start(c) { streamController = c; }});
const callReaderThisGlobal = OtherReadableStream.prototype.getReader.call(stream);
const newReaderOtherGlobal = new OtherReadableStreamDefaultReader(new ReadableStream());
// Relevant Global Checking.
assert_equals(callReaderThisGlobal instanceof ReadableStreamDefaultReader, true, "reader was created in this global (.call)");
assert_equals(newReaderOtherGlobal instanceof ReadableStreamDefaultReader, false, "reader was created in other global (new)");
assert_equals(callReaderThisGlobal instanceof OtherReadableStreamDefaultReader, false, "reader isn't coming from other global (.call)" );
assert_equals(newReaderOtherGlobal instanceof OtherReadableStreamDefaultReader, true, "reader isn't coming from other global (new)");
assert_equals(otherController instanceof ReadableStreamDefaultController, false, "otherController should come from other gloal")
const request = callReaderThisGlobal.read();
assert_equals(request instanceof Promise, true, "Promise comes from this global");
streamController.close();
const requestResult = await request;
assert_equals(requestResult instanceof Object, true, "returned object comes from this global");
}, "Stream objects created in expected globals")
promise_test(async () => {
const stream = new ReadableStream();
const otherReader = new OtherReadableStreamDefaultReader(stream);
const cancelPromise = ReadableStreamDefaultReader.prototype.cancel.call(otherReader);
assert_equals(cancelPromise instanceof Promise, true, "Cancel promise comes from the same global as the stream");
assert_equals(await cancelPromise, undefined, "Cancel promise resolves to undefined");
}, "Cancel promise is created in same global as stream")
// Refresh the streams and controllers.
function getFreshInstances() {
let controller;
let otherController;
let stream = new ReadableStream({
start(c) {
controller = c;
}
});
new OtherReadableStream({
start(c) {
otherController = c;
}
});
return {stream, controller, otherController}
}
promise_test(async () => {
// Test closed promise on reader from another global (connected to a this-global stream)
const {stream, controller, otherController} = getFreshInstances();
const otherReader = new OtherReadableStreamDefaultReader(stream);
const closedPromise = otherReader.closed;
assert_equals(closedPromise instanceof otherGlobal.Promise, true, "Closed promise in other global.");
}, "Closed Promise in correct global");
promise_test(async () => {
const {stream, controller, otherController} = getFreshInstances();
const otherReader = OtherReadableStream.prototype.getReader.call(stream);
assert_equals(otherReader instanceof ReadableStreamDefaultReader, true, "Reader comes from this global")
const request = otherReader.read();
assert_equals(request instanceof Promise, true, "Promise still comes from stream's realm (this realm)");
otherController.close.call(controller);
assert_equals((await request) instanceof otherGlobal.Object, true, "Object comes from other realm");
}, "Reader objects in correct global");
promise_test(async () => {
const {stream, controller, otherController} = getFreshInstances();
assert_equals(controller.desiredSize, 1, "Desired size is expected");
Object.defineProperty(controller, "desiredSize",
Object.getOwnPropertyDescriptor(OtherReadableStreamDefaultController.prototype, "desiredSize"));
assert_equals(controller.desiredSize, 1, "Grafting getter from other prototype still returns desired size");
}, "Desired size can be grafted from one prototype to another");
promise_test(async () => {
const {stream, controller, otherController} = getFreshInstances();
// Make sure the controller close method returns the correct TypeError
const enqueuedError = { name: "enqueuedError" };
controller.error(enqueuedError);
assert_throws_js(TypeError, () => controller.close(), "Current Global controller");
assert_throws_js(otherGlobal.TypeError, () => otherController.close.call(controller), "Other global controller");
}, "Closing errored stream throws object in appropriate global")
promise_test(async () => {
const {otherController} = getFreshInstances();
// We can enqueue chunks from multiple globals
const chunk = { name: "chunk" };
let controller;
const stream = new ReadableStream({ start(c) { controller = c; } }, { size() {return 1} });
otherController.enqueue.call(controller, chunk);
otherController.enqueue.call(controller, new otherGlobal.Uint8Array(10));
controller.enqueue(new otherGlobal.Uint8Array(10));
}, "Can enqueue chunks from multiple globals")
promise_test(async () => {
const {stream, controller, otherController} = getFreshInstances();
const chunk = { name: "chunk" };
// We get the correct type errors out of a closed stream.
controller.close();
assert_throws_js(TypeError, () => controller.enqueue(new otherGlobal.Uint8Array(10)));
assert_throws_js(otherGlobal.TypeError, () => otherController.enqueue.call(controller, chunk));
assert_throws_js(otherGlobal.TypeError, () => otherController.enqueue.call(controller, new otherGlobal.Uint8Array(10)));
}, "Correct errors and globals for closed streams");
promise_test(async () => {
const {stream, controller, otherController} = getFreshInstances();
// Branches out of tee are in the correct global
const [branch1, branch2] = otherGlobal.ReadableStream.prototype.tee.call(stream);
assert_equals(branch1 instanceof ReadableStream, true, "Branch created in this global (as stream is in this global)");
assert_equals(branch2 instanceof ReadableStream, true, "Branch created in this global (as stream is in this global)");
}, "Tee Branches in correct global");
</script>

View File

@ -0,0 +1,49 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
// META: script=../resources/rs-utils.js
'use strict';
promise_test(async () => {
const channel = new MessageChannel;
const port1 = channel.port1;
const port2 = channel.port2;
const source = {
start(controller) {
controller.enqueue(port1, { transfer : [ port1 ] });
},
type: 'owning'
};
const stream = new ReadableStream(source);
const chunk = await stream.getReader().read();
assert_not_equals(chunk.value, port1);
let promise = new Promise(resolve => port2.onmessage = e => resolve(e.data));
chunk.value.postMessage("toPort2");
assert_equals(await promise, "toPort2");
promise = new Promise(resolve => chunk.value.onmessage = e => resolve(e.data));
port2.postMessage("toPort1");
assert_equals(await promise, "toPort1");
}, 'Transferred MessageChannel works as expected');
promise_test(async t => {
const channel = new MessageChannel;
const port1 = channel.port1;
const port2 = channel.port2;
const source = {
start(controller) {
controller.enqueue({ port1 }, { transfer : [ port1 ] });
},
type: 'owning'
};
const stream = new ReadableStream(source);
const [clone1, clone2] = stream.tee();
await promise_rejects_dom(t, "DataCloneError", clone2.getReader().read());
}, 'Second branch of owning ReadableStream tee should end up into errors with transfer only values');

View File

@ -0,0 +1,128 @@
// META: global=window,worker
// META: script=../resources/test-utils.js
// META: script=../resources/rs-utils.js
'use strict';
function createVideoFrame()
{
let init = {
format: 'I420',
timestamp: 1234,
codedWidth: 4,
codedHeight: 2
};
let data = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8, // y
1, 2, // u
1, 2, // v
]);
return new VideoFrame(data, init);
}
promise_test(async () => {
const videoFrame = createVideoFrame();
videoFrame.test = 1;
const source = {
start(controller) {
assert_equals(videoFrame.format, 'I420');
controller.enqueue(videoFrame, { transfer : [ videoFrame ] });
assert_equals(videoFrame.format, null);
assert_equals(videoFrame.test, 1);
},
type: 'owning'
};
const stream = new ReadableStream(source);
// Cancelling the stream should close all video frames, thus no console messages of GCing VideoFrames should happen.
stream.cancel();
}, 'ReadableStream of type owning should close serialized chunks');
promise_test(async () => {
const videoFrame = createVideoFrame();
videoFrame.test = 1;
const source = {
start(controller) {
assert_equals(videoFrame.format, 'I420');
controller.enqueue({ videoFrame }, { transfer : [ videoFrame ] });
assert_equals(videoFrame.format, null);
assert_equals(videoFrame.test, 1);
},
type: 'owning'
};
const stream = new ReadableStream(source);
const reader = stream.getReader();
const chunk = await reader.read();
assert_equals(chunk.value.videoFrame.format, 'I420');
assert_equals(chunk.value.videoFrame.test, undefined);
chunk.value.videoFrame.close();
}, 'ReadableStream of type owning should transfer JS chunks with transferred values');
promise_test(async t => {
const videoFrame = createVideoFrame();
videoFrame.close();
const source = {
start(controller) {
assert_throws_dom("DataCloneError", () => controller.enqueue(videoFrame, { transfer : [ videoFrame ] }));
},
type: 'owning'
};
const stream = new ReadableStream(source);
const reader = stream.getReader();
await promise_rejects_dom(t, "DataCloneError", reader.read());
}, 'ReadableStream of type owning should error when trying to enqueue not serializable values');
promise_test(async () => {
const videoFrame = createVideoFrame();
const source = {
start(controller) {
controller.enqueue(videoFrame, { transfer : [ videoFrame ] });
},
type: 'owning'
};
const stream = new ReadableStream(source);
const [clone1, clone2] = stream.tee();
const chunk1 = await clone1.getReader().read();
const chunk2 = await clone2.getReader().read();
assert_equals(videoFrame.format, null);
assert_equals(chunk1.value.format, 'I420');
assert_equals(chunk2.value.format, 'I420');
chunk1.value.close();
chunk2.value.close();
}, 'ReadableStream of type owning should clone serializable objects when teeing');
promise_test(async () => {
const videoFrame = createVideoFrame();
videoFrame.test = 1;
const source = {
start(controller) {
assert_equals(videoFrame.format, 'I420');
controller.enqueue({ videoFrame }, { transfer : [ videoFrame ] });
assert_equals(videoFrame.format, null);
assert_equals(videoFrame.test, 1);
},
type: 'owning'
};
const stream = new ReadableStream(source);
const [clone1, clone2] = stream.tee();
const chunk1 = await clone1.getReader().read();
const chunk2 = await clone2.getReader().read();
assert_equals(videoFrame.format, null);
assert_equals(chunk1.value.videoFrame.format, 'I420');
assert_equals(chunk2.value.videoFrame.format, 'I420');
chunk1.value.videoFrame.close();
chunk2.value.videoFrame.close();
}, 'ReadableStream of type owning should clone JS Objects with serializables when teeing');

View File

@ -0,0 +1,91 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
// META: script=../resources/rs-utils.js
'use strict';
test(() => {
new ReadableStream({ type: 'owning' }); // ReadableStream constructed with 'owning' type
}, 'ReadableStream can be constructed with owning type');
test(() => {
let startCalled = false;
const source = {
start(controller) {
assert_equals(this, source, 'source is this during start');
assert_true(controller instanceof ReadableStreamDefaultController, 'default controller');
startCalled = true;
},
type: 'owning'
};
new ReadableStream(source);
assert_true(startCalled);
}, 'ReadableStream of type owning should call start with a ReadableStreamDefaultController');
test(() => {
let startCalled = false;
const source = {
start(controller) {
controller.enqueue("a", { transfer: [] });
controller.enqueue("a", { transfer: undefined });
startCalled = true;
},
type: 'owning'
};
new ReadableStream(source);
assert_true(startCalled);
}, 'ReadableStream should be able to call enqueue with an empty transfer list');
test(() => {
let startCalled = false;
const uint8Array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
const buffer = uint8Array.buffer;
let source = {
start(controller) {
startCalled = true;
assert_throws_js(TypeError, () => { controller.enqueue(buffer, { transfer : [ buffer ] }); }, "transfer list is not empty");
}
};
new ReadableStream(source);
assert_true(startCalled);
startCalled = false;
source = {
start(controller) {
startCalled = true;
assert_throws_js(TypeError, () => { controller.enqueue(buffer, { get transfer() { throw new TypeError(); } }) }, "getter throws");
}
};
new ReadableStream(source);
assert_true(startCalled);
}, 'ReadableStream should check transfer parameter');
promise_test(async () => {
const uint8Array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
const buffer = uint8Array.buffer;
buffer.test = 1;
const source = {
start(controller) {
assert_equals(buffer.byteLength, 8);
controller.enqueue(buffer, { transfer : [ buffer ] });
assert_equals(buffer.byteLength, 0);
assert_equals(buffer.test, 1);
},
type: 'owning'
};
const stream = new ReadableStream(source);
const reader = stream.getReader();
const chunk = await reader.read();
assert_not_equals(chunk.value, buffer);
assert_equals(chunk.value.byteLength, 8);
assert_equals(chunk.value.test, undefined);
}, 'ReadableStream of type owning should transfer enqueued chunks');

View File

@ -0,0 +1,142 @@
// META: global=window,worker,shadowrealm
'use strict';
// Tests which patch the global environment are kept separate to avoid
// interfering with other tests.
const ReadableStream_prototype_locked_get =
Object.getOwnPropertyDescriptor(ReadableStream.prototype, 'locked').get;
// Verify that |rs| passes the brand check as a readable stream.
function isReadableStream(rs) {
try {
ReadableStream_prototype_locked_get.call(rs);
return true;
} catch (e) {
return false;
}
}
test(t => {
const rs = new ReadableStream();
const trappedProperties = ['highWaterMark', 'size', 'start', 'type', 'mode'];
for (const property of trappedProperties) {
// eslint-disable-next-line no-extend-native, accessor-pairs
Object.defineProperty(Object.prototype, property, {
get() { throw new Error(`${property} getter called`); },
configurable: true
});
}
t.add_cleanup(() => {
for (const property of trappedProperties) {
delete Object.prototype[property];
}
});
const [branch1, branch2] = rs.tee();
assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream');
assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream');
}, 'ReadableStream tee() should not touch Object.prototype properties');
test(t => {
const rs = new ReadableStream();
const oldReadableStream = self.ReadableStream;
self.ReadableStream = function() {
throw new Error('ReadableStream called on global object');
};
t.add_cleanup(() => {
self.ReadableStream = oldReadableStream;
});
const [branch1, branch2] = rs.tee();
assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream');
assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream');
}, 'ReadableStream tee() should not call the global ReadableStream');
promise_test(async t => {
const rs = new ReadableStream({
start(c) {
c.enqueue(1);
c.enqueue(2);
c.enqueue(3);
c.close();
}
});
const oldReadableStreamGetReader = ReadableStream.prototype.getReader;
const ReadableStreamDefaultReader = (new ReadableStream()).getReader().constructor;
const oldDefaultReaderRead = ReadableStreamDefaultReader.prototype.read;
const oldDefaultReaderCancel = ReadableStreamDefaultReader.prototype.cancel;
const oldDefaultReaderReleaseLock = ReadableStreamDefaultReader.prototype.releaseLock;
self.ReadableStream.prototype.getReader = function() {
throw new Error('patched getReader() called');
};
ReadableStreamDefaultReader.prototype.read = function() {
throw new Error('patched read() called');
};
ReadableStreamDefaultReader.prototype.cancel = function() {
throw new Error('patched cancel() called');
};
ReadableStreamDefaultReader.prototype.releaseLock = function() {
throw new Error('patched releaseLock() called');
};
t.add_cleanup(() => {
self.ReadableStream.prototype.getReader = oldReadableStreamGetReader;
ReadableStreamDefaultReader.prototype.read = oldDefaultReaderRead;
ReadableStreamDefaultReader.prototype.cancel = oldDefaultReaderCancel;
ReadableStreamDefaultReader.prototype.releaseLock = oldDefaultReaderReleaseLock;
});
// read the first chunk, then cancel
for await (const chunk of rs) {
break;
}
// should be able to acquire a new reader
const reader = oldReadableStreamGetReader.call(rs);
// stream should be cancelled
await reader.closed;
}, 'ReadableStream async iterator should use the original values of getReader() and ReadableStreamDefaultReader ' +
'methods');
test(t => {
const oldPromiseThen = Promise.prototype.then;
Promise.prototype.then = () => {
throw new Error('patched then() called');
};
t.add_cleanup(() => {
Promise.prototype.then = oldPromiseThen;
});
const [branch1, branch2] = new ReadableStream().tee();
assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream');
assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream');
}, 'tee() should not call Promise.prototype.then()');
test(t => {
const oldPromiseThen = Promise.prototype.then;
Promise.prototype.then = () => {
throw new Error('patched then() called');
};
t.add_cleanup(() => {
Promise.prototype.then = oldPromiseThen;
});
let readableController;
const rs = new ReadableStream({
start(c) {
readableController = c;
}
});
const ws = new WritableStream();
rs.pipeTo(ws);
readableController.close();
}, 'pipeTo() should not call Promise.prototype.then()');

View File

@ -0,0 +1,46 @@
// META: global=window,worker
'use strict';
function performMicrotaskCheckpoint() {
document.createNodeIterator(document, -1, {
acceptNode() {
return NodeFilter.FILTER_ACCEPT;
}
}).nextNode();
}
test(() => {
// Add a getter for "then" that will incidentally be invoked
// during promise resolution.
Object.prototype.__defineGetter__('then', () => {
// Clean up behind ourselves.
delete Object.prototype.then;
// This promise should (like all promises) be resolved
// asynchronously.
var executed = false;
Promise.resolve().then(_ => { executed = true; });
// This shouldn't run microtasks! They should only run
// after the fetch is resolved.
performMicrotaskCheckpoint();
// The fulfill handler above shouldn't have run yet. If it has run,
// throw to reject this promise and fail the test.
assert_false(executed, "shouldn't have run microtasks yet");
// Otherwise act as if there's no "then" property so the promise
// fulfills and the test passes.
return undefined;
});
const readable = new ReadableStream({
pull(c) {
c.enqueue({});
}
}, { highWaterMark: 0 });
// Create a read request, incidentally resolving a promise with an
// object value, thereby invoking the getter installed above.
readable.getReader().read();
}, "reading from a stream should occur in a microtask scope");

View File

@ -0,0 +1,264 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/recording-streams.js
// META: script=../resources/rs-utils.js
// META: script=../resources/test-utils.js
'use strict';
// The size() function of the readable strategy can re-entrantly call back into the ReadableStream implementation. This
// makes it risky to cache state across the call to ReadableStreamDefaultControllerEnqueue. These tests attempt to catch
// such errors. They are separated from the other strategy tests because no real user code should ever do anything like
// this.
const error1 = new Error('error1');
error1.name = 'error1';
promise_test(() => {
let controller;
let calls = 0;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
++calls;
if (calls < 2) {
controller.enqueue('b');
}
return 1;
}
});
controller.enqueue('a');
controller.close();
return readableStreamToArray(rs)
.then(array => assert_array_equals(array, ['b', 'a'], 'array should contain two chunks'));
}, 'enqueue() inside size() should work');
promise_test(() => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
// The queue is empty.
controller.close();
// The state has gone from "readable" to "closed".
return 1;
// This chunk will be enqueued, but will be impossible to read because the state is already "closed".
}
});
controller.enqueue('a');
return readableStreamToArray(rs)
.then(array => assert_array_equals(array, [], 'array should contain no chunks'));
// The chunk 'a' is still in rs's queue. It is closed so 'a' cannot be read.
}, 'close() inside size() should not crash');
promise_test(() => {
let controller;
let calls = 0;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
++calls;
if (calls === 2) {
// The queue contains one chunk.
controller.close();
// The state is still "readable", but closeRequest is now true.
}
return 1;
}
});
controller.enqueue('a');
controller.enqueue('b');
return readableStreamToArray(rs)
.then(array => assert_array_equals(array, ['a', 'b'], 'array should contain two chunks'));
}, 'close request inside size() should work');
promise_test(t => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
controller.error(error1);
return 1;
}
});
controller.enqueue('a');
return promise_rejects_exactly(t, error1, rs.getReader().read(), 'read() should reject');
}, 'error() inside size() should work');
promise_test(() => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
assert_equals(controller.desiredSize, 1, 'desiredSize should be 1');
return 1;
},
highWaterMark: 1
});
controller.enqueue('a');
controller.close();
return readableStreamToArray(rs)
.then(array => assert_array_equals(array, ['a'], 'array should contain one chunk'));
}, 'desiredSize inside size() should work');
promise_test(t => {
let cancelPromise;
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
},
cancel: t.step_func(reason => {
assert_equals(reason, error1, 'reason should be error1');
assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue() should throw');
})
}, {
size() {
cancelPromise = rs.cancel(error1);
return 1;
},
highWaterMark: Infinity
});
controller.enqueue('a');
const reader = rs.getReader();
return Promise.all([
reader.closed,
cancelPromise
]);
}, 'cancel() inside size() should work');
promise_test(() => {
let controller;
let pipeToPromise;
const ws = recordingWritableStream();
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
if (!pipeToPromise) {
pipeToPromise = rs.pipeTo(ws);
}
return 1;
},
highWaterMark: 1
});
controller.enqueue('a');
assert_not_equals(pipeToPromise, undefined);
// Some pipeTo() implementations need an additional chunk enqueued in order for the first one to be processed. See
// https://github.com/whatwg/streams/issues/794 for background.
controller.enqueue('a');
// Give pipeTo() a chance to process the queued chunks.
return delay(0).then(() => {
assert_array_equals(ws.events, ['write', 'a', 'write', 'a'], 'ws should contain two chunks');
controller.close();
return pipeToPromise;
}).then(() => {
assert_array_equals(ws.events, ['write', 'a', 'write', 'a', 'close'], 'target should have been closed');
});
}, 'pipeTo() inside size() should behave as expected');
promise_test(() => {
let controller;
let readPromise;
let calls = 0;
let readResolved = false;
let reader;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
// This is triggered by controller.enqueue(). The queue is empty and there are no pending reads. This read is
// added to the list of pending reads.
readPromise = reader.read();
++calls;
return 1;
},
highWaterMark: 0
});
reader = rs.getReader();
controller.enqueue('a');
readPromise.then(() => {
readResolved = true;
});
return flushAsyncEvents().then(() => {
assert_false(readResolved);
controller.enqueue('b');
assert_equals(calls, 1, 'size() should have been called once');
return delay(0);
}).then(() => {
assert_true(readResolved);
assert_equals(calls, 1, 'size() should only be called once');
return readPromise;
}).then(({ value, done }) => {
assert_false(done, 'done should be false');
// See https://github.com/whatwg/streams/issues/794 for why this chunk is not 'a'.
assert_equals(value, 'b', 'chunk should have been read');
assert_equals(calls, 1, 'calls should still be 1');
return reader.read();
}).then(({ value, done }) => {
assert_false(done, 'done should be false again');
assert_equals(value, 'a', 'chunk a should come after b');
});
}, 'read() inside of size() should behave as expected');
promise_test(() => {
let controller;
let reader;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
reader = rs.getReader();
return 1;
}
});
controller.enqueue('a');
return reader.read().then(({ value, done }) => {
assert_false(done, 'done should be false');
assert_equals(value, 'a', 'value should be a');
});
}, 'getReader() inside size() should work');
promise_test(() => {
let controller;
let branch1;
let branch2;
const rs = new ReadableStream({
start(c) {
controller = c;
}
}, {
size() {
[branch1, branch2] = rs.tee();
return 1;
}
});
controller.enqueue('a');
assert_true(rs.locked, 'rs should be locked');
controller.close();
return Promise.all([
readableStreamToArray(branch1).then(array => assert_array_equals(array, ['a'], 'branch1 should have one chunk')),
readableStreamToArray(branch2).then(array => assert_array_equals(array, ['a'], 'branch2 should have one chunk'))
]);
}, 'tee() inside size() should work');

View File

@ -0,0 +1,13 @@
<!doctype html>
<body>
<script>
const i = document.createElement("iframe");
document.body.appendChild(i);
const rs = new i.contentWindow.ReadableStream();
i.remove();
// tee() on a ReadableStream from a detached iframe should not crash.
rs.tee();
</script>
</body>

View File

@ -0,0 +1,479 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/rs-utils.js
// META: script=../resources/test-utils.js
// META: script=../resources/recording-streams.js
// META: script=../resources/rs-test-templates.js
'use strict';
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_exactly(t, theError, reader1.closed),
promise_rejects_exactly(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_exactly(t, theError, reader2.read());
})
.then(() => promise_rejects_exactly(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 branch1');
templatedRSTeeCancel('ReadableStream teeing', (extras) => {
return new ReadableStream({ ...extras });
});
promise_test(t => {
let controller;
const stream = new ReadableStream({ start(c) { controller = c; } });
const [branch1, branch2] = stream.tee();
const error = new Error();
error.name = 'distinctive';
// Ensure neither branch is waiting in ReadableStreamDefaultReaderRead().
controller.enqueue();
controller.enqueue();
return delay(0).then(() => {
// This error will have to be detected via [[closedPromise]].
controller.error(error);
const reader1 = branch1.getReader();
const reader2 = branch2.getReader();
return Promise.all([
promise_rejects_exactly(t, error, reader1.closed, 'reader1.closed should reject'),
promise_rejects_exactly(t, error, reader2.closed, 'reader2.closed should reject')
]);
});
}, 'ReadableStream teeing: erroring a teed stream should error both 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_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
controller.error(theError);
return promise;
}, 'ReadableStream teeing: erroring the original should immediately error the branches');
promise_test(async t => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
const cancelPromise = reader2.cancel();
controller.enqueue('a');
const read1 = await reader1.read();
assert_object_equals(read1, { value: 'a', done: false }, 'first read() from branch1 should fulfill with the chunk');
controller.close();
const read2 = await reader1.read();
assert_object_equals(read2, { value: undefined, done: true }, 'second read() from branch1 should be done');
await Promise.all([
reader1.closed,
cancelPromise
]);
}, 'ReadableStream teeing: canceling branch1 should finish when branch2 reads until end of stream');
promise_test(async t => {
let controller;
const theError = { name: 'boo!' };
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
const cancelPromise = reader2.cancel();
controller.error(theError);
await Promise.all([
promise_rejects_exactly(t, theError, reader1.read()),
cancelPromise
]);
}, 'ReadableStream teeing: canceling branch1 should finish when original stream errors');
promise_test(async () => {
const rs = new ReadableStream({});
const [branch1, branch2] = rs.tee();
const cancel1 = branch1.cancel();
await flushAsyncEvents();
const cancel2 = branch2.cancel();
await Promise.all([cancel1, cancel2]);
}, 'ReadableStream teeing: canceling both branches in sequence with delay');
promise_test(async t => {
const theError = { name: 'boo!' };
const rs = new ReadableStream({
cancel() {
throw theError;
}
});
const [branch1, branch2] = rs.tee();
const cancel1 = branch1.cancel();
await flushAsyncEvents();
const cancel2 = branch2.cancel();
await Promise.all([
promise_rejects_exactly(t, theError, cancel1),
promise_rejects_exactly(t, theError, cancel2)
]);
}, 'ReadableStream teeing: failing to cancel when canceling both branches in sequence with delay');
test(t => {
// Copy original global.
const oldReadableStream = ReadableStream;
const getReader = ReadableStream.prototype.getReader;
const origRS = new ReadableStream();
// Replace the global ReadableStream constructor with one that doesn't work.
ReadableStream = function() {
throw new Error('global ReadableStream constructor called');
};
t.add_cleanup(() => {
ReadableStream = oldReadableStream;
});
// This will probably fail if the global ReadableStream constructor was used.
const [rs1, rs2] = origRS.tee();
// These will definitely fail if the global ReadableStream constructor was used.
assert_not_equals(getReader.call(rs1), undefined, 'getReader should work on rs1');
assert_not_equals(getReader.call(rs2), undefined, 'getReader should work on rs2');
}, 'ReadableStreamTee should not use a modified ReadableStream constructor from the global object');
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
// Create two branches, each with a HWM of 1. This should result in one
// chunk being pulled, not two.
rs.tee();
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, ['pull'], 'pull should only be called once');
});
}, 'ReadableStreamTee should not pull more chunks than can fit in the branch queue');
promise_test(t => {
const rs = recordingReadableStream({
pull(controller) {
controller.enqueue('a');
}
}, { highWaterMark: 0 });
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return Promise.all([reader1.read(), reader2.read()])
.then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
});
}, 'ReadableStreamTee should only pull enough to fill the emptiest queue');
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
const theError = { name: 'boo!' };
rs.controller.error(theError);
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, [], 'pull should not be called');
return Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
});
}, 'ReadableStreamTee should not pull when original is already errored');
for (const branch of [1, 2]) {
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
const theError = { name: 'boo!' };
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, ['pull'], 'pull should be called once');
rs.controller.enqueue('a');
const reader = (branch === 1) ? reader1 : reader2;
return reader.read();
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
rs.controller.error(theError);
return Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
});
}, `ReadableStreamTee stops pulling when original stream errors while branch ${branch} is reading`);
}
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
const theError = { name: 'boo!' };
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, ['pull'], 'pull should be called once');
rs.controller.enqueue('a');
return Promise.all([reader1.read(), reader2.read()]);
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
rs.controller.error(theError);
return Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
});
}, 'ReadableStreamTee stops pulling when original stream errors while both branches are reading');
promise_test(async () => {
const rs = recordingReadableStream();
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
const branch1Reads = [reader1.read(), reader1.read()];
const branch2Reads = [reader2.read(), reader2.read()];
await flushAsyncEvents();
rs.controller.enqueue('a');
rs.controller.close();
assert_object_equals(await branch1Reads[0], { value: 'a', done: false }, 'first chunk from branch1 should be correct');
assert_object_equals(await branch2Reads[0], { value: 'a', done: false }, 'first chunk from branch2 should be correct');
assert_object_equals(await branch1Reads[1], { value: undefined, done: true }, 'second read() from branch1 should be done');
assert_object_equals(await branch2Reads[1], { value: undefined, done: true }, 'second read() from branch2 should be done');
}, 'ReadableStream teeing: enqueue() and close() while both branches are pulling');

View File

@ -0,0 +1,143 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
// META: script=../resources/rs-test-templates.js
'use strict';
// 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() };
}