Commit 847da074bdd for nodejs
commit 847da074bdd53ea4314edab98b52d0545997b835
Author: James M Snell <jasnell@gmail.com>
Date: Tue Sep 22 19:45:17 2026 +0000
stream: fill out stream/iter test coverage
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode
PR-URL: https://github.com/nodejs/node/pull/66079
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
diff --git a/lib/internal/streams/iter/classic.js b/lib/internal/streams/iter/classic.js
index ac6e6976657..59eb5be5819 100644
--- a/lib/internal/streams/iter/classic.js
+++ b/lib/internal/streams/iter/classic.js
@@ -631,6 +631,7 @@ function fromWritable(writable, options = kNullPrototype) {
entry.onAbort = undefined;
}
+ // Without preserveReason (a clean finish), reason is unused.
function cleanup(
reason,
preserveReason = false,
@@ -639,13 +640,10 @@ function fromWritable(writable, options = kNullPrototype) {
const pending = drainWaiters;
drainWaiters = [];
for (let i = 0; i < pending.length; i++) {
- if (!preserveReason &&
- (reason === undefined || reason === null) &&
- pending[i].close !== undefined) {
- pending[i].close();
+ if (preserveReason) {
+ pending[i].reject(reason);
} else {
- pending[i].reject(
- preserveReason ? reason : reason ?? new AbortError());
+ pending[i].close();
}
}
@@ -654,8 +652,7 @@ function fromWritable(writable, options = kNullPrototype) {
while (writes.length !== 0) {
const entry = writes.shift();
cleanupPendingSignal(entry);
- entry.reject(
- preserveReason ? reason : reason ?? new AbortError());
+ entry.reject(preserveReason ? reason : new AbortError());
}
if (drainListenerInstalled) {
@@ -773,6 +770,7 @@ function fromWritable(writable, options = kNullPrototype) {
entry.signal = signal;
entry.onAbort = () => {
const index = pendingWrites.indexOf(entry);
+ // Defensive: every dequeue path removes this listener first.
if (index === -1) return;
pendingWrites.removeAt(index);
cleanupPendingSignal(entry);
@@ -792,6 +790,7 @@ function fromWritable(writable, options = kNullPrototype) {
}
function finishWithError(reason, keepTerminalListeners = false) {
+ // Defensive: callers already check errored.
if (errored) return;
errored = true;
error = reason;
@@ -834,7 +833,8 @@ function fromWritable(writable, options = kNullPrototype) {
removeTerminalListeners();
return;
}
- if (finished || (writable.writableFinished ?? false)) {
+ // Duck-typed Writables may finish without emitting 'finish'.
+ if (writable.writableFinished ?? false) {
onFinish();
removeTerminalListeners();
return;
@@ -850,6 +850,7 @@ function fromWritable(writable, options = kNullPrototype) {
}
function startEnd() {
+ // Defensive: nothing flushes again once the end has started.
if (endStarted || pendingEnd === undefined) return;
endStarted = true;
const end = pendingEnd;
diff --git a/test/parallel/test-stream-iter-from-writable-lifecycle.js b/test/parallel/test-stream-iter-from-writable-lifecycle.js
new file mode 100644
index 00000000000..cf6732c4184
--- /dev/null
+++ b/test/parallel/test-stream-iter-from-writable-lifecycle.js
@@ -0,0 +1,296 @@
+// Flags: --experimental-stream-iter
+'use strict';
+
+// Tests for fromWritable() error, close, and end edge cases.
+
+const common = require('../common');
+const assert = require('assert');
+const { EventEmitter, once } = require('events');
+const { Writable } = require('stream');
+const { setImmediate } = require('timers/promises');
+const { fromWritable, ondrain } = require('stream/iter');
+
+function createDuckWritable(methods) {
+ const writable = new EventEmitter();
+ writable.write = () => true;
+ writable.end = () => {};
+ writable.destroy = () => {};
+ return Object.assign(writable, methods);
+}
+
+function assertNoTerminalListeners(writable) {
+ assert.strictEqual(writable.listenerCount('error'), 0);
+ assert.strictEqual(writable.listenerCount('finish'), 0);
+ assert.strictEqual(writable.listenerCount('close'), 0);
+}
+
+// A queued write that throws while being flushed on drain rejects on its own;
+// later queued writes are still committed.
+async function testQueuedWriteThrowsDuringFlush() {
+ const reason = new Error('queued write failed');
+ const written = [];
+ let calls = 0;
+ const writable = createDuckWritable({
+ write(chunk) {
+ calls++;
+ if (calls === 1) return false;
+ if (calls === 2) throw reason;
+ written.push(Buffer.from(chunk).toString());
+ return true;
+ },
+ });
+ const writer = fromWritable(writable, { backpressure: 'unbounded' });
+
+ await writer.write('a');
+ const second = writer.write('b');
+ const third = writer.write('c');
+ writable.emit('drain');
+
+ await assert.rejects(second, (error) => error === reason);
+ await third;
+ assert.deepStrictEqual(written, ['c']);
+}
+
+// A queued write that errors the Writable while being flushed rejects with
+// that error, as does every write still queued behind it.
+async function testQueuedWriteErrorsDuringFlush() {
+ const reason = new Error('queued write errored');
+ let calls = 0;
+ const writable = createDuckWritable({
+ write() {
+ calls++;
+ if (calls === 1) return false;
+ writable.emit('error', reason);
+ return true;
+ },
+ });
+ const writer = fromWritable(writable, { backpressure: 'unbounded' });
+
+ await writer.write('a');
+ const second = writer.write('b');
+ const third = writer.write('c');
+ writable.emit('drain');
+
+ await assert.rejects(second, (error) => error === reason);
+ await assert.rejects(third, (error) => error === reason);
+ await assert.rejects(writer.end(), (error) => error === reason);
+ assert.strictEqual(calls, 2);
+ assertNoTerminalListeners(writable);
+}
+
+// A Writable that errors synchronously inside write() rejects the write that
+// triggered it.
+async function testWriteErrorsSynchronously() {
+ for (const method of ['write', 'writev']) {
+ const reason = new Error(`sync ${method} error`);
+ let emitted = false;
+ const writable = createDuckWritable({
+ write() {
+ if (!emitted) {
+ emitted = true;
+ writable.emit('error', reason);
+ }
+ return true;
+ },
+ });
+ const writer = fromWritable(writable);
+ const chunk = method === 'write' ? 'a' : ['a', 'b'];
+ await assert.rejects(writer[method](chunk),
+ (error) => error === reason);
+ assertNoTerminalListeners(writable);
+ }
+}
+
+async function testWritevThrowRejects() {
+ const reason = new Error('writev failed');
+ const writable = createDuckWritable({
+ write() { throw reason; },
+ });
+ const writer = fromWritable(writable);
+
+ await assert.rejects(writer.writev(['a', 'b']), (error) => error === reason);
+ writer.fail();
+}
+
+async function testEmptyWritev() {
+ const writable = new Writable({ write: common.mustNotCall() });
+ const writer = fromWritable(writable);
+
+ await writer.writev([]);
+ assert.strictEqual(await writer.end(), 0);
+}
+
+// Ending the Writable directly while writes are queued in the adapter rejects
+// them, since they can no longer be committed, and settles drain waiters.
+async function testExternalEndRejectsQueuedWrites() {
+ const callbacks = [];
+ const writable = new Writable({
+ highWaterMark: 1,
+ write(chunk, encoding, cb) { callbacks.push(cb); },
+ });
+ const writer = fromWritable(writable, { backpressure: 'unbounded' });
+
+ await writer.write('a');
+ const queued = writer.write('b');
+ const drained = ondrain(writer);
+ writable.end();
+ callbacks.shift()();
+
+ await assert.rejects(queued, { name: 'AbortError' });
+ assert.strictEqual(await drained, false);
+ assert.strictEqual(callbacks.length, 0);
+}
+
+// A Writable that errored before the adapter was created is detected
+// synchronously; the adapter keeps its listeners until 'close'.
+async function testErroredBeforeAdapterReleasesListenersOnClose() {
+ const reason = new Error('write failed');
+ const writable = new Writable({
+ autoDestroy: false,
+ write(chunk, encoding, cb) { cb(reason); },
+ });
+ writable.on('error', common.mustCall());
+ writable.write('a');
+ await setImmediate();
+
+ const writer = fromWritable(writable);
+ await assert.rejects(writer.write('b'), (error) => error === reason);
+ assert.strictEqual(writable.listenerCount('close'), 1);
+
+ writable.destroy();
+ await setImmediate();
+ assert.strictEqual(writable.listenerCount('close'), 0);
+ assert.strictEqual(writable.listenerCount('finish'), 0);
+}
+
+// Duck-typed Writables may report completion only through writableFinished
+// and 'close', without emitting 'finish'.
+async function testCloseAfterFinishWithoutFinishEvent() {
+ const writable = createDuckWritable({
+ end() {
+ process.nextTick(() => {
+ writable.writableFinished = true;
+ writable.emit('close');
+ });
+ },
+ });
+ const writer = fromWritable(writable);
+
+ await writer.write('ab');
+ assert.strictEqual(await writer.end(), 2);
+ assertNoTerminalListeners(writable);
+}
+
+// A 'close' without any indication of finishing is a premature close.
+async function testDuckPrematureClose() {
+ const writable = createDuckWritable({
+ end() {
+ process.nextTick(() => writable.emit('close'));
+ },
+ });
+ const writer = fromWritable(writable);
+
+ await assert.rejects(writer.end(), { name: 'AbortError' });
+ assertNoTerminalListeners(writable);
+}
+
+async function testAlreadyFinished() {
+ const writable = new Writable({ write(chunk, encoding, cb) { cb(); } });
+ writable.end();
+ await once(writable, 'finish');
+
+ const writer = fromWritable(writable);
+ assert.strictEqual(writer.canWrite, null);
+ await assert.rejects(writer.write('a'),
+ { code: 'ERR_STREAM_WRITE_AFTER_END' });
+ assert.strictEqual(await writer.end(), 0);
+ assertNoTerminalListeners(writable);
+}
+
+async function testAlreadyDestroyed() {
+ const writable = new Writable({ write: common.mustNotCall() });
+ writable.destroy();
+ await once(writable, 'close');
+
+ const writer = fromWritable(writable);
+ assert.strictEqual(writer.canWrite, null);
+ await assert.rejects(writer.write('a'),
+ { code: 'ERR_STREAM_WRITE_AFTER_END' });
+ assertNoTerminalListeners(writable);
+}
+
+// When end() and the destroy() that follows both throw, the end() failure
+// is reported and the adapter's listeners are released.
+async function testEndAndDestroyThrow() {
+ const endError = new Error('end failed');
+ const writable = createDuckWritable({
+ end() { throw endError; },
+ destroy() { throw new Error('destroy failed'); },
+ });
+ const writer = fromWritable(writable);
+
+ await assert.rejects(writer.end(), (error) => error === endError);
+ assertNoTerminalListeners(writable);
+}
+
+async function testFailDestroyThrows() {
+ const reason = new Error('fail reason');
+ const destroyError = new Error('destroy failed');
+ const writable = createDuckWritable({
+ destroy() { throw destroyError; },
+ });
+ const writer = fromWritable(writable);
+
+ assert.throws(() => writer.fail(reason), (error) => error === destroyError);
+ assertNoTerminalListeners(writable);
+ await assert.rejects(writer.write('a'), (error) => error === reason);
+}
+
+// An end() with a signal rejects with the Writable's error if ending fails.
+async function testFinalErrorRejectsSignaledEnd() {
+ const reason = new Error('final failed');
+ const writable = new Writable({
+ write(chunk, encoding, cb) { cb(); },
+ final(cb) { cb(reason); },
+ });
+ writable.on('error', common.mustCall());
+ const writer = fromWritable(writable);
+ const { signal } = new AbortController();
+
+ await writer.write('a');
+ await assert.rejects(writer.end({ signal }), (error) => error === reason);
+}
+
+// The signal can be aborted synchronously by the Writable's own end(), after
+// end() has checked it but before it waits on it.
+async function testSignalAbortedByUnderlyingEnd() {
+ const controller = new AbortController();
+ const writable = createDuckWritable({
+ end: common.mustCall(() => controller.abort('stop')),
+ });
+ const writer = fromWritable(writable);
+
+ await assert.rejects(writer.end({ signal: controller.signal }),
+ (reason) => reason === 'stop');
+ const ending = writer.end();
+ writable.emit('finish');
+ assert.strictEqual(await ending, 0);
+}
+
+Promise.all([
+ testQueuedWriteThrowsDuringFlush(),
+ testQueuedWriteErrorsDuringFlush(),
+ testWriteErrorsSynchronously(),
+ testWritevThrowRejects(),
+ testEmptyWritev(),
+ testExternalEndRejectsQueuedWrites(),
+ testErroredBeforeAdapterReleasesListenersOnClose(),
+ testCloseAfterFinishWithoutFinishEvent(),
+ testDuckPrematureClose(),
+ testAlreadyFinished(),
+ testAlreadyDestroyed(),
+ testEndAndDestroyThrow(),
+ testFailDestroyThrows(),
+ testFinalErrorRejectsSignaledEnd(),
+ testSignalAbortedByUnderlyingEnd(),
+]).then(common.mustCall());
diff --git a/test/parallel/test-stream-iter-readable-interop.js b/test/parallel/test-stream-iter-readable-interop.js
index 5c7b8ed42cd..ba403a74680 100644
--- a/test/parallel/test-stream-iter-readable-interop.js
+++ b/test/parallel/test-stream-iter-readable-interop.js
@@ -621,6 +621,93 @@ async function testAbortWhileReadIsPending() {
assert.strictEqual(readable.listenerCount('readable'), 0);
}
+async function testReturnBeforeNext() {
+ const readable = new Readable({ read() {} });
+ const iterator = from(readable)[Symbol.asyncIterator]();
+
+ assert.deepStrictEqual(await iterator.return('stopped'), {
+ value: 'stopped',
+ done: true,
+ });
+ assert.strictEqual(readable.destroyed, true);
+}
+
+async function testThrowBeforeNext() {
+ const readable = new Readable({ read() {} });
+ const iterator = from(readable)[Symbol.asyncIterator]();
+ const reason = new Error('stop');
+
+ await assert.rejects(iterator.throw(reason), (error) => error === reason);
+ assert.strictEqual(readable.destroyed, true);
+}
+
+async function testThrowAfterNext() {
+ const readable = new Readable({ read() {} });
+ readable.push('a');
+ const iterator = from(readable)[Symbol.asyncIterator]();
+ const reason = new Error('stop');
+
+ const first = await iterator.next();
+ assert.strictEqual(Buffer.concat(first.value).toString(), 'a');
+ await assert.rejects(iterator.throw(reason), (error) => error === reason);
+ await setImmediate();
+ assert.strictEqual(readable.destroyed, true);
+ assert.strictEqual(readable.listenerCount('readable'), 0);
+}
+
+async function testThrowWhileReadIsPending() {
+ const readable = new Readable({ read() {} });
+ const iterator = from(readable)[Symbol.asyncIterator]();
+ const reason = new Error('stop');
+ const pending = iterator.next();
+ await setImmediate();
+
+ const thrown = iterator.throw(reason);
+ assert.deepStrictEqual(await pending, { value: undefined, done: true });
+ await assert.rejects(thrown, (error) => error === reason);
+ await setImmediate();
+ assert.strictEqual(readable.destroyed, true);
+ assert.strictEqual(readable.listenerCount('readable'), 0);
+}
+
+// Without autoDestroy, an errored Readable is left for the caller to destroy,
+// but the iterator must still release its listeners.
+async function testErrorWithoutAutoDestroy() {
+ const readable = new Readable({ autoDestroy: false, read() {} });
+ const closeListeners = readable.listenerCount('close');
+ const iterator = from(readable)[Symbol.asyncIterator]();
+ const pending = iterator.next();
+ await setImmediate();
+
+ readable.push(42);
+ await assert.rejects(pending, { code: 'ERR_INVALID_ARG_TYPE' });
+ assert.strictEqual(readable.destroyed, false);
+ assert.strictEqual(readable.listenerCount('readable'), 0);
+ assert.strictEqual(readable.listenerCount('close'), closeListeners);
+ readable.destroy();
+}
+
+// A read() override may return null while data is still buffered; the batch
+// ends early rather than including the null.
+async function testNullReadWithBufferedData() {
+ const readable = new Readable({ objectMode: true, read() {} });
+ readable.push('a');
+ readable.push('b');
+ readable.push(null);
+ const read = readable.read;
+ let calls = 0;
+ readable.read = function(...args) {
+ if (++calls === 2) return null;
+ return read.apply(this, args);
+ };
+
+ const batches = [];
+ for await (const batch of from(readable)) {
+ batches.push(batch.map((chunk) => Buffer.from(chunk).toString()));
+ }
+ assert.deepStrictEqual(batches, [['a'], ['b']]);
+}
+
// =============================================================================
// kValidatedSource identity - from() returns same object for validated sources
// =============================================================================
@@ -672,4 +759,10 @@ Promise.all([
testAbortSignal(),
testReturnWhileReadIsPending(),
testAbortWhileReadIsPending(),
+ testReturnBeforeNext(),
+ testThrowBeforeNext(),
+ testThrowAfterNext(),
+ testThrowWhileReadIsPending(),
+ testErrorWithoutAutoDestroy(),
+ testNullReadWithBufferedData(),
]).then(common.mustCall());
diff --git a/test/parallel/test-stream-iter-transform-buffering.js b/test/parallel/test-stream-iter-transform-buffering.js
index 7e90479d70c..b1b813f60b9 100644
--- a/test/parallel/test-stream-iter-transform-buffering.js
+++ b/test/parallel/test-stream-iter-transform-buffering.js
@@ -3,9 +3,9 @@
const common = require('../common');
const assert = require('assert');
-const { gzipSync } = require('zlib');
-const { decompressGzip } = require('zlib/iter');
-const { from } = require('stream/iter');
+const { brotliDecompressSync, gunzipSync, gzipSync } = require('zlib');
+const { compressBrotli, compressGzip, decompressGzip } = require('zlib/iter');
+const { bytes, from, pull } = require('stream/iter');
async function testDecompressionOutputIsBounded() {
let input = Buffer.alloc(32 * 1024 * 1024, 0x61);
@@ -28,4 +28,88 @@ async function testDecompressionOutputIsBounded() {
await iterator.return();
}
-testDecompressionOutputIsBounded().then(common.mustCall());
+// An output buffer smaller than the batch high water mark fills repeatedly
+// before a batch is ready, so the engine is re-entered from the write
+// callback without yielding.
+async function testSmallChunkSizeDecompression() {
+ const input = Buffer.alloc(256 * 1024, 0x61);
+ const output = await bytes(pull(from(gzipSync(input)),
+ decompressGzip({ chunkSize: 1024 })));
+ assert.deepStrictEqual(Buffer.from(output), input);
+}
+
+// Deterministic incompressible data. Brotli's buffering decisions depend on
+// content, so random data would make the tests below flaky.
+function incompressible(size) {
+ const buffer = Buffer.allocUnsafe(size);
+ let x = 0x9e3779b9;
+ for (let i = 0; i < size; i++) {
+ x ^= x << 13;
+ x ^= x >>> 17;
+ x ^= x << 5;
+ buffer[i] = x & 0xff;
+ }
+ return buffer;
+}
+
+// Brotli buffers 1MB of incompressible input internally, so finalizing the
+// stream produces more than one batch of output that must be yielded while
+// the finish operation is still in progress.
+async function testLargeFinishOutput() {
+ const input = incompressible(1024 * 1024);
+ const output = await bytes(pull(from(input), compressBrotli()));
+ assert.deepStrictEqual(brotliDecompressSync(output), input);
+}
+
+// Unwrapping the transform function drops the validated fast path, so pull()
+// delivers an explicit null flush signal to the compression transform.
+async function testExplicitFlushSignal() {
+ const input = incompressible(1024 * 1024);
+ const { transform } = compressBrotli();
+ const output = await bytes(pull(from(input), { transform }));
+ assert.deepStrictEqual(brotliDecompressSync(output), input);
+}
+
+// Failures accessing or calling the source iterator's return() method must
+// not replace the transform's successful completion.
+async function testSourceReturnFailuresAreIgnored() {
+ const input = Buffer.from('hello world');
+ function makeSource(returnDescriptor) {
+ let done = false;
+ const iterator = {
+ next() {
+ if (done) return Promise.resolve({ done: true, value: undefined });
+ done = true;
+ return Promise.resolve({ done: false, value: [input] });
+ },
+ };
+ Object.defineProperty(iterator, 'return', returnDescriptor);
+ return { [Symbol.asyncIterator]() { return iterator; } };
+ }
+
+ const sources = [
+ makeSource({ get: common.mustCall(() => { throw new Error('getter'); }) }),
+ makeSource({
+ value: common.mustCall(() => { throw new Error('return'); }),
+ }),
+ ];
+ for (const source of sources) {
+ const { signal } = new AbortController();
+ const batches = [];
+ for await (const batch of compressGzip().transform(source, { signal })) {
+ batches.push(...batch);
+ }
+ assert.deepStrictEqual(gunzipSync(Buffer.concat(batches)), input);
+ }
+}
+
+(async () => {
+ // Run the memory measurement on its own so other tests don't skew it.
+ await testDecompressionOutputIsBounded();
+ await Promise.all([
+ testSmallChunkSizeDecompression(),
+ testLargeFinishOutput(),
+ testExplicitFlushSignal(),
+ testSourceReturnFailuresAreIgnored(),
+ ]);
+})().then(common.mustCall());
diff --git a/test/parallel/test-stream-iter-validation.js b/test/parallel/test-stream-iter-validation.js
index 3f311ec8c46..b130a92ae3d 100644
--- a/test/parallel/test-stream-iter-validation.js
+++ b/test/parallel/test-stream-iter-validation.js
@@ -199,6 +199,18 @@ assert.throws(
{ code: 'ERR_INVALID_ARG_TYPE' },
);
+// Duck-typed readables must provide each of the required methods.
+{
+ const required = ['read', 'pipe', 'destroy', 'on', 'removeListener'];
+ for (const missing of required) {
+ const duck = {};
+ for (const name of required) {
+ if (name !== missing) duck[name] = () => {};
+ }
+ assert.throws(() => fromReadable(duck), { code: 'ERR_INVALID_ARG_TYPE' });
+ }
+}
+
// fromWritable Writer options.signal must be AbortSignal
{
const writable = new Writable({
diff --git a/test/parallel/test-stream-iter-writable-from.js b/test/parallel/test-stream-iter-writable-from.js
index 12ea80df9fb..e67d75a810e 100644
--- a/test/parallel/test-stream-iter-writable-from.js
+++ b/test/parallel/test-stream-iter-writable-from.js
@@ -589,6 +589,59 @@ async function testDestroyUsesDisposeFallback() {
assert.strictEqual(disposed, true);
}
+async function testDestroyUsesAsyncDisposeFallback() {
+ const { promise: disposing, resolve } = Promise.withResolvers();
+ const writable = toWritable({
+ write(chunk) { return Promise.resolve(); },
+ [Symbol.asyncDispose]: common.mustCall(() => disposing),
+ });
+ let closed = false;
+ writable.on('close', () => { closed = true; });
+
+ writable.destroy();
+ await setImmediate();
+ // Destruction completes only once the async dispose settles.
+ assert.strictEqual(closed, false);
+ resolve();
+ await once(writable, 'close');
+}
+
+async function testDestroyWithErrorUsesAsyncDisposeFallback() {
+ const reason = new Error('destroyed');
+ const writable = toWritable({
+ write(chunk) { return Promise.resolve(); },
+ [Symbol.asyncDispose]: common.mustCall(() => Promise.resolve()),
+ });
+
+ writable.destroy(reason);
+ const [error] = await once(writable, 'error');
+ assert.strictEqual(error, reason);
+}
+
+async function testAsyncDisposeRejectionErrorsDestroy() {
+ const reason = new Error('dispose failed');
+ const writable = toWritable({
+ write(chunk) { return Promise.resolve(); },
+ [Symbol.asyncDispose]: common.mustCall(() => Promise.reject(reason)),
+ });
+
+ writable.destroy();
+ const [error] = await once(writable, 'error');
+ assert.strictEqual(error, reason);
+}
+
+async function testDisposeThrowErrorsDestroy() {
+ const reason = new Error('dispose failed');
+ const writable = toWritable({
+ write(chunk) { return Promise.resolve(); },
+ [Symbol.dispose]: common.mustCall(() => { throw reason; }),
+ });
+
+ writable.destroy();
+ const [error] = await once(writable, 'error');
+ assert.strictEqual(error, reason);
+}
+
// =============================================================================
// Destroy with error calls fail() when available
// =============================================================================
@@ -762,6 +815,10 @@ Promise.all([
testDestroyDelegatesToFail(),
testDestroyWithoutError(),
testDestroyUsesDisposeFallback(),
+ testDestroyUsesAsyncDisposeFallback(),
+ testDestroyWithErrorUsesAsyncDisposeFallback(),
+ testAsyncDisposeRejectionErrorsDestroy(),
+ testDisposeThrowErrorsDestroy(),
testDestroyWithError(),
testDestroyWithoutFail(),
testWriteErrorPropagation(),