abort.any.js (16802B)
1 // META: global=window,worker,shadowrealm 2 // META: script=../resources/recording-streams.js 3 // META: script=../resources/test-utils.js 4 'use strict'; 5 6 // Tests for the use of pipeTo with AbortSignal. 7 // There is some extra complexity to avoid timeouts in environments where abort is not implemented. 8 9 const error1 = new Error('error1'); 10 error1.name = 'error1'; 11 const error2 = new Error('error2'); 12 error2.name = 'error2'; 13 14 const errorOnPull = { 15 pull(controller) { 16 // This will cause the test to error if pipeTo abort is not implemented. 17 controller.error('failed to abort'); 18 } 19 }; 20 21 // To stop pull() being called immediately when the stream is created, we need to set highWaterMark to 0. 22 const hwm0 = { highWaterMark: 0 }; 23 24 for (const invalidSignal of [null, 'AbortSignal', true, -1, Object.create(AbortSignal.prototype)]) { 25 promise_test(t => { 26 const rs = recordingReadableStream(errorOnPull, hwm0); 27 const ws = recordingWritableStream(); 28 return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { signal: invalidSignal }), 'pipeTo should reject') 29 .then(() => { 30 assert_equals(rs.events.length, 0, 'no ReadableStream methods should have been called'); 31 assert_equals(ws.events.length, 0, 'no WritableStream methods should have been called'); 32 }); 33 }, `a signal argument '${invalidSignal}' should cause pipeTo() to reject`); 34 } 35 36 promise_test(t => { 37 const rs = recordingReadableStream(errorOnPull, hwm0); 38 const ws = new WritableStream(); 39 const abortController = new AbortController(); 40 const signal = abortController.signal; 41 abortController.abort(); 42 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject') 43 .then(() => Promise.all([ 44 rs.getReader().closed, 45 promise_rejects_dom(t, 'AbortError', ws.getWriter().closed, 'writer.closed should reject') 46 ])) 47 .then(() => { 48 assert_equals(rs.events.length, 2, 'cancel should have been called'); 49 assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); 50 assert_equals(rs.events[1].name, 'AbortError', 'the argument to cancel should be an AbortError'); 51 assert_equals(rs.events[1].constructor.name, 'DOMException', 52 'the argument to cancel should be a DOMException'); 53 }); 54 }, 'an aborted signal should cause the writable stream to reject with an AbortError'); 55 56 for (const reason of [null, undefined, error1]) { 57 promise_test(async t => { 58 const rs = recordingReadableStream(errorOnPull, hwm0); 59 const ws = new WritableStream(); 60 const abortController = new AbortController(); 61 const signal = abortController.signal; 62 abortController.abort(reason); 63 const pipeToPromise = rs.pipeTo(ws, { signal }); 64 if (reason !== undefined) { 65 await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); 66 } else { 67 await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); 68 } 69 const error = await pipeToPromise.catch(e => e); 70 await rs.getReader().closed; 71 await promise_rejects_exactly(t, error, ws.getWriter().closed, 'the writable should be errored with the same object'); 72 assert_equals(signal.reason, error, 'signal.reason should be error'), 73 assert_equals(rs.events.length, 2, 'cancel should have been called'); 74 assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); 75 assert_equals(rs.events[1], error, 'the readable should be canceled with the same object'); 76 }, `(reason: '${reason}') all the error objects should be the same object`); 77 } 78 79 promise_test(t => { 80 const rs = recordingReadableStream(errorOnPull, hwm0); 81 const ws = new WritableStream(); 82 const abortController = new AbortController(); 83 const signal = abortController.signal; 84 abortController.abort(); 85 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true }), 'pipeTo should reject') 86 .then(() => assert_equals(rs.events.length, 0, 'cancel should not be called')); 87 }, 'preventCancel should prevent canceling the readable'); 88 89 promise_test(t => { 90 const rs = new ReadableStream(errorOnPull, hwm0); 91 const ws = recordingWritableStream(); 92 const abortController = new AbortController(); 93 const signal = abortController.signal; 94 abortController.abort(); 95 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventAbort: true }), 'pipeTo should reject') 96 .then(() => { 97 assert_equals(ws.events.length, 0, 'writable should not have been aborted'); 98 return ws.getWriter().ready; 99 }); 100 }, 'preventAbort should prevent aborting the readable'); 101 102 promise_test(t => { 103 const rs = recordingReadableStream(errorOnPull, hwm0); 104 const ws = recordingWritableStream(); 105 const abortController = new AbortController(); 106 const signal = abortController.signal; 107 abortController.abort(); 108 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true, preventAbort: true }), 109 'pipeTo should reject') 110 .then(() => { 111 assert_equals(rs.events.length, 0, 'cancel should not be called'); 112 assert_equals(ws.events.length, 0, 'writable should not have been aborted'); 113 return ws.getWriter().ready; 114 }); 115 }, 'preventCancel and preventAbort should prevent canceling the readable and aborting the readable'); 116 117 for (const reason of [null, undefined, error1]) { 118 promise_test(async t => { 119 const rs = new ReadableStream({ 120 start(controller) { 121 controller.enqueue('a'); 122 controller.enqueue('b'); 123 controller.close(); 124 } 125 }); 126 const abortController = new AbortController(); 127 const signal = abortController.signal; 128 const ws = recordingWritableStream({ 129 write() { 130 abortController.abort(reason); 131 } 132 }); 133 const pipeToPromise = rs.pipeTo(ws, { signal }); 134 if (reason !== undefined) { 135 await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); 136 } else { 137 await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); 138 } 139 const error = await pipeToPromise.catch(e => e); 140 assert_equals(signal.reason, error, 'signal.reason should be error'); 141 assert_equals(ws.events.length, 4, 'only chunk "a" should have been written'); 142 assert_array_equals(ws.events.slice(0, 3), ['write', 'a', 'abort'], 'events should match'); 143 assert_equals(ws.events[3], error, 'abort reason should be error'); 144 }, `(reason: '${reason}') abort should prevent further reads`); 145 } 146 147 for (const reason of [null, undefined, error1]) { 148 promise_test(async t => { 149 let readController; 150 const rs = new ReadableStream({ 151 start(c) { 152 readController = c; 153 c.enqueue('a'); 154 c.enqueue('b'); 155 } 156 }); 157 const abortController = new AbortController(); 158 const signal = abortController.signal; 159 let resolveWrite; 160 const writePromise = new Promise(resolve => { 161 resolveWrite = resolve; 162 }); 163 const ws = recordingWritableStream({ 164 write() { 165 return writePromise; 166 } 167 }, new CountQueuingStrategy({ highWaterMark: Infinity })); 168 const pipeToPromise = rs.pipeTo(ws, { signal }); 169 await delay(0); 170 await abortController.abort(reason); 171 await readController.close(); // Make sure the test terminates when signal is not implemented. 172 await resolveWrite(); 173 if (reason !== undefined) { 174 await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); 175 } else { 176 await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); 177 } 178 const error = await pipeToPromise.catch(e => e); 179 assert_equals(signal.reason, error, 'signal.reason should be error'); 180 assert_equals(ws.events.length, 6, 'chunks "a" and "b" should have been written'); 181 assert_array_equals(ws.events.slice(0, 5), ['write', 'a', 'write', 'b', 'abort'], 'events should match'); 182 assert_equals(ws.events[5], error, 'abort reason should be error'); 183 }, `(reason: '${reason}') all pending writes should complete on abort`); 184 } 185 186 for (const reason of [null, undefined, error1]) { 187 promise_test(async t => { 188 let rejectPull; 189 const pullPromise = new Promise((_, reject) => { 190 rejectPull = reject; 191 }); 192 let rejectCancel; 193 const cancelPromise = new Promise((_, reject) => { 194 rejectCancel = reject; 195 }); 196 const rs = recordingReadableStream({ 197 async pull() { 198 await Promise.race([ 199 pullPromise, 200 cancelPromise, 201 ]); 202 }, 203 cancel(reason) { 204 rejectCancel(reason); 205 }, 206 }); 207 const ws = new WritableStream(); 208 const abortController = new AbortController(); 209 const signal = abortController.signal; 210 const pipeToPromise = rs.pipeTo(ws, { signal }); 211 pipeToPromise.catch(() => {}); // Prevent unhandled rejection. 212 await delay(0); 213 abortController.abort(reason); 214 rejectPull('should not catch pull rejection'); 215 await delay(0); 216 assert_equals(rs.eventsWithoutPulls.length, 2, 'cancel should have been called'); 217 assert_equals(rs.eventsWithoutPulls[0], 'cancel', 'first event should be cancel'); 218 if (reason !== undefined) { 219 await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); 220 } else { 221 await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); 222 } 223 }, `(reason: '${reason}') underlyingSource.cancel() should called when abort, even with pending pull`); 224 } 225 226 promise_test(t => { 227 const rs = new ReadableStream({ 228 pull(controller) { 229 controller.error('failed to abort'); 230 }, 231 cancel() { 232 return Promise.reject(error1); 233 } 234 }, hwm0); 235 const ws = new WritableStream(); 236 const abortController = new AbortController(); 237 const signal = abortController.signal; 238 abortController.abort(); 239 return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); 240 }, 'a rejection from underlyingSource.cancel() should be returned by pipeTo()'); 241 242 promise_test(t => { 243 const rs = new ReadableStream(errorOnPull, hwm0); 244 const ws = new WritableStream({ 245 abort() { 246 return Promise.reject(error1); 247 } 248 }); 249 const abortController = new AbortController(); 250 const signal = abortController.signal; 251 abortController.abort(); 252 return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); 253 }, 'a rejection from underlyingSink.abort() should be returned by pipeTo()'); 254 255 promise_test(t => { 256 const events = []; 257 const rs = new ReadableStream({ 258 pull(controller) { 259 controller.error('failed to abort'); 260 }, 261 cancel() { 262 events.push('cancel'); 263 return Promise.reject(error1); 264 } 265 }, hwm0); 266 const ws = new WritableStream({ 267 abort() { 268 events.push('abort'); 269 return Promise.reject(error2); 270 } 271 }); 272 const abortController = new AbortController(); 273 const signal = abortController.signal; 274 abortController.abort(); 275 return promise_rejects_exactly(t, error2, rs.pipeTo(ws, { signal }), 'pipeTo should reject') 276 .then(() => assert_array_equals(events, ['abort', 'cancel'], 'abort() should be called before cancel()')); 277 }, 'a rejection from underlyingSink.abort() should be preferred to one from underlyingSource.cancel()'); 278 279 promise_test(t => { 280 const rs = new ReadableStream({ 281 start(controller) { 282 controller.close(); 283 } 284 }); 285 const ws = new WritableStream(); 286 const abortController = new AbortController(); 287 const signal = abortController.signal; 288 abortController.abort(); 289 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); 290 }, 'abort signal takes priority over closed readable'); 291 292 promise_test(t => { 293 const rs = new ReadableStream({ 294 start(controller) { 295 controller.error(error1); 296 } 297 }); 298 const ws = new WritableStream(); 299 const abortController = new AbortController(); 300 const signal = abortController.signal; 301 abortController.abort(); 302 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); 303 }, 'abort signal takes priority over errored readable'); 304 305 promise_test(t => { 306 const rs = new ReadableStream({ 307 pull(controller) { 308 controller.error('failed to abort'); 309 } 310 }, hwm0); 311 const ws = new WritableStream(); 312 const abortController = new AbortController(); 313 const signal = abortController.signal; 314 abortController.abort(); 315 const writer = ws.getWriter(); 316 return writer.close().then(() => { 317 writer.releaseLock(); 318 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); 319 }); 320 }, 'abort signal takes priority over closed writable'); 321 322 promise_test(t => { 323 const rs = new ReadableStream({ 324 pull(controller) { 325 controller.error('failed to abort'); 326 } 327 }, hwm0); 328 const ws = new WritableStream({ 329 start(controller) { 330 controller.error(error1); 331 } 332 }); 333 const abortController = new AbortController(); 334 const signal = abortController.signal; 335 abortController.abort(); 336 return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); 337 }, 'abort signal takes priority over errored writable'); 338 339 promise_test(() => { 340 let readController; 341 const rs = new ReadableStream({ 342 start(c) { 343 readController = c; 344 } 345 }); 346 const ws = new WritableStream(); 347 const abortController = new AbortController(); 348 const signal = abortController.signal; 349 const pipeToPromise = rs.pipeTo(ws, { signal, preventClose: true }); 350 readController.close(); 351 return Promise.resolve().then(() => { 352 abortController.abort(); 353 return pipeToPromise; 354 }).then(() => ws.getWriter().write('this should succeed')); 355 }, 'abort should do nothing after the readable is closed'); 356 357 promise_test(t => { 358 let readController; 359 const rs = new ReadableStream({ 360 start(c) { 361 readController = c; 362 } 363 }); 364 const ws = new WritableStream(); 365 const abortController = new AbortController(); 366 const signal = abortController.signal; 367 const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); 368 readController.error(error1); 369 return Promise.resolve().then(() => { 370 abortController.abort(); 371 return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); 372 }).then(() => ws.getWriter().write('this should succeed')); 373 }, 'abort should do nothing after the readable is errored'); 374 375 promise_test(t => { 376 let readController; 377 const rs = new ReadableStream({ 378 start(c) { 379 readController = c; 380 } 381 }); 382 let resolveWrite; 383 const writePromise = new Promise(resolve => { 384 resolveWrite = resolve; 385 }); 386 const ws = new WritableStream({ 387 write() { 388 readController.error(error1); 389 return writePromise; 390 } 391 }); 392 const abortController = new AbortController(); 393 const signal = abortController.signal; 394 const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); 395 readController.enqueue('a'); 396 return delay(0).then(() => { 397 abortController.abort(); 398 resolveWrite(); 399 return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); 400 }).then(() => ws.getWriter().write('this should succeed')); 401 }, 'abort should do nothing after the readable is errored, even with pending writes'); 402 403 promise_test(t => { 404 const rs = recordingReadableStream({ 405 pull(controller) { 406 return delay(0).then(() => controller.close()); 407 } 408 }); 409 let writeController; 410 const ws = new WritableStream({ 411 start(c) { 412 writeController = c; 413 } 414 }); 415 const abortController = new AbortController(); 416 const signal = abortController.signal; 417 const pipeToPromise = rs.pipeTo(ws, { signal, preventCancel: true }); 418 return Promise.resolve().then(() => { 419 writeController.error(error1); 420 return Promise.resolve(); 421 }).then(() => { 422 abortController.abort(); 423 return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); 424 }).then(() => { 425 assert_array_equals(rs.events, ['pull'], 'cancel should not have been called'); 426 }); 427 }, 'abort should do nothing after the writable is errored'); 428 429 promise_test(async t => { 430 const rs = new ReadableStream({ 431 pull(c) { 432 c.enqueue(new Uint8Array([])); 433 }, 434 type: "bytes", 435 }); 436 const ws = new WritableStream(); 437 const [first, second] = rs.tee(); 438 439 let aborted = false; 440 first.pipeTo(ws, { signal: AbortSignal.abort() }).catch(() => { 441 aborted = true; 442 }); 443 await delay(0); 444 assert_true(!aborted, "pipeTo should not resolve yet"); 445 await second.cancel(); 446 await delay(0); 447 assert_true(aborted, "pipeTo should be aborted now"); 448 }, "pipeTo on a teed readable byte stream should only be aborted when both branches are aborted");