Skip to content

Commit

Permalink
Bug 1651860 [wpt PR 24546] - Add tests for transferable streams, a=te…
Browse files Browse the repository at this point in the history
…stonly

Automatic update from web-platform-tests
Add tests for transferable streams (#24546)

The Streams Standard change whatwg/streams#1053
adds the ability to transfer streams to a different realm using
postMessage(). Add tests for this feature.

--

wpt-commits: f2eb8b9fa0d0d11429ce4952dbb9dd796c18e0da
wpt-pr: 24546
  • Loading branch information
ricea authored and moz-wptsync-bot committed Oct 9, 2020
1 parent 309780f commit 54c4597
Show file tree
Hide file tree
Showing 17 changed files with 1,050 additions and 0 deletions.
255 changes: 255 additions & 0 deletions testing/web-platform/tests/streams/transferable/readable-stream.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script src="../resources/recording-streams.js"></script>
<script src="../resources/test-utils.js"></script>
<script>
'use strict';

promise_test(async () => {
const rs = await createTransferredReadableStream({
start(controller) {
controller.enqueue('a');
controller.close();
}
});
const reader = rs.getReader();
{
const {value, done} = await reader.read();
assert_false(done, 'should not be done yet');
assert_equals(value, 'a', 'first chunk should be a');
}
{
const {done} = await reader.read();
assert_true(done, 'should be done now');
}
}, 'sending one chunk through a transferred stream should work');

promise_test(async () => {
let controller;
const rs = await createTransferredReadableStream({
start(c) {
controller = c;
}
});
for (let i = 0; i < 10; ++i) {
controller.enqueue(i);
}
controller.close();
const reader = rs.getReader();
for (let i = 0; i < 10; ++i) {
const {value, done} = await reader.read();
assert_false(done, 'should not be done yet');
assert_equals(value, i, 'chunk content should match index');
}
const {done} = await reader.read();
assert_true(done, 'should be done now');
}, 'sending ten chunks through a transferred stream should work');

promise_test(async () => {
let controller;
const rs = await createTransferredReadableStream({
start(c) {
controller = c;
}
});
const reader = rs.getReader();
for (let i = 0; i < 10; ++i) {
controller.enqueue(i);
const {value, done} = await reader.read();
assert_false(done, 'should not be done yet');
assert_equals(value, i, 'chunk content should match index');
}
controller.close();
const {done} = await reader.read();
assert_true(done, 'should be done now');
}, 'sending ten chunks one at a time should work');

promise_test(async () => {
let controller;
const rs = await createTransferredReadableStream({
start() {
this.counter = 0;
},
pull(controller) {
controller.enqueue(this.counter);
++this.counter;
if (this.counter === 10)
controller.close();
}
});
const reader = rs.getReader();
for (let i = 0; i < 10; ++i) {
const {value, done} = await reader.read();
assert_false(done, 'should not be done yet');
assert_equals(value, i, 'chunk content should match index');
}
const {done} = await reader.read();
assert_true(done, 'should be done now');
}, 'sending ten chunks on demand should work');

promise_test(async () => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
await delay(0);
assert_array_equals(rs.events, [], 'pull() should not have been called');
// Eat the message so it can't interfere with other tests.
addEventListener('message', () => {}, {once: true});
// The transfer is done manually to verify that it is posting the stream that
// relieves backpressure, not receiving it.
postMessage(rs, '*', [rs]);
await delay(0);
assert_array_equals(rs.events, ['pull'], 'pull() should have been called');
}, 'transferring a stream should relieve backpressure');

promise_test(async () => {
const rs = await recordingTransferredReadableStream({
pull(controller) {
controller.enqueue('a');
}
}, { highWaterMark: 2 });
await delay(0);
assert_array_equals(rs.events, ['pull', 'pull', 'pull'],
'pull() should have been called three times');
}, 'transferring a stream should add one chunk to the queue size');

promise_test(async () => {
const rs = await recordingTransferredReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(1024));
controller.enqueue(new Uint8Array(1024));
}
}, new ByteLengthQueuingStrategy({highWaterMark: 512}));
await delay(0);
// At this point the queue contains 1024/512 bytes and 1/1 chunk, so it's full
// and pull() is not called.
assert_array_equals(rs.events, [], 'pull() should not have been called');
const reader = rs.getReader();
const {value, done} = await reader.read();
assert_false(done, 'we should not be done');
assert_equals(value.byteLength, 1024, 'expected chunk should be returned');
// Now the queue contains 0/512 bytes and 1/1 chunk, so pull() is called. If
// the implementation erroneously counted the extra queue space in bytes, then
// the queue would contain 1024/513 bytes and pull() wouldn't be called.
assert_array_equals(rs.events, ['pull'], 'pull() should have been called');
}, 'the extra queue from transferring is counted in chunks');

promise_test(async () => {
const rs = await recordingTransferredReadableStream();
rs.cancel('message');
await delay(0);
assert_array_equals(rs.events, ['pull', 'cancel', 'message'],
'cancel() should have been called');
const reader = rs.getReader();
// Check the stream really got closed.
await reader.closed;
}, 'cancel should be propagated to the original');

promise_test(async () => {
let resolveCancelCalled;
const cancelCalled = new Promise(resolve => {
resolveCancelCalled = resolve;
});
const rs = await recordingTransferredReadableStream({
cancel() {
resolveCancelCalled();
}
});
const reader = rs.getReader();
const readPromise = reader.read();
reader.cancel('done');
const { done } = await readPromise;
assert_true(done, 'should be done');
await cancelCalled;
assert_array_equals(rs.events, ['pull', 'cancel', 'done'],
'events should match');
}, 'cancel should abort a pending read()');

promise_test(async () => {
let cancelComplete = false;
const rs = await createTransferredReadableStream({
async cancel() {
await flushAsyncEvents();
cancelComplete = true;
}
});
await rs.cancel();
assert_false(cancelComplete,
'cancel() on the underlying sink should not have completed');
}, 'stream cancel should not wait for underlying source cancel');

promise_test(async t => {
const rs = await recordingTransferredReadableStream();
const reader = rs.getReader();
let serializationHappened = false;
rs.controller.enqueue({
get getter() {
serializationHappened = true;
return 'a';
}
});
await flushAsyncEvents();
assert_false(serializationHappened,
'serialization should not have happened yet');
const {value, done} = await reader.read();
assert_false(done, 'should not be done');
assert_equals(value.getter, 'a', 'getter should be a');
assert_true(serializationHappened,
'serialization should have happened');
}, 'serialization should not happen until the value is read');

promise_test(async t => {
const rs = await recordingTransferredReadableStream();
const reader = rs.getReader();
rs.controller.enqueue(new ReadableStream());
await promise_rejects_dom(t, 'DataCloneError', reader.read(),
'closed promise should reject');
assert_throws_js(TypeError, () => rs.controller.enqueue(),
'original stream should be errored');
}, 'transferring a non-serializable chunk should error both sides');

promise_test(async t => {
const rs = await createTransferredReadableStream({
start(controller) {
controller.error('foo');
}
});
const reader = rs.getReader();
return promise_rejects_exactly(t, 'foo', reader.read(),
'error should be passed through');
}, 'errors should be passed through');

promise_test(async () => {
const rs = await recordingTransferredReadableStream();
await delay(0);
const reader = rs.getReader();
reader.cancel();
rs.controller.error();
const {done} = await reader.read();
assert_true(done, 'should be done');
assert_throws_js(TypeError, () => rs.controller.enqueue(),
'enqueue should throw');
}, 'race between cancel() and error() should leave sides in different states');

promise_test(async () => {
const rs = await recordingTransferredReadableStream();
await delay(0);
const reader = rs.getReader();
reader.cancel();
rs.controller.close();
const {done} = await reader.read();
assert_true(done, 'should be done');
}, 'race between cancel() and close() should be benign');

promise_test(async () => {
const rs = await recordingTransferredReadableStream();
await delay(0);
const reader = rs.getReader();
reader.cancel();
rs.controller.enqueue('a');
const {done} = await reader.read();
assert_true(done, 'should be done');
}, 'race between cancel() and enqueue() should be benign');

</script>
132 changes: 132 additions & 0 deletions testing/web-platform/tests/streams/transferable/reason.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
'use strict';

// Chrome used to special-case the reason for cancel() and abort() in order to
// handle exceptions correctly. This is no longer necessary. These tests are
// retained to avoid regressions.

async function getTransferredReason(originalReason) {
let resolvePromise;
const rv = new Promise(resolve => {
resolvePromise = resolve;
});
const rs = await createTransferredReadableStream({
cancel(reason) {
resolvePromise(reason);
}
});
await rs.cancel(originalReason);
return rv;
}

for (const value of ['hi', '\t\r\n', 7, 3.0, undefined, null, true, false,
NaN, Infinity]) {
promise_test(async () => {
const reason = await getTransferredReason(value);
assert_equals(reason, value, 'reason should match');
}, `reason with a simple value of '${value}' should be preserved`);
}

for (const badType of [Symbol('hi'), _ => 'hi']) {
promise_test(async t => {
return promise_rejects_dom(t, 'DataCloneError',
getTransferredReason(badType),
'cancel() should reject');
}, `reason with a type of '${typeof badType}' should be rejected and ` +
`error the stream`);
}

promise_test(async () => {
const reasonAsJson =
`{"foo":[1,"col"],"bar":{"hoge":0.2,"baz":{},"shan":null}}`;
const reason = await getTransferredReason(JSON.parse(reasonAsJson));
assert_equals(JSON.stringify(reason), reasonAsJson,
'object should be preserved');
}, 'objects that can be completely expressed in JSON should be preserved');

promise_test(async () => {
const circularObject = {};
circularObject.self = circularObject;
const reason = await getTransferredReason(circularObject);
assert_true(reason instanceof Object, 'an Object should be output');
assert_equals(reason.self, reason,
'the object should have a circular reference');
}, 'objects that cannot be expressed in JSON should also be preserved');

promise_test(async () => {
const originalReason = new TypeError('hi');
const reason = await getTransferredReason(originalReason);
assert_true(reason instanceof TypeError,
'type should be preserved');
assert_equals(reason.message, originalReason.message,
'message should be preserved');
}, 'the type and message of a TypeError should be preserved');

promise_test(async () => {
const originalReason = new TypeError('hi');
originalReason.foo = 'bar';
const reason = await getTransferredReason(originalReason);
assert_false('foo' in reason,
'foo should not be preserved');
}, 'other attributes of a TypeError should not be preserved');

promise_test(async () => {
const originalReason = new TypeError();
originalReason.message = [1, 2, 3];
const reason = await getTransferredReason(originalReason);
assert_equals(reason.message, '1,2,3', 'message should be stringified');
}, 'a TypeError message should be converted to a string');

promise_test(async () => {
const originalReason = new TypeError();
Object.defineProperty(originalReason, 'message', {
get() { return 'words'; }
});
const reason = await getTransferredReason(originalReason);
assert_equals(reason.message, '', 'message should not be preserved');
}, 'a TypeError message should not be preserved if it is a getter');

promise_test(async () => {
const originalReason = new TypeError();
delete originalReason.message;
TypeError.prototype.message = 'inherited message';
const reason = await getTransferredReason(originalReason);
delete TypeError.prototype.message;
assert_equals(reason.message, '', 'message should not be preserved');
}, 'a TypeError message should not be preserved if it is inherited');

promise_test(async () => {
const originalReason = new DOMException('yes', 'AbortError');
const reason = await getTransferredReason(originalReason);
assert_true(reason instanceof DOMException,
'reason should be a DOMException');
assert_equals(reason.message, originalReason.message,
'the messages should match');
assert_equals(reason.name, originalReason.name,
'the names should match');
}, 'DOMException errors should be preserved');

for (const errorConstructor of [EvalError, RangeError,
ReferenceError, SyntaxError, TypeError,
URIError]) {
promise_test(async () => {
const originalReason = new errorConstructor('nope');
const reason = await getTransferredReason(originalReason);
assert_equals(typeof reason, 'object', 'reason should have type object');
assert_true(reason instanceof errorConstructor,
`reason should inherit ${errorConstructor.name}`);
assert_true(reason instanceof Error, 'reason should inherit Error');
assert_equals(reason.constructor, errorConstructor,
'reason should have the right constructor');
assert_equals(reason.name, errorConstructor.name,
`name should match constructor name`);
assert_equals(reason.message, 'nope', 'message should match');
}, `${errorConstructor.name} should be preserved`);
}

</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script>
addEventListener('message', evt => {
evt.source.postMessage(evt.data, '*', [evt.data]);
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// A worker that just transfers back any message that is sent to it.
onmessage = evt => postMessage(evt.data, [evt.data]);
Loading

0 comments on commit 54c4597

Please sign in to comment.