tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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");