-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bug 1651860 [wpt PR 24546] - Add tests for transferable streams, a=te…
…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
1 parent
309780f
commit 54c4597
Showing
17 changed files
with
1,050 additions
and
0 deletions.
There are no files selected for viewing
255 changes: 255 additions & 0 deletions
255
testing/web-platform/tests/streams/transferable/readable-stream.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
132
testing/web-platform/tests/streams/transferable/reason.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
7 changes: 7 additions & 0 deletions
7
testing/web-platform/tests/streams/transferable/resources/echo-iframe.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
2 changes: 2 additions & 0 deletions
2
testing/web-platform/tests/streams/transferable/resources/echo-worker.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); |
Oops, something went wrong.