From a8872d92b147fc87200eb0c14fe7a4a9e7cd4f73 Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Fri, 29 Sep 2023 16:35:17 +0900 Subject: [PATCH] Streams: tests for `Transformer.cancel` (#40453) Co-authored-by: Mattias Buelens Co-authored-by: Domenic Denicola --- streams/transform-streams/cancel.any.js | 115 ++++++++++++++++++ streams/transform-streams/errors.any.js | 43 +++++-- streams/transform-streams/flush.any.js | 15 +++ streams/transform-streams/general.any.js | 19 ++- .../reentrant-strategies.any.js | 4 + 5 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 streams/transform-streams/cancel.any.js diff --git a/streams/transform-streams/cancel.any.js b/streams/transform-streams/cancel.any.js new file mode 100644 index 00000000000000..5c7fc4eae5d55b --- /dev/null +++ b/streams/transform-streams/cancel.any.js @@ -0,0 +1,115 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +'use strict'; + +const thrownError = new Error('bad things are happening!'); +thrownError.name = 'error1'; + +const originalReason = new Error('original reason'); +originalReason.name = 'error2'; + +promise_test(async t => { + let cancelled = undefined; + const ts = new TransformStream({ + cancel(reason) { + cancelled = reason; + } + }); + const res = await ts.readable.cancel(thrownError); + assert_equals(res, undefined, 'readable.cancel() should return undefined'); + assert_equals(cancelled, thrownError, 'transformer.cancel() should be called with the passed reason'); +}, 'cancelling the readable side should call transformer.cancel()'); + +promise_test(async t => { + const ts = new TransformStream({ + cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + throw thrownError; + } + }); + const writer = ts.writable.getWriter(); + const cancelPromise = ts.readable.cancel(originalReason); + await promise_rejects_exactly(t, thrownError, cancelPromise, 'readable.cancel() should reject with thrownError'); + await promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject with thrownError'); +}, 'cancelling the readable side should reject if transformer.cancel() throws'); + +promise_test(async t => { + let aborted = undefined; + const ts = new TransformStream({ + cancel(reason) { + aborted = reason; + }, + flush: t.unreached_func('flush should not be called') + }); + const res = await ts.writable.abort(thrownError); + assert_equals(res, undefined, 'writable.abort() should return undefined'); + assert_equals(aborted, thrownError, 'transformer.abort() should be called with the passed reason'); +}, 'aborting the writable side should call transformer.abort()'); + +promise_test(async t => { + const ts = new TransformStream({ + cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + throw thrownError; + }, + flush: t.unreached_func('flush should not be called') + }); + const reader = ts.readable.getReader(); + const abortPromise = ts.writable.abort(originalReason); + await promise_rejects_exactly(t, thrownError, abortPromise, 'writable.abort() should reject with thrownError'); + await promise_rejects_exactly(t, thrownError, reader.closed, 'reader.closed should reject with thrownError'); +}, 'aborting the writable side should reject if transformer.cancel() throws'); + +promise_test(async t => { + const ts = new TransformStream({ + async cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + throw thrownError; + }, + flush: t.unreached_func('flush should not be called') + }); + const cancelPromise = ts.readable.cancel(originalReason); + const closePromise = ts.writable.close(); + await Promise.all([ + promise_rejects_exactly(t, thrownError, cancelPromise, 'cancelPromise should reject with thrownError'), + promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject with thrownError'), + ]); +}, 'closing the writable side should reject if a parallel transformer.cancel() throws'); + +promise_test(async t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + async cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + controller.error(thrownError); + }, + flush: t.unreached_func('flush should not be called') + }); + const cancelPromise = ts.readable.cancel(originalReason); + const closePromise = ts.writable.close(); + await Promise.all([ + promise_rejects_exactly(t, thrownError, cancelPromise, 'cancelPromise should reject with thrownError'), + promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject with thrownError'), + ]); +}, 'readable.cancel() and a parallel writable.close() should reject if a transformer.cancel() calls controller.error()'); + +promise_test(async t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + async cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + controller.error(thrownError); + }, + flush: t.unreached_func('flush should not be called') + }); + const cancelPromise = ts.writable.abort(originalReason); + await promise_rejects_exactly(t, thrownError, cancelPromise, 'cancelPromise should reject with thrownError'); + const closePromise = ts.readable.cancel(1); + await promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject with thrownError'); +}, 'writable.abort() and readable.cancel() should reject if a transformer.cancel() calls controller.error()'); diff --git a/streams/transform-streams/errors.any.js b/streams/transform-streams/errors.any.js index 678234cc738789..bea060b6590818 100644 --- a/streams/transform-streams/errors.any.js +++ b/streams/transform-streams/errors.any.js @@ -9,7 +9,8 @@ promise_test(t => { const ts = new TransformStream({ transform() { throw thrownError; - } + }, + cancel: t.unreached_func('cancel should not be called') }); const reader = ts.readable.getReader(); @@ -34,7 +35,8 @@ promise_test(t => { }, flush() { throw thrownError; - } + }, + cancel: t.unreached_func('cancel should not be called') }); const reader = ts.readable.getReader(); @@ -54,13 +56,14 @@ promise_test(t => { ]); }, 'TransformStream errors thrown in flush put the writable and readable in an errored state'); -test(() => { +test(t => { new TransformStream({ start(c) { c.enqueue('a'); c.error(new Error('generic error')); assert_throws_js(TypeError, () => c.enqueue('b'), 'enqueue() should throw'); - } + }, + cancel: t.unreached_func('cancel should not be called') }); }, 'errored TransformStream should not enqueue new chunks'); @@ -72,7 +75,8 @@ promise_test(t => { }); }, transform: t.unreached_func('transform should not be called'), - flush: t.unreached_func('flush should not be called') + flush: t.unreached_func('flush should not be called'), + cancel: t.unreached_func('cancel should not be called') }); const writer = ts.writable.getWriter(); @@ -96,7 +100,8 @@ promise_test(t => { }); }, transform: t.unreached_func('transform should never be called if start() fails'), - flush: t.unreached_func('flush should never be called if start() fails') + flush: t.unreached_func('flush should never be called if start() fails'), + cancel: t.unreached_func('cancel should never be called if start() fails') }); const writer = ts.writable.getWriter(); @@ -202,9 +207,10 @@ promise_test(t => { return Promise.all([ abortPromise, cancelPromise, - promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject with thrownError')]); -}, 'abort should set the close reason for the writable when it happens before cancel during start, but cancel should ' + - 'still succeed'); + promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject'), + ]); +}, 'abort should set the close reason for the writable when it happens before cancel during start, and cancel should ' + + 'reject'); promise_test(t => { let resolveTransform; @@ -251,13 +257,26 @@ promise_test(t => { controller = c; } }); - const cancelPromise = ts.readable.cancel(thrownError); - controller.error(ignoredError); + const cancelPromise = ts.readable.cancel(ignoredError); + controller.error(thrownError); return Promise.all([ cancelPromise, promise_rejects_exactly(t, thrownError, ts.writable.getWriter().closed, 'closed should reject with thrownError') ]); -}, 'controller.error() should do nothing after readable.cancel()'); +}, 'controller.error() should close writable immediately after readable.cancel()'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + return ts.readable.cancel(thrownError).then(() => { + controller.error(ignoredError); + return promise_rejects_exactly(t, thrownError, ts.writable.getWriter().closed, 'closed should reject with thrownError'); + }); +}, 'controller.error() should do nothing after readable.cancel() resolves'); promise_test(t => { let controller; diff --git a/streams/transform-streams/flush.any.js b/streams/transform-streams/flush.any.js index 059d8ce954e245..c95d8ae1186518 100644 --- a/streams/transform-streams/flush.any.js +++ b/streams/transform-streams/flush.any.js @@ -129,3 +129,18 @@ promise_test(t => { }); return promise_rejects_exactly(t, error1, ts.writable.getWriter().close(), 'close() should reject'); }, 'error() during flush should cause writer.close() to reject'); + +promise_test(async t => { + let flushed = false; + const ts = new TransformStream({ + flush() { + flushed = true; + }, + cancel: t.unreached_func('cancel should not be called') + }); + const closePromise = ts.writable.close(); + await delay(0); + const cancelPromise = ts.readable.cancel(error1); + await Promise.all([closePromise, cancelPromise]); + assert_equals(flushed, true, 'transformer.flush() should be called'); +}, 'closing the writable side should call transformer.flush() and a parallel readable.cancel() should not reject'); diff --git a/streams/transform-streams/general.any.js b/streams/transform-streams/general.any.js index 76bf90262a2695..a40ef30843ec4d 100644 --- a/streams/transform-streams/general.any.js +++ b/streams/transform-streams/general.any.js @@ -388,9 +388,24 @@ promise_test(t => { controller.terminate(); return Promise.all([ cancelPromise, - promise_rejects_exactly(t, cancelReason, ts.writable.getWriter().closed, 'closed should reject with cancelReason') + promise_rejects_js(t, TypeError, ts.writable.getWriter().closed, 'closed should reject with TypeError') ]); -}, 'terminate() should do nothing after readable.cancel()'); +}, 'terminate() should abort writable immediately after readable.cancel()'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + const cancelReason = { name: 'cancelReason' }; + return ts.readable.cancel(cancelReason).then(() => { + controller.terminate(); + return promise_rejects_exactly(t, cancelReason, ts.writable.getWriter().closed, 'closed should reject with TypeError'); + }) +}, 'terminate() should do nothing after readable.cancel() resolves'); + promise_test(() => { let calls = 0; diff --git a/streams/transform-streams/reentrant-strategies.any.js b/streams/transform-streams/reentrant-strategies.any.js index 15596546f70f18..a6d459655856a7 100644 --- a/streams/transform-streams/reentrant-strategies.any.js +++ b/streams/transform-streams/reentrant-strategies.any.js @@ -314,6 +314,10 @@ promise_test(t => { // call to TransformStreamDefaultSink. return delay(0).then(() => { controller.enqueue('a'); + return reader.read(); + }).then(({ value, done }) => { + assert_false(done, 'done should be false'); + assert_equals(value, 'a', 'value should be correct'); return Promise.all([promise_rejects_exactly(t, error1, reader.read(), 'read() should reject'), abortPromise]); }); }, 'writer.abort() inside size() should work');