Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] ReadableStream @@asyncIterator tests #13362

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions streams/readable-streams/async-iterator.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>async-iterator.js browser context wrapper file</title>

<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<script src="../resources/rs-utils.js"></script>
<script src="../resources/recording-streams.js"></script>

<script src="async-iterator.js"></script>
150 changes: 150 additions & 0 deletions streams/readable-streams/async-iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict';

if (self.importScripts) {
self.importScripts('../resources/rs-utils.js');
self.importScripts('/resources/testharness.js');
self.importScripts('../resources/recording-streams.js');
}

test(() => {
assert_equals(ReadableStream.prototype[Symbol.asyncIterator], ReadableStream.prototype.getIterator);
}, '@@asyncIterator() method is === to getIterator() method');

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 iterator 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 iterator pull source');

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 () => {

}, 'Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function');

promise_test(async () => {
const test = async (type, preventCancel) => {
const s = recordingReadableStream({
start(c) {
c.enqueue(0);
}
});

await (async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this immediately-executed-async-function structure hard to understand. Is there a reason why a simple try ... catch wouldn't work?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing return. I suppose it can be reworked a little bit though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably if you just split the creation of the function from the execution of the function it will be enough to make it less confusing.

for await (const c of s.getIterator({ preventCancel })) {
if (type === 'throw') {
throw new Error();
} else if (type === 'break') {
break;
} else if (type === 'return') {
return;
}
}
})().catch(() => 0);

if (preventCancel) {
assert_array_equals(s.events, ['pull'], `cancel() should not be called when type = '${type}' and preventCancel is true`);
} else {
assert_array_equals(s.events, ['pull', 'cancel', undefined], `cancel() should be called when type = '${type}' and preventCancel is false`);
}
};

for (const t of ['throw', 'break', 'return']) {
await test(t, true);
await test(t, false);
}
}, 'cancellation behavior');

promise_test(async () => {
{
const s = new ReadableStream();
const it = s[Symbol.asyncIterator]();
await it.return();
try {
await it.return();
assert_unreached();
} catch (e) {}
}

{
const s = new ReadableStream({
start(c) {
c.enqueue(0);
c.close();
},
});
const it = s[Symbol.asyncIterator]();
const next = await it.next();
assert_equals(Object.getPrototypeOf(next), Object.prototype);
assert_array_equals(Object.keys(next), ['value', 'done']);
}
}, 'manual manipulation');

test(() => {
const s = new ReadableStream({
start(c) {
c.enqueue(0);
c.close();
},
});
const it = s.getIterator();
try {
s.getIterator();
assert_unreached();
} catch (e) {}
}, 'getIterator throws if there\'s already a lock');
29 changes: 29 additions & 0 deletions streams/readable-streams/brand-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ if (self.importScripts) {

let ReadableStreamDefaultReader;
let ReadableStreamDefaultController;
let ReadableStreamAsyncIteratorPrototype;

test(() => {

Expand All @@ -26,6 +27,13 @@ test(() => {

}, 'Can get the ReadableStreamDefaultController constructor indirectly');

test(() => {

const rs = new ReadableStream();
ReadableStreamAsyncIteratorPrototype = Object.getPrototypeOf(rs.getIterator());

}, 'Can get ReadableStreamAsyncIteratorPrototype object indirectly');

function fakeRS() {
return Object.setPrototypeOf({
cancel() { return Promise.resolve(); },
Expand Down Expand Up @@ -71,6 +79,13 @@ function realRSDefaultController() {
return controller;
}

function fakeRSAsyncIterator() {
return Object.setPrototypeOf({
next() { },
return(value = undefined) { }
}, ReadableStreamAsyncIteratorPrototype);
}

promise_test(t => {

return methodRejectsForAll(t, ReadableStream.prototype, 'cancel',
Expand Down Expand Up @@ -161,4 +176,18 @@ test(() => {

}, 'ReadableStreamDefaultController.prototype.error enforces a brand check');

promise_test(t => {

return methodRejectsForAll(t, ReadableStreamAsyncIteratorPrototype, 'next',
[fakeRSAsyncIterator(), realRS(), realRSDefaultReader(), undefined, null]);

}, 'ReadableStreamAsyncIteratorPrototype.next enforces a brand check');

promise_test(t => {

return methodRejectsForAll(t, ReadableStreamAsyncIteratorPrototype, 'return',
[fakeRSAsyncIterator(), realRS(), realRSDefaultReader(), undefined, null]);

}, 'ReadableStreamAsyncIteratorPrototype.return enforces a brand check');

done();
3 changes: 2 additions & 1 deletion streams/readable-streams/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test(() => {

test(() => {

const methods = ['cancel', 'constructor', 'getReader', 'pipeThrough', 'pipeTo', 'tee'];
const methods = ['cancel', 'constructor', 'getReader', 'pipeThrough', 'pipeTo', 'tee', 'getIterator'];
const properties = methods.concat(['locked']).sort();

const rs = new ReadableStream();
Expand Down Expand Up @@ -73,6 +73,7 @@ test(() => {
assert_equals(rs.pipeThrough.length, 2, 'pipeThrough should have 2 parameters');
assert_equals(rs.pipeTo.length, 1, 'pipeTo should have 1 parameter');
assert_equals(rs.tee.length, 0, 'tee should have no parameters');
assert_equals(rs.getIterator.length, 0, 'getIterator should have no required parameters');

}, 'ReadableStream instances should have the correct list of properties');

Expand Down