Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions src/tests/streams/piping/error-propagation.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,115 @@ export const errorTypePreservationPipeThrough = {
strictEqual(reason.code, 'ERR_PIPE');
},
};

// The WPT 'starts errored … abort promise' residue: the destination's
// abort() hook returns a PROMISE. Fulfilled: consumed silently, the
// pipe still rejects with the SOURCE error. Rejected: per spec the
// shutdown action's failure REPLACES the rejection reason.
export const destAbortPromiseStates = {
async test() {
// Fulfilled abort promise.
{
const err = new Error('src-err');
let abortCalled = false;
const rs = new ReadableStream({
start(c) {
c.error(err);
},
});
const ws = new WritableStream({
abort() {
abortCalled = true;
return Promise.resolve('ignored');
},
});
strictEqual(await rejectionOf(rs.pipeTo(ws)), err);
strictEqual(abortCalled, true);
}
// Rejected abort promise.
{
const err = new Error('src-err');
const abortErr = new Error('abort-failed');
const rs = new ReadableStream({
start(c) {
c.error(err);
},
});
const ws = new WritableStream({
abort() {
return Promise.reject(abortErr);
},
});
const reason = await rejectionOf(rs.pipeTo(ws));
strictEqual(reason, abortErr);
}
},
};

// preventAbort AND preventCancel together on a starts-errored source:
// both suppressions hold and both ends stay un-shut-down.
export const preventAbortAndCancelCombo = {
async test() {
const err = new Error('src-err');
let abortCalled = false;
const rs = new ReadableStream({
start(c) {
c.error(err);
},
});
const ws = new WritableStream({
abort() {
abortCalled = true;
},
});
strictEqual(
await rejectionOf(
rs.pipeTo(ws, {
preventAbort: true,
preventCancel: true,
preventClose: true,
})
),
err
);
strictEqual(abortCalled, false);
ws.getWriter(); // dest untouched and re-lockable
},
};
Comment on lines +285 to +314

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A source error only takes the destination-abort branch; it never attempts to cancel the source. This test therefore cannot observe preventCancel, so a regression in that option passes while the comment claims both suppressions are covered. An abort signal initiates both shutdown actions.

Suggested change
// preventAbort AND preventCancel together on a starts-errored source:
// both suppressions hold and both ends stay un-shut-down.
export const preventAbortAndCancelCombo = {
async test() {
const err = new Error('src-err');
let abortCalled = false;
const rs = new ReadableStream({
start(c) {
c.error(err);
},
});
const ws = new WritableStream({
abort() {
abortCalled = true;
},
});
strictEqual(
await rejectionOf(
rs.pipeTo(ws, {
preventAbort: true,
preventCancel: true,
preventClose: true,
})
),
err
);
strictEqual(abortCalled, false);
ws.getWriter(); // dest untouched and re-lockable
},
};
// An abort signal triggers both abort-destination and cancel-source
// shutdown actions; preventAbort and preventCancel must suppress both.
export const preventAbortAndCancelCombo = {
async test() {
const err = new Error('abort-reason');
const abortController = new AbortController();
let abortCalled = false;
let cancelCalled = false;
const rs = new ReadableStream({
cancel() {
cancelCalled = true;
},
});
const ws = new WritableStream({
abort() {
abortCalled = true;
},
});
const pipeP = rs.pipeTo(ws, {
preventAbort: true,
preventCancel: true,
preventClose: true,
signal: abortController.signal,
});
await scheduler.wait(1);
abortController.abort(err);
strictEqual(await rejectionOf(pipeP), err);
strictEqual(abortCalled, false);
strictEqual(cancelCalled, false);
strictEqual(rs.locked, false);
ws.getWriter(); // dest untouched and re-lockable
},
};


// The WPT 'shutdown must not occur until the final write completes'
// shape: the source errors while a write is IN FLIGHT — the
// destination's abort must not run until that write settles.
export const shutdownWaitsForInFlightWrite = {
async test() {
const err = new Error('src-err');
const events = [];
let releaseWrite;
const parked = new Promise((resolve) => (releaseWrite = resolve));
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
},
});
const ws = new WritableStream({
write() {
events.push('write-start');
return parked;
},
abort(reason) {
events.push(`abort:${reason.message}`);
},
});
const pipeP = rs.pipeTo(ws);
controller.enqueue('chunk');
await scheduler.wait(10);
controller.error(err);
await scheduler.wait(20);
// The write is still parked: abort must not have run yet.
strictEqual(events.join(','), 'write-start');
releaseWrite();
strictEqual(await rejectionOf(pipeP), err);
Comment on lines +340 to +348

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only observes that abort() has not run before the write is released. A broken pipe could reject before releaseWrite() while deferring the abort hook, and would still pass here. Track the pipe promise itself to verify that shutdown does not settle early.

Suggested change
const pipeP = rs.pipeTo(ws);
controller.enqueue('chunk');
await scheduler.wait(10);
controller.error(err);
await scheduler.wait(20);
// The write is still parked: abort must not have run yet.
strictEqual(events.join(','), 'write-start');
releaseWrite();
strictEqual(await rejectionOf(pipeP), err);
const pipeP = rs.pipeTo(ws);
let pipeSettled = false;
pipeP.then(
() => (pipeSettled = true),
() => (pipeSettled = true)
);
controller.enqueue('chunk');
await scheduler.wait(10);
controller.error(err);
await scheduler.wait(20);
// The write is still parked: the pipe and its abort action must not settle yet.
strictEqual(events.join(','), 'write-start');
strictEqual(pipeSettled, false);
releaseWrite();
strictEqual(await rejectionOf(pipeP), err);

strictEqual(events.join(','), 'write-start,abort:src-err');
},
};
3 changes: 3 additions & 0 deletions src/tests/streams/piping/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export {
destStartsErroredPreventCancel,
errorTypePreservationPipeTo,
errorTypePreservationPipeThrough,
destAbortPromiseStates,
preventAbortAndCancelCombo,
shutdownWaitsForInFlightWrite,
} from 'error-propagation';

export {
Expand Down
35 changes: 35 additions & 0 deletions src/tests/streams/readable-byte/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ bridge drives to consume TypeScript streams (conduit basics in the
identity suite's draining-reader.js). No such global exists under the
C++ implementation; `draining-reader.js` asserts both sides.

## WPT map: readable-byte-streams/general.any.js expectedFailures

The 34 C++ expectedFailures in that WPT file (see
src/wpt/streams-test.ts), each mapped to the suite pin that owns the
divergence root. "Family" means the WPT test fails for the same root a
named suite test pins directly, differing only in incidental asserts.

| WPT test (abbreviated) | Root | Suite pin |
| --- | --- | --- |
| start() throws an exception | ctor captures sync start throws (ledger #4) | `syncStartThrow` |
| Automatic pull() after start() / after read() / after read(view) | proactive pull (ledger #3) | `pullCountShape` |
| autoAllocateChunkSize | auto-allocated byobRequest on default reads (ledger #5) | `byobRequestOnDefaultRead` |
| Respond to pull() by enqueue() asynchronously / multiple pull() by separate enqueue() / read() twice then enqueue() twice / Push source without pull signal / enqueue()+getReader()+read() | pull-count and coalescing family (ledger #3, #17) | `pullCountShape`, `byteDesiredSizeAccounting` |
| constructor rejects size with type "bytes" | ledger #1 | `sizeStrategyForBytes` |
| cancel() with partially filled pending pull() | done-shape family + partial discard | `cancelWithPartiallyFilledPull` (direct) |
| getReader(), read(view), then cancel() | pull runs before cancel under C++ | `readViewThenCancelOrdering` (direct) |
| enqueue() with Uint16Array then read() / 3 byte + 2-element Uint16Array | mismatched view/enqueue granularity | `readableStreamBytesMismatchedSizes`, `byobUint16Array` |
| read(view) Uint32Array filled by multiple enqueue() | partial fills across enqueues | `byobUint32Array`, `byobPartialRespondMisalignsFillOffset` |
| enqueue(), read(view) partially, then read() | remainder to a default read | `partialViewThenDefaultRead` (direct; PARITY) |
| read(view) Uint16 on close()-d with 1 byte / errored if close()-d before fulfilling read(view) | close-with-partial (ledger #7) | `closeWithPartiallyFilledView` |
| Throwing in pull ignored if errored / pull throw errors stream | pull-throw shapes | `pullThrowIgnoredIfErrored`, `pullThrowErrorsStream` |
| enqueue() discards auto-allocated BYOB request | request invalidation | `enqueueDiscardsByobRequest` |
| releaseLock()+second-reader ×9 (respond / respond(1) Uint16 / respond(3) / respondWithNewView / autoAllocate ×3 / Uint16 respond(1) chains ×2) | the release-relock cluster (ledger #9, #10) | `release-relock.js` (whole module) |
| Multiple read(view): close() and respond() / big enqueue() / multiple enqueue() | multi-pending-read delivery | `readableStreamMultiplePendingReads` |

## Compatibility flags

| Flag | Pinned in main cells | Other cells |
Expand Down Expand Up @@ -101,3 +126,13 @@ streams-byte-cancel-uaf, streams-byte-handlePush-uaf,
streams-byob-close-reentry, streams-byob-concurrent-readatleast,
streams-internal-read-buffer-gc, streams-circ-ref-regression,
streams-consumer-reentry-gc.

## IDL shape (deliberately not pinned here)

WebIDL function metadata — operation `.length` values (optional
arguments do not count), and promise-typed attributes/operations
REJECTING rather than throwing on a broken `this` — is enumerated
per-implementation by WPT's `idlharness.any.js`: the C++ implementation
carries the known deviations as expectedFailures in
`src/wpt/streams-test.ts`; the TypeScript implementation matches spec.
The suites do not duplicate that enumeration.
31 changes: 31 additions & 0 deletions src/tests/streams/readable-byte/byob-reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,34 @@ export const byobreaderRegression = {
ok(done);
},
};

// A BYOB read consumes part of an enqueued chunk; a later DEFAULT read
// picks up the remainder (WPT 'enqueue(), read(view) partially, then
// read()'). PARITY: [1,2] to the view, then Uint8Array [3] to the
// default reader.
export const partialViewThenDefaultRead = {
async test() {
const rs = new ReadableStream({
type: 'bytes',
start(c) {
c.enqueue(new Uint8Array([1, 2, 3]));
},
});
const byob = rs.getReader({ mode: 'byob' });
const first = await byob.read(new Uint8Array(2));
byob.releaseLock();
const dflt = rs.getReader();
const second = await Promise.race([
dflt
.read()
.then(
(r) =>
`second:done=${r.done},type=${r.value?.constructor?.name},bytes=[${r.value ? Array.from(r.value) : ''}]`
),
scheduler.wait(200).then(() => 'second:pending'),
]);
strictEqual(first.done, false);
strictEqual(Array.from(first.value).join(','), '1,2');
strictEqual(second, 'second:done=false,type=Uint8Array,bytes=[3]');
},
};
80 changes: 80 additions & 0 deletions src/tests/streams/readable-byte/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,83 @@ export const closeWithPendingUnfilledByobRead = {
await reader.closed;
},
};

// cancel() while a partially filled pull-into is pending (WPT
// 'cancel() with partially filled pending pull() request'): the read
// resolves done with the partial bytes DISCARDED on both sides —
// DIVERGENCE only in the done shape (C++ an empty view, TypeScript
// undefined; the done-read family). The cancel hook gets the reason
// and the cancel fulfills on both.
export const cancelWithPartiallyFilledPull = {
async test() {
const events = [];
let controller;
const rs = new ReadableStream({
type: 'bytes',
start(c) {
controller = c;
},
cancel(reason) {
events.push(`cancel:${reason}`);
},
});
const reader = rs.getReader({ mode: 'byob' });
const readP = reader.read(new Uint16Array(1)); // wants 2 bytes
controller.enqueue(new Uint8Array([0x11])); // partial: 1 byte
await scheduler.wait(1);
const cancelP = reader.cancel('why');
const read = await Promise.race([
readP.then(
(r) =>
`read:done=${r.done},len=${r.value ? r.value.byteLength : 'undef'}`,
(e) => `read-rejected:${e.name}`
),
scheduler.wait(200).then(() => 'read:pending'),
]);
const cancel = await Promise.race([
cancelP.then(
() => 'cancel:fulfilled',
(e) => `cancel-rejected:${e.name}`
),
scheduler.wait(200).then(() => 'cancel:pending'),
]);
strictEqual(
read,
usingTsImpl ? 'read:done=true,len=undef' : 'read:done=true,len=0'
);
strictEqual(cancel, 'cancel:fulfilled');
strictEqual(events.join(','), 'cancel:why');
},
};

// read(view) then immediate cancel() (WPT 'getReader(), read(view),
// then cancel()'): DIVERGENCE — C++ pulls proactively on the read, so
// pull runs BEFORE the cancel hook; TypeScript never pulls (spec: the
// cancel wins). The read resolves done on both.
export const readViewThenCancelOrdering = {
async test() {
const events = [];
const rs = new ReadableStream({
type: 'bytes',
pull() {
events.push('pull');
},
cancel(reason) {
events.push(`cancel:${reason}`);
},
});
const reader = rs.getReader({ mode: 'byob' });
const readP = reader.read(new Uint8Array(4));
const cancelP = reader.cancel('stop');
await Promise.all([
readP.then((r) => events.push(`read:done=${r.done}`)),
cancelP,
]);
strictEqual(
events.join(','),
usingTsImpl
? 'cancel:stop,read:done=true'
: 'pull,cancel:stop,read:done=true'
);
},
};
3 changes: 3 additions & 0 deletions src/tests/streams/readable-byte/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export {
readAfterCloseReturnsEmptyView,
readDetachesCallerBuffer,
closeWithPendingUnfilledByobRead,
cancelWithPartiallyFilledPull,
readViewThenCancelOrdering,
} from 'controller';

export {
Expand All @@ -68,6 +70,7 @@ export {
readableStreamBytesEnqueueSubarray,
readableStreamMultiplePendingReads,
byobreaderRegression,
partialViewThenDefaultRead,
} from 'byob-reader';

export {
Expand Down
10 changes: 10 additions & 0 deletions src/tests/streams/readable/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,13 @@ ResponseTextLargeBody), ts-webstreams-test.js (three parity body tests;
its TS-identity assertions remain). streams-js-test.js is deliberately
untouched: its tests interleave value and byte sections, so its value
halves move when the readable-byte suite consumes the byte halves.

## IDL shape (deliberately not pinned here)

WebIDL function metadata — operation `.length` values (optional
arguments do not count), and promise-typed attributes/operations
REJECTING rather than throwing on a broken `this` — is enumerated
per-implementation by WPT's `idlharness.any.js`: the C++ implementation
carries the known deviations as expectedFailures in
`src/wpt/streams-test.ts`; the TypeScript implementation matches spec.
The suites do not duplicate that enumeration.
10 changes: 10 additions & 0 deletions src/tests/streams/transform/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,13 @@ C++ implementation; `draining-reader.js` asserts both sides.
| `draining-reader.js` | TS only (C++ cell asserts the global's absence): writes flow through the transformer into conduit reads; a readable-side backlog plus close sentinel swept in one batch; flush() output rides the final batch; expectedLength undefined; transformer errors propagate |
| `data-volumes.js` | volumes through JS transformers with concurrent producer/consumer: 1 MiB passthrough, 8 MiB XOR (proves every byte passed through the transformer), 4096-chunk value mapping |
| `which-impl.js` / `helpers.js` | implementation detection; consume helpers |

## IDL shape (deliberately not pinned here)

WebIDL function metadata — operation `.length` values (optional
arguments do not count), and promise-typed attributes/operations
REJECTING rather than throwing on a broken `this` — is enumerated
per-implementation by WPT's `idlharness.any.js`: the C++ implementation
carries the known deviations as expectedFailures in
`src/wpt/streams-test.ts`; the TypeScript implementation matches spec.
The suites do not duplicate that enumeration.
10 changes: 10 additions & 0 deletions src/tests/streams/writable/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,13 @@ promises (they resolve with undefined).
| `legacy-writer.js` | pre-flag writer semantics (see Compatibility flags) |
| `data-volumes.js` | write-side volumes: 4096 × 16 B writes, single 1 MiB write, 1 MiB / 8 MiB chunked with writer.ready honored; the sink verifies the continuous prime-modulus pattern as chunks arrive |
| `which-impl.js` | implementation + pedantic detection |

## IDL shape (deliberately not pinned here)

WebIDL function metadata — operation `.length` values (optional
arguments do not count), and promise-typed attributes/operations
REJECTING rather than throwing on a broken `this` — is enumerated
per-implementation by WPT's `idlharness.any.js`: the C++ implementation
carries the known deviations as expectedFailures in
`src/wpt/streams-test.ts`; the TypeScript implementation matches spec.
The suites do not duplicate that enumeration.
Loading
Loading