Bug 1526596 [wpt PR 15097] - ReadableStream @@asyncIterator, a=testonly
authorMattias Buelens <mattias@buelens.com>
Mon, 18 Feb 2019 19:25:06 +0000
changeset 461109 daffa4526e1850355356d0131230159b9eed8724
parent 461108 0390eb5bdf53561523d61429d7ffbf268ad1429a
child 461110 ebae3e84214df938e7d5091ea9f6efd8adfec3d4
push id112159
push userjames@hoppipolla.co.uk
push dateTue, 26 Feb 2019 12:08:48 +0000
treeherdermozilla-inbound@20be3ebad986 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1526596, 15097
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1526596 [wpt PR 15097] - ReadableStream @@asyncIterator, a=testonly Automatic update from web-platform-tests ReadableStream @@asyncIterator (#15097) Test async iteration of ReadableStream. Standard changes are in https://github.com/whatwg/streams/pull/980. -- wpt-commits: de6f8fcf9b87e80811e9267a886cf891f6f864e0 wpt-pr: 15097
testing/web-platform/tests/streams/readable-streams/async-iterator.any.js
testing/web-platform/tests/streams/readable-streams/brand-checks.any.js
testing/web-platform/tests/streams/readable-streams/general.any.js
testing/web-platform/tests/streams/readable-streams/patched-global.any.js
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js
@@ -0,0 +1,340 @@
+// META: global=worker,jsshell
+// META: script=../resources/rs-utils.js
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+test(() => {
+  assert_equals(ReadableStream.prototype[Symbol.asyncIterator], ReadableStream.prototype.getIterator);
+}, '@@asyncIterator() method is === to getIterator() method');
+
+test(() => {
+  const s = new ReadableStream();
+  const it = s.getIterator();
+  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_false(propDesc.enumerable, 'method should be non-enumerable');
+    assert_true(propDesc.configurable, 'method should be configurable');
+    assert_true(propDesc.writable, 'method should be writable');
+    assert_equals(typeof 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 () => {
+  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.getIterator();
+  assert_array_equals(s.events, []);
+
+  const read1 = await it.next();
+  assert_equals(read1.done, false);
+  assert_equals(read1.value, 1);
+  assert_array_equals(s.events, ['pull']);
+
+  const read2 = await it.next();
+  assert_equals(read2.done, false);
+  assert_equals(read2.value, 2);
+  assert_array_equals(s.events, ['pull', 'pull']);
+
+  const read3 = await it.next();
+  assert_equals(read3.done, false);
+  assert_equals(read3.value, 3);
+  assert_array_equals(s.events, ['pull', 'pull', 'pull']);
+
+  const read4 = await it.next();
+  assert_equals(read4.done, true);
+  assert_equals(read4.value, undefined);
+  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_equals(readResult.done, false);
+  assert_equals(readResult.value, 1);
+  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.getIterator({ 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.getIterator({ 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 () => {
+  const s = new ReadableStream();
+  const it = s[Symbol.asyncIterator]();
+  await it.return();
+  try {
+    await it.return();
+    assert_unreached();
+  } catch (e) {}
+}, 'Calling return() twice rejects');
+
+promise_test(async () => {
+  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.getOwnPropertyNames(next).sort(), ['done', 'value']);
+}, 'next()\'s fulfillment value has the right shape');
+
+promise_test(async t => {
+  const s = recordingReadableStream();
+  const it = s[Symbol.asyncIterator]();
+  it.next();
+
+  await promise_rejects(t, new TypeError(), it.return(), 'return() should reject');
+  assert_array_equals(s.events, ['pull']);
+}, 'calling return() while there are pending reads rejects');
+
+test(() => {
+  const s = new ReadableStream({
+    start(c) {
+      c.enqueue(0);
+      c.close();
+    },
+  });
+  const it = s.getIterator();
+  assert_throws(new TypeError(), () => s.getIterator(), 'getIterator() should throw');
+}, 'getIterator() 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 () => {
+  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.getIterator({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_equals(readResult.done, false, 'should not be closed yet');
+  assert_equals(readResult.value, 3, 'should read remaining chunk');
+  await reader.closed;
+}, 'Acquiring a reader and reading the remaining chunks after partially async-iterating a stream with preventCancel = true');
--- a/testing/web-platform/tests/streams/readable-streams/brand-checks.any.js
+++ b/testing/web-platform/tests/streams/readable-streams/brand-checks.any.js
@@ -1,14 +1,15 @@
 // META: global=worker,jsshell
 // META: script=../resources/test-utils.js
 'use strict';
 
 let ReadableStreamDefaultReader;
 let ReadableStreamDefaultController;
+let ReadableStreamAsyncIteratorPrototype;
 
 test(() => {
 
   // It's not exposed globally, but we test a few of its properties here.
   ReadableStreamDefaultReader = (new ReadableStream()).getReader().constructor;
 
 }, 'Can get the ReadableStreamDefaultReader constructor indirectly');
 
@@ -18,16 +19,23 @@ test(() => {
   new ReadableStream({
     start(c) {
       ReadableStreamDefaultController = c.constructor;
     }
   });
 
 }, '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(); },
     getReader() { return new ReadableStreamDefaultReader(new ReadableStream()); },
     pipeThrough(obj) { return obj.readable; },
     pipeTo() { return Promise.resolve(); },
     tee() { return [realRS(), realRS()]; }
   }, ReadableStream.prototype);
@@ -63,16 +71,23 @@ function realRSDefaultController() {
   new ReadableStream({
     start(c) {
       controller = c;
     }
   });
   return controller;
 }
 
+function fakeRSAsyncIterator() {
+  return Object.setPrototypeOf({
+    next() { },
+    return(value = undefined) { }
+  }, ReadableStreamAsyncIteratorPrototype);
+}
+
 promise_test(t => {
 
   return methodRejectsForAll(t, ReadableStream.prototype, 'cancel',
                              [fakeRS(), realRSDefaultReader(), realRSDefaultController(), undefined, null]);
 
 }, 'ReadableStream.prototype.cancel enforces a brand check');
 
 test(() => {
@@ -152,8 +167,22 @@ test(() => {
 }, 'ReadableStreamDefaultController.prototype.enqueue enforces a brand check');
 
 test(() => {
 
   methodThrowsForAll(ReadableStreamDefaultController.prototype, 'error',
                      [fakeRSDefaultController(), realRS(), realRSDefaultReader(), undefined, null]);
 
 }, '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');
--- a/testing/web-platform/tests/streams/readable-streams/general.any.js
+++ b/testing/web-platform/tests/streams/readable-streams/general.any.js
@@ -34,23 +34,25 @@ test(() => {
     'constructor should throw when the type is asdf');
   assert_throws(error1, () => new ReadableStream({ type: { get toString() {throw error1;} } }), 'constructor should throw when ToString() throws');
   assert_throws(error1, () => new ReadableStream({ type: { toString() {throw error1;} } }), 'constructor should throw when ToString() throws');
 
 }, 'ReadableStream can\'t be constructed with an invalid type');
 
 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 symbols = [Symbol.asyncIterator];
 
   const rs = new ReadableStream();
   const proto = Object.getPrototypeOf(rs);
 
-  assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties, 'should have all the correct methods');
+  assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties, 'should have all the correct properties');
+  assert_array_equals(Object.getOwnPropertySymbols(proto).sort(), symbols, 'should have all the correct symbols');
 
   for (const m of methods) {
     const propDesc = Object.getOwnPropertyDescriptor(proto, m);
     assert_false(propDesc.enumerable, 'method should be non-enumerable');
     assert_true(propDesc.configurable, 'method should be configurable');
     assert_true(propDesc.writable, 'method should be writable');
     assert_equals(typeof rs[m], 'function', 'method should be a function');
     const expectedName = m === 'constructor' ? 'ReadableStream' : m;
@@ -65,16 +67,25 @@ test(() => {
   assert_true(lockedPropDesc.configurable, 'locked should be configurable');
 
   assert_equals(rs.cancel.length, 1, 'cancel should have 1 parameter');
   assert_equals(rs.constructor.length, 0, 'constructor should have no parameters');
   assert_equals(rs.getReader.length, 0, 'getReader should have no parameters');
   assert_equals(rs.pipeThrough.length, 1, 'pipeThrough should have 1 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');
+  assert_equals(rs[Symbol.asyncIterator].length, 0, '@@asyncIterator should have no required parameters');
+
+  const asyncIteratorPropDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator);
+  assert_false(asyncIteratorPropDesc.enumerable, '@@asyncIterator should be non-enumerable');
+  assert_true(asyncIteratorPropDesc.configurable, '@@asyncIterator should be configurable');
+  assert_true(asyncIteratorPropDesc.writable, '@@asyncIterator should be writable');
+  assert_equals(typeof rs[Symbol.asyncIterator], 'function', '@@asyncIterator should be a function');
+  assert_equals(rs[Symbol.asyncIterator].name, 'getIterator', '@@asyncIterator should have the correct name');
 
 }, 'ReadableStream instances should have the correct list of properties');
 
 test(() => {
 
   assert_throws(new TypeError(), () => {
     new ReadableStream({ start: 'potato' });
   }, 'constructor should throw when start is not a function');
--- a/testing/web-platform/tests/streams/readable-streams/patched-global.any.js
+++ b/testing/web-platform/tests/streams/readable-streams/patched-global.any.js
@@ -52,8 +52,58 @@ test(t => {
     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 getIterator() should use the original values of getReader() and ReadableStreamDefaultReader methods');