diff --git a/doc/api/test.md b/doc/api/test.md index 3b1ea94eff9781..4a44fa91e8ea09 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -3464,6 +3464,45 @@ object, streaming a series of events representing the execution of the tests. Some of the events are guaranteed to be emitted in the same order as the tests are defined, while others are emitted in the order that the tests execute. +The following tables summarize all events by scope. + +Test scoped events are emitted once per test or suite. Most of them come in +pairs: a declaration ordered event, buffered so that events are emitted in the +same order as the tests are defined, and one or more corresponding execution +ordered events, emitted immediately as the tests execute. + +| Declaration ordered (buffered) | Execution ordered (immediate) | +| ------------------------------ | ----------------------------------------------------- | +| [`'test:start'`][] | [`'test:enqueue'`][] followed by [`'test:dequeue'`][] | +| [`'test:pass'`][] | [`'test:complete'`][] (`details.passed` is `true`) | +| [`'test:fail'`][] | [`'test:complete'`][] (`details.passed` is `false`) | +| [`'test:plan'`][] | | +| [`'test:diagnostic'`][] | | + +File scoped and global events are always emitted immediately, in execution +order. + +File scoped events are emitted once per test file: + +| Event | Notes | +| -------------------- | ---------------------------------------------- | +| [`'test:stderr'`][] | Only emitted if the `--test` flag is passed. | +| [`'test:stdout'`][] | Only emitted if the `--test` flag is passed. | +| [`'test:summary'`][] | Per file, only when process isolation is used. | + +Global events are emitted once per test run: + +| Event | Notes | +| ---------------------------- | ------------------------------------ | +| [`'test:summary'`][] | The final cumulative summary. | +| [`'test:coverage'`][] | Only when code coverage is enabled. | +| [`'test:interrupted'`][] | Only when the run receives `SIGINT`. | +| [`'test:watch:drained'`][] | Watch mode only. | +| [`'test:watch:restarted'`][] | Watch mode only. | + +The root test also emits [`'test:plan'`][] and [`'test:diagnostic'`][] events +at the end of the run to report run level totals. + ### Event: `'test:coverage'` * `data` {Object} @@ -4724,6 +4763,21 @@ test.describe('my suite', (suite) => { [TAP]: https://testanything.org/ [Test tags]: #test-tags +[`'test:complete'`]: #event-testcomplete +[`'test:coverage'`]: #event-testcoverage +[`'test:dequeue'`]: #event-testdequeue +[`'test:diagnostic'`]: #event-testdiagnostic +[`'test:enqueue'`]: #event-testenqueue +[`'test:fail'`]: #event-testfail +[`'test:interrupted'`]: #event-testinterrupted +[`'test:pass'`]: #event-testpass +[`'test:plan'`]: #event-testplan +[`'test:start'`]: #event-teststart +[`'test:stderr'`]: #event-teststderr +[`'test:stdout'`]: #event-teststdout +[`'test:summary'`]: #event-testsummary +[`'test:watch:drained'`]: #event-testwatchdrained +[`'test:watch:restarted'`]: #event-testwatchrestarted [`--experimental-test-coverage`]: cli.md#--experimental-test-coverage [`--experimental-test-module-mocks`]: cli.md#--experimental-test-module-mocks [`--experimental-test-tag-filter`]: cli.md#--experimental-test-tag-filterexpr diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js index 7a2d5719b9b97b..8e18c1b5b930ea 100644 --- a/lib/internal/streams/iter/consumers.js +++ b/lib/internal/streams/iter/consumers.js @@ -22,6 +22,8 @@ const { TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetByteLength, TypedArrayPrototypeGetByteOffset, + TypedArrayPrototypeSet, + Uint8Array, } = primordials; const { @@ -164,21 +166,17 @@ async function collectAsync(source, signal, limit) { /** * Convert a Uint8Array to its backing ArrayBuffer, slicing if necessary. - * Handles both ArrayBuffer and SharedArrayBuffer backing stores. * @param {Uint8Array} data - * @returns {ArrayBuffer|SharedArrayBuffer} + * @returns {ArrayBuffer} */ function toArrayBuffer(data) { const byteOffset = TypedArrayPrototypeGetByteOffset(data); const byteLength = TypedArrayPrototypeGetByteLength(data); const buffer = TypedArrayPrototypeGetBuffer(data); - // SharedArrayBuffer is not available in primordials, so use - // direct property access for its byteLength and slice. if (isSharedArrayBuffer(buffer)) { - if (byteOffset === 0 && byteLength === buffer.byteLength) { - return buffer; - } - return buffer.slice(byteOffset, byteOffset + byteLength); + const copy = new Uint8Array(byteLength); + TypedArrayPrototypeSet(copy, data); + return TypedArrayPrototypeGetBuffer(copy); } if (byteOffset === 0 && byteLength === ArrayBufferPrototypeGetByteLength(buffer)) { diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js index ca3d2531d6f9e0..c52393493bc0a8 100644 --- a/lib/internal/streams/iter/utils.js +++ b/lib/internal/streams/iter/utils.js @@ -203,6 +203,12 @@ function allUint8Array(chunks) { return true; } +function copyBytes(chunk) { + const copy = new Uint8Array(TypedArrayPrototypeGetByteLength(chunk)); + TypedArrayPrototypeSet(copy, chunk); + return copy; +} + /** * Concatenate multiple Uint8Arrays into a single Uint8Array. * @param {Uint8Array[]} chunks @@ -220,16 +226,15 @@ function concatBytes(chunks) { // If non-zero offset, skip the remaining buffer checks. if (TypedArrayPrototypeGetByteOffset(chunk) === 0) { const buf = TypedArrayPrototypeGetBuffer(chunk); - // SharedArrayBuffer is not available in primordials, so use - // direct property access for its byteLength. - const bufByteLength = isSharedArrayBuffer(buf) ? - buf.byteLength : - ArrayBufferPrototypeGetByteLength(buf); - if (TypedArrayPrototypeGetByteLength(chunk) === bufByteLength) { + if ( + !isSharedArrayBuffer(buf) && + TypedArrayPrototypeGetByteLength(chunk) === + ArrayBufferPrototypeGetByteLength(buf) + ) { return chunk; } } - return new Uint8Array(chunk); + return copyBytes(chunk); } // Multiple chunks: concatenate let totalByteLength = 0; diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index c5a2c437d9b78b..d0e3bfafc19fcd 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -599,8 +599,12 @@ class ReadableStream { !controller[kState].queue.length) { readableStreamDefaultControllerClearAlgorithms(controller); readableStreamClose(stream); - } else { - readableStreamDefaultControllerCallPullIfNeeded(controller); + } else if (!controller[kState].closeRequested && + controller[kState].started && + controller[kState].highWaterMark - + controller[kState].queueTotalSize > 0) { + // Reduced ShouldCallPull, as in the read() fast path. + readableStreamDefaultControllerPull(controller); } return PromiseResolve({ done: false, value: chunk }); @@ -949,8 +953,14 @@ class ReadableStreamDefaultReader { if (controller[kState].closeRequested && !controller[kState].queue.length) { readableStreamDefaultControllerClearAlgorithms(controller); readableStreamClose(stream); - } else { - readableStreamDefaultControllerCallPullIfNeeded(controller); + } else if (!controller[kState].closeRequested && + controller[kState].started && + controller[kState].highWaterMark - + controller[kState].queueTotalSize > 0) { + // ShouldCallPull reduced to the conditions not already + // established on this path: the state is readable and the + // queue was non-empty, so no read requests can be parked. + readableStreamDefaultControllerPull(controller); } return PromiseResolve({ done: false, value: chunk }); @@ -2524,13 +2534,28 @@ function readableStreamDefaultControllerEnqueue(controller, chunk) { reader[kState] !== undefined && reader[kType] === 'ReadableStreamDefaultReader' && reader[kState].readRequests.length) { + // Fulfilling a read request can run user code synchronously (the + // pipeTo read request invokes the sink's write algorithm), so the + // full ShouldCallPull predicate has to be re-evaluated afterwards. readableStreamFulfillReadRequest(stream, chunk, false); } else if (controllerState.sizeAlgorithm === defaultSizeAlgorithm) { // The internal default size algorithm is never observable by user // code, always returns 1, and cannot throw: enqueue with the - // constant instead of calling it. - enqueueValueWithSize(controller, chunk, 1); + // constant size instead of calling it or validating the result. + // No user code runs between the guards at the top of this function + // and this point, so ShouldCallPull reduces to the started flag and + // the desired size (this branch implies no parked read requests, + // ruling out the reader arm of the predicate). + materializeQueue(controllerState).pushPair(chunk, 1); + controllerState.queueTotalSize++; + if (controllerState.started && + controllerState.highWaterMark - controllerState.queueTotalSize > 0) { + readableStreamDefaultControllerPull(controller); + } + return; } else { + // The user-supplied size algorithm may run arbitrary code, so the + // full ShouldCallPull predicate has to be re-evaluated afterwards. try { const chunkSize = FunctionPrototypeCall( @@ -2599,6 +2624,13 @@ function readableStreamDefaultControllerShouldCallPull(controller) { function readableStreamDefaultControllerCallPullIfNeeded(controller) { if (!readableStreamDefaultControllerShouldCallPull(controller)) return; + readableStreamDefaultControllerPull(controller); +} + +// The ShouldCallPull half of CallPullIfNeeded, split out so that callers +// that have already established the predicate from state in scope (the +// enqueue path above) can skip re-running it. +function readableStreamDefaultControllerPull(controller) { if (controller[kState].pulling) { controller[kState].pullAgain = true; return; @@ -2659,14 +2691,24 @@ function readableStreamDefaultControllerPullSteps(controller, readRequest) { if (controller[kState].closeRequested && !queue.length) { readableStreamDefaultControllerClearAlgorithms(controller); readableStreamClose(stream); - } else { - readableStreamDefaultControllerCallPullIfNeeded(controller); + } else if (!controller[kState].closeRequested && + controller[kState].started && + controller[kState].highWaterMark - + controller[kState].queueTotalSize > 0) { + // Reduced ShouldCallPull: the state is known to be readable and the + // queue was non-empty, so no read requests can be parked. + readableStreamDefaultControllerPull(controller); } readRequest[kChunk](chunk); return; } readableStreamAddReadRequest(stream, readRequest); - readableStreamDefaultControllerCallPullIfNeeded(controller); + // Reduced ShouldCallPull: the state is known to be readable, an empty + // queue in that state implies close has not been requested (close with + // an empty queue closes the stream immediately), and the read request + // parked above already satisfies the reader-with-pending-reads arm. + if (controller[kState].started) + readableStreamDefaultControllerPull(controller); } function setupReadableStreamDefaultController( diff --git a/test/parallel/test-stream-iter-sharedarraybuffer.js b/test/parallel/test-stream-iter-sharedarraybuffer.js index 7aff205bf7664a..de0f8d6f3fc636 100644 --- a/test/parallel/test-stream-iter-sharedarraybuffer.js +++ b/test/parallel/test-stream-iter-sharedarraybuffer.js @@ -52,6 +52,7 @@ function testBytesSyncSAB() { const sab = new SharedArrayBuffer(5); new Uint8Array(sab).set([104, 101, 108, 108, 111]); // 'hello' const data = bytesSync(fromSync(sab)); + assert.ok(data.buffer instanceof ArrayBuffer); assert.deepStrictEqual(data, new Uint8Array([104, 101, 108, 108, 111])); } @@ -59,6 +60,7 @@ async function testBytesAsyncSAB() { const sab = new SharedArrayBuffer(5); new Uint8Array(sab).set([104, 101, 108, 108, 111]); // 'hello' const data = await bytes(from(sab)); + assert.ok(data.buffer instanceof ArrayBuffer); assert.deepStrictEqual(data, new Uint8Array([104, 101, 108, 108, 111])); } @@ -80,7 +82,7 @@ function testArrayBufferSyncSAB() { const sab = new SharedArrayBuffer(4); new Uint8Array(sab).set([1, 2, 3, 4]); const result = arrayBufferSync(fromSync(sab)); - assert.ok(result instanceof SharedArrayBuffer || result instanceof ArrayBuffer); + assert.ok(result instanceof ArrayBuffer); assert.deepStrictEqual(new Uint8Array(result), new Uint8Array([1, 2, 3, 4])); } @@ -88,7 +90,7 @@ async function testArrayBufferAsyncSAB() { const sab = new SharedArrayBuffer(4); new Uint8Array(sab).set([1, 2, 3, 4]); const result = await arrayBuffer(from(sab)); - assert.ok(result instanceof SharedArrayBuffer || result instanceof ArrayBuffer); + assert.ok(result instanceof ArrayBuffer); assert.deepStrictEqual(new Uint8Array(result), new Uint8Array([1, 2, 3, 4])); }