From 05667991cae9eeab89270c6b50ec5b73b4672df6 Mon Sep 17 00:00:00 2001 From: Renegade334 Date: Tue, 24 Jun 2025 20:08:05 +0100 Subject: [PATCH 01/80] deps: V8: backport 1c3e018e7d48 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original commit message: Expose dispose and asyncDispose symbols to C++ Since `Symbol.dispose` has been exposed to JS we need to be able to access the native v8::Symbol too. Change-Id: I9111d2806630efdd8bb5f931effa9adad3c85ae5 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6410112 Commit-Queue: Erik Corry Reviewed-by: Erik Corry Reviewed-by: Leszek Swirski Cr-Commit-Position: refs/heads/main@{#99590} PR-URL: https://github.com/nodejs/node/pull/58818 Refs: https://github.com/v8/v8/commit/1c3e018e7d488d43b97b4e30f77b50070c456473 Reviewed-By: Michaël Zasso Reviewed-By: Rafael Gonzaga --- common.gypi | 2 +- deps/v8/include/v8-primitive.h | 2 ++ deps/v8/src/api/api.cc | 4 +++- deps/v8/test/cctest/test-api.cc | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/common.gypi b/common.gypi index 9c3707d46cb053..9ee982c8351121 100644 --- a/common.gypi +++ b/common.gypi @@ -38,7 +38,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.21', + 'v8_embedder_string': '-node.22', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/include/v8-primitive.h b/deps/v8/include/v8-primitive.h index f966ed55674841..50b07d9c10a7bb 100644 --- a/deps/v8/include/v8-primitive.h +++ b/deps/v8/include/v8-primitive.h @@ -819,6 +819,8 @@ class V8_EXPORT Symbol : public Name { static Local GetToPrimitive(Isolate* isolate); static Local GetToStringTag(Isolate* isolate); static Local GetUnscopables(Isolate* isolate); + static Local GetDispose(Isolate* isolate); + static Local GetAsyncDispose(Isolate* isolate); V8_INLINE static Symbol* Cast(Data* data) { #ifdef V8_ENABLE_CHECKS diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc index 0f2a526b2b3453..ba759168aa92f5 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -9548,7 +9548,9 @@ Local v8::Symbol::ForApi(Isolate* v8_isolate, Local name) { V(Split, split) \ V(ToPrimitive, to_primitive) \ V(ToStringTag, to_string_tag) \ - V(Unscopables, unscopables) + V(Unscopables, unscopables) \ + V(Dispose, dispose) \ + V(AsyncDispose, async_dispose) #define SYMBOL_GETTER(Name, name) \ Local v8::Symbol::Get##Name(Isolate* v8_isolate) { \ diff --git a/deps/v8/test/cctest/test-api.cc b/deps/v8/test/cctest/test-api.cc index 0f023d15cfc181..876de6efb525ab 100644 --- a/deps/v8/test/cctest/test-api.cc +++ b/deps/v8/test/cctest/test-api.cc @@ -4006,6 +4006,8 @@ THREADED_TEST(WellKnownSymbols) { CheckWellKnownSymbol(v8::Symbol::GetSplit, "Symbol.split"); CheckWellKnownSymbol(v8::Symbol::GetToPrimitive, "Symbol.toPrimitive"); CheckWellKnownSymbol(v8::Symbol::GetToStringTag, "Symbol.toStringTag"); + CheckWellKnownSymbol(v8::Symbol::GetDispose, "Symbol.dispose"); + CheckWellKnownSymbol(v8::Symbol::GetAsyncDispose, "Symbol.asyncDispose"); } From 7ab13b7477c44e704571bfc7c1d61722486d05ee Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sun, 27 Jul 2025 19:25:59 -0800 Subject: [PATCH 02/80] test: don't use expose internals in test-http-outgoing-buffer.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59219 Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Luigi Pinca Reviewed-By: Michaël Zasso Reviewed-By: Juan José Arboleda --- test/parallel/test-http-outgoing-buffer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/parallel/test-http-outgoing-buffer.js b/test/parallel/test-http-outgoing-buffer.js index d7db15f0ffae9c..ba102b382542b1 100644 --- a/test/parallel/test-http-outgoing-buffer.js +++ b/test/parallel/test-http-outgoing-buffer.js @@ -1,8 +1,7 @@ -// Flags: --expose-internals 'use strict'; require('../common'); const assert = require('assert'); -const { getDefaultHighWaterMark } = require('internal/streams/state'); +const { getDefaultHighWaterMark } = require('stream'); const http = require('http'); const OutgoingMessage = http.OutgoingMessage; From 4a907bdad114e60f02ebc9eb43c92946c79be62b Mon Sep 17 00:00:00 2001 From: Asaf Federman <46937909+Asaf-Federman@users.noreply.github.com> Date: Mon, 28 Jul 2025 08:06:16 +0300 Subject: [PATCH 03/80] src: add percentage support to --max-old-space-size This commit adds support for specifying --max-old-space-size as a percentage of system memory, in addition to the existing MB format. A new HandleMaxOldSpaceSizePercentage method parses percentage values, validates that they are within the 0-100% range, and provides clear error messages for invalid input. The heap size is now calculated based on available system memory when a percentage is used. Test coverage has been added for both valid and invalid cases. Documentation and the JSON schema for CLI options have been updated with examples for both formats. Refs: https://github.com/nodejs/node/issues/57447 PR-URL: https://github.com/nodejs/node/pull/59082 Reviewed-By: Chengzhong Wu Reviewed-By: James M Snell Reviewed-By: Moshe Atlow Reviewed-By: theanarkh Reviewed-By: Daeyeon Jeong --- doc/api/cli.md | 17 +++ doc/node-config-schema.json | 3 + doc/node.1 | 10 ++ src/node.cc | 7 + src/node_options.cc | 48 +++++++ src/node_options.h | 4 + .../test-max-old-space-size-percentage.js | 134 ++++++++++++++++++ 7 files changed, 223 insertions(+) create mode 100644 test/parallel/test-max-old-space-size-percentage.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 491bb1cb3fc8f1..7389146d6ec439 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1680,6 +1680,22 @@ changes: Specify the maximum size, in bytes, of HTTP headers. Defaults to 16 KiB. +### `--max-old-space-size-percentage=PERCENTAGE` + +Sets the max memory size of V8's old memory section as a percentage of available system memory. +This flag takes precedence over `--max-old-space-size` when both are specified. + +The `PERCENTAGE` parameter must be a number greater than 0 and up to 100. representing the percentage +of available system memory to allocate to the V8 heap. + +```bash +# Using 50% of available system memory +node --max-old-space-size-percentage=50 index.js + +# Using 75% of available system memory +node --max-old-space-size-percentage=75 index.js +``` + ### `--napi-modules` + +* Returns: {Promise} + +This method returns a `Promise` that will resolve to an object identical to [`process.threadCpuUsage()`][], +or reject with an [`ERR_WORKER_NOT_RUNNING`][] error if the worker is no longer running. +This methods allows the statistics to be observed from outside the actual thread. + ### `worker.getHeapSnapshot([options])` -* `type`: {string} The intended use of the generated secret key. Currently +* `type` {string} The intended use of the generated secret key. Currently accepted values are `'hmac'` and `'aes'`. -* `options`: {Object} - * `length`: {number} The bit length of the key to generate. This must be a +* `options` {Object} + * `length` {number} The bit length of the key to generate. This must be a value greater than 0. * If `type` is `'hmac'`, the minimum is 8, and the maximum length is 231-1. If the value is not a multiple of 8, the generated key will be truncated to `Math.floor(length / 8)`. * If `type` is `'aes'`, the length must be one of `128`, `192`, or `256`. -* `callback`: {Function} - * `err`: {Error} - * `key`: {KeyObject} +* `callback` {Function} + * `err` {Error} + * `key` {KeyObject} Asynchronously generates a new random secret key of the given `length`. The `type` will determine which validations will be performed on the `length`. @@ -3677,30 +3677,30 @@ changes: produce key objects if no encoding was specified. --> -* `type`: {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, +* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. -* `options`: {Object} - * `modulusLength`: {number} Key size in bits (RSA, DSA). - * `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`. - * `hashAlgorithm`: {string} Name of the message digest (RSA-PSS). - * `mgf1HashAlgorithm`: {string} Name of the message digest used by +* `options` {Object} + * `modulusLength` {number} Key size in bits (RSA, DSA). + * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. + * `hashAlgorithm` {string} Name of the message digest (RSA-PSS). + * `mgf1HashAlgorithm` {string} Name of the message digest used by MGF1 (RSA-PSS). - * `saltLength`: {number} Minimal salt length in bytes (RSA-PSS). - * `divisorLength`: {number} Size of `q` in bits (DSA). - * `namedCurve`: {string} Name of the curve to use (EC). - * `prime`: {Buffer} The prime parameter (DH). - * `primeLength`: {number} Prime length in bits (DH). - * `generator`: {number} Custom generator (DH). **Default:** `2`. - * `groupName`: {string} Diffie-Hellman group name (DH). See + * `saltLength` {number} Minimal salt length in bytes (RSA-PSS). + * `divisorLength` {number} Size of `q` in bits (DSA). + * `namedCurve` {string} Name of the curve to use (EC). + * `prime` {Buffer} The prime parameter (DH). + * `primeLength` {number} Prime length in bits (DH). + * `generator` {number} Custom generator (DH). **Default:** `2`. + * `groupName` {string} Diffie-Hellman group name (DH). See [`crypto.getDiffieHellman()`][]. - * `paramEncoding`: {string} Must be `'named'` or `'explicit'` (EC). + * `paramEncoding` {string} Must be `'named'` or `'explicit'` (EC). **Default:** `'named'`. - * `publicKeyEncoding`: {Object} See [`keyObject.export()`][]. - * `privateKeyEncoding`: {Object} See [`keyObject.export()`][]. -* `callback`: {Function} - * `err`: {Error} - * `publicKey`: {string | Buffer | KeyObject} - * `privateKey`: {string | Buffer | KeyObject} + * `publicKeyEncoding` {Object} See [`keyObject.export()`][]. + * `privateKeyEncoding` {Object} See [`keyObject.export()`][]. +* `callback` {Function} + * `err` {Error} + * `publicKey` {string | Buffer | KeyObject} + * `privateKey` {string | Buffer | KeyObject} Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC, Ed25519, Ed448, X25519, X448, and DH are currently supported. @@ -3791,29 +3791,29 @@ changes: produce key objects if no encoding was specified. --> -* `type`: {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, +* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. -* `options`: {Object} - * `modulusLength`: {number} Key size in bits (RSA, DSA). - * `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`. - * `hashAlgorithm`: {string} Name of the message digest (RSA-PSS). - * `mgf1HashAlgorithm`: {string} Name of the message digest used by +* `options` {Object} + * `modulusLength` {number} Key size in bits (RSA, DSA). + * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. + * `hashAlgorithm` {string} Name of the message digest (RSA-PSS). + * `mgf1HashAlgorithm` {string} Name of the message digest used by MGF1 (RSA-PSS). - * `saltLength`: {number} Minimal salt length in bytes (RSA-PSS). - * `divisorLength`: {number} Size of `q` in bits (DSA). - * `namedCurve`: {string} Name of the curve to use (EC). - * `prime`: {Buffer} The prime parameter (DH). - * `primeLength`: {number} Prime length in bits (DH). - * `generator`: {number} Custom generator (DH). **Default:** `2`. - * `groupName`: {string} Diffie-Hellman group name (DH). See + * `saltLength` {number} Minimal salt length in bytes (RSA-PSS). + * `divisorLength` {number} Size of `q` in bits (DSA). + * `namedCurve` {string} Name of the curve to use (EC). + * `prime` {Buffer} The prime parameter (DH). + * `primeLength` {number} Prime length in bits (DH). + * `generator` {number} Custom generator (DH). **Default:** `2`. + * `groupName` {string} Diffie-Hellman group name (DH). See [`crypto.getDiffieHellman()`][]. - * `paramEncoding`: {string} Must be `'named'` or `'explicit'` (EC). + * `paramEncoding` {string} Must be `'named'` or `'explicit'` (EC). **Default:** `'named'`. - * `publicKeyEncoding`: {Object} See [`keyObject.export()`][]. - * `privateKeyEncoding`: {Object} See [`keyObject.export()`][]. + * `publicKeyEncoding` {Object} See [`keyObject.export()`][]. + * `privateKeyEncoding` {Object} See [`keyObject.export()`][]. * Returns: {Object} - * `publicKey`: {string | Buffer | KeyObject} - * `privateKey`: {string | Buffer | KeyObject} + * `publicKey` {string | Buffer | KeyObject} + * `privateKey` {string | Buffer | KeyObject} Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC, Ed25519, Ed448, X25519, X448, and DH are currently supported. @@ -3882,10 +3882,10 @@ it will be a buffer containing the data encoded as DER. added: v15.0.0 --> -* `type`: {string} The intended use of the generated secret key. Currently +* `type` {string} The intended use of the generated secret key. Currently accepted values are `'hmac'` and `'aes'`. -* `options`: {Object} - * `length`: {number} The bit length of the key to generate. +* `options` {Object} + * `length` {number} The bit length of the key to generate. * If `type` is `'hmac'`, the minimum is 8, and the maximum length is 231-1. If the value is not a multiple of 8, the generated key will be truncated to `Math.floor(length / 8)`. @@ -4026,10 +4026,10 @@ the process unresponsive. added: v15.0.0 --> -* `nameOrNid`: {string|number} The name or nid of the cipher to query. -* `options`: {Object} - * `keyLength`: {number} A test key length. - * `ivLength`: {number} A test IV length. +* `nameOrNid` {string|number} The name or nid of the cipher to query. +* `options` {Object} + * `keyLength` {number} A test key length. + * `ivLength` {number} A test IV length. * Returns: {Object} * `name` {string} The name of the cipher * `nid` {number} The nid of the cipher diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index 7f50ca2dbf39c4..1c742f7f972ce0 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1107,35 +1107,35 @@ for the sync error and one for the async error. > Stability: 1 - Experimental -`console.log` +##### Event: `'console.log'` * `args` {any\[]} Emitted when `console.log()` is called. Receives and array of the arguments passed to `console.log()`. -`console.info` +##### Event: `'console.info'` * `args` {any\[]} Emitted when `console.info()` is called. Receives and array of the arguments passed to `console.info()`. -`console.debug` +##### Event: `'console.debug'` * `args` {any\[]} Emitted when `console.debug()` is called. Receives and array of the arguments passed to `console.debug()`. -`console.warn` +##### Event: `'console.warn'` * `args` {any\[]} Emitted when `console.warn()` is called. Receives and array of the arguments passed to `console.warn()`. -`console.error` +##### Event: `'console.error'` * `args` {any\[]} @@ -1146,34 +1146,34 @@ passed to `console.error()`. > Stability: 1 - Experimental -`http.client.request.created` +##### Event: `'http.client.request.created'` * `request` {http.ClientRequest} Emitted when client creates a request object. Unlike `http.client.request.start`, this event is emitted before the request has been sent. -`http.client.request.start` +##### Event: `'http.client.request.start'` * `request` {http.ClientRequest} Emitted when client starts a request. -`http.client.request.error` +##### Event: `'http.client.request.error'` * `request` {http.ClientRequest} * `error` {Error} Emitted when an error occurs during a client request. -`http.client.response.finish` +##### Event: `'http.client.response.finish'` * `request` {http.ClientRequest} * `response` {http.IncomingMessage} Emitted when client receives a response. -`http.server.request.start` +##### Event: `'http.server.request.start'` * `request` {http.IncomingMessage} * `response` {http.ServerResponse} @@ -1182,7 +1182,7 @@ Emitted when client receives a response. Emitted when server receives a request. -`http.server.response.created` +##### Event: `'http.server.response.created'` * `request` {http.IncomingMessage} * `response` {http.ServerResponse} @@ -1190,7 +1190,7 @@ Emitted when server receives a request. Emitted when server creates a response. The event is emitted before the response is sent. -`http.server.response.finish` +##### Event: `'http.server.response.finish'` * `request` {http.IncomingMessage} * `response` {http.ServerResponse} @@ -1203,28 +1203,28 @@ Emitted when server sends a response. > Stability: 1 - Experimental -`http2.client.stream.created` +##### Event: `'http2.client.stream.created'` * `stream` {ClientHttp2Stream} * `headers` {HTTP/2 Headers Object} Emitted when a stream is created on the client. -`http2.client.stream.start` +##### Event: `'http2.client.stream.start'` * `stream` {ClientHttp2Stream} * `headers` {HTTP/2 Headers Object} Emitted when a stream is started on the client. -`http2.client.stream.error` +##### Event: `'http2.client.stream.error'` * `stream` {ClientHttp2Stream} * `error` {Error} Emitted when an error occurs during the processing of a stream on the client. -`http2.client.stream.finish` +##### Event: `'http2.client.stream.finish'` * `stream` {ClientHttp2Stream} * `headers` {HTTP/2 Headers Object} @@ -1232,35 +1232,35 @@ Emitted when an error occurs during the processing of a stream on the client. Emitted when a stream is received on the client. -`http2.client.stream.close` +##### Event: `'http2.client.stream.close'` * `stream` {ClientHttp2Stream} Emitted when a stream is closed on the client. The HTTP/2 error code used when closing the stream can be retrieved using the `stream.rstCode` property. -`http2.server.stream.created` +##### Event: `'http2.server.stream.created'` * `stream` {ServerHttp2Stream} * `headers` {HTTP/2 Headers Object} Emitted when a stream is created on the server. -`http2.server.stream.start` +##### Event: `'http2.server.stream.start'` * `stream` {ServerHttp2Stream} * `headers` {HTTP/2 Headers Object} Emitted when a stream is started on the server. -`http2.server.stream.error` +##### Event: `'http2.server.stream.error'` * `stream` {ServerHttp2Stream} * `error` {Error} Emitted when an error occurs during the processing of a stream on the server. -`http2.server.stream.finish` +##### Event: `'http2.server.stream.finish'` * `stream` {ServerHttp2Stream} * `headers` {HTTP/2 Headers Object} @@ -1268,7 +1268,7 @@ Emitted when an error occurs during the processing of a stream on the server. Emitted when a stream is sent on the server. -`http2.server.stream.close` +##### Event: `'http2.server.stream.close'` * `stream` {ServerHttp2Stream} @@ -1279,52 +1279,52 @@ closing the stream can be retrieved using the `stream.rstCode` property. > Stability: 1 - Experimental -`module.require.start` +##### Event: `'module.require.start'` * `event` {Object} containing the following properties - * `id` - Argument passed to `require()`. Module name. - * `parentFilename` - Name of the module that attempted to require(id). + * `id` Argument passed to `require()`. Module name. + * `parentFilename` Name of the module that attempted to require(id). Emitted when `require()` is executed. See [`start` event][]. -`module.require.end` +##### Event: `'module.require.end'` * `event` {Object} containing the following properties - * `id` - Argument passed to `require()`. Module name. - * `parentFilename` - Name of the module that attempted to require(id). + * `id` Argument passed to `require()`. Module name. + * `parentFilename` Name of the module that attempted to require(id). Emitted when a `require()` call returns. See [`end` event][]. -`module.require.error` +##### Event: `'module.require.error'` * `event` {Object} containing the following properties - * `id` - Argument passed to `require()`. Module name. - * `parentFilename` - Name of the module that attempted to require(id). + * `id` Argument passed to `require()`. Module name. + * `parentFilename` Name of the module that attempted to require(id). * `error` {Error} Emitted when a `require()` throws an error. See [`error` event][]. -`module.import.asyncStart` +##### Event: `'module.import.asyncStart'` * `event` {Object} containing the following properties - * `id` - Argument passed to `import()`. Module name. - * `parentURL` - URL object of the module that attempted to import(id). + * `id` Argument passed to `import()`. Module name. + * `parentURL` URL object of the module that attempted to import(id). Emitted when `import()` is invoked. See [`asyncStart` event][]. -`module.import.asyncEnd` +##### Event: `'module.import.asyncEnd'` * `event` {Object} containing the following properties - * `id` - Argument passed to `import()`. Module name. - * `parentURL` - URL object of the module that attempted to import(id). + * `id` Argument passed to `import()`. Module name. + * `parentURL` URL object of the module that attempted to import(id). Emitted when `import()` has completed. See [`asyncEnd` event][]. -`module.import.error` +##### Event: `'module.import.error'` * `event` {Object} containing the following properties - * `id` - Argument passed to `import()`. Module name. - * `parentURL` - URL object of the module that attempted to import(id). + * `id` Argument passed to `import()`. Module name. + * `parentURL` URL object of the module that attempted to import(id). * `error` {Error} Emitted when a `import()` throws an error. See [`error` event][]. @@ -1333,32 +1333,32 @@ Emitted when a `import()` throws an error. See [`error` event][]. > Stability: 1 - Experimental -`net.client.socket` +##### Event: `'net.client.socket'` * `socket` {net.Socket|tls.TLSSocket} Emitted when a new TCP or pipe client socket connection is created. -`net.server.socket` +##### Event: `'net.server.socket'` * `socket` {net.Socket} Emitted when a new TCP or pipe connection is received. -`tracing:net.server.listen:asyncStart` +##### Event: `'tracing:net.server.listen:asyncStart'` * `server` {net.Server} * `options` {Object} Emitted when [`net.Server.listen()`][] is invoked, before the port or pipe is actually setup. -`tracing:net.server.listen:asyncEnd` +##### Event: `'tracing:net.server.listen:asyncEnd'` * `server` {net.Server} Emitted when [`net.Server.listen()`][] has completed and thus the server is ready to accept connection. -`tracing:net.server.listen:error` +##### Event: `'tracing:net.server.listen:error'` * `server` {net.Server} * `error` {Error} @@ -1369,7 +1369,7 @@ Emitted when [`net.Server.listen()`][] is returning an error. > Stability: 1 - Experimental -`udp.socket` +##### Event: `'udp.socket'` * `socket` {dgram.Socket} @@ -1383,13 +1383,13 @@ Emitted when a new UDP socket is created. added: v16.18.0 --> -`child_process` +##### Event: `'child_process'` * `process` {ChildProcess} Emitted when a new process is created. -`execve` +##### Event: `'execve'` * `execPath` {string} * `args` {string\[]} @@ -1405,16 +1405,15 @@ Emitted when [`process.execve()`][] is invoked. added: v16.18.0 --> -`worker_threads` +##### Event: `'worker_threads'` -* `worker` [`Worker`][] +* `worker` {Worker} Emitted when a new thread is created. [TracingChannel Channels]: #tracingchannel-channels [`'uncaughtException'`]: process.md#event-uncaughtexception [`TracingChannel`]: #class-tracingchannel -[`Worker`]: worker_threads.md#class-worker [`asyncEnd` event]: #asyncendevent [`asyncStart` event]: #asyncstartevent [`channel.bindStore(store)`]: #channelbindstorestore-transform diff --git a/doc/api/dns.md b/doc/api/dns.md index f5da52f0cc1e26..61846f6f621014 100644 --- a/doc/api/dns.md +++ b/doc/api/dns.md @@ -856,14 +856,10 @@ changes: `ERR_INVALID_CALLBACK`. --> - - * `hostname` {string} * `callback` {Function} * `err` {Error} - * `records` \ - - + * `records` {string\[]} Uses the DNS protocol to resolve text queries (`TXT` records) for the `hostname`. The `records` argument passed to the `callback` function is a diff --git a/doc/api/events.md b/doc/api/events.md index f1c1683528490c..bad5814c373281 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -1099,7 +1099,7 @@ changes: description: No longer experimental. --> -* `err` Error +* `err` {Error} * `eventName` {string|symbol} * `...args` {any} @@ -1600,7 +1600,7 @@ changes: description: No longer experimental. --> -Value: {boolean} +* Type: {boolean} Change the default `captureRejections` option on all new `EventEmitter` objects. @@ -1618,7 +1618,7 @@ changes: description: No longer experimental. --> -Value: `Symbol.for('nodejs.rejection')` +* Type: {symbol} `Symbol.for('nodejs.rejection')` See how to write a custom [rejection handler][rejection]. @@ -1681,12 +1681,12 @@ changes: * `eventName` {string|symbol} The name of the event being listened for * `options` {Object} * `signal` {AbortSignal} Can be used to cancel awaiting events. - * `close` - {string\[]} Names of events that will end the iteration. - * `highWaterMark` - {integer} **Default:** `Number.MAX_SAFE_INTEGER` + * `close` {string\[]} Names of events that will end the iteration. + * `highWaterMark` {integer} **Default:** `Number.MAX_SAFE_INTEGER` The high watermark. The emitter is paused every time the size of events being buffered is higher than it. Supported only on emitters implementing `pause()` and `resume()` methods. - * `lowWaterMark` - {integer} **Default:** `1` + * `lowWaterMark` {integer} **Default:** `1` The low watermark. The emitter is resumed every time the size of events being buffered is lower than it. Supported only on emitters implementing `pause()` and `resume()` methods. @@ -1986,7 +1986,7 @@ same options as `EventEmitter` and `AsyncResource` themselves. ### `eventemitterasyncresource.asyncResource` -* The underlying {AsyncResource}. +* Type: {AsyncResource} The underlying {AsyncResource}. The returned `AsyncResource` object has an additional `eventEmitter` property that provides a reference to this `EventEmitterAsyncResource`. diff --git a/doc/api/http2.md b/doc/api/http2.md index c8e243021dcd8f..084bf1d3a2d3eb 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -2914,7 +2914,7 @@ changes: and trailing whitespace validation for HTTP/2 header field names and values as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1). **Default:** `true`. - * ...: Any [`net.createServer()`][] option can be provided. + * `...options` {Object} Any [`net.createServer()`][] option can be provided. * `onRequestHandler` {Function} See [Compatibility API][] * Returns: {Http2Server} @@ -3077,8 +3077,8 @@ changes: settings types, which are included in the `customSettings`-property of the received remoteSettings. Please see the `customSettings`-property of the `Http2Settings` object for more information, on the allowed setting types. - * ...: Any [`tls.createServer()`][] options can be provided. For - servers, the identity options (`pfx` or `key`/`cert`) are usually required. + * `...options` {Object} Any [`tls.createServer()`][] options can be provided. + For servers, the identity options (`pfx` or `key`/`cert`) are usually required. * `origins` {string\[]} An array of origin strings to send within an `ORIGIN` frame immediately following creation of a new server `Http2Session`. * `unknownProtocolTimeout` {number} Specifies a timeout in milliseconds that @@ -3239,7 +3239,8 @@ changes: * `createConnection` {Function} An optional callback that receives the `URL` instance passed to `connect` and the `options` object, and returns any [`Duplex`][] stream that is to be used as the connection for this session. - * ...: Any [`net.connect()`][] or [`tls.connect()`][] options can be provided. + * `...options` {Object} Any [`net.connect()`][] or [`tls.connect()`][] options + can be provided. * `unknownProtocolTimeout` {number} Specifies a timeout in milliseconds that a server should wait when an [`'unknownProtocol'`][] event is emitted. If the socket has not been destroyed by that time the server will destroy it. @@ -3364,8 +3365,7 @@ added: --> * `socket` {stream.Duplex} -* `options` {Object} - * ...: Any [`http2.createServer()`][] option can be provided. +* `options` {Object} Any [`http2.createServer()`][] option can be provided. * Returns: {ServerHttp2Session} Create an HTTP/2 server session from an existing socket. diff --git a/doc/api/inspector.md b/doc/api/inspector.md index 9a34b4652afa66..0ab07b995749d1 100644 --- a/doc/api/inspector.md +++ b/doc/api/inspector.md @@ -79,7 +79,7 @@ session.on('inspectorNotification', (message) => console.log(message.method)); It is also possible to subscribe only to notifications with specific method: -#### Event: ``; +#### Event: `` - - * Type: {string} The directory name of the current module. This is the same as the @@ -795,8 +793,6 @@ console.log(path.dirname(__filename)); added: v0.0.1 --> - - * Type: {string} The file name of the current module. This is the current module file's absolute @@ -834,8 +830,6 @@ References to `__filename` within `b.js` will return added: v0.1.12 --> - - * Type: {Object} A reference to the `module.exports` that is shorter to type. @@ -848,8 +842,6 @@ See the section about the [exports shortcut][] for details on when to use added: v0.1.16 --> - - * Type: {module} A reference to the current module, see the section about the @@ -862,8 +854,6 @@ a module exports and makes available through `require()`. added: v0.1.13 --> - - * `id` {string} module name or path * Returns: {any} exported module content @@ -1029,8 +1019,6 @@ Returns an array containing the paths searched during resolution of `request` or added: v0.1.16 --> - - * Type: {Object} diff --git a/doc/api/stream.md b/doc/api/stream.md index 416ae30903521f..4ca97a67f844e7 100644 --- a/doc/api/stream.md +++ b/doc/api/stream.md @@ -62,10 +62,10 @@ functions for streams that return `Promise` objects rather than using callbacks. The API is accessible via `require('node:stream/promises')` or `require('node:stream').promises`. -### `stream.pipeline(source[, ...transforms], destination[, options])` - ### `stream.pipeline(streams[, options])` +### `stream.pipeline(source[, ...transforms], destination[, options])` + * `options` {Object} - * `ALPNProtocols`: {string\[]|Buffer\[]|TypedArray\[]|DataView\[]|Buffer| + * `ALPNProtocols` {string\[]|Buffer\[]|TypedArray\[]|DataView\[]|Buffer| TypedArray|DataView} An array of strings, `Buffer`s, `TypedArray`s, or `DataView`s, or a single `Buffer`, `TypedArray`, or `DataView` containing the supported ALPN @@ -2128,7 +2128,7 @@ changes: e.g. `0x05hello0x05world`, where the first byte is the length of the next protocol name. Passing an array is usually much simpler, e.g. `['hello', 'world']`. (Protocols should be ordered by their priority.) - * `ALPNCallback`: {Function} If set, this will be called when a + * `ALPNCallback` {Function} If set, this will be called when a client opens a connection using the ALPN extension. One argument will be passed to the callback: an object containing `servername` and `protocols` fields, respectively containing the server name from @@ -2167,7 +2167,7 @@ changes: If `callback` is called with a falsy `ctx` argument, the default secure context of the server will be used. If `SNICallback` wasn't provided the default callback with high-level API will be used (see below). - * `ticketKeys`: {Buffer} 48-bytes of cryptographically strong pseudorandom + * `ticketKeys` {Buffer} 48-bytes of cryptographically strong pseudorandom data. See [Session Resumption][] for more information. * `pskCallback` {Function} For TLS-PSK negotiation, see [Pre-shared keys][]. * `pskIdentityHint` {string} optional hint to send to a client to help diff --git a/doc/api/util.md b/doc/api/util.md index 75e09f61738587..b2df99241dccd3 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -340,8 +340,8 @@ added: * `expected` {Array|string} The second value to compare * Returns: {Array} An array of difference entries. Each entry is an array with two elements: - * Index 0: {number} Operation code: `-1` for delete, `0` for no-op/unchanged, `1` for insert - * Index 1: {string} The value associated with the operation + * `0` {number} Operation code: `-1` for delete, `0` for no-op/unchanged, `1` for insert + * `1` {string} The value associated with the operation * Algorithm complexity: O(N\*D), where: @@ -1305,19 +1305,19 @@ ignored, if not supported. * `reset` - Resets all (color) modifiers to their defaults * **bold** - Make text bold * _italic_ - Make text italic -* underline - Make text underlined +* underline - Make text underlined * ~~strikethrough~~ - Puts a horizontal line through the center of the text (Alias: `strikeThrough`, `crossedout`, `crossedOut`) * `hidden` - Prints the text, but makes it invisible (Alias: conceal) * dim - Decreased color intensity (Alias: `faint`) -* overlined - Make text overlined +* overlined - Make text overlined * blink - Hides and shows the text in an interval -* inverse - Swap foreground and +* inverse - Swap foreground and background colors (Alias: `swapcolors`, `swapColors`) * doubleunderline - Make text double underlined (Alias: `doubleUnderline`) -* framed - Draw a frame around the text +* framed - Draw a frame around the text #### Foreground colors diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 4db5b84842c5d4..3c461d566ffd92 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -555,9 +555,9 @@ added: v15.0.0 added: v15.0.0 --> -* `algorithm`: {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} -* `key`: {CryptoKey} -* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* `algorithm` {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `key` {CryptoKey} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. Using the method and parameters specified in `algorithm` and the keying @@ -593,9 +593,9 @@ changes: -* `algorithm`: {EcdhKeyDeriveParams|HkdfParams|Pbkdf2Params} -* `baseKey`: {CryptoKey} -* `length`: {number|null} **Default:** `null` +* `algorithm` {EcdhKeyDeriveParams|HkdfParams|Pbkdf2Params} +* `baseKey` {CryptoKey} +* `length` {number|null} **Default:** `null` * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -633,11 +633,11 @@ changes: -* `algorithm`: {EcdhKeyDeriveParams|HkdfParams|Pbkdf2Params} -* `baseKey`: {CryptoKey} -* `derivedKeyAlgorithm`: {string|Algorithm|HmacImportParams|AesDerivedKeyParams} -* `extractable`: {boolean} -* `keyUsages`: {string\[]} See [Key usages][]. +* `algorithm` {EcdhKeyDeriveParams|HkdfParams|Pbkdf2Params} +* `baseKey` {CryptoKey} +* `derivedKeyAlgorithm` {string|Algorithm|HmacImportParams|AesDerivedKeyParams} +* `extractable` {boolean} +* `keyUsages` {string\[]} See [Key usages][]. * Returns: {Promise} Fulfills with a {CryptoKey} upon success. @@ -665,8 +665,8 @@ The algorithms currently supported include: added: v15.0.0 --> -* `algorithm`: {string|Algorithm} -* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* `algorithm` {string|Algorithm} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. Using the method identified by `algorithm`, `subtle.digest()` attempts to @@ -689,9 +689,9 @@ whose value is one of the above. added: v15.0.0 --> -* `algorithm`: {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} -* `key`: {CryptoKey} -* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* `algorithm` {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `key` {CryptoKey} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. Using the method and parameters specified by `algorithm` and the keying @@ -722,8 +722,8 @@ changes: description: Removed `'NODE-DSA'` JWK export. --> -* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. -* `key`: {CryptoKey} +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `key` {CryptoKey} * Returns: {Promise} Fulfills with an {ArrayBuffer|Object} upon success. Exports the given key into the specified format, if supported. @@ -761,12 +761,12 @@ added: v15.0.0 -* `algorithm`: {string|Algorithm|RsaHashedKeyGenParams|EcKeyGenParams|HmacKeyGenParams|AesKeyGenParams} +* `algorithm` {string|Algorithm|RsaHashedKeyGenParams|EcKeyGenParams|HmacKeyGenParams|AesKeyGenParams} -* `extractable`: {boolean} -* `keyUsages`: {string\[]} See [Key usages][]. +* `extractable` {boolean} +* `keyUsages` {string\[]} See [Key usages][]. * Returns: {Promise} Fulfills with a {CryptoKey|CryptoKeyPair} upon success. Using the method and parameters provided in `algorithm`, `subtle.generateKey()` @@ -810,17 +810,17 @@ changes: description: Removed `'NODE-DSA'` JWK import. --> -* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. -* `keyData`: {ArrayBuffer|TypedArray|DataView|Buffer|Object} +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `keyData` {ArrayBuffer|TypedArray|DataView|Buffer|Object} -* `algorithm`: {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams} +* `algorithm` {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams} -* `extractable`: {boolean} -* `keyUsages`: {string\[]} See [Key usages][]. +* `extractable` {boolean} +* `keyUsages` {string\[]} See [Key usages][]. * Returns: {Promise} Fulfills with a {CryptoKey} upon success. The `subtle.importKey()` method attempts to interpret the provided `keyData` @@ -865,9 +865,9 @@ changes: -* `algorithm`: {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params} -* `key`: {CryptoKey} -* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params} +* `key` {CryptoKey} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -892,19 +892,19 @@ The algorithms currently supported include: added: v15.0.0 --> -* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. -* `wrappedKey`: {ArrayBuffer|TypedArray|DataView|Buffer} -* `unwrappingKey`: {CryptoKey} +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `wrappedKey` {ArrayBuffer|TypedArray|DataView|Buffer} +* `unwrappingKey` {CryptoKey} -* `unwrapAlgo`: {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} -* `unwrappedKeyAlgo`: {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams} +* `unwrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `unwrappedKeyAlgo` {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams} -* `extractable`: {boolean} -* `keyUsages`: {string\[]} See [Key usages][]. +* `extractable` {boolean} +* `keyUsages` {string\[]} See [Key usages][]. * Returns: {Promise} Fulfills with a {CryptoKey} upon success. In cryptography, "wrapping a key" refers to exporting and then encrypting the @@ -955,10 +955,10 @@ changes: -* `algorithm`: {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params} -* `key`: {CryptoKey} -* `signature`: {ArrayBuffer|TypedArray|DataView|Buffer} -* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params} +* `key` {CryptoKey} +* `signature` {ArrayBuffer|TypedArray|DataView|Buffer} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with a {boolean} upon success. @@ -985,10 +985,10 @@ added: v15.0.0 -* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. -* `key`: {CryptoKey} -* `wrappingKey`: {CryptoKey} -* `wrapAlgo`: {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `key` {CryptoKey} +* `wrappingKey` {CryptoKey} +* `wrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. diff --git a/doc/api/webstreams.md b/doc/api/webstreams.md index 7509451a933ffc..b1b22a56e2b45e 100644 --- a/doc/api/webstreams.md +++ b/doc/api/webstreams.md @@ -807,7 +807,7 @@ queue. added: v16.5.0 --> -* `chunk`: {Buffer|TypedArray|DataView} +* `chunk` {Buffer|TypedArray|DataView} Appends a new chunk of data to the {ReadableStream}'s queue. @@ -1077,7 +1077,7 @@ Releases this writer's lock on the underlying {ReadableStream}. added: v16.5.0 --> -* `chunk`: {any} +* `chunk` {any} * Returns: A promise fulfilled with `undefined`. Appends a new chunk of data to the {WritableStream}'s queue. From 31a92835916a2354d1b239763ca87fe3b749959e Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Fri, 11 Jul 2025 10:07:44 +0800 Subject: [PATCH 07/80] doc: add note on process memoryUsage Co-authored-by: Joyee Cheung PR-URL: https://github.com/nodejs/node/pull/59026 Reviewed-By: Joyee Cheung Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Luigi Pinca --- doc/api/process.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/api/process.md b/doc/api/process.md index b2f795e2034f47..e590bbbff73589 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -2890,6 +2890,13 @@ The `process.memoryUsage()` method iterates over each page to gather information about memory usage which might be slow depending on the program memory allocations. +### A note on process memoryUsage + +On Linux or other systems where glibc is commonly used, an application may have sustained +`rss` growth despite stable `heapTotal` due to fragmentation caused by the glibc `malloc` +implementation. See [nodejs/node#21973][] on how to switch to an alternative `malloc` +implementation to address the performance issue. + ## `process.memoryUsage.rss()` + +> Stability: 1 - Experimental + +An optimized UTF-8 stream writer that allows for flushing all the internal +buffering on demand. It handles `EAGAIN` errors correctly, allowing for +customization, for example, by dropping content if the disk is busy. + +#### Event: `'close'` + +The `'close'` event is emitted when the stream is fully closed. + +#### Event: `'drain'` + +The `'drain'` event is emitted when the internal buffer has drained sufficiently +to allow continued writing. + +#### Event: `'drop'` + +The `'drop'` event is emitted when to maximal length is reached and that data +will not be written. The data that was dropped is passed as the first argument +to the event handle. + +#### Event: `'error'` + +The `'error'` event is emitted when an error occurs. + +#### Event: `'finish'` + +The `'finish'` event is emitted when the stream has been ended and all data has +been flushed to the underlying file. + +#### Event: `'ready'` + +The `'ready'` event is emitted when the stream is ready to accept writes. + +#### Event: `'write'` + +The `'write'` event is emitted when a write operation has completed. The number +of bytes written is passed as the first argument to the event handler. + +#### `new fs.Utf8Stream([options])` + +* `options` {Object} + * `append`: {boolean} Appends writes to dest file instead of truncating it. + **Default**: `true`. + * `contentMode`: {string} Which type of data you can send to the write + function, supported values are `'utf8'` or `'buffer'`. **Default**: + `'utf8'`. + * `dest`: {string} A path to a file to be written to (mode controlled by the + append option). + * `fd`: {number} A file descriptor, something that is returned by `fs.open()` + or `fs.openSync()`. + * `fs`: {Object} An object that has the same API as the `fs` module, useful + for mocking, testing, or customizing the behavior of the stream. + * `fsync`: {boolean} Perform a `fs.fsyncSync()` every time a write is + completed. + * `maxLength`: {number} The maximum length of the internal buffer. If a write + operation would cause the buffer to exceed `maxLength`, the data written is + dropped and a drop event is emitted with the dropped data + * `maxWrite`: {number} The maximum number of bytes that can be written; + **Default**: `16384` + * `minLength`: {number} The minimum length of the internal buffer that is + required to be full before flushing. + * `mkdir`: {boolean} Ensure directory for `dest` file exists when true. + **Default**: `false`. + * `mode`: {number|string} Specify the creating file mode (see `fs.open()`). + * `periodicFlush`: {number} Calls flush every `periodicFlush` milliseconds. + * `retryEAGAIN` {Function} A function that will be called when `write()`, + `writeSync()`, or `flushSync()` encounters an `EAGAIN` or `EBUSY` error. + If the return value is `true` the operation will be retried, otherwise it + will bubble the error. The `err` is the error that caused this function to + be called, `writeBufferLen` is the length of the buffer that was written, + and `remainingBufferLen` is the length of the remaining buffer that the + stream did not try to write. + * `err` {any} An error or `null`. + * `writeBufferLen` {number} + * `remainingBufferLen`: {number} + * `sync`: {boolean} Perform writes synchronously. + +#### `utf8Stream.append` + +* {boolean} Whether the stream is appending to the file or truncating it. + +#### `utf8Stream.contentMode` + +* {string} The type of data that can be written to the stream. Supported + values are `'utf8'` or `'buffer'`. **Default**: `'utf8'`. + +#### `utf8Stream.destroy()` + +Close the stream immediately, without flushing the internal buffer. + +#### `utf8Stream.end()` + +Close the stream gracefully, flushing the internal buffer before closing. + +#### `utf8Stream.fd` + +* {number} The file descriptor that is being written to. + +#### `utf8Stream.file` + +* {string} The file that is being written to. + +#### `utf8Stream.flush(callback)` + +* `callback` {Function} + * `err` {Error|null} An error if the flush failed, otherwise `null`. + +Writes the current buffer to the file if a write was not in progress. Do +nothing if `minLength` is zero or if it is already writing. + +#### `utf8Stream.flushSync()` + +Flushes the buffered data synchronously. This is a costly operation. + +#### `utf8Stream.fsync` + +* {boolean} Whether the stream is performing a `fs.fsyncSync()` after every + write operation. + +#### `utf8Stream.maxLength` + +* {number} The maximum length of the internal buffer. If a write + operation would cause the buffer to exceed `maxLength`, the data written is + dropped and a drop event is emitted with the dropped data. + +#### `utf8Stream.minLength` + +* {number} The minimum length of the internal buffer that is required to be + full before flushing. + +#### `utf8Stream.mkdir` + +* {boolean} Whether the stream should ensure that the directory for the + `dest` file exists. If `true`, it will create the directory if it does not + exist. **Default**: `false`. + +#### `utf8Stream.mode` + +* {number|string} The mode of the file that is being written to. + +#### `utf8Stream.periodicFlush` + +* {number} The number of milliseconds between flushes. If set to `0`, no + periodic flushes will be performed. + +#### `utf8Stream.reopen(file)` + +* `file`: {string|Buffer|URL} A path to a file to be written to (mode + controlled by the append option). + +Reopen the file in place, useful for log rotation. + +#### `utf8Stream.sync` + +* {boolean} Whether the stream is writing synchronously or asynchronously. + +#### `utf8Stream.write(data)` + +* `data` {string|Buffer} The data to write. +* Returns {boolean} + +When the `options.contentMode` is set to `'utf8'` when the stream is created, +the `data` argument must be a string. If the `contentMode` is set to `'buffer'`, +the `data` argument must be a {Buffer}. + +#### `utf8Stream.writing` + +* {boolean} Whether the stream is currently writing data to the file. + +#### `utf8Stream[Symbol.dispose]()` + +Calls `utf8Stream.destroy()`. + ### Class: `fs.WriteStream` + +Type: Documentation-only + +The `node:_http_agent`, `node:_http_client`, `node:_http_common`, `node:_http_incoming`, +`node:_http_outgoing` and `node:_http_server` modules are deprecated as they should be considered +an internal nodejs implementation rather than a public facing API, use `node:http` instead. + [DEP0142]: #dep0142-repl_builtinlibs [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3 From 244d0c38a847eb934c4c034e295f06bec52ae3c6 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Fri, 1 Aug 2025 22:25:35 -0600 Subject: [PATCH 31/80] test: deflake stream-readable-to-web test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Luigi Pinca PR-URL: https://github.com/nodejs/node/pull/58948 Fixes: https://github.com/nodejs/node/issues/58949 Reviewed-By: Matteo Collina Reviewed-By: Mattias Buelens Reviewed-By: Juan José Arboleda Reviewed-By: Luigi Pinca Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Daeyeon Jeong --- test/parallel/test-stream-readable-to-web.js | 62 ------------------ test/parallel/test-stream-readable-to-web.mjs | 64 +++++++++++++++++++ 2 files changed, 64 insertions(+), 62 deletions(-) delete mode 100644 test/parallel/test-stream-readable-to-web.js create mode 100644 test/parallel/test-stream-readable-to-web.mjs diff --git a/test/parallel/test-stream-readable-to-web.js b/test/parallel/test-stream-readable-to-web.js deleted file mode 100644 index 753672b509c173..00000000000000 --- a/test/parallel/test-stream-readable-to-web.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; -const common = require('../common'); -if (!common.hasCrypto) { common.skip('missing crypto'); } - -const { Readable } = require('stream'); -const process = require('process'); -const { randomBytes } = require('crypto'); -const assert = require('assert'); - -// Based on: https://github.com/nodejs/node/issues/46347#issuecomment-1413886707 -// edit: make it cross-platform as /dev/urandom is not available on Windows -{ - let currentMemoryUsage = process.memoryUsage().arrayBuffers; - - // We initialize a stream, but not start consuming it - const randomNodeStream = new Readable({ - read(size) { - randomBytes(size, (err, buffer) => { - if (err) { - // If an error occurs, emit an 'error' event - this.emit('error', err); - return; - } - - // Push the random bytes to the stream - this.push(buffer); - }); - } - }); - // after 2 seconds, it'll get converted to web stream - let randomWebStream; - - // We check memory usage every second - // since it's a stream, it shouldn't be higher than the chunk size - const reportMemoryUsage = () => { - const { arrayBuffers } = process.memoryUsage(); - currentMemoryUsage = arrayBuffers; - - assert(currentMemoryUsage <= 256 * 1024 * 1024); - }; - setInterval(reportMemoryUsage, 1000); - - // after 1 second we use Readable.toWeb - // memory usage should stay pretty much the same since it's still a stream - setTimeout(() => { - randomWebStream = Readable.toWeb(randomNodeStream); - }, 1000); - - // after 2 seconds we start consuming the stream - // memory usage will grow, but the old chunks should be garbage-collected pretty quickly - setTimeout(async () => { - // eslint-disable-next-line no-unused-vars - for await (const _ of randomWebStream) { - // Do nothing, just let the stream flow - } - }, 2000); - - setTimeout(() => { - // Test considered passed if we don't crash - process.exit(0); - }, 5000); -} diff --git a/test/parallel/test-stream-readable-to-web.mjs b/test/parallel/test-stream-readable-to-web.mjs new file mode 100644 index 00000000000000..8ff9f2fb16ed1c --- /dev/null +++ b/test/parallel/test-stream-readable-to-web.mjs @@ -0,0 +1,64 @@ +import { mustCall } from '../common/index.mjs'; +import { Readable } from 'node:stream'; +import { memoryUsage } from 'node:process'; +import assert from 'node:assert'; +import { setImmediate } from 'node:timers/promises'; + +// Based on: https://github.com/nodejs/node/issues/46347#issuecomment-1413886707 +// edit: make it cross-platform as /dev/urandom is not available on Windows + +const MAX_MEM = 256 * 1024 * 1024; // 256 MiB + +function checkMemoryUsage() { + assert(memoryUsage().arrayBuffers < MAX_MEM); +} + +const MAX_BUFFERS = 1000; +let buffersCreated = 0; + +const randomNodeStream = new Readable({ + read(size) { + if (buffersCreated >= MAX_BUFFERS) { + this.push(null); + return; + } + + this.push(Buffer.alloc(size)); + buffersCreated++; + } +}); + +randomNodeStream.on('error', (err) => { + assert.fail(err); +}); + +// Before doing anything, make sure memory usage is okay +checkMemoryUsage(); + +// Create stream and check memory usage remains okay + +const randomWebStream = Readable.toWeb(randomNodeStream); + +checkMemoryUsage(); + +let timeout; +try { + // Wait two seconds before consuming the stream to see if memory usage increases + timeout = setTimeout(mustCall(async () => { + // Did the stream leak memory? + checkMemoryUsage(); + // eslint-disable-next-line no-unused-vars + for await (const _ of randomWebStream) { + // Yield event loop to allow garbage collection + await setImmediate(); + // consume the stream + // check memory usage remains okay + checkMemoryUsage(); + } + }), 2000); +} catch (err) { + if (timeout) { + clearTimeout(timeout); + } + assert.fail(err); +} From 76bc4d659bb04f2c7b2cf661fc52083934737760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Sat, 2 Aug 2025 07:43:00 +0200 Subject: [PATCH 32/80] typings: improve internal binding types - Add typings for `async_context_frame`, `icu`, and `sea` bindings - Add a few missing exports on other bindings - Add a few missing primordials PR-URL: https://github.com/nodejs/node/pull/59176 Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Yagiz Nizipli --- typings/globals.d.ts | 7 +++++++ .../internalBinding/async_context_frame.d.ts | 4 ++++ typings/internalBinding/constants.d.ts | 3 +++ typings/internalBinding/http_parser.d.ts | 13 ++++++++++++- typings/internalBinding/icu.d.ts | 18 ++++++++++++++++++ typings/internalBinding/sea.d.ts | 5 +++++ typings/internalBinding/symbols.d.ts | 2 ++ typings/internalBinding/util.d.ts | 18 ++++++++++++++++++ typings/primordials.d.ts | 3 +++ 9 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 typings/internalBinding/async_context_frame.d.ts create mode 100644 typings/internalBinding/icu.d.ts create mode 100644 typings/internalBinding/sea.d.ts diff --git a/typings/globals.d.ts b/typings/globals.d.ts index 7b53a869b9ca11..2ac9699b1f8d5b 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -1,3 +1,4 @@ +import { AsyncContextFrameBinding } from './internalBinding/async_context_frame'; import { AsyncWrapBinding } from './internalBinding/async_wrap'; import { BlobBinding } from './internalBinding/blob'; import { ConfigBinding } from './internalBinding/config'; @@ -7,11 +8,13 @@ import { HttpParserBinding } from './internalBinding/http_parser'; import { InspectorBinding } from './internalBinding/inspector'; import { FsBinding } from './internalBinding/fs'; import { FsDirBinding } from './internalBinding/fs_dir'; +import { ICUBinding } from './internalBinding/icu'; import { LocksBinding } from './internalBinding/locks'; import { MessagingBinding } from './internalBinding/messaging'; import { OptionsBinding } from './internalBinding/options'; import { OSBinding } from './internalBinding/os'; import { ProcessBinding } from './internalBinding/process'; +import { SeaBinding } from './internalBinding/sea'; import { SerdesBinding } from './internalBinding/serdes'; import { SymbolsBinding } from './internalBinding/symbols'; import { TimersBinding } from './internalBinding/timers'; @@ -25,6 +28,7 @@ import { ModulesBinding } from './internalBinding/modules'; import { ZlibBinding } from './internalBinding/zlib'; interface InternalBindingMap { + async_context_frame: AsyncContextFrameBinding; async_wrap: AsyncWrapBinding; blob: BlobBinding; config: ConfigBinding; @@ -33,6 +37,7 @@ interface InternalBindingMap { fs: FsBinding; fs_dir: FsDirBinding; http_parser: HttpParserBinding; + icu: ICUBinding; inspector: InspectorBinding; locks: LocksBinding; messaging: MessagingBinding; @@ -40,6 +45,7 @@ interface InternalBindingMap { options: OptionsBinding; os: OSBinding; process: ProcessBinding; + sea: SeaBinding; serdes: SerdesBinding; symbols: SymbolsBinding; timers: TimersBinding; @@ -65,6 +71,7 @@ declare global { | Int8Array | Int16Array | Int32Array + | Float16Array | Float32Array | Float64Array | BigUint64Array diff --git a/typings/internalBinding/async_context_frame.d.ts b/typings/internalBinding/async_context_frame.d.ts new file mode 100644 index 00000000000000..e7387b57653fd1 --- /dev/null +++ b/typings/internalBinding/async_context_frame.d.ts @@ -0,0 +1,4 @@ +export interface AsyncContextFrameBinding { + getContinuationPreservedEmbedderData(): unknown, + setContinuationPreservedEmbedderData(frame: unknown): void, +} diff --git a/typings/internalBinding/constants.d.ts b/typings/internalBinding/constants.d.ts index 75d2dcc65e8ea7..3c29df44c13303 100644 --- a/typings/internalBinding/constants.d.ts +++ b/typings/internalBinding/constants.d.ts @@ -287,6 +287,9 @@ export interface ConstantsBinding { BROTLI_ENCODE: 9; ZSTD_COMPRESS: 10; ZSTD_DECOMPRESS: 11; + ZSTD_e_continue: 0; + ZSTD_e_flush: 1; + ZSTD_e_end: 2; Z_MIN_WINDOWBITS: 8; Z_MAX_WINDOWBITS: 15; Z_DEFAULT_WINDOWBITS: 15; diff --git a/typings/internalBinding/http_parser.d.ts b/typings/internalBinding/http_parser.d.ts index 5ab5651b637176..124bdd5af2f152 100644 --- a/typings/internalBinding/http_parser.d.ts +++ b/typings/internalBinding/http_parser.d.ts @@ -2,6 +2,15 @@ declare namespace InternalHttpParserBinding { type Buffer = Uint8Array; type Stream = object; + class ConnectionsList { + constructor(); + + all(): HTTPParser[]; + idle(): HTTPParser[]; + active(): HTTPParser[]; + expired(): HTTPParser[]; + } + class HTTPParser { static REQUEST: 1; static RESPONSE: 2; @@ -40,6 +49,8 @@ declare namespace InternalHttpParserBinding { } export interface HttpParserBinding { - methods: string[]; + ConnectionsList: typeof InternalHttpParserBinding.ConnectionsList; HTTPParser: typeof InternalHttpParserBinding.HTTPParser; + allMethods: string[]; + methods: string[]; } diff --git a/typings/internalBinding/icu.d.ts b/typings/internalBinding/icu.d.ts new file mode 100644 index 00000000000000..7a688a5f08233e --- /dev/null +++ b/typings/internalBinding/icu.d.ts @@ -0,0 +1,18 @@ +export interface ICUBinding { + Converter: object; + decode( + converter: object, + input: ArrayBufferView | ArrayBuffer | SharedArrayBuffer, + flags: number, + fromEncoding: string, + ): string; + getConverter(label: string, flags: number): object | undefined; + getStringWidth(value: string, ambiguousAsFullWidth?: boolean, expandEmojiSequence?: boolean): number; + hasConverter(label: string): boolean; + icuErrName(status: number): string; + transcode( + input: ArrayBufferView | ArrayBuffer | SharedArrayBuffer, + fromEncoding: string, + toEncoding: string, + ): Buffer | number; +} diff --git a/typings/internalBinding/sea.d.ts b/typings/internalBinding/sea.d.ts new file mode 100644 index 00000000000000..15f4430d87d1b4 --- /dev/null +++ b/typings/internalBinding/sea.d.ts @@ -0,0 +1,5 @@ +export interface SeaBinding { + getAsset(key: string): ArrayBuffer | undefined; + isExperimentalSeaWarningNeeded(): boolean; + isSea(): boolean; +} diff --git a/typings/internalBinding/symbols.d.ts b/typings/internalBinding/symbols.d.ts index 96310970d5cdee..8f754b283c0351 100644 --- a/typings/internalBinding/symbols.d.ts +++ b/typings/internalBinding/symbols.d.ts @@ -1,5 +1,6 @@ export const async_id_symbol: unique symbol; export const handle_onclose_symbol: unique symbol; +export const imported_cjs_symbol: unique symbol; export const no_message_symbol: unique symbol; export const messaging_deserialize_symbol: unique symbol; export const messaging_transfer_symbol: unique symbol; @@ -13,6 +14,7 @@ export const trigger_async_id_symbol: unique symbol; export interface SymbolsBinding { async_id_symbol: typeof async_id_symbol; handle_onclose_symbol: typeof handle_onclose_symbol; + imported_cjs_symbol: typeof imported_cjs_symbol; no_message_symbol: typeof no_message_symbol; messaging_deserialize_symbol: typeof messaging_deserialize_symbol; messaging_transfer_symbol: typeof messaging_transfer_symbol; diff --git a/typings/internalBinding/util.d.ts b/typings/internalBinding/util.d.ts index 2cd52dc7b8f4b4..2a68699283debe 100644 --- a/typings/internalBinding/util.d.ts +++ b/typings/internalBinding/util.d.ts @@ -46,4 +46,22 @@ export interface UtilBinding { parseEnv(content: string): Record; styleText(format: Array | string, text: string): string; isInsideNodeModules(frameLimit: number, defaultValue: unknown): boolean; + + constants: { + kPending: 0; + kFulfilled: 1; + kRejected: 2; + kExiting: 0; + kExitCode: 1; + kHasExitCode: 2; + ALL_PROPERTIES: 0; + ONLY_WRITABLE: 1; + ONLY_ENUMERABLE: 2; + ONLY_CONFIGURABLE: 4; + SKIP_STRINGS: 8; + SKIP_SYMBOLS: 16; + kDisallowCloneAndTransfer: 0; + kTransferable: 1; + kCloneable: 2; + }; } diff --git a/typings/primordials.d.ts b/typings/primordials.d.ts index cba2cd0aead902..ed77d4112e2984 100644 --- a/typings/primordials.d.ts +++ b/typings/primordials.d.ts @@ -280,6 +280,7 @@ declare namespace primordials { export const FunctionPrototypeApply: UncurryThis export const FunctionPrototypeBind: UncurryThis export const FunctionPrototypeCall: UncurryThis + export const FunctionPrototypeSymbolHasInstance: UncurryMethod export const FunctionPrototypeToString: UncurryThis export import Int16Array = globalThis.Int16Array; export const Int16ArrayPrototype: typeof Int16Array.prototype @@ -371,6 +372,8 @@ declare namespace primordials { export const RegExpPrototypeGetSource: UncurryGetter; export const RegExpPrototypeGetSticky: UncurryGetter; export const RegExpPrototypeGetUnicode: UncurryGetter; + export const RegExpPrototypeSymbolReplace: UncurryMethod + export const RegExpPrototypeSymbolSplit: UncurryMethod export import Set = globalThis.Set; export const SetLength: typeof Set.length export const SetName: typeof Set.name From 95e9cabf9d376ba9d5a195491be7e9349b64f9a8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 2 Aug 2025 12:58:21 +0200 Subject: [PATCH 33/80] doc: clarify release candidate stability index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59295 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Rafael Gonzaga Reviewed-By: Jacob Smith Reviewed-By: Michaël Zasso Reviewed-By: Ruy Adorno Reviewed-By: Luigi Pinca Reviewed-By: Beth Griggs Reviewed-By: Claudio Wunder --- doc/api/documentation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/documentation.md b/doc/api/documentation.md index c6edb13ad613cd..6938ac40a21374 100644 --- a/doc/api/documentation.md +++ b/doc/api/documentation.md @@ -41,9 +41,9 @@ The stability indexes are as follows: > minimum viability. > * 1.2 - Release candidate. Experimental features at this stage are hopefully > ready to become stable. No further breaking changes are anticipated but may -> still occur in response to user feedback. We encourage user testing and -> feedback so that we can know that this feature is ready to be marked as -> stable. +> still occur in response to user feedback or the features' underlying +> specification development. We encourage user testing and feedback so that +> we can know that this feature is ready to be marked as stable. > > Experimental features leave the experimental status typically either by > graduating to stable, or are removed without a deprecation cycle. From 4ee467905dd11b9367afc4b56f6af2ce2e6d5b63 Mon Sep 17 00:00:00 2001 From: hotpineapple <77835382+hotpineapple@users.noreply.github.com> Date: Sat, 2 Aug 2025 20:31:47 +0900 Subject: [PATCH 34/80] lib: use validateString refactor validation code using validate function PR-URL: https://github.com/nodejs/node/pull/59296 Reviewed-By: Ethan Arrowood Reviewed-By: Luigi Pinca Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Filip Skokan --- lib/_http_client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/_http_client.js b/lib/_http_client.js index f5d3d711a7f165..63a7befc8ebbb3 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -80,6 +80,7 @@ const { const { validateInteger, validateBoolean, + validateString, } = require('internal/validators'); const { getTimerDuration } = require('internal/timers'); const { @@ -270,12 +271,11 @@ function ClientRequest(input, options, cb) { delete optsWithoutSignal.signal; } let method = options.method; - const methodIsString = (typeof method === 'string'); - if (method !== null && method !== undefined && !methodIsString) { - throw new ERR_INVALID_ARG_TYPE('options.method', 'string', method); + if (method != null) { + validateString(method, 'options.method'); } - if (methodIsString && method) { + if (method) { if (!checkIsHttpToken(method)) { throw new ERR_INVALID_HTTP_TOKEN('Method', method); } From a2b72c23042711beeef9d8a2f725bdf30ce58dc1 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Sat, 2 Aug 2025 10:59:50 -0300 Subject: [PATCH 35/80] meta: add tsc and build team as codeowners building.md PR-URL: https://github.com/nodejs/node/pull/59298 Reviewed-By: Antoine du Hamel Reviewed-By: Marco Ippolito Reviewed-By: Ruy Adorno Reviewed-By: Darshan Sen Reviewed-By: Chengzhong Wu Reviewed-By: Matteo Collina Reviewed-By: Luigi Pinca --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cc363c492a5c4f..5ef23759b3e18e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,6 +18,7 @@ /doc/contributing/**/* @nodejs/tsc /GOVERNANCE.md @nodejs/tsc /SECURITY.md @nodejs/tsc +/BUILDING.md @nodejs/build @nodejs/tsc /LICENSE @nodejs/tsc /onboarding.md @nodejs/tsc From f9f3dc94cb61abc544cb02b522e448e4643d828e Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 2 Aug 2025 07:23:15 -0700 Subject: [PATCH 36/80] test: add known issue test for fs.cpSync dereference bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs: https://github.com/nodejs/node/issues/58939 PR-URL: https://github.com/nodejs/node/pull/58941 Reviewed-By: Juan José Arboleda Reviewed-By: Luigi Pinca --- .../test-fs-cp-sync-dereference.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/known_issues/test-fs-cp-sync-dereference.js diff --git a/test/known_issues/test-fs-cp-sync-dereference.js b/test/known_issues/test-fs-cp-sync-dereference.js new file mode 100644 index 00000000000000..fbb07a8f781520 --- /dev/null +++ b/test/known_issues/test-fs-cp-sync-dereference.js @@ -0,0 +1,39 @@ +'use strict'; + +// Refs: https://github.com/nodejs/node/issues/58939 +// +// The cpSync function is not correctly handling the `dereference` option. +// In this test, both the cp and cpSync functions are attempting to copy +// a file over a symlinked directory. In the cp case it works fine. In the +// cpSync case it fails with an error. + +const common = require('../common'); + +const { + cp, + cpSync, + mkdirSync, + symlinkSync, + writeFileSync, +} = require('fs'); + +const { + join, +} = require('path'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const pathA = join(tmpdir.path, 'a'); +const pathB = join(tmpdir.path, 'b'); +const pathC = join(tmpdir.path, 'c'); +const pathD = join(tmpdir.path, 'd'); + +writeFileSync(pathA, 'file a'); +mkdirSync(pathB); +symlinkSync(pathB, pathC, 'dir'); +symlinkSync(pathB, pathD, 'dir'); + +cp(pathA, pathD, { dereference: false }, common.mustSucceed()); + +cpSync(pathA, pathC, { dereference: false }); From e91b54df59572992a285b429f27fec821cd1aafc Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Sat, 2 Aug 2025 14:39:16 -0300 Subject: [PATCH 37/80] lib: handle superscript variants on windows device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows recognizes the 8-bit [ISO/IEC 8859-1](https://en.wikipedia.org/wiki/ISO/IEC_8859-1) superscript digits ¹, ², and ³ as digits and treats them as valid parts of COM# and LPT# device names, making them reserved in every directory. Signed-off-by: RafaelGSS PR-URL: https://github.com/nodejs/node/pull/59261 Reviewed-By: Robert Nagy Reviewed-By: Filip Skokan Reviewed-By: Matteo Collina --- lib/path.js | 2 ++ test/parallel/test-path-win32-normalize-device-names.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/lib/path.js b/lib/path.js index 70b0f67e93bf1e..bee827c06e9973 100644 --- a/lib/path.js +++ b/lib/path.js @@ -73,6 +73,8 @@ const WINDOWS_RESERVED_NAMES = [ 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9', + 'COM\xb9', 'COM\xb2', 'COM\xb3', + 'LPT\xb9', 'LPT\xb2', 'LPT\xb3', ]; function isWindowsReservedName(path, colonIndex) { diff --git a/test/parallel/test-path-win32-normalize-device-names.js b/test/parallel/test-path-win32-normalize-device-names.js index 927bc5cec8a2e5..2c6dcf142a2674 100644 --- a/test/parallel/test-path-win32-normalize-device-names.js +++ b/test/parallel/test-path-win32-normalize-device-names.js @@ -45,10 +45,18 @@ const normalizeDeviceNameTests = [ { input: 'COM1:', expected: '.\\COM1:.' }, { input: 'COM9:', expected: '.\\COM9:.' }, + { input: 'COM¹:', expected: '.\\COM¹:.' }, + { input: 'COM²:', expected: '.\\COM²:.' }, + { input: 'COM³:', expected: '.\\COM³:.' }, { input: 'COM1:.\\..\\..\\foo', expected: '.\\COM1:..\\..\\foo' }, + { input: 'COM¹:.\\..\\..\\foo', expected: '.\\COM¹:..\\..\\foo' }, { input: 'LPT1:', expected: '.\\LPT1:.' }, + { input: 'LPT¹:', expected: '.\\LPT¹:.' }, + { input: 'LPT²:', expected: '.\\LPT²:.' }, + { input: 'LPT³:', expected: '.\\LPT³:.' }, { input: 'LPT9:', expected: '.\\LPT9:.' }, { input: 'LPT1:.\\..\\..\\foo', expected: '.\\LPT1:..\\..\\foo' }, + { input: 'LPT¹:.\\..\\..\\foo', expected: '.\\LPT¹:..\\..\\foo' }, { input: 'LpT5:/another/path', expected: '.\\LpT5:another\\path' }, { input: 'C:\\foo', expected: 'C:\\foo' }, From d140c3713e27ab1cad77c592f990451944175d72 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Sat, 2 Aug 2025 15:23:28 -0300 Subject: [PATCH 38/80] doc: clarify the need of compiler compatible with c++20 Very often someone appear in the #node-core slack channel asking for guidance on this compilation error PR-URL: https://github.com/nodejs/node/pull/59297 Reviewed-By: Jacob Smith Reviewed-By: Luigi Pinca --- BUILDING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BUILDING.md b/BUILDING.md index 4c46468139f7da..3e28e3349950d2 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -272,6 +272,11 @@ export CXX=g++-12 make -j4 ``` +> \[!IMPORTANT] +> If you face a compilation error during this process such as +> `error: no matching conversion for functional-style cast from 'unsigned int' to 'TypeIndex'` +> Make sure to use a `g++` or `clang` version compatible with C++20. + We can speed up the builds by using [Ninja](https://ninja-build.org/). For more information, see [Building Node.js with Ninja](doc/contributing/building-node-with-ninja.md). From 46527e8cea09d042ba6d98f352ef129746ea80d8 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sun, 3 Aug 2025 10:27:14 +0200 Subject: [PATCH 39/80] =?UTF-8?q?doc:=20correct=20orthography=20`eg.`=20?= =?UTF-8?q?=E2=86=92=20`e.g.`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59329 Reviewed-By: Michaël Zasso Reviewed-By: Edy Silva Reviewed-By: James M Snell Reviewed-By: Rafael Gonzaga Reviewed-By: Luigi Pinca Reviewed-By: Zeyu "Alex" Yang --- doc/contributing/investigating-native-memory-leaks.md | 2 +- tools/msvs/msi/nodemsi/product.wxs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/contributing/investigating-native-memory-leaks.md b/doc/contributing/investigating-native-memory-leaks.md index 4d565fe031244f..6639617b54c53c 100644 --- a/doc/contributing/investigating-native-memory-leaks.md +++ b/doc/contributing/investigating-native-memory-leaks.md @@ -396,7 +396,7 @@ call: } ``` -Create a file (eg. `node-12.14.1.supp`) with the contents of the suppression +Create a file (e.g. `node-12.14.1.supp`) with the contents of the suppression records, and run Valgrind with the suppression file previously created: ```bash diff --git a/tools/msvs/msi/nodemsi/product.wxs b/tools/msvs/msi/nodemsi/product.wxs index ff66ade6816aaa..a0100cc564405f 100644 --- a/tools/msvs/msi/nodemsi/product.wxs +++ b/tools/msvs/msi/nodemsi/product.wxs @@ -41,7 +41,7 @@ - + From 41bcf5f659adbd8872f3d4023f94e30ad63a5878 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 3 Aug 2025 13:31:39 +0200 Subject: [PATCH 40/80] test: update WPT resources,WebCryptoAPI,webstorage PR-URL: https://github.com/nodejs/node/pull/59311 Refs: https://github.com/nodejs/node/issues/58987 Refs: https://github.com/nodejs/node/issues/59310 Reviewed-By: Luigi Pinca Reviewed-By: James M Snell --- test/fixtures/wpt/README.md | 6 +- ...rves_bits_curve448.tentative.https.any.js} | 0 ...rves_keys_curve448.tentative.https.any.js} | 0 ... => failures_Ed448.tentative.https.any.js} | 0 ...s => failures_X448.tentative.https.any.js} | 0 ...=> successes_Ed448.tentative.https.any.js} | 0 ... => successes_X448.tentative.https.any.js} | 0 .../wpt/WebCryptoAPI/getRandomValues.any.js | 6 +- ...kp_importKey_Ed448.tentative.https.any.js} | 0 ...okp_importKey_X448.tentative.https.any.js} | 0 ...Key_failures_Ed448.tentative.https.any.js} | 0 ...tKey_failures_X448.tentative.https.any.js} | 0 ... => eddsa_curve448.tentative.https.any.js} | 0 test/fixtures/wpt/resources/channel.sub.js | 2 +- .../fixtures/wpt/resources/check-layout-th.js | 3 +- test/fixtures/wpt/resources/check-layout.js | 245 --- .../declarative-shadow-dom-polyfill.js | 25 - .../wpt/resources/idlharness-shadowrealm.js | 61 - test/fixtures/wpt/resources/idlharness.js | 31 +- .../wpt/resources/out-of-scope-test.js | 5 + .../wpt/resources/testdriver-actions.js | 4 +- .../resources/testdriver-actions.js.headers | 2 + test/fixtures/wpt/resources/testdriver.js | 1366 ++++++++++++++++- ...rness-shadowrealm-audioworkletprocessor.js | 52 + .../testharness-shadowrealm-inner.js | 38 + .../testharness-shadowrealm-outer.js | 151 ++ test/fixtures/wpt/resources/testharness.js | 656 +++++--- .../wpt/resources/testharnessreport.js | 25 - .../wpt/resources/web-bluetooth-bidi-test.js | 408 +++++ test/fixtures/wpt/versions.json | 6 +- ...e_local_setitem_quotaexceedederr.window.js | 12 +- ...session_setitem_quotaexceedederr.window.js | 12 +- .../wpt/webstorage/symbol-props.window.js | 6 +- test/wpt/status/WebCryptoAPI.cjs | 16 + test/wpt/status/webstorage.json | 25 + 35 files changed, 2570 insertions(+), 593 deletions(-) rename test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/{cfrg_curves_bits_curve448.https.any.js => cfrg_curves_bits_curve448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/{cfrg_curves_keys_curve448.https.any.js => cfrg_curves_keys_curve448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/generateKey/{failures_Ed448.https.any.js => failures_Ed448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/generateKey/{failures_X448.https.any.js => failures_X448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/generateKey/{successes_Ed448.https.any.js => successes_Ed448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/generateKey/{successes_X448.https.any.js => successes_X448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/import_export/{okp_importKey_Ed448.https.any.js => okp_importKey_Ed448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/import_export/{okp_importKey_X448.https.any.js => okp_importKey_X448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/import_export/{okp_importKey_failures_Ed448.https.any.js => okp_importKey_failures_Ed448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/import_export/{okp_importKey_failures_X448.https.any.js => okp_importKey_failures_X448.tentative.https.any.js} (100%) rename test/fixtures/wpt/WebCryptoAPI/sign_verify/{eddsa_curve448.https.any.js => eddsa_curve448.tentative.https.any.js} (100%) delete mode 100644 test/fixtures/wpt/resources/check-layout.js delete mode 100644 test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js delete mode 100644 test/fixtures/wpt/resources/idlharness-shadowrealm.js create mode 100644 test/fixtures/wpt/resources/out-of-scope-test.js create mode 100644 test/fixtures/wpt/resources/testdriver-actions.js.headers create mode 100644 test/fixtures/wpt/resources/testharness-shadowrealm-audioworkletprocessor.js create mode 100644 test/fixtures/wpt/resources/testharness-shadowrealm-inner.js create mode 100644 test/fixtures/wpt/resources/testharness-shadowrealm-outer.js create mode 100644 test/fixtures/wpt/resources/web-bluetooth-bidi-test.js diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 34aed56641dd84..06872e85e556f2 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -26,7 +26,7 @@ Last update: - interfaces: https://github.com/web-platform-tests/wpt/tree/e1b27be06b/interfaces - performance-timeline: https://github.com/web-platform-tests/wpt/tree/94caab7038/performance-timeline - resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing -- resources: https://github.com/web-platform-tests/wpt/tree/1e140d63ec/resources +- resources: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/resources - streams: https://github.com/web-platform-tests/wpt/tree/bc9dcbbf1a/streams - url: https://github.com/web-platform-tests/wpt/tree/9504a83e01/url - urlpattern: https://github.com/web-platform-tests/wpt/tree/84b75f0880/urlpattern @@ -34,10 +34,10 @@ Last update: - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi - web-locks: https://github.com/web-platform-tests/wpt/tree/10a122a6bc/web-locks -- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/ab08796857/WebCryptoAPI +- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/WebCryptoAPI - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/2f96fa1996/webidl/ecmascript-binding/es-exceptions - webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/6495c91853/webmessaging/broadcastchannel -- webstorage: https://github.com/web-platform-tests/wpt/tree/1291340aaa/webstorage +- webstorage: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/webstorage [Web Platform Tests]: https://github.com/web-platform-tests/wpt [`git node wpt`]: https://github.com/nodejs/node-core-utils/blob/main/docs/git-node.md#git-node-wpt diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/generateKey/failures_Ed448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/generateKey/failures_Ed448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/generateKey/failures_Ed448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/generateKey/failures_Ed448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/generateKey/failures_X448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/generateKey/failures_X448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/generateKey/failures_X448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/generateKey/failures_X448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/generateKey/successes_Ed448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/generateKey/successes_Ed448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/generateKey/successes_Ed448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/generateKey/successes_Ed448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/generateKey/successes_X448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/generateKey/successes_X448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/generateKey/successes_X448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/generateKey/successes_X448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/getRandomValues.any.js b/test/fixtures/wpt/WebCryptoAPI/getRandomValues.any.js index 574134eb76dcd8..aecd38efd60bac 100644 --- a/test/fixtures/wpt/WebCryptoAPI/getRandomValues.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/getRandomValues.any.js @@ -60,9 +60,9 @@ for (const array of arrays) { test(function() { const maxlength = 65536 / ctor.BYTES_PER_ELEMENT; - assert_throws_dom("QuotaExceededError", function() { - self.crypto.getRandomValues(new ctor(maxlength + 1)) - }, "crypto.getRandomValues length over 65536") + assert_throws_quotaexceedederror(() => { + self.crypto.getRandomValues(new ctor(maxlength + 1)); + }, null, null, "crypto.getRandomValues length over 65536"); }, "Large length: " + array); test(function() { diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_Ed448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_Ed448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_Ed448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_Ed448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_X448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_X448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_X448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_X448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_Ed448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_Ed448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_Ed448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_Ed448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_X448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_X448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_X448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_X448.tentative.https.any.js diff --git a/test/fixtures/wpt/WebCryptoAPI/sign_verify/eddsa_curve448.https.any.js b/test/fixtures/wpt/WebCryptoAPI/sign_verify/eddsa_curve448.tentative.https.any.js similarity index 100% rename from test/fixtures/wpt/WebCryptoAPI/sign_verify/eddsa_curve448.https.any.js rename to test/fixtures/wpt/WebCryptoAPI/sign_verify/eddsa_curve448.tentative.https.any.js diff --git a/test/fixtures/wpt/resources/channel.sub.js b/test/fixtures/wpt/resources/channel.sub.js index d93b3b3091947d..370d4f5905e3fa 100644 --- a/test/fixtures/wpt/resources/channel.sub.js +++ b/test/fixtures/wpt/resources/channel.sub.js @@ -511,7 +511,7 @@ * * @param {SendChannel|string} [dest] - Either a SendChannel * to the destination, or the UUID of the destination. If - * omitted, a new UUID is generated, which can be used when + * ommitted, a new UUID is generated, which can be used when * constructing the URL for the global. * */ diff --git a/test/fixtures/wpt/resources/check-layout-th.js b/test/fixtures/wpt/resources/check-layout-th.js index f14ca3246b8ea2..2965a25d146a1f 100644 --- a/test/fixtures/wpt/resources/check-layout-th.js +++ b/test/fixtures/wpt/resources/check-layout-th.js @@ -218,6 +218,7 @@ window.checkLayout = function(selectorList, callDone = true) nodes = Array.prototype.slice.call(nodes); var checkedLayout = false; Array.prototype.forEach.call(nodes, function(node) { + const title = node.title ? `: ${node.title}` : ''; test(function(t) { var container = node.parentNode.className == 'container' ? node.parentNode : node; var prefix = @@ -240,7 +241,7 @@ window.checkLayout = function(selectorList, callDone = true) } checkedLayout |= !passed; } - }, selectorList + ' ' + String(++testNumber)); + }, `${selectorList} ${++testNumber}${title}`); }); if (!checkedLayout) { console.error("No valid data-* attributes found in selector list : " + selectorList); diff --git a/test/fixtures/wpt/resources/check-layout.js b/test/fixtures/wpt/resources/check-layout.js deleted file mode 100644 index 8634481497d701..00000000000000 --- a/test/fixtures/wpt/resources/check-layout.js +++ /dev/null @@ -1,245 +0,0 @@ -(function() { - -function insertAfter(nodeToAdd, referenceNode) -{ - if (referenceNode == document.body) { - document.body.appendChild(nodeToAdd); - return; - } - - if (referenceNode.nextSibling) - referenceNode.parentNode.insertBefore(nodeToAdd, referenceNode.nextSibling); - else - referenceNode.parentNode.appendChild(nodeToAdd); -} - -function positionedAncestor(node) -{ - var ancestor = node.parentNode; - while (getComputedStyle(ancestor).position == 'static') - ancestor = ancestor.parentNode; - return ancestor; -} - -function checkSubtreeExpectedValues(parent, failures) -{ - var checkedLayout = checkExpectedValues(parent, failures); - Array.prototype.forEach.call(parent.childNodes, function(node) { - checkedLayout |= checkSubtreeExpectedValues(node, failures); - }); - return checkedLayout; -} - -function checkAttribute(output, node, attribute) -{ - var result = node.getAttribute && node.getAttribute(attribute); - output.checked |= !!result; - return result; -} - -function checkExpectedValues(node, failures) -{ - var output = { checked: false }; - var expectedWidth = checkAttribute(output, node, "data-expected-width"); - if (expectedWidth) { - if (isNaN(expectedWidth) || Math.abs(node.offsetWidth - expectedWidth) >= 1) - failures.push("Expected " + expectedWidth + " for width, but got " + node.offsetWidth + ". "); - } - - var expectedHeight = checkAttribute(output, node, "data-expected-height"); - if (expectedHeight) { - if (isNaN(expectedHeight) || Math.abs(node.offsetHeight - expectedHeight) >= 1) - failures.push("Expected " + expectedHeight + " for height, but got " + node.offsetHeight + ". "); - } - - var expectedOffset = checkAttribute(output, node, "data-offset-x"); - if (expectedOffset) { - if (isNaN(expectedOffset) || Math.abs(node.offsetLeft - expectedOffset) >= 1) - failures.push("Expected " + expectedOffset + " for offsetLeft, but got " + node.offsetLeft + ". "); - } - - var expectedOffset = checkAttribute(output, node, "data-offset-y"); - if (expectedOffset) { - if (isNaN(expectedOffset) || Math.abs(node.offsetTop - expectedOffset) >= 1) - failures.push("Expected " + expectedOffset + " for offsetTop, but got " + node.offsetTop + ". "); - } - - var expectedOffset = checkAttribute(output, node, "data-positioned-offset-x"); - if (expectedOffset) { - var actualOffset = node.getBoundingClientRect().left - positionedAncestor(node).getBoundingClientRect().left; - if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1) - failures.push("Expected " + expectedOffset + " for getBoundingClientRect().left offset, but got " + actualOffset + ". "); - } - - var expectedOffset = checkAttribute(output, node, "data-positioned-offset-y"); - if (expectedOffset) { - var actualOffset = node.getBoundingClientRect().top - positionedAncestor(node).getBoundingClientRect().top; - if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1) - failures.push("Expected " + expectedOffset + " for getBoundingClientRect().top offset, but got " + actualOffset + ". "); - } - - var expectedWidth = checkAttribute(output, node, "data-expected-client-width"); - if (expectedWidth) { - if (isNaN(expectedWidth) || Math.abs(node.clientWidth - expectedWidth) >= 1) - failures.push("Expected " + expectedWidth + " for clientWidth, but got " + node.clientWidth + ". "); - } - - var expectedHeight = checkAttribute(output, node, "data-expected-client-height"); - if (expectedHeight) { - if (isNaN(expectedHeight) || Math.abs(node.clientHeight - expectedHeight) >= 1) - failures.push("Expected " + expectedHeight + " for clientHeight, but got " + node.clientHeight + ". "); - } - - var expectedWidth = checkAttribute(output, node, "data-expected-scroll-width"); - if (expectedWidth) { - if (isNaN(expectedWidth) || Math.abs(node.scrollWidth - expectedWidth) >= 1) - failures.push("Expected " + expectedWidth + " for scrollWidth, but got " + node.scrollWidth + ". "); - } - - var expectedHeight = checkAttribute(output, node, "data-expected-scroll-height"); - if (expectedHeight) { - if (isNaN(expectedHeight) || Math.abs(node.scrollHeight - expectedHeight) >= 1) - failures.push("Expected " + expectedHeight + " for scrollHeight, but got " + node.scrollHeight + ". "); - } - - var expectedOffset = checkAttribute(output, node, "data-total-x"); - if (expectedOffset) { - var totalLeft = node.clientLeft + node.offsetLeft; - if (isNaN(expectedOffset) || Math.abs(totalLeft - expectedOffset) >= 1) - failures.push("Expected " + expectedOffset + " for clientLeft+offsetLeft, but got " + totalLeft + ", clientLeft: " + node.clientLeft + ", offsetLeft: " + node.offsetLeft + ". "); - } - - var expectedOffset = checkAttribute(output, node, "data-total-y"); - if (expectedOffset) { - var totalTop = node.clientTop + node.offsetTop; - if (isNaN(expectedOffset) || Math.abs(totalTop - expectedOffset) >= 1) - failures.push("Expected " + expectedOffset + " for clientTop+offsetTop, but got " + totalTop + ", clientTop: " + node.clientTop + ", + offsetTop: " + node.offsetTop + ". "); - } - - var expectedDisplay = checkAttribute(output, node, "data-expected-display"); - if (expectedDisplay) { - var actualDisplay = getComputedStyle(node).display; - if (actualDisplay != expectedDisplay) - failures.push("Expected " + expectedDisplay + " for display, but got " + actualDisplay + ". "); - } - - var expectedPaddingTop = checkAttribute(output, node, "data-expected-padding-top"); - if (expectedPaddingTop) { - var actualPaddingTop = getComputedStyle(node).paddingTop; - // Trim the unit "px" from the output. - actualPaddingTop = actualPaddingTop.substring(0, actualPaddingTop.length - 2); - if (actualPaddingTop != expectedPaddingTop) - failures.push("Expected " + expectedPaddingTop + " for padding-top, but got " + actualPaddingTop + ". "); - } - - var expectedPaddingBottom = checkAttribute(output, node, "data-expected-padding-bottom"); - if (expectedPaddingBottom) { - var actualPaddingBottom = getComputedStyle(node).paddingBottom; - // Trim the unit "px" from the output. - actualPaddingBottom = actualPaddingBottom.substring(0, actualPaddingBottom.length - 2); - if (actualPaddingBottom != expectedPaddingBottom) - failures.push("Expected " + expectedPaddingBottom + " for padding-bottom, but got " + actualPaddingBottom + ". "); - } - - var expectedPaddingLeft = checkAttribute(output, node, "data-expected-padding-left"); - if (expectedPaddingLeft) { - var actualPaddingLeft = getComputedStyle(node).paddingLeft; - // Trim the unit "px" from the output. - actualPaddingLeft = actualPaddingLeft.substring(0, actualPaddingLeft.length - 2); - if (actualPaddingLeft != expectedPaddingLeft) - failures.push("Expected " + expectedPaddingLeft + " for padding-left, but got " + actualPaddingLeft + ". "); - } - - var expectedPaddingRight = checkAttribute(output, node, "data-expected-padding-right"); - if (expectedPaddingRight) { - var actualPaddingRight = getComputedStyle(node).paddingRight; - // Trim the unit "px" from the output. - actualPaddingRight = actualPaddingRight.substring(0, actualPaddingRight.length - 2); - if (actualPaddingRight != expectedPaddingRight) - failures.push("Expected " + expectedPaddingRight + " for padding-right, but got " + actualPaddingRight + ". "); - } - - var expectedMarginTop = checkAttribute(output, node, "data-expected-margin-top"); - if (expectedMarginTop) { - var actualMarginTop = getComputedStyle(node).marginTop; - // Trim the unit "px" from the output. - actualMarginTop = actualMarginTop.substring(0, actualMarginTop.length - 2); - if (actualMarginTop != expectedMarginTop) - failures.push("Expected " + expectedMarginTop + " for margin-top, but got " + actualMarginTop + ". "); - } - - var expectedMarginBottom = checkAttribute(output, node, "data-expected-margin-bottom"); - if (expectedMarginBottom) { - var actualMarginBottom = getComputedStyle(node).marginBottom; - // Trim the unit "px" from the output. - actualMarginBottom = actualMarginBottom.substring(0, actualMarginBottom.length - 2); - if (actualMarginBottom != expectedMarginBottom) - failures.push("Expected " + expectedMarginBottom + " for margin-bottom, but got " + actualMarginBottom + ". "); - } - - var expectedMarginLeft = checkAttribute(output, node, "data-expected-margin-left"); - if (expectedMarginLeft) { - var actualMarginLeft = getComputedStyle(node).marginLeft; - // Trim the unit "px" from the output. - actualMarginLeft = actualMarginLeft.substring(0, actualMarginLeft.length - 2); - if (actualMarginLeft != expectedMarginLeft) - failures.push("Expected " + expectedMarginLeft + " for margin-left, but got " + actualMarginLeft + ". "); - } - - var expectedMarginRight = checkAttribute(output, node, "data-expected-margin-right"); - if (expectedMarginRight) { - var actualMarginRight = getComputedStyle(node).marginRight; - // Trim the unit "px" from the output. - actualMarginRight = actualMarginRight.substring(0, actualMarginRight.length - 2); - if (actualMarginRight != expectedMarginRight) - failures.push("Expected " + expectedMarginRight + " for margin-right, but got " + actualMarginRight + ". "); - } - - return output.checked; -} - -window.checkLayout = function(selectorList, outputContainer) -{ - var result = true; - if (!selectorList) { - document.body.appendChild(document.createTextNode("You must provide a CSS selector of nodes to check.")); - return; - } - var nodes = document.querySelectorAll(selectorList); - nodes = Array.prototype.slice.call(nodes); - nodes.reverse(); - var checkedLayout = false; - Array.prototype.forEach.call(nodes, function(node) { - var failures = []; - checkedLayout |= checkExpectedValues(node.parentNode, failures); - checkedLayout |= checkSubtreeExpectedValues(node, failures); - - var container = node.parentNode.className == 'container' ? node.parentNode : node; - - var pre = document.createElement('pre'); - if (failures.length) { - pre.className = 'FAIL'; - result = false; - } - pre.appendChild(document.createTextNode(failures.length ? "FAIL:\n" + failures.join('\n') + '\n\n' + container.outerHTML : "PASS")); - - var referenceNode = container; - if (outputContainer) { - if (!outputContainer.lastChild) { - // Inserting a text node so we have something to insertAfter. - outputContainer.textContent = " "; - } - referenceNode = outputContainer.lastChild; - } - insertAfter(pre, referenceNode); - }); - - if (!checkedLayout) { - document.body.appendChild(document.createTextNode("FAIL: No valid data-* attributes found in selector list : " + selectorList)); - return false; - } - - return result; -} - -})(); diff --git a/test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js b/test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js deleted file mode 100644 index 99a3e911eb6336..00000000000000 --- a/test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Polyfill for attaching shadow trees for declarative Shadow DOM for - * implementations that do not support declarative Shadow DOM. - * - * Note: this polyfill will feature-detect the native feature, and do nothing - * if supported. - * - * See: https://github.com/whatwg/html/pull/5465 - * - * root: The root of the subtree in which to upgrade shadow roots - * - */ - -function polyfill_declarative_shadow_dom(root) { - if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) - return; - root.querySelectorAll("template[shadowrootmode]").forEach(template => { - const mode = template.getAttribute("shadowrootmode"); - const delegatesFocus = template.hasAttribute("shadowrootdelegatesfocus"); - const shadowRoot = template.parentNode.attachShadow({ mode, delegatesFocus }); - shadowRoot.appendChild(template.content); - template.remove(); - polyfill_declarative_shadow_dom(shadowRoot); - }); -} diff --git a/test/fixtures/wpt/resources/idlharness-shadowrealm.js b/test/fixtures/wpt/resources/idlharness-shadowrealm.js deleted file mode 100644 index 9484ca6f512ad0..00000000000000 --- a/test/fixtures/wpt/resources/idlharness-shadowrealm.js +++ /dev/null @@ -1,61 +0,0 @@ -// TODO: it would be nice to support `idl_array.add_objects` -function fetch_text(url) { - return fetch(url).then(function (r) { - if (!r.ok) { - throw new Error("Error fetching " + url + "."); - } - return r.text(); - }); -} - -/** - * idl_test_shadowrealm is a promise_test wrapper that handles the fetching of the IDL, and - * running the code in a `ShadowRealm`, avoiding repetitive boilerplate. - * - * @see https://github.com/tc39/proposal-shadowrealm - * @param {String[]} srcs Spec name(s) for source idl files (fetched from - * /interfaces/{name}.idl). - * @param {String[]} deps Spec name(s) for dependency idl files (fetched - * from /interfaces/{name}.idl). Order is important - dependencies from - * each source will only be included if they're already know to be a - * dependency (i.e. have already been seen). - */ -function idl_test_shadowrealm(srcs, deps) { - promise_setup(async t => { - const realm = new ShadowRealm(); - // https://github.com/web-platform-tests/wpt/issues/31996 - realm.evaluate("globalThis.self = globalThis; undefined;"); - - realm.evaluate(` - globalThis.self.GLOBAL = { - isWindow: function() { return false; }, - isWorker: function() { return false; }, - isShadowRealm: function() { return true; }, - }; undefined; - `); - const specs = await Promise.all(srcs.concat(deps).map(spec => { - return fetch_text("/interfaces/" + spec + ".idl"); - })); - const idls = JSON.stringify(specs); - await new Promise( - realm.evaluate(`(resolve,reject) => { - (async () => { - await import("/resources/testharness.js"); - await import("/resources/WebIDLParser.js"); - await import("/resources/idlharness.js"); - const idls = ${idls}; - const idl_array = new IdlArray(); - for (let i = 0; i < ${srcs.length}; i++) { - idl_array.add_idls(idls[i]); - } - for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) { - idl_array.add_dependency_idls(idls[i]); - } - idl_array.test(); - })().then(resolve, (e) => reject(e.toString())); - }`) - ); - await fetch_tests_from_shadow_realm(realm); - }); -} -// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker: diff --git a/test/fixtures/wpt/resources/idlharness.js b/test/fixtures/wpt/resources/idlharness.js index 056fcdd4a1076a..2eb710c1827cda 100644 --- a/test/fixtures/wpt/resources/idlharness.js +++ b/test/fixtures/wpt/resources/idlharness.js @@ -566,6 +566,7 @@ IdlArray.prototype.is_json_type = function(type) case "Uint8ClampedArray": case "BigInt64Array": case "BigUint64Array": + case "Float16Array": case "Float32Array": case "Float64Array": case "ArrayBuffer": @@ -733,7 +734,7 @@ IdlArray.prototype.test = function() Object.getOwnPropertyNames(this.members).forEach(function(memberName) { var member = this.members[memberName]; - if (!(member instanceof IdlInterface)) { + if (!(member instanceof IdlInterface || member instanceof IdlNamespace)) { return; } @@ -782,6 +783,10 @@ IdlArray.prototype.merge_partials = function() } testedPartials.set(parsed_idl.name, partialTestCount); + if (!self.shouldRunSubTest(partialTestName)) { + return; + } + if (!parsed_idl.untested) { test(function () { assert_true(originalExists, `Original ${parsed_idl.type} should be defined`); @@ -809,7 +814,7 @@ IdlArray.prototype.merge_partials = function() { // Special-case "Exposed". Must be a subset of original interface's exposure. // Exposed on a partial is the equivalent of having the same Exposed on all nested members. - // See https://github.com/heycam/webidl/issues/154 for discrepancy between Exposed and + // See https://github.com/heycam/webidl/issues/154 for discrepency between Exposed and // other extended attributes on partial interfaces. const exposureAttr = parsed_idl.extAttrs.find(a => a.name === "Exposed"); if (exposureAttr) { @@ -870,6 +875,7 @@ IdlArray.prototype.merge_mixins = function() { const lhs = parsed_idl.target; const rhs = parsed_idl.includes; + const testName = lhs + " includes " + rhs + ": member names are unique"; var errStr = lhs + " includes " + rhs + ", but "; if (!(lhs in this.members)) throw errStr + lhs + " is undefined."; @@ -877,7 +883,7 @@ IdlArray.prototype.merge_mixins = function() if (!(rhs in this.members)) throw errStr + rhs + " is undefined."; if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface."; - if (this.members[rhs].members.length) { + if (this.members[rhs].members.length && self.shouldRunSubTest(testName)) { test(function () { var clash = this.members[rhs].members.find(function(member) { return this.members[lhs].members.find(function(m) { @@ -891,7 +897,7 @@ IdlArray.prototype.merge_mixins = function() this.members[lhs].members.push(new IdlInterfaceMember(member)); }.bind(this)); assert_true(!clash, "member " + (clash && clash.name) + " is unique"); - }.bind(this), lhs + " includes " + rhs + ": member names are unique"); + }.bind(this), testName); } } this.includes = []; @@ -1420,7 +1426,7 @@ IdlInterface.prototype.test = function() if (!this.untested) { subsetTestByKey(this.name, test, function() { - assert_false(this.name in self); + assert_false(this.name in self, this.name + " interface should not exist"); }.bind(this), this.name + " interface: existence and properties of interface object"); } return; @@ -3450,6 +3456,17 @@ IdlNamespace.prototype.test_self = function () IdlNamespace.prototype.test = function () { + // If the namespace object is not exposed, only test that. Members can't be + // tested either + if (!this.exposed) { + if (!this.untested) { + subsetTestByKey(this.name, test, function() { + assert_false(this.name in self, this.name + " namespace should not exist"); + }.bind(this), this.name + " namespace: existence and properties of namespace object"); + } + return; + } + if (!this.untested) { this.test_self(); } @@ -3497,7 +3514,7 @@ function idl_test(srcs, deps, idl_setup_func) { "require-exposed" ]; return Promise.all( - srcs.concat(deps).map(fetch_spec)) + srcs.concat(deps).map(globalThis.fetch_spec)) .then(function(results) { const astArray = results.map(result => WebIDL2.parse(result.idl, { sourceName: result.spec }) @@ -3538,9 +3555,11 @@ function idl_test(srcs, deps, idl_setup_func) { }); }, 'idl_test setup'); } +globalThis.idl_test = idl_test; /** * fetch_spec is a shorthand for a Promise that fetches the spec's content. + * Note: ShadowRealm-specific implementation in testharness-shadowrealm-inner.js */ function fetch_spec(spec) { var url = '/interfaces/' + spec + '.idl'; diff --git a/test/fixtures/wpt/resources/out-of-scope-test.js b/test/fixtures/wpt/resources/out-of-scope-test.js new file mode 100644 index 00000000000000..ce24124646dc83 --- /dev/null +++ b/test/fixtures/wpt/resources/out-of-scope-test.js @@ -0,0 +1,5 @@ +// Testing that the resolution is correct using `resolve`, as you can't import +// the same module twice. +window.outscope_test_result = import.meta.resolve("a"); +window.outscope_test_result2 = import.meta.resolve("../resources/log.sub.js?name=E"); + diff --git a/test/fixtures/wpt/resources/testdriver-actions.js b/test/fixtures/wpt/resources/testdriver-actions.js index e550ff0b1dc903..edb4759954d4c3 100644 --- a/test/fixtures/wpt/resources/testdriver-actions.js +++ b/test/fixtures/wpt/resources/testdriver-actions.js @@ -32,7 +32,7 @@ * await actions.send(); * * @param {number} [defaultTickDuration] - The default duration of a - * tick. Be default this is set to 16ms, which is one frame time + * tick. Be default this is set ot 16ms, which is one frame time * based on 60Hz display. */ function Actions(defaultTickDuration=16) { @@ -290,7 +290,7 @@ }, /** - * Create a keyDown event for the current default key source + * Create a keyUp event for the current default key source * * @param {String} key - Key to release * @param {String?} sourceName - Named key source to use or null for the default key source diff --git a/test/fixtures/wpt/resources/testdriver-actions.js.headers b/test/fixtures/wpt/resources/testdriver-actions.js.headers new file mode 100644 index 00000000000000..5e8f640c6659d1 --- /dev/null +++ b/test/fixtures/wpt/resources/testdriver-actions.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/test/fixtures/wpt/resources/testdriver.js b/test/fixtures/wpt/resources/testdriver.js index ddf723cb3ee8a5..5b390dedeb72bb 100644 --- a/test/fixtures/wpt/resources/testdriver.js +++ b/test/fixtures/wpt/resources/testdriver.js @@ -3,6 +3,26 @@ var idCounter = 0; let testharness_context = null; + const features = (() => { + function getFeatures(scriptSrc) { + try { + const url = new URL(scriptSrc); + return url.searchParams.getAll('feature'); + } catch (e) { + return []; + } + } + + return getFeatures(document?.currentScript?.src ?? ''); + })(); + + function assertBidiIsEnabled(){ + if (!features.includes('bidi')) { + throw new Error( + "`?feature=bidi` is missing when importing testdriver.js but the test is using WebDriver BiDi APIs"); + } + } + function getInViewCenterPoint(rect) { var left = Math.max(0, rect.left); var right = Math.min(window.innerWidth, rect.right); @@ -49,6 +69,924 @@ * @namespace {test_driver} */ window.test_driver = { + /** + Represents `WebDriver BiDi `_ protocol. + */ + bidi: { + /** + * @typedef {(String|WindowProxy)} Context A browsing context. Can + * be specified by its ID (a string) or using a `WindowProxy` + * object. + */ + /** + * `bluetooth `_ module. + */ + bluetooth: { + /** + * Handle a bluetooth device prompt with the given params. Matches the + * `bluetooth.handleRequestDevicePrompt + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.handleRequestDevicePrompt({ + * prompt: "pmt-e0a234b", + * accept: true, + * device: "dvc-9b3b872" + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.prompt - The id of a bluetooth device prompt. + * Matches the + * `bluetooth.HandleRequestDevicePromptParameters:prompt `_ + * value. + * @param {bool} params.accept - Whether to accept a bluetooth device prompt. + * Matches the + * `bluetooth.HandleRequestDevicePromptAcceptParameters:accept `_ + * value. + * @param {string} params.device - The device id from a bluetooth device + * prompt to be accepted. Matches the + * `bluetooth.HandleRequestDevicePromptAcceptParameters:device `_ + * value. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the bluetooth device prompt should be handled. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the bluetooth device prompt + * is handled, or rejected if the operation fails. + */ + handle_request_device_prompt: function(params) { + return window.test_driver_internal.bidi.bluetooth + .handle_request_device_prompt(params); + }, + /** + * Creates a simulated bluetooth adapter with the given params. Matches the + * `bluetooth.simulateAdapter `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_adapter({ + * state: "powered-on" + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.state The state of the simulated bluetooth adapter. + * Matches the + * `bluetooth.SimulateAdapterParameters:state `_ + * value. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the simulated bluetooth adapter should be set. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the simulated bluetooth adapter is created + * and set, or rejected if the operation fails. + */ + simulate_adapter: function (params) { + return window.test_driver_internal.bidi.bluetooth.simulate_adapter(params); + }, + /** + * Disables the bluetooth simulation with the given params. Matches the + * `bluetooth.disableSimulation `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.disable_simulation(); + * + * @param {object} params - Parameters for the command. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context to disable the simulation for. If not provided, the + * current browsing context is used. + * @returns {Promise} fulfilled after the simulation is disabled, or rejected if + * the operation fails. + */ + disable_simulation: function (params) { + return window.test_driver_internal.bidi.bluetooth.disable_simulation(params); + }, + /** + * Creates a simulated bluetooth peripheral with the given params. + * Matches the + * `bluetooth.simulatePreconnectedPeripheral `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulatePreconnectedPeripheral({ + * "address": "09:09:09:09:09:09", + * "name": "Some Device", + * "manufacturerData": [{key: 17, data: "AP8BAX8="}], + * "knownServiceUuids": [ + * "12345678-1234-5678-9abc-def123456789", + * ], + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated + * bluetooth peripheral. Matches the + * `bluetooth.SimulatePreconnectedPeripheralParameters:address `_ + * value. + * @param {string} params.name - The name of the simulated bluetooth + * peripheral. Matches the + * `bluetooth.SimulatePreconnectedPeripheralParameters:name `_ + * value. + * @param {Array.ManufacturerData} params.manufacturerData - The manufacturerData of the + * simulated bluetooth peripheral. Matches the + * `bluetooth.SimulatePreconnectedPeripheralParameters:manufacturerData `_ + * value. + * @param {string} params.knownServiceUuids - The knownServiceUuids of + * the simulated bluetooth peripheral. Matches the + * `bluetooth.SimulatePreconnectedPeripheralParameters:knownServiceUuids `_ + * value. + * @param {Context} [params.context] The optional context parameter + * specifies in which browsing context the simulated bluetooth peripheral should be + * set. If not provided, the current browsing context is used. + * @returns {Promise} fulfilled after the simulated bluetooth peripheral is created + * and set, or rejected if the operation fails. + */ + simulate_preconnected_peripheral: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_preconnected_peripheral(params); + }, + /** + * Simulates a GATT connection response for a given peripheral. + * Matches the `bluetooth.simulateGattConnectionResponse + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_gatt_connection_response({ + * "address": "09:09:09:09:09:09", + * "code": 0x0 + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated + * bluetooth peripheral. Matches the + * `bluetooth.SimulateGattConnectionResponseParameters:peripheral `_ + * value. + * @param {number} params.code - The response code for a GATT connection attempted. + * Matches the + * `bluetooth.SimulateGattConnectionResponseParameters:code `_ + * value. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the GATT connection response should be simulated. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the GATT connection response + * is simulated, or rejected if the operation fails. + */ + simulate_gatt_connection_response: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_gatt_connection_response(params); + }, + /** + * Simulates a GATT disconnection for a given peripheral. + * Matches the `bluetooth.simulateGattDisconnection + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_gatt_disconnection({ + * "address": "09:09:09:09:09:09", + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated + * bluetooth peripheral. Matches the + * `bluetooth.SimulateGattDisconnectionParameters:address `_ + * value. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the GATT disconnection should be simulated. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the GATT disconnection + * is simulated, or rejected if the operation fails. + */ + simulate_gatt_disconnection: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_gatt_disconnection(params); + }, + /** + * Simulates a GATT service. + * Matches the `bluetooth.simulateService + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_service({ + * "address": "09:09:09:09:09:09", + * "uuid": "0000180d-0000-1000-8000-00805f9b34fb", + * "type": "add" + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated bluetooth peripheral this service belongs to. + * Matches the + * `bluetooth.SimulateServiceParameters:address `_ + * value. + * @param {string} params.uuid - The uuid of the simulated GATT service. + * Matches the + * `bluetooth.SimulateServiceParameters:address `_ + * value. + * @param {string} params.type - The type of the GATT service simulation, either "add" or "remove". + * Matches the + * `bluetooth.SimulateServiceParameters:address `_ + * value. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the GATT service should be simulated. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the GATT service + * is simulated, or rejected if the operation fails. + */ + simulate_service: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_service(params); + }, + /** + * Simulates a GATT characteristic. + * Matches the `bluetooth.simulateCharacteristic + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_characteristic({ + * "address": "09:09:09:09:09:09", + * "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb", + * "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb", + * "characteristicProperties": { + * "read": true, + * "write": true, + * "notify": true + * }, + * "type": "add" + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated bluetooth peripheral the characterisitc belongs to. + * Matches the + * `bluetooth.SimulateCharacteristicParameters:address `_ + * value. + * @param {string} params.serviceUuid - The uuid of the simulated GATT service the characterisitc belongs to. + * Matches the + * `bluetooth.SimulateCharacteristicParameters:address `_ + * value. + * @param {string} params.characteristicUuid - The uuid of the simulated GATT characteristic. + * Matches the + * `bluetooth.SimulateCharacteristicParameters:address `_ + * value. + * @param {string} params.characteristicProperties - The properties of the simulated GATT characteristic. + * Matches the + * `bluetooth.SimulateCharacteristicParameters:address `_ + * value. + * @param {string} params.type - The type of the GATT characterisitc simulation, either "add" or "remove". + * Matches the + * `bluetooth.SimulateCharacteristicParameters:address `_ + * value. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the GATT characteristic should be simulated. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the GATT characteristic + * is simulated, or rejected if the operation fails. + */ + simulate_characteristic: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_characteristic(params); + }, + /** + * Simulates a GATT characteristic response. + * Matches the `bluetooth.simulateCharacteristicResponse + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_characteristic({ + * "address": "09:09:09:09:09:09", + * "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb", + * "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb", + * "type": "read", + * "code": 0, + * "data": [1, 2] + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated + * bluetooth peripheral. Matches the + * `bluetooth.SimulateCharacteristicResponseParameters:address `_ + * value. + * @param {string} params.serviceUuid - The uuid of the simulated GATT service the characterisitc belongs to. + * Matches the + * `bluetooth.SimulateCharacteristicResponseParameters:address `_ + * value. + * @param {string} params.characteristicUuid - The uuid of the simulated characteristic. + * Matches the + * `bluetooth.SimulateCharacteristicResponseParameters:address `_ + * value. + * @param {string} params.type - The type of the simulated GATT characteristic operation." + * Can be "read", "write", "subscribe-to-notifications" or "unsubscribe-from-notifications". + * Matches the + * `bluetooth.SimulateCharacteristicResponseParameters:address `_ + * value. + * @param {string} params.code - The simulated GATT characteristic response code. + * Matches the + * `bluetooth.SimulateCharacteristicResponseParameters:address `_ + * value.* + * @param {string} params.data - The data along with the simulated GATT characteristic response. + * Matches the + * `bluetooth.SimulateCharacteristicResponseParameters:address `_ + * value.** + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the GATT characteristic belongs to. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the GATT characteristic + * is simulated, or rejected if the operation fails. + */ + simulate_characteristic_response: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_characteristic_response(params); + }, + /** + * Simulates a GATT descriptor. + * Matches the `bluetooth.simulateDescriptor + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_descriptor({ + * "address": "09:09:09:09:09:09", + * "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb", + * "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb", + * "descriptorUuid": "00002901-0000-1000-8000-00805f9b34fb", + * "type": "add" + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated bluetooth peripheral the descriptor belongs to. + * Matches the + * `bluetooth.SimulateDescriptorParameters:address `_ + * value. + * @param {string} params.serviceUuid - The uuid of the simulated GATT service the descriptor belongs to. + * Matches the + * `bluetooth.SimulateDescriptorParameters:address `_ + * value. + * @param {string} params.characteristicUuid - The uuid of the simulated GATT characterisitc the descriptor belongs to. + * Matches the + * `bluetooth.SimulateDescriptorParameters:address `_ + * value. + * @param {string} params.descriptorUuid - The uuid of the simulated GATT descriptor. + * Matches the + * `bluetooth.SimulateDescriptorParameters:address `_ + * value.* + * @param {string} params.type - The type of the GATT descriptor simulation, either "add" or "remove". + * Matches the + * `bluetooth.SimulateDescriptorParameters:address `_ + * value. + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the GATT descriptor should be simulated. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the GATT descriptor + * is simulated, or rejected if the operation fails. + */ + simulate_descriptor: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_descriptor(params); + }, + /** + * Simulates a GATT descriptor response. + * Matches the `bluetooth.simulateDescriptorResponse + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.bluetooth.simulate_descriptor_response({ + * "address": "09:09:09:09:09:09", + * "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb", + * "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb", + * "descriptorUuid": "00002901-0000-1000-8000-00805f9b34fb", + * "type": "read", + * "code": 0, + * "data": [1, 2] + * }); + * + * @param {object} params - Parameters for the command. + * @param {string} params.address - The address of the simulated bluetooth peripheral the descriptor belongs to. + * Matches the + * `bluetooth.SimulateDescriptorResponseParameters:address `_ + * value. + * @param {string} params.serviceUuid - The uuid of the simulated GATT service the descriptor belongs to. + * Matches the + * `bluetooth.SimulateDescriptorResponseParameters:address `_ + * value. + * @param {string} params.characteristicUuid - The uuid of the simulated GATT characterisitc the descriptor belongs to. + * Matches the + * `bluetooth.SimulateDescriptorResponseParameters:address `_ + * value. + * @param {string} params.descriptorUuid - The uuid of the simulated GATT descriptor. + * Matches the + * `bluetooth.SimulateDescriptorResponseParameters:address `_ + * value. + * @param {string} params.type - The type of the simulated GATT descriptor operation. + * Matches the + * `bluetooth.SimulateDescriptorResponseParameters:address `_ + * value. + * @param {string} params.code - The simulated GATT descriptor response code. + * Matches the + * `bluetooth.SimulateDescriptorResponseParameters:address `_ + * value.* + * @param {string} params.data - The data along with the simulated GATT descriptor response. + * Matches the + * `bluetooth.SimulateDescriptorResponseParameters:address `_ + * value.** + * @param {Context} [params.context] The optional context parameter specifies in + * which browsing context the GATT descriptor belongs to. If not + * provided, the current browsing context is used. + * @returns {Promise} fulfilled after the GATT descriptor response + * is simulated, or rejected if the operation fails. + */ + simulate_descriptor_response: function(params) { + return window.test_driver_internal.bidi.bluetooth + .simulate_descriptor_response(params); + }, + /** + * `bluetooth.RequestDevicePromptUpdatedParameters `_ + * event. + */ + request_device_prompt_updated: { + /** + * @typedef {object} RequestDevicePromptUpdated + * `bluetooth.RequestDevicePromptUpdatedParameters `_ + * event. + */ + + /** + * Subscribes to the event. Events will be emitted only if + * there is a subscription for the event. This method does + * not add actual listeners. To listen to the event, use the + * `on` or `once` methods. The buffered events will be + * emitted before the command promise is resolved. + * + * @param {object} [params] Parameters for the subscription. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing + * contexts to subscribe to the event on. It should be + * either an array of Context objects, or null. If null, the + * event will be subscribed to globally. If omitted, the + * event will be subscribed to on the current browsing + * context. + * @returns {Promise<(function(): Promise)>} Callback + * for unsubscribing from the created subscription. + */ + subscribe: async function(params = {}) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .request_device_prompt_updated.subscribe(params); + }, + /** + * Adds an event listener for the event. + * + * @param {function(RequestDevicePromptUpdated): void} callback The + * callback to be called when the event is emitted. The + * callback is called with the event object as a parameter. + * @returns {function(): void} A function that removes the + * added event listener when called. + */ + on: function(callback) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .request_device_prompt_updated.on(callback); + }, + /** + * Adds an event listener for the event that is only called + * once and removed afterward. + * + * @return {Promise} The promise which + * is resolved with the event object when the event is emitted. + */ + once: function() { + assertBidiIsEnabled(); + return new Promise(resolve => { + const remove_handler = + window.test_driver_internal.bidi.bluetooth + .request_device_prompt_updated.on(event => { + resolve(event); + remove_handler(); + }); + }); + }, + }, + /** + * `bluetooth.GattConnectionAttemptedParameters `_ + * event. + */ + gatt_connection_attempted: { + /** + * @typedef {object} GattConnectionAttempted + * `bluetooth.GattConnectionAttempted `_ + * event. + */ + + /** + * Subscribes to the event. Events will be emitted only if + * there is a subscription for the event. This method does + * not add actual listeners. To listen to the event, use the + * `on` or `once` methods. The buffered events will be + * emitted before the command promise is resolved. + * + * @param {object} [params] Parameters for the subscription. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing + * contexts to subscribe to the event on. It should be + * either an array of Context objects, or null. If null, the + * event will be subscribed to globally. If omitted, the + * event will be subscribed to on the current browsing + * context. + * @returns {Promise<(function(): Promise)>} Callback + * for unsubscribing from the created subscription. + */ + subscribe: async function(params = {}) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .gatt_connection_attempted.subscribe(params); + }, + /** + * Adds an event listener for the event. + * + * @param {function(GattConnectionAttempted): void} callback The + * callback to be called when the event is emitted. The + * callback is called with the event object as a parameter. + * @returns {function(): void} A function that removes the + * added event listener when called. + */ + on: function(callback) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .gatt_connection_attempted.on(callback); + }, + /** + * Adds an event listener for the event that is only called + * once and removed afterward. + * + * @return {Promise} The promise which + * is resolved with the event object when the event is emitted. + */ + once: function() { + assertBidiIsEnabled(); + return new Promise(resolve => { + const remove_handler = + window.test_driver_internal.bidi.bluetooth + .gatt_connection_attempted.on(event => { + resolve(event); + remove_handler(); + }); + }); + }, + }, + /** + * `bluetooth.CharacteristicEventGeneratedParameters `_ + * event. + */ + characteristic_event_generated: { + /** + * @typedef {object} CharacteristicEventGenerated + * `bluetooth.CharacteristicEventGenerated `_ + * event. + */ + + /** + * Subscribes to the event. Events will be emitted only if + * there is a subscription for the event. This method does + * not add actual listeners. To listen to the event, use the + * `on` or `once` methods. The buffered events will be + * emitted before the command promise is resolved. + * + * @param {object} [params] Parameters for the subscription. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing + * contexts to subscribe to the event on. It should be + * either an array of Context objects, or null. If null, the + * event will be subscribed to globally. If omitted, the + * event will be subscribed to on the current browsing + * context. + * @returns {Promise<(function(): Promise)>} Callback + * for unsubscribing from the created subscription. + */ + subscribe: async function(params = {}) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .characteristic_event_generated.subscribe(params); + }, + /** + * Adds an event listener for the event. + * + * @param {function(CharacteristicEventGenerated): void} callback The + * callback to be called when the event is emitted. The + * callback is called with the event object as a parameter. + * @returns {function(): void} A function that removes the + * added event listener when called. + */ + on: function(callback) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .characteristic_event_generated.on(callback); + }, + /** + * Adds an event listener for the event that is only called + * once and removed afterward. + * + * @return {Promise} The promise which + * is resolved with the event object when the event is emitted. + */ + once: function() { + assertBidiIsEnabled(); + return new Promise(resolve => { + const remove_handler = + window.test_driver_internal.bidi.bluetooth + .characteristic_event_generated.on(event => { + resolve(event); + remove_handler(); + }); + }); + }, + }, + /** + * `bluetooth.DescriptorEventGeneratedParameters `_ + * event. + */ + descriptor_event_generated: { + /** + * @typedef {object} DescriptorEventGenerated + * `bluetooth.DescriptorEventGenerated `_ + * event. + */ + + /** + * Subscribes to the event. Events will be emitted only if + * there is a subscription for the event. This method does + * not add actual listeners. To listen to the event, use the + * `on` or `once` methods. The buffered events will be + * emitted before the command promise is resolved. + * + * @param {object} [params] Parameters for the subscription. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing + * contexts to subscribe to the event on. It should be + * either an array of Context objects, or null. If null, the + * event will be subscribed to globally. If omitted, the + * event will be subscribed to on the current browsing + * context. + * @returns {Promise<(function(): Promise)>} Callback + * for unsubscribing from the created subscription. + */ + subscribe: async function(params = {}) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .descriptor_event_generated.subscribe(params); + }, + /** + * Adds an event listener for the event. + * + * @param {function(DescriptorEventGenerated): void} callback The + * callback to be called when the event is emitted. The + * callback is called with the event object as a parameter. + * @returns {function(): void} A function that removes the + * added event listener when called. + */ + on: function(callback) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.bluetooth + .descriptor_event_generated.on(callback); + }, + /** + * Adds an event listener for the event that is only called + * once and removed afterward. + * + * @return {Promise} The promise which + * is resolved with the event object when the event is emitted. + */ + once: function() { + assertBidiIsEnabled(); + return new Promise(resolve => { + const remove_handler = + window.test_driver_internal.bidi.bluetooth + .descriptor_event_generated.on(event => { + resolve(event); + remove_handler(); + }); + }); + }, + } + }, + /** + * `emulation `_ module. + */ + emulation: { + /** + * Overrides the geolocation coordinates for the specified + * browsing contexts. + * Matches the `emulation.setGeolocationOverride + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.emulation.set_geolocation_override({ + * coordinates: { + * latitude: 52.51, + * longitude: 13.39, + * accuracy: 0.5, + * altitude: 34, + * altitudeAccuracy: 0.75, + * heading: 180, + * speed: 2.77 + * } + * }); + * + * @param {object} params - Parameters for the command. + * @param {null|object} params.coordinates - The optional + * geolocation coordinates to set. Matches the + * `emulation.GeolocationCoordinates `_ + * value. If null or omitted and the `params.error` is set, the + * emulation will be removed. Mutually exclusive with + * `params.error`. + * @param {object} params.error - The optional + * geolocation error to emulate. Matches the + * `emulation.GeolocationPositionError `_ + * value. Mutually exclusive with `params.coordinates`. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing contexts + * to set the geolocation override on. It should be either an + * array of Context objects (window or browsing context id), or + * null. If null or omitted, the override will be set on the + * current browsing context. + * @returns {Promise} Resolves when the geolocation + * override is successfully set. + */ + set_geolocation_override: function (params) { + // Ensure the bidi feature is enabled before calling the internal method + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.emulation.set_geolocation_override( + params); + }, + /** + * Overrides the locale for the specified browsing contexts. + * Matches the `emulation.setLocaleOverride + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.emulation.set_locale_override({ + * locale: 'de-DE' + * }); + * + * @param {object} params - Parameters for the command. + * @param {null|string} params.locale - The optional + * locale to set. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing contexts + * to set the locale override on. It should be either an array + * of Context objects (window or browsing context id), or null. + * If null or omitted, the override will be set on the current + * browsing context. + * @returns {Promise} Resolves when the locale override + * is successfully set. + */ + set_locale_override: function (params) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.emulation.set_locale_override( + params); + }, + /** + * Overrides the screen orientation for the specified browsing + * contexts. + * Matches the `emulation.setScreenOrientationOverride + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.emulation.set_screen_orientation_override({ + * screenOrientation: { + * natural: 'portrait', + * type: 'landscape-secondary' + * } + * }); + * + * @param {object} params - Parameters for the command. + * @param {null|object} params.screenOrientation - The optional + * screen orientation. Matches the + * `emulation.ScreenOrientation `_ + * type. If null or omitted, the override will be removed. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing contexts + * to set the screen orientation override on. It should be + * either an array of Context objects (window or browsing + * context id), or null. If null or omitted, the override will + * be set on the current browsing context. + * @returns {Promise} Resolves when the screen orientation + * override is successfully set. + */ + set_screen_orientation_override: function (params) { + // Ensure the bidi feature is enabled before calling the internal method + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.emulation.set_screen_orientation_override( + params); + }, + }, + /** + * `log `_ module. + */ + log: { + entry_added: { + /** + * @typedef {object} LogEntryAdded `log.entryAdded `_ event. + */ + + /** + * Subscribes to the event. Events will be emitted only if + * there is a subscription for the event. This method does + * not add actual listeners. To listen to the event, use the + * `on` or `once` methods. The buffered events will be + * emitted before the command promise is resolved. + * + * @param {object} [params] Parameters for the subscription. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing + * contexts to subscribe to the event on. It should be + * either an array of Context objects, or null. If null, the + * event will be subscribed to globally. If omitted, the + * event will be subscribed to on the current browsing + * context. + * @returns {Promise<(function(): Promise)>} Callback + * for unsubscribing from the created subscription. + */ + subscribe: async function (params = {}) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.log.entry_added.subscribe(params); + }, + /** + * Adds an event listener for the event. + * + * @param {function(LogEntryAdded): void} callback The + * callback to be called when the event is emitted. The + * callback is called with the event object as a parameter. + * @returns {function(): void} A function that removes the + * added event listener when called. + */ + on: function (callback) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.log.entry_added.on(callback); + }, + /** + * Adds an event listener for the event that is only called + * once and removed afterward. + * + * @return {Promise} The promise which is resolved + * with the event object when the event is emitted. + */ + once: function () { + assertBidiIsEnabled(); + return new Promise(resolve => { + const remove_handler = window.test_driver_internal.bidi.log.entry_added.on( + event => { + resolve(event); + remove_handler(); + }); + }); + }, + } + }, + /** + * `permissions `_ module. + */ + permissions: { + /** + * Sets the state of a permission + * + * This function causes permission requests and queries for the status + * of a certain permission type (e.g. "push", or "background-fetch") to + * always return ``state`` for the specific origin. + * + * Matches the `permissions.setPermission `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.permissions.set_permission({ + * {name: "geolocation"}, + * state: "granted", + * }); + * + * @param {object} params - Parameters for the command. + * @param {PermissionDescriptor} params.descriptor - a `PermissionDescriptor + * `_ + * or derived object. + * @param {PermissionState} params.state - a `PermissionState + * `_ + * value. + * @param {string} [params.origin] - an optional `origin` string to set the + * permission for. If omitted, the permission is set for the + * current window's origin. + * @returns {Promise} fulfilled after the permission is set, or rejected if setting + * the permission fails. + */ + set_permission: function (params) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.permissions.set_permission( + params); + } + } + }, + /** * Set the context in which testharness.js is loaded * @@ -112,8 +1050,8 @@ let wait_click = new Promise(resolve => button.addEventListener("click", resolve)); return test_driver.click(button) - .then(wait_click) - .then(function() { + .then(() => wait_click) + .then(() => { button.remove(); if (typeof action === "function") { @@ -124,11 +1062,10 @@ }, /** - * Triggers a user-initiated click + * Triggers a user-initiated mouse click. * - * If ``element`` isn't inside the - * viewport, it will be scrolled into view before the click - * occurs. + * If ``element`` isn't inside the viewport, it will be + * scrolled into view before the click occurs. * * If ``element`` is from a different browsing context, the * command will be run in that context. @@ -322,7 +1259,7 @@ /** * Minimizes the browser window. * - * Matches the the behaviour of the `Minimize + * Matches the behaviour of the `Minimize * `_ * WebDriver command * @@ -357,6 +1294,25 @@ return window.test_driver_internal.set_window_rect(rect, context); }, + /** + * Gets a rect with the size and position on the screen from the current window state. + * + * Matches the behaviour of the `Get Window Rect + * `_ + * WebDriver command + * + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} fulfilled after the window rect is returned, or rejected + * in cases the WebDriver command returns errors. Returns a + * `WindowRect `_ + */ + get_window_rect: function(context=null) { + return window.test_driver_internal.get_window_rect(context); + }, + /** * Send a sequence of actions * @@ -647,7 +1603,7 @@ * * This function places `Secure Payment * Confirmation `_ into - * an automated 'autoaccept' or 'autoreject' mode, to allow testing + * an automated 'autoAccept' or 'autoReject' mode, to allow testing * without user interaction with the transaction UX prompt. * * Matches the `Set SPC Transaction Mode @@ -667,8 +1623,8 @@ * @param {String} mode - The `transaction mode * `_ * to set. Must be one of "``none``", - * "``autoaccept``", or - * "``autoreject``". + * "``autoAccept``", or + * "``autoReject``". * @param {WindowProxy} context - Browsing context in which * to run the call, or null for the current * browsing context. @@ -680,6 +1636,42 @@ return window.test_driver_internal.set_spc_transaction_mode(mode, context); }, + /** + * Sets the current registration automation mode for Register Protocol Handlers. + * + * This function places `Register Protocol Handlers + * `_ into + * an automated 'autoAccept' or 'autoReject' mode, to allow testing + * without user interaction with the transaction UX prompt. + * + * Matches the `Set Register Protocol Handler Mode + * `_ + * WebDriver command. + * + * @example + * await test_driver.set_rph_registration_mode("autoAccept"); + * test.add_cleanup(() => { + * return test_driver.set_rph_registration_mode("none"); + * }); + * + * navigator.registerProtocolHandler('web+soup', 'soup?url=%s'); + * + * @param {String} mode - The `registration mode + * `_ + * to set. Must be one of "``none``", + * "``autoAccept``", or + * "``autoReject``". + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} Fulfilled after the transaction mode has been set, + * or rejected if setting the mode fails. + */ + set_rph_registration_mode: function(mode, context=null) { + return window.test_driver_internal.set_rph_registration_mode(mode, context); + }, + /** * Cancels the Federated Credential Management dialog * @@ -968,6 +1960,215 @@ */ get_virtual_sensor_information: function(sensor_type, context=null) { return window.test_driver_internal.get_virtual_sensor_information(sensor_type, context); + }, + + /** + * Overrides device posture set by hardware. + * + * Matches the `Set device posture + * `_ + * WebDriver command. + * + * @param {String} posture - A `DevicePostureType + * `_ + * either "continuous" or "folded". + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled when device posture is set. + * Rejected in case the WebDriver command errors out + * (including if a device posture of the given type + * does not exist). + */ + set_device_posture: function(posture, context=null) { + return window.test_driver_internal.set_device_posture(posture, context); + }, + + /** + * Removes device posture override and returns device posture control + * back to hardware. + * + * Matches the `Clear device posture + * `_ + * WebDriver command. + * + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled after the device posture override has + * been removed. Rejected in case the WebDriver + * command errors out. + */ + clear_device_posture: function(context=null) { + return window.test_driver_internal.clear_device_posture(context); + }, + + /** + * Runs the `bounce tracking timer algorithm + * `_, + * which removes all hosts from the stateful bounce tracking map, without + * regard for the bounce tracking grace period and returns a list of the + * deleted hosts. + * + * Matches the `Run Bounce Tracking Mitigations + * `_ + * WebDriver command. + * + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * @returns {Promise} Fulfilled after the bounce tracking timer + * algorithm has finished running. Returns an array + * of all hosts that were in the stateful bounce + * tracking map before deletion occurred. + */ + run_bounce_tracking_mitigations: function (context = null) { + return window.test_driver_internal.run_bounce_tracking_mitigations(context); + }, + + /** + * Creates a virtual pressure source. + * + * Matches the `Create virtual pressure source + * `_ + * WebDriver command. + * + * @param {String} source_type - A `virtual pressure source type + * `_ + * such as "cpu". + * @param {Object} [metadata={}] - Optional parameters described + * in `Create virtual pressure source + * `_. + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled when virtual pressure source is created. + * Rejected in case the WebDriver command errors out + * (including if a virtual pressure source of the + * same type already exists). + */ + create_virtual_pressure_source: function(source_type, metadata={}, context=null) { + return window.test_driver_internal.create_virtual_pressure_source(source_type, metadata, context); + }, + + /** + * Causes a virtual pressure source to report a new reading. + * + * Matches the `Update virtual pressure source + * `_ + * WebDriver command. + * + * @param {String} source_type - A `virtual pressure source type + * `_ + * such as "cpu". + * @param {String} sample - A `virtual pressure state + * `_ + * such as "critical". + * @param {number} own_contribution_estimate - Optional, A `virtual own contribution estimate` + * `_ + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled after the reading update reaches the + * virtual pressure source. Rejected in case the + * WebDriver command errors out (including if a + * virtual pressure source of the given type does not + * exist). + */ + update_virtual_pressure_source: function(source_type, sample, own_contribution_estimate, context=null) { + return window.test_driver_internal.update_virtual_pressure_source(source_type, sample, own_contribution_estimate, context); + }, + + /** + * Removes created virtual pressure source. + * + * Matches the `Delete virtual pressure source + * `_ + * WebDriver command. + * + * @param {String} source_type - A `virtual pressure source type + * `_ + * such as "cpu". + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled after the virtual pressure source has + * been removed or if a pressure source of the given + * type does not exist. Rejected in case the + * WebDriver command errors out. + */ + remove_virtual_pressure_source: function(source_type, context=null) { + return window.test_driver_internal.remove_virtual_pressure_source(source_type, context); + }, + + /** + * Sets which hashes are considered k-anonymous for the Protected + * Audience interest group with specified `owner` and `name`. + * + * Matches the `Set Protected Audience K-Anonymity + * + * WebDriver command. + * + * @param {String} owner - Origin of the owner of the interest group + * to modify + * @param {String} name - Name of the interest group to modify + * @param {Array} hashes - An array of strings, each of which is a + * base64 ecoded hash to consider k-anonymous. + * + * @returns {Promise} Fulfilled after the k-anonymity status for the + * specified Protected Audience interest group has + * been updated. + * + */ + set_protected_audience_k_anonymity: function(owner, name, hashes, context = null) { + return window.test_driver_internal.set_protected_audience_k_anonymity(owner, name, hashes, context); + }, + + /** + * Overrides the display features provided by the hardware so the viewport segments + * can be emulated. + * + * Matches the `Set display features + * `_ + * WebDriver command. + * + * @param {Array} features - An array of `DisplayFeatureOverride + * `. + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled when the display features are set. + * Rejected in case the WebDriver command errors out + * (including if the array is malformed). + */ + set_display_features: function(features, context=null) { + return window.test_driver_internal.set_display_features(features, context); + }, + + /** + * Removes display features override and returns the control + * back to hardware. + * + * Matches the `Clear display features + * `_ + * WebDriver command. + * + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled after the display features override has + * been removed. Rejected in case the WebDriver + * command errors out. + */ + clear_display_features: function(context=null) { + return window.test_driver_internal.clear_display_features(context); } }; @@ -980,6 +2181,99 @@ */ in_automation: false, + bidi: { + bluetooth: { + handle_request_device_prompt: function() { + throw new Error( + 'bidi.bluetooth.handle_request_device_prompt is not implemented by testdriver-vendor.js'); + }, + simulate_adapter: function () { + throw new Error( + "bidi.bluetooth.simulate_adapter is not implemented by testdriver-vendor.js"); + }, + disable_simulation: function () { + throw new Error( + "bidi.bluetooth.disable_simulation is not implemented by testdriver-vendor.js"); + }, + simulate_preconnected_peripheral: function() { + throw new Error( + 'bidi.bluetooth.simulate_preconnected_peripheral is not implemented by testdriver-vendor.js'); + }, + request_device_prompt_updated: { + async subscribe() { + throw new Error( + 'bidi.bluetooth.request_device_prompt_updated.subscribe is not implemented by testdriver-vendor.js'); + }, + on() { + throw new Error( + 'bidi.bluetooth.request_device_prompt_updated.on is not implemented by testdriver-vendor.js'); + } + }, + gatt_connection_attempted: { + async subscribe() { + throw new Error( + 'bidi.bluetooth.gatt_connection_attempted.subscribe is not implemented by testdriver-vendor.js'); + }, + on() { + throw new Error( + 'bidi.bluetooth.gatt_connection_attempted.on is not implemented by testdriver-vendor.js'); + } + }, + characteristic_event_generated: { + async subscribe() { + throw new Error( + 'bidi.bluetooth.characteristic_event_generated.subscribe is not implemented by testdriver-vendor.js'); + }, + on() { + throw new Error( + 'bidi.bluetooth.characteristic_event_generated.on is not implemented by testdriver-vendor.js'); + } + }, + descriptor_event_generated: { + async subscribe() { + throw new Error( + 'bidi.bluetooth.descriptor_event_generated.subscribe is not implemented by testdriver-vendor.js'); + }, + on() { + throw new Error( + 'bidi.bluetooth.descriptor_event_generated.on is not implemented by testdriver-vendor.js'); + } + } + }, + emulation: { + set_geolocation_override: function (params) { + throw new Error( + "bidi.emulation.set_geolocation_override is not implemented by testdriver-vendor.js"); + }, + set_locale_override: function (params) { + throw new Error( + "bidi.emulation.set_locale_override is not implemented by testdriver-vendor.js"); + }, + set_screen_orientation_override: function (params) { + throw new Error( + "bidi.emulation.set_screen_orientation_override is not implemented by testdriver-vendor.js"); + } + }, + log: { + entry_added: { + async subscribe() { + throw new Error( + "bidi.log.entry_added.subscribe is not implemented by testdriver-vendor.js"); + }, + on() { + throw new Error( + "bidi.log.entry_added.on is not implemented by testdriver-vendor.js"); + } + } + }, + permissions: { + async set_permission() { + throw new Error( + "bidi.permissions.set_permission() is not implemented by testdriver-vendor.js"); + } + } + }, + async click(element, coords) { if (this.in_automation) { throw new Error("click() is not implemented by testdriver-vendor.js"); @@ -1002,6 +2296,14 @@ throw new Error("get_named_cookie() is not implemented by testdriver-vendor.js"); }, + async get_computed_role(element) { + throw new Error("get_computed_role is a testdriver.js function which cannot be run in this context."); + }, + + async get_computed_name(element) { + throw new Error("get_computed_name is a testdriver.js function which cannot be run in this context."); + }, + async send_keys(element, keys) { if (this.in_automation) { throw new Error("send_keys() is not implemented by testdriver-vendor.js"); @@ -1046,6 +2348,10 @@ throw new Error("set_window_rect() is not implemented by testdriver-vendor.js"); }, + async get_window_rect(context=null) { + throw new Error("get_window_rect() is not implemented by testdriver-vendor.js"); + }, + async action_sequence(actions, context=null) { throw new Error("action_sequence() is not implemented by testdriver-vendor.js"); }, @@ -1094,6 +2400,10 @@ throw new Error("set_spc_transaction_mode() is not implemented by testdriver-vendor.js"); }, + set_rph_registration_mode: function(mode, context=null) { + return Promise.reject(new Error("unimplemented")); + }, + async cancel_fedcm_dialog(context=null) { throw new Error("cancel_fedcm_dialog() is not implemented by testdriver-vendor.js"); }, @@ -1140,6 +2450,42 @@ async get_virtual_sensor_information(sensor_type, context=null) { throw new Error("get_virtual_sensor_information() is not implemented by testdriver-vendor.js"); + }, + + async set_device_posture(posture, context=null) { + throw new Error("set_device_posture() is not implemented by testdriver-vendor.js"); + }, + + async clear_device_posture(context=null) { + throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js"); + }, + + async run_bounce_tracking_mitigations(context=null) { + throw new Error("run_bounce_tracking_mitigations() is not implemented by testdriver-vendor.js"); + }, + + async create_virtual_pressure_source(source_type, metadata={}, context=null) { + throw new Error("create_virtual_pressure_source() is not implemented by testdriver-vendor.js"); + }, + + async update_virtual_pressure_source(source_type, sample, own_contribution_estimate, context=null) { + throw new Error("update_virtual_pressure_source() is not implemented by testdriver-vendor.js"); + }, + + async remove_virtual_pressure_source(source_type, context=null) { + throw new Error("remove_virtual_pressure_source() is not implemented by testdriver-vendor.js"); + }, + + async set_protected_audience_k_anonymity(owner, name, hashes, context=null) { + throw new Error("set_protected_audience_k_anonymity() is not implemented by testdriver-vendor.js"); + }, + + async set_display_features(features, context=null) { + throw new Error("set_display_features() is not implemented by testdriver-vendor.js"); + }, + + async clear_display_features(context=null) { + throw new Error("clear_display_features() is not implemented by testdriver-vendor.js"); } }; })(); diff --git a/test/fixtures/wpt/resources/testharness-shadowrealm-audioworkletprocessor.js b/test/fixtures/wpt/resources/testharness-shadowrealm-audioworkletprocessor.js new file mode 100644 index 00000000000000..a87d9130908f86 --- /dev/null +++ b/test/fixtures/wpt/resources/testharness-shadowrealm-audioworkletprocessor.js @@ -0,0 +1,52 @@ +/** + * AudioWorkletProcessor intended for hosting a ShadowRealm and running a test + * inside of that ShadowRealm. + */ +globalThis.TestRunner = class TestRunner extends AudioWorkletProcessor { + constructor() { + super(); + this.createShadowRealmAndStartTests(); + } + + /** + * Fetch adaptor function intended as a drop-in replacement for fetchAdaptor() + * (see testharness-shadowrealm-outer.js), but it does not assume fetch() is + * present in the realm. Instead, it relies on setupFakeFetchOverMessagePort() + * having been called on the port on the other side of this.port's channel. + */ + fetchOverPortExecutor(resource) { + return (resolve, reject) => { + const listener = (event) => { + if (typeof event.data !== "string" || !event.data.startsWith("fetchResult::")) { + return; + } + + const result = event.data.slice("fetchResult::".length); + if (result.startsWith("success::")) { + resolve(result.slice("success::".length)); + } else { + reject(result.slice("fail::".length)); + } + + this.port.removeEventListener("message", listener); + } + this.port.addEventListener("message", listener); + this.port.start(); + this.port.postMessage(`fetchRequest::${resource}`); + } + } + + /** + * Async method, which is patched over in + * (test).any.audioworklet-shadowrealm.js; see serve.py + */ + async createShadowRealmAndStartTests() { + throw new Error("Forgot to overwrite this method!"); + } + + /** Overrides AudioWorkletProcessor.prototype.process() */ + process() { + return false; + } +}; +registerProcessor("test-runner", TestRunner); diff --git a/test/fixtures/wpt/resources/testharness-shadowrealm-inner.js b/test/fixtures/wpt/resources/testharness-shadowrealm-inner.js new file mode 100644 index 00000000000000..a9bdf9fc76ceea --- /dev/null +++ b/test/fixtures/wpt/resources/testharness-shadowrealm-inner.js @@ -0,0 +1,38 @@ +// testharness file with ShadowRealm utilities to be imported inside ShadowRealm + +/** + * Set up all properties on the ShadowRealm's global object that tests will + * expect to be present. + * + * @param {string} queryString - string to use as value for location.search, + * used for subsetting some tests + * @param {function} fetchAdaptor - a function that takes a resource URI and + * returns a function which itself takes a (resolve, reject) pair from the + * hosting realm, and calls resolve with the text result of fetching the + * resource, or reject with a string indicating the error that occurred + */ +globalThis.setShadowRealmGlobalProperties = function (queryString, fetchAdaptor) { + globalThis.fetch_json = (resource) => { + const executor = fetchAdaptor(resource); + return new Promise(executor).then((s) => JSON.parse(s)); + }; + + // Used only by idlharness.js + globalThis.fetch_spec = (spec) => { + const resource = `/interfaces/${spec}.idl`; + const executor = fetchAdaptor(resource); + return new Promise(executor).then( + idl => ({ spec, idl }), + () => { + throw new IdlHarnessError(`Error fetching ${resource}.`); + }); + } + + globalThis.location = { search: queryString }; +}; + +globalThis.GLOBAL = { + isWindow: function() { return false; }, + isWorker: function() { return false; }, + isShadowRealm: function() { return true; }, +}; diff --git a/test/fixtures/wpt/resources/testharness-shadowrealm-outer.js b/test/fixtures/wpt/resources/testharness-shadowrealm-outer.js new file mode 100644 index 00000000000000..1affa72c2c56a2 --- /dev/null +++ b/test/fixtures/wpt/resources/testharness-shadowrealm-outer.js @@ -0,0 +1,151 @@ +// testharness file with ShadowRealm utilities to be imported in the realm +// hosting the ShadowRealm + +/** + * Convenience function for evaluating some async code in the ShadowRealm and + * waiting for the result. + * + * In case of error, this function intentionally exposes the stack trace (if it + * is available) to the hosting realm, for debugging purposes. + * + * @param {ShadowRealm} realm - the ShadowRealm to evaluate the code in + * @param {string} asyncBody - the code to evaluate; will be put in the body of + * an async function, and must return a value explicitly if a value is to be + * returned to the hosting realm. + */ +globalThis.shadowRealmEvalAsync = function (realm, asyncBody) { + return new Promise(realm.evaluate(` + (resolve, reject) => { + (async () => { + ${asyncBody} + })().then(resolve, (e) => reject(e.toString() + "\\n" + (e.stack || ""))); + } + `)); +}; + +/** + * Convenience adaptor function for fetch() that can be passed to + * setShadowRealmGlobalProperties() (see testharness-shadowrealm-inner.js). + * Used to adapt the hosting realm's fetch(), if present, to fetch a resource + * and pass its text through the callable boundary to the ShadowRealm. + */ +globalThis.fetchAdaptor = (resource) => (resolve, reject) => { + fetch(resource) + .then(res => res.text()) + .then(resolve, (e) => reject(e.toString())); +}; + +let workerMessagePortPromise; +/** + * Used when the hosting realm is a worker. This value is a Promise that + * resolves to a function that posts a message to the worker's message port, + * just like postMessage(). The message port is only available asynchronously in + * SharedWorkers and ServiceWorkers. + */ +globalThis.getPostMessageFunc = async function () { + if (typeof postMessage === "function") { + return postMessage; // postMessage available directly in dedicated worker + } + + if (workerMessagePortPromise) { + return await workerMessagePortPromise; + } + + throw new Error("getPostMessageFunc is intended for Worker scopes"); +} + +// Port available asynchronously in shared worker, but not via an async func +let savedResolver; +if (globalThis.constructor.name === "SharedWorkerGlobalScope") { + workerMessagePortPromise = new Promise((resolve) => { + savedResolver = resolve; + }); + addEventListener("connect", function (event) { + const port = event.ports[0]; + savedResolver(port.postMessage.bind(port)); + }); +} else if (globalThis.constructor.name === "ServiceWorkerGlobalScope") { + workerMessagePortPromise = new Promise((resolve) => { + savedResolver = resolve; + }); + addEventListener("message", (e) => { + if (typeof e.data === "object" && e.data !== null && e.data.type === "connect") { + const client = e.source; + savedResolver(client.postMessage.bind(client)); + } + }); +} + +/** + * Used when the hosting realm does not permit dynamic import, e.g. in + * ServiceWorkers or AudioWorklets. Requires an adaptor function such as + * fetchAdaptor() above, or an equivalent if fetch() is not present in the + * hosting realm. + * + * @param {ShadowRealm} realm - the ShadowRealm in which to setup a + * fakeDynamicImport() global function. + * @param {function} adaptor - an adaptor function that does what fetchAdaptor() + * does. + */ +globalThis.setupFakeDynamicImportInShadowRealm = function(realm, adaptor) { + function fetchModuleTextExecutor(url) { + return (resolve, reject) => { + new Promise(adaptor(url)) + .then(text => realm.evaluate(text + ";\nundefined")) + .then(resolve, (e) => reject(e.toString())); + } + } + + realm.evaluate(` + (fetchModuleTextExecutor) => { + globalThis.fakeDynamicImport = function (url) { + return new Promise(fetchModuleTextExecutor(url)); + } + } + `)(fetchModuleTextExecutor); +}; + +/** + * Used when the hosting realm does not expose fetch(), i.e. in worklets. The + * port on the other side of the channel needs to send messages starting with + * 'fetchRequest::' and listen for messages starting with 'fetchResult::'. See + * testharness-shadowrealm-audioworkletprocessor.js. + * + * @param {port} MessagePort - the message port on which to listen for fetch + * requests + */ +globalThis.setupFakeFetchOverMessagePort = function (port) { + port.addEventListener("message", (event) => { + if (typeof event.data !== "string" || !event.data.startsWith("fetchRequest::")) { + return; + } + + fetch(event.data.slice("fetchRequest::".length)) + .then(res => res.text()) + .then( + text => port.postMessage(`fetchResult::success::${text}`), + error => port.postMessage(`fetchResult::fail::${error}`), + ); + }); + port.start(); +} + +/** + * Returns a message suitable for posting with postMessage() that will signal to + * the test harness that the tests are finished and there was an error in the + * setup code. + * + * @param {message} any - error + */ +globalThis.createSetupErrorResult = function (message) { + return { + type: "complete", + tests: [], + asserts: [], + status: { + status: 1, // TestsStatus.ERROR, + message: String(message), + stack: typeof message === "object" && message !== null && "stack" in message ? message.stack : undefined, + }, + }; +}; diff --git a/test/fixtures/wpt/resources/testharness.js b/test/fixtures/wpt/resources/testharness.js index 126ae96423bc83..f495b62458ba75 100644 --- a/test/fixtures/wpt/resources/testharness.js +++ b/test/fixtures/wpt/resources/testharness.js @@ -88,10 +88,15 @@ status: harness_status.structured_clone(), asserts: asserts.map(assert => assert.structured_clone())}); }] - } + }; on_event(window, 'load', function() { + setTimeout(() => { this_obj.all_loaded = true; + if (tests.all_done()) { + tests.complete(); + } + },0); }); on_event(window, 'message', function(event) { @@ -198,7 +203,7 @@ } }); this.message_events = new_events; - } + }; WindowTestEnvironment.prototype.next_default_test_name = function() { var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; @@ -220,8 +225,8 @@ WindowTestEnvironment.prototype.test_timeout = function() { var metas = document.getElementsByTagName("meta"); for (var i = 0; i < metas.length; i++) { - if (metas[i].name == "timeout") { - if (metas[i].content == "long") { + if (metas[i].name === "timeout") { + if (metas[i].content === "long") { return settings.harness_timeout.long; } break; @@ -482,7 +487,7 @@ this.all_loaded = false; this.on_loaded_callback = null; Promise.resolve().then(function() { - this.all_loaded = true + this.all_loaded = true; if (this.on_loaded_callback) { this.on_loaded_callback(); } @@ -558,7 +563,7 @@ // The worker object may be from another execution context, // so do not use instanceof here. return 'ServiceWorker' in global_scope && - Object.prototype.toString.call(worker) == '[object ServiceWorker]'; + Object.prototype.toString.call(worker) === '[object ServiceWorker]'; } var seen_func_name = Object.create(null); @@ -600,7 +605,7 @@ /** * @callback TestFunction - * @param {Test} test - The test currently being run. + * @param {Test} test - The test currnetly being run. * @param {Any[]} args - Additional args to pass to function. * */ @@ -805,7 +810,7 @@ return bring_promise_to_current_realm(promise) .then(test.unreached_func("Should have rejected: " + description)) .catch(function(e) { - assert_throws_js_impl(constructor, function() { throw e }, + assert_throws_js_impl(constructor, function() { throw e; }, description, "promise_rejects_js"); }); } @@ -852,16 +857,64 @@ promise = promiseOrConstructor; description = descriptionOrPromise; assert(maybeDescription === undefined, - "Too many args pased to no-constructor version of promise_rejects_dom"); + "Too many args passed to no-constructor version of promise_rejects_dom, or accidentally explicitly passed undefined"); } return bring_promise_to_current_realm(promise) .then(test.unreached_func("Should have rejected: " + description)) .catch(function(e) { - assert_throws_dom_impl(type, function() { throw e }, description, + assert_throws_dom_impl(type, function() { throw e; }, description, "promise_rejects_dom", constructor); }); } +/** + * Assert that a `Promise` is rejected with a `QuotaExceededError` with the + * expected values. + * + * For the remaining arguments, there are two ways of calling + * `promise_rejects_quotaexceedederror`: + * + * 1) If the `QuotaExceededError` is expected to come from the + * current global, the second argument should be the promise + * expected to reject, the third and a fourth the expected + * `requested` and `quota` property values, and the fifth, + * optional, argument is the assertion description. + * + * 2) If the `QuotaExceededError` is expected to come from some + * other global, the second argument should be the + * `QuotaExceededError` constructor from that global, the third + * argument should be the promise expected to reject, the fourth + * and fifth the expected `requested` and `quota` property + * values, and the sixth, optional, argument is the assertion + * description. + * + */ + function promise_rejects_quotaexceedederror(test, promiseOrConstructor, requestedOrPromise, quotaOrRequested, descriptionOrQuota, maybeDescription) + { + let constructor, promise, requested, quota, description; + if (typeof promiseOrConstructor === "function" && + promiseOrConstructor.name === "QuotaExceededError") { + constructor = promiseOrConstructor; + promise = requestedOrPromise; + requested = quotaOrRequested; + quota = descriptionOrQuota; + description = maybeDescription; + } else { + constructor = self.QuotaExceededError; + promise = promiseOrConstructor; + requested = requestedOrPromise; + quota = quotaOrRequested; + description = descriptionOrQuota; + assert(maybeDescription === undefined, + "Too many args passed to no-constructor version of promise_rejects_quotaexceedederror"); + } + return bring_promise_to_current_realm(promise) + .then(test.unreached_func("Should have rejected: " + description)) + .catch(function(e) { + assert_throws_quotaexceedederror_impl(function() { throw e; }, requested, quota, description, "promise_rejects_quotaexceedederror", constructor); + }); + } + /** * Assert that a Promise is rejected with the provided value. * @@ -876,7 +929,7 @@ return bring_promise_to_current_realm(promise) .then(test.unreached_func("Should have rejected: " + description)) .catch(function(e) { - assert_throws_exactly_impl(exception, function() { throw e }, + assert_throws_exactly_impl(exception, function() { throw e; }, description, "promise_rejects_exactly"); }); } @@ -902,7 +955,7 @@ */ function EventWatcher(test, watchedNode, eventTypes, timeoutPromise) { - if (typeof eventTypes == 'string') { + if (typeof eventTypes === 'string') { eventTypes = [eventTypes]; } @@ -967,7 +1020,7 @@ if (waitingFor) { return Promise.reject('Already waiting for an event or events'); } - if (typeof types == 'string') { + if (typeof types === 'string') { types = [types]; } if (options && options.record && options.record === 'all') { @@ -982,7 +1035,7 @@ // This should always fail, otherwise we should have // resolved the promise. - assert_true(waitingFor.types.length == 0, + assert_true(waitingFor.types.length === 0, 'Timed out waiting for ' + waitingFor.types.join(', ')); var result = recordedEvents; recordedEvents = null; @@ -1006,13 +1059,13 @@ /** * Stop listening for events */ - function stop_watching() { + this.stop_watching = function() { for (var i = 0; i < eventTypes.length; i++) { watchedNode.removeEventListener(eventTypes[i], eventHandler, false); } }; - test._add_cleanup(stop_watching); + test._add_cleanup(this.stop_watching); return this; } @@ -1089,7 +1142,7 @@ { if (typeof func !== "function") { tests.set_status(tests.status.ERROR, - "promise_test invoked without a function"); + "`promise_setup` invoked without a function"); tests.complete(); return; } @@ -1127,7 +1180,7 @@ * * Typically this function is called implicitly on page load; it's * only necessary for users to call this when either the - * ``explicit_done`` or ``single_page`` properties have been set + * ``explicit_done`` or ``single_test`` properties have been set * via the :js:func:`setup` function. * * For single page tests this marks the test as complete and sets its status. @@ -1196,6 +1249,23 @@ object.addEventListener(event, callback, false); } + // Internal helper function to provide timeout-like functionality in + // environments where there is no setTimeout(). (No timeout ID or + // clearTimeout().) + function fake_set_timeout(callback, delay) { + var p = Promise.resolve(); + var start = Date.now(); + var end = start + delay; + function check() { + if ((end - Date.now()) > 0) { + p.then(check); + } else { + callback(); + } + } + p.then(check); + } + /** * Global version of :js:func:`Test.step_timeout` for use in single page tests. * @@ -1207,7 +1277,8 @@ function step_timeout(func, timeout) { var outer_this = this; var args = Array.prototype.slice.call(arguments, 2); - return setTimeout(function() { + var local_set_timeout = typeof global_scope.setTimeout === "undefined" ? fake_set_timeout : setTimeout; + return local_set_timeout(function() { func.apply(outer_this, args); }, timeout * tests.timeout_multiplier); } @@ -1217,6 +1288,7 @@ expose(promise_test, 'promise_test'); expose(promise_rejects_js, 'promise_rejects_js'); expose(promise_rejects_dom, 'promise_rejects_dom'); + expose(promise_rejects_quotaexceedederror, 'promise_rejects_quotaexceedederror'); expose(promise_rejects_exactly, 'promise_rejects_exactly'); expose(generate_tests, 'generate_tests'); expose(setup, 'setup'); @@ -1307,6 +1379,15 @@ "0xffff": "uffff", }; + const formatEscapeMap = { + "\\": "\\\\", + '"': '\\"' + }; + for (const p in replacements) { + formatEscapeMap[String.fromCharCode(p)] = "\\" + replacements[p]; + } + const formatEscapePattern = new RegExp(`[${Object.keys(formatEscapeMap).map(k => k === "\\" ? "\\\\" : k).join("")}]`, "g"); + /** * Convert a value to a nice, human-readable string * @@ -1357,12 +1438,7 @@ switch (typeof val) { case "string": - val = val.replace(/\\/g, "\\\\"); - for (var p in replacements) { - var replace = "\\" + replacements[p]; - val = val.replace(RegExp(String.fromCharCode(p), "g"), replace); - } - return '"' + val.replace(/"/g, '\\"') + '"'; + return '"' + val.replace(formatEscapePattern, match => formatEscapeMap[match]) + '"'; case "boolean": case "undefined": return String(val); @@ -1373,6 +1449,8 @@ return "-0"; } return String(val); + case "bigint": + return String(val) + 'n'; case "object": if (val === null) { return "null"; @@ -1396,11 +1474,11 @@ case Node.COMMENT_NODE: return "Comment node "; case Node.DOCUMENT_NODE: - return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + return "Document node with " + val.childNodes.length + (val.childNodes.length === 1 ? " child" : " children"); case Node.DOCUMENT_TYPE_NODE: return "DocumentType node"; case Node.DOCUMENT_FRAGMENT_NODE: - return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length === 1 ? " child" : " children"); default: return "Node object of unknown type"; } @@ -1744,20 +1822,25 @@ /** * Assert that ``actual`` is a number less than ``expected``. * - * @param {number} actual - Test value. - * @param {number} expected - Number that ``actual`` must be less than. + * @param {number|bigint} actual - Test value. + * @param {number|bigint} expected - Value that ``actual`` must be less than. * @param {string} [description] - Description of the condition being tested. */ function assert_less_than(actual, expected, description) { /* - * Test if a primitive number is less than another + * Test if a primitive number (or bigint) is less than another */ - assert(typeof actual === "number", + assert(typeof actual === "number" || typeof actual === "bigint", "assert_less_than", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); + assert(typeof actual === typeof expected, + "assert_less_than", description, + "expected a ${type_expected} but got a ${type_actual}", + {type_expected:typeof expected, type_actual:typeof actual}); + assert(actual < expected, "assert_less_than", description, "expected a number less than ${expected} but got ${actual}", @@ -1768,20 +1851,25 @@ /** * Assert that ``actual`` is a number greater than ``expected``. * - * @param {number} actual - Test value. - * @param {number} expected - Number that ``actual`` must be greater than. + * @param {number|bigint} actual - Test value. + * @param {number|bigint} expected - Value that ``actual`` must be greater than. * @param {string} [description] - Description of the condition being tested. */ function assert_greater_than(actual, expected, description) { /* - * Test if a primitive number is greater than another + * Test if a primitive number (or bigint) is greater than another */ - assert(typeof actual === "number", + assert(typeof actual === "number" || typeof actual === "bigint", "assert_greater_than", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); + assert(typeof actual === typeof expected, + "assert_greater_than", description, + "expected a ${type_expected} but got a ${type_actual}", + {type_expected:typeof expected, type_actual:typeof actual}); + assert(actual > expected, "assert_greater_than", description, "expected a number greater than ${expected} but got ${actual}", @@ -1793,21 +1881,31 @@ * Assert that ``actual`` is a number greater than ``lower`` and less * than ``upper`` but not equal to either. * - * @param {number} actual - Test value. - * @param {number} lower - Number that ``actual`` must be greater than. - * @param {number} upper - Number that ``actual`` must be less than. + * @param {number|bigint} actual - Test value. + * @param {number|bigint} lower - Value that ``actual`` must be greater than. + * @param {number|bigint} upper - Value that ``actual`` must be less than. * @param {string} [description] - Description of the condition being tested. */ function assert_between_exclusive(actual, lower, upper, description) { /* - * Test if a primitive number is between two others + * Test if a primitive number (or bigint) is between two others */ - assert(typeof actual === "number", + assert(typeof lower === typeof upper, + "assert_between_exclusive", description, + "expected lower (${type_lower}) and upper (${type_upper}) types to match (test error)", + {type_lower:typeof lower, type_upper:typeof upper}); + + assert(typeof actual === "number" || typeof actual === "bigint", "assert_between_exclusive", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); + assert(typeof actual === typeof lower, + "assert_between_exclusive", description, + "expected a ${type_lower} but got a ${type_actual}", + {type_lower:typeof lower, type_actual:typeof actual}); + assert(actual > lower && actual < upper, "assert_between_exclusive", description, "expected a number greater than ${lower} " + @@ -1819,21 +1917,26 @@ /** * Assert that ``actual`` is a number less than or equal to ``expected``. * - * @param {number} actual - Test value. - * @param {number} expected - Number that ``actual`` must be less + * @param {number|bigint} actual - Test value. + * @param {number|bigint} expected - Value that ``actual`` must be less * than or equal to. * @param {string} [description] - Description of the condition being tested. */ function assert_less_than_equal(actual, expected, description) { /* - * Test if a primitive number is less than or equal to another + * Test if a primitive number (or bigint) is less than or equal to another */ - assert(typeof actual === "number", + assert(typeof actual === "number" || typeof actual === "bigint", "assert_less_than_equal", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); + assert(typeof actual === typeof expected, + "assert_less_than_equal", description, + "expected a ${type_expected} but got a ${type_actual}", + {type_expected:typeof expected, type_actual:typeof actual}); + assert(actual <= expected, "assert_less_than_equal", description, "expected a number less than or equal to ${expected} but got ${actual}", @@ -1844,21 +1947,26 @@ /** * Assert that ``actual`` is a number greater than or equal to ``expected``. * - * @param {number} actual - Test value. - * @param {number} expected - Number that ``actual`` must be greater + * @param {number|bigint} actual - Test value. + * @param {number|bigint} expected - Value that ``actual`` must be greater * than or equal to. * @param {string} [description] - Description of the condition being tested. */ function assert_greater_than_equal(actual, expected, description) { /* - * Test if a primitive number is greater than or equal to another + * Test if a primitive number (or bigint) is greater than or equal to another */ - assert(typeof actual === "number", + assert(typeof actual === "number" || typeof actual === "bigint", "assert_greater_than_equal", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); + assert(typeof actual === typeof expected, + "assert_greater_than_equal", description, + "expected a ${type_expected} but got a ${type_actual}", + {type_expected:typeof expected, type_actual:typeof actual}); + assert(actual >= expected, "assert_greater_than_equal", description, "expected a number greater than or equal to ${expected} but got ${actual}", @@ -1870,21 +1978,31 @@ * Assert that ``actual`` is a number greater than or equal to ``lower`` and less * than or equal to ``upper``. * - * @param {number} actual - Test value. - * @param {number} lower - Number that ``actual`` must be greater than or equal to. - * @param {number} upper - Number that ``actual`` must be less than or equal to. + * @param {number|bigint} actual - Test value. + * @param {number|bigint} lower - Value that ``actual`` must be greater than or equal to. + * @param {number|bigint} upper - Value that ``actual`` must be less than or equal to. * @param {string} [description] - Description of the condition being tested. */ function assert_between_inclusive(actual, lower, upper, description) { /* - * Test if a primitive number is between to two others or equal to either of them + * Test if a primitive number (or bigint) is between to two others or equal to either of them */ - assert(typeof actual === "number", + assert(typeof lower === typeof upper, + "assert_between_inclusive", description, + "expected lower (${type_lower}) and upper (${type_upper}) types to match (test error)", + {type_lower:typeof lower, type_upper:typeof upper}); + + assert(typeof actual === "number" || typeof actual === "bigint", "assert_between_inclusive", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); + assert(typeof actual === typeof lower, + "assert_between_inclusive", description, + "expected a ${type_lower} but got a ${type_actual}", + {type_lower:typeof lower, type_actual:typeof actual}); + assert(actual >= lower && actual <= upper, "assert_between_inclusive", description, "expected a number greater than or equal to ${lower} " + @@ -2009,30 +2127,46 @@ /** - * Assert that ``object`` has a property named ``property_name`` and that the property is readonly. + * Assert that ``object`` has a property named ``property_name`` and that the property is not writable or has no setter. * - * Note: The implementation tries to update the named property, so - * any side effects of updating will be triggered. Users are - * encouraged to instead inspect the property descriptor of ``property_name`` on ``object``. - * - * @param {Object} object - Object that should have the given property in its prototype chain. + * @param {Object} object - Object that should have the given (not necessarily own) property. * @param {string} property_name - Expected property name. * @param {string} [description] - Description of the condition being tested. */ function assert_readonly(object, property_name, description) { - var initial_value = object[property_name]; - try { - //Note that this can have side effects in the case where - //the property has PutForwards - object[property_name] = initial_value + "a"; //XXX use some other value here? - assert(same_value(object[property_name], initial_value), - "assert_readonly", description, - "changing property ${p} succeeded", - {p:property_name}); - } finally { - object[property_name] = initial_value; - } + assert(property_name in object, + "assert_readonly", description, + "property ${p} not found", + {p:property_name}); + + let desc; + while (object && (desc = Object.getOwnPropertyDescriptor(object, property_name)) === undefined) { + object = Object.getPrototypeOf(object); + } + + assert(desc !== undefined, + "assert_readonly", description, + "could not find a descriptor for property ${p}", + {p:property_name}); + + if (desc.hasOwnProperty("value")) { + // We're a data property descriptor + assert(desc.writable === false, "assert_readonly", description, + "descriptor [[Writable]] expected false got ${actual}", {actual:desc.writable}); + } else if (desc.hasOwnProperty("get") || desc.hasOwnProperty("set")) { + // We're an accessor property descriptor + assert(desc.set === undefined, "assert_readonly", description, + "property ${p} is an accessor property with a [[Set]] attribute, cannot test readonly-ness", + {p:property_name}); + } else { + // We're a generic property descriptor + // This shouldn't happen, because Object.getOwnPropertyDescriptor + // forwards the return value of [[GetOwnProperty]] (P), which must + // be a fully populated Property Descriptor or Undefined. + assert(false, "assert_readonly", description, + "Object.getOwnPropertyDescriptor must return a fully populated property descriptor"); + } } expose_assert(assert_readonly, "assert_readonly"); @@ -2078,7 +2212,7 @@ {func:func}); // Basic sanity-check on the passed-in constructor - assert(typeof constructor == "function", + assert(typeof constructor === "function", assertion_type, description, "${constructor} is not a constructor", {constructor:constructor}); @@ -2151,9 +2285,9 @@ func = funcOrConstructor; description = descriptionOrFunc; assert(maybeDescription === undefined, - "Too many args pased to no-constructor version of assert_throws_dom"); + "Too many args passed to no-constructor version of assert_throws_dom, or accidentally explicitly passed undefined"); } - assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor) + assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor); } expose_assert(assert_throws_dom, "assert_throws_dom"); @@ -2186,8 +2320,8 @@ {func:func}); // Sanity-check our type - assert(typeof type == "number" || - typeof type == "string", + assert(typeof type === "number" || + typeof type === "string", assertion_type, description, "${type} is not a number or string", {type:type}); @@ -2211,7 +2345,6 @@ NETWORK_ERR: 'NetworkError', ABORT_ERR: 'AbortError', URL_MISMATCH_ERR: 'URLMismatchError', - QUOTA_EXCEEDED_ERR: 'QuotaExceededError', TIMEOUT_ERR: 'TimeoutError', INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', DATA_CLONE_ERR: 'DataCloneError' @@ -2236,7 +2369,6 @@ NetworkError: 19, AbortError: 20, URLMismatchError: 21, - QuotaExceededError: 22, TimeoutError: 23, InvalidNodeTypeError: 24, DataCloneError: 25, @@ -2267,12 +2399,19 @@ if (typeof type === "number") { if (type === 0) { throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()'); - } else if (!(type in code_name_map)) { + } + if (type === 22) { + throw new AssertionError('Test bug: QuotaExceededError needs to be tested for using assert_throws_quotaexceedederror()'); + } + if (!(type in code_name_map)) { throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()'); } name = code_name_map[type]; required_props.code = type; } else if (typeof type === "string") { + if (name === "QuotaExceededError") { + throw new AssertionError('Test bug: QuotaExceededError needs to be tested for using assert_throws_quotaexceedederror()'); + } name = type in codename_name_map ? codename_name_map[type] : type; if (!(name in name_code_map)) { throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()'); @@ -2307,6 +2446,137 @@ } } + /** + * Assert a `QuotaExceededError` with the expected values is thrown. + * + * There are two ways of calling `assert_throws_quotaexceedederror`: + * + * 1) If the `QuotaExceededError` is expected to come from the + * current global, the first argument should be the function + * expected to throw, the second and a third the expected + * `requested` and `quota` property values, and the fourth, + * optional, argument is the assertion description. + * + * 2) If the `QuotaExceededError` is expected to come from some + * other global, the first argument should be the + * `QuotaExceededError` constructor from that global, the second + * argument should be the function expected to throw, the third + * and fourth the expected `requested` and `quota` property + * values, and the fifth, optional, argument is the assertion + * description. + * + * For the `requested` and `quota` values, instead of `null` or a + * number, the caller can provide a function which determines + * whether the value is acceptable by returning a boolean. + * + */ + function assert_throws_quotaexceedederror(funcOrConstructor, requestedOrFunc, quotaOrRequested, descriptionOrQuota, maybeDescription) + { + let constructor, func, requested, quota, description; + if (funcOrConstructor.name === "QuotaExceededError") { + constructor = funcOrConstructor; + func = requestedOrFunc; + requested = quotaOrRequested; + quota = descriptionOrQuota; + description = maybeDescription; + } else { + constructor = self.QuotaExceededError; + func = funcOrConstructor; + requested = requestedOrFunc; + quota = quotaOrRequested; + description = descriptionOrQuota; + assert(maybeDescription === undefined, + "Too many args passed to no-constructor version of assert_throws_quotaexceedederror"); + } + assert_throws_quotaexceedederror_impl(func, requested, quota, description, "assert_throws_quotaexceedederror", constructor); + } + expose_assert(assert_throws_quotaexceedederror, "assert_throws_quotaexceedederror"); + + /** + * Similar to `assert_throws_quotaexceedederror` but allows + * specifying the assertion type + * (`"assert_throws_quotaexceedederror"` or + * `"promise_rejects_quotaexceedederror"`, in practice). The + * `constructor` argument must be the `QuotaExceededError` + * constructor from the global we expect the exception to come from. + */ + function assert_throws_quotaexceedederror_impl(func, requested, quota, description, assertion_type, constructor) + { + try { + func.call(this); + assert(false, assertion_type, description, "${func} did not throw", + {func}); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + + // Basic sanity-checks on the thrown exception. + assert(typeof e === "object", + assertion_type, description, + "${func} threw ${e} with type ${type}, not an object", + {func, e, type:typeof e}); + + assert(e !== null, + assertion_type, description, + "${func} threw null, not an object", + {func}); + + // Sanity-check our requested and quota. + assert(requested === null || + typeof requested === "number" || + typeof requested === "function", + assertion_type, description, + "${requested} is not null, a number, or a function", + {requested}); + assert(quota === null || + typeof quota === "number" || + typeof quota === "function", + assertion_type, description, + "${quota} is not null or a number", + {quota}); + + const required_props = { + code: 22, + name: "QuotaExceededError" + }; + if (typeof requested !== "function") { + required_props.requested = requested; + } + if (typeof quota !== "function") { + required_props.quota = quota; + } + + for (const [prop, expected] of Object.entries(required_props)) { + assert(prop in e && e[prop] == expected, + assertion_type, description, + "${func} threw ${e} that is not a correct QuotaExceededError: property ${prop} is equal to ${actual}, expected ${expected}", + {func, e, prop, actual:e[prop], expected}); + } + + if (typeof requested === "function") { + assert(requested(e.requested), + assertion_type, description, + "${func} threw ${e} that is not a correct QuotaExceededError: requested value ${requested} did not pass the requested predicate", + {func, e, requested}); + } + if (typeof quota === "function") { + assert(quota(e.quota), + assertion_type, description, + "${func} threw ${e} that is not a correct QuotaExceededError: quota value ${quota} did not pass the quota predicate", + {func, e, quota}); + } + + // Check that the exception is from the right global. This check is last + // so more specific, and more informative, checks on the properties can + // happen in case a totally incorrect exception is thrown. + assert(e.constructor === constructor, + assertion_type, description, + "${func} threw an exception from the wrong global", + {func}); + } + } + /** * Assert the provided value is thrown. * @@ -2416,7 +2686,7 @@ function assert_implements(condition, description) { assert(!!condition, "assert_implements", description); } - expose_assert(assert_implements, "assert_implements") + expose_assert(assert_implements, "assert_implements"); /** * Assert that an optional feature is implemented, based on a 'truthy' condition. @@ -2530,11 +2800,11 @@ 2: "Timeout", 3: "Not Run", 4: "Optional Feature Unsupported", - } + }; Test.prototype.format_status = function() { return this.status_formats[this.status]; - } + }; Test.prototype.structured_clone = function() { @@ -2715,7 +2985,8 @@ Test.prototype.step_timeout = function(func, timeout) { var test_this = this; var args = Array.prototype.slice.call(arguments, 2); - return setTimeout(this.step_func(function() { + var local_set_timeout = typeof global_scope.setTimeout === "undefined" ? fake_set_timeout : setTimeout; + return local_set_timeout(this.step_func(function() { return func.apply(test_this, args); }), timeout * tests.timeout_multiplier); }; @@ -2746,6 +3017,7 @@ var timeout_full = timeout * tests.timeout_multiplier; var remaining = Math.ceil(timeout_full / interval); var test_this = this; + var local_set_timeout = typeof global_scope.setTimeout === 'undefined' ? fake_set_timeout : setTimeout; const step = test_this.step_func((result) => { if (result) { @@ -2756,7 +3028,7 @@ "Timed out waiting on condition"); } remaining--; - setTimeout(wait_for_inner, interval); + local_set_timeout(wait_for_inner, interval); } }); @@ -2842,7 +3114,7 @@ return new Promise(resolve => { this.step_wait_func(cond, resolve, description, timeout, interval); }); - } + }; /* * Private method for registering cleanup functions. `testharness.js` @@ -3075,7 +3347,7 @@ throw new Error("AbortController is not supported in this browser"); } return this._abortController.signal; - } + }; /** * A RemoteTest object mirrors a Test object on a remote worker. The @@ -3151,11 +3423,11 @@ function(callback) { callback(); }); - } + }; RemoteTest.prototype.format_status = function() { return Test.prototype.status_formats[this.status]; - } + }; /* * A RemoteContext listens for test events from a remote test context, such @@ -3427,7 +3699,7 @@ this.all_done_callbacks = []; this.hide_test_state = false; - this.pending_remotes = []; + this.remotes = []; this.current_test = null; this.asserts_run = []; @@ -3469,26 +3741,26 @@ for (var p in properties) { if (properties.hasOwnProperty(p)) { var value = properties[p]; - if (p == "allow_uncaught_exception") { + if (p === "allow_uncaught_exception") { this.allow_uncaught_exception = value; - } else if (p == "explicit_done" && value) { + } else if (p === "explicit_done" && value) { this.wait_for_finish = true; - } else if (p == "explicit_timeout" && value) { + } else if (p === "explicit_timeout" && value) { this.timeout_length = null; if (this.timeout_id) { clearTimeout(this.timeout_id); } - } else if (p == "single_test" && value) { + } else if (p === "single_test" && value) { this.set_file_is_test(); - } else if (p == "timeout_multiplier") { + } else if (p === "timeout_multiplier") { this.timeout_multiplier = value; if (this.timeout_length) { this.timeout_length *= this.timeout_multiplier; } - } else if (p == "hide_test_state") { + } else if (p === "hide_test_state") { this.hide_test_state = value; - } else if (p == "output") { + } else if (p === "output") { this.output = value; } else if (p === "debug") { settings.debug = value; @@ -3581,11 +3853,14 @@ Tests.prototype.push = function(test) { + if (this.phase === this.phases.COMPLETE) { + return; + } if (this.phase < this.phases.HAVE_TESTS) { this.start(); } this.num_pending++; - test.index = this.tests.push(test); + test.index = this.tests.push(test) - 1; this.notify_test_state(test); }; @@ -3598,11 +3873,11 @@ }; Tests.prototype.all_done = function() { - return (this.tests.length > 0 || this.pending_remotes.length > 0) && + return (this.tests.length > 0 || this.remotes.length > 0) && test_environment.all_loaded && (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish && !this.processing_callbacks && - !this.pending_remotes.some(function(w) { return w.running; }); + !this.remotes.some(function(w) { return w.running; }); }; Tests.prototype.start = function() { @@ -3671,7 +3946,8 @@ function(test, testDone) { if (test.phase === test.phases.INITIAL) { - test.phase = test.phases.COMPLETE; + test.phase = test.phases.HAS_RESULT; + test.done(); testDone(); } else { add_test_done_callback(test, testDone); @@ -3682,14 +3958,14 @@ }; Tests.prototype.set_assert = function(assert_name, args) { - this.asserts_run.push(new AssertRecord(this.current_test, assert_name, args)) - } + this.asserts_run.push(new AssertRecord(this.current_test, assert_name, args)); + }; Tests.prototype.set_assert_status = function(index, status, stack) { let assert_record = this.asserts_run[index]; assert_record.status = status; assert_record.stack = stack; - } + }; /** * Update the harness status to reflect an unrecoverable harness error that @@ -3831,7 +4107,7 @@ } var remoteContext = this.create_remote_worker(worker); - this.pending_remotes.push(remoteContext); + this.remotes.push(remoteContext); return remoteContext.done; }; @@ -3854,7 +4130,7 @@ } var remoteContext = this.create_remote_window(remote); - this.pending_remotes.push(remoteContext); + this.remotes.push(remoteContext); return remoteContext.done; }; @@ -4066,8 +4342,8 @@ } else { var root = output_document.documentElement; var is_html = (root && - root.namespaceURI == "http://www.w3.org/1999/xhtml" && - root.localName == "html"); + root.namespaceURI === "http://www.w3.org/1999/xhtml" && + root.localName === "html"); var is_svg = (output_document.defaultView && "SVGSVGElement" in output_document.defaultView && root instanceof output_document.defaultView.SVGSVGElement); @@ -4169,11 +4445,7 @@ status ], ], - ["button", - {"onclick": "let evt = new Event('__test_restart'); " + - "let canceled = !window.dispatchEvent(evt);" + - "if (!canceled) { location.reload() }"}, - "Rerun"] + ["button", {"id":"rerun"}, "Rerun"] ]]; if (harness_status.status === harness_status.ERROR) { @@ -4205,6 +4477,13 @@ log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); + output_document.getElementById("rerun").addEventListener("click", + function() { + let evt = new Event('__test_restart'); + let canceled = !window.dispatchEvent(evt); + if (!canceled) { location.reload(); } + }); + forEach(output_document.querySelectorAll("section#summary label"), function(element) { @@ -4229,18 +4508,6 @@ }); }); - // This use of innerHTML plus manual escaping is not recommended in - // general, but is necessary here for performance. Using textContent - // on each individual adds tens of seconds of execution time for - // large test suites (tens of thousands of tests). - function escape_html(s) - { - return s.replace(/\&/g, "&") - .replace(/ { - var output_fn = "" + escape_html(assert.assert_name) + "("; - var prefix_len = output_fn.length; - var output_args = assert.args; - var output_len = output_args.reduce((prev, current) => prev+current, prefix_len); - if (output_len[output_len.length - 1] > 50) { - output_args = output_args.map((x, i) => - (i > 0 ? " ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : "")); - } else { - output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : "")); - } - output_fn += escape_html(output_args.join("")); - output_fn += ')'; - var output_location; + + const table = asserts_output.querySelector("table"); + for (const assert of asserts) { + const status_class_name = status_class(Test.prototype.status_formats[assert.status]); + var output_fn = "(" + assert.args.join(", ") + ")"; if (assert.stack) { - output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " "); + output_fn += "\n"; + output_fn += assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " "); } - return "" + - "" + - Test.prototype.status_formats[assert.status] + "" + - "
" +
-                    output_fn +
-                    (output_location ? "\n" + escape_html(output_location) : "") +
-                    "
"; + table.appendChild(render( + ["tr", {"class":"overall-" + status_class_name}, + ["td", {"class":status_class_name}, Test.prototype.status_formats[assert.status]], + ["td", {}, ["pre", {}, ["strong", {}, assert.assert_name], output_fn]] ])); } - ).join("\n"); - rv += ""; - return rv; + return asserts_output; } - log.appendChild(document.createElementNS(xhtml_ns, "section")); var assertions = has_assertions(); - var html = "

Details

" + - "" + - (assertions ? "" : "") + - "" + - ""; - for (var i = 0; i < tests.length; i++) { - var test = tests[i]; - html += '' + - '"; - } - html += "
ResultTest NameAssertionMessage
' + - test.format_status() + - "" + - escape_html(test.name) + - "" + - (assertions ? escape_html(get_assertion(test)) + "" : "") + - escape_html(test.message ? tests[i].message : " ") + - (tests[i].stack ? "
" +
-                 escape_html(tests[i].stack) +
-                 "
": ""); + const section = render( + ["section", {}, + ["h2", {}, "Details"], + ["table", {"id":"results", "class":(assertions ? "assertions" : "")}, + ["thead", {}, + ["tr", {}, + ["th", {}, "Result"], + ["th", {}, "Test Name"], + (assertions ? ["th", {}, "Assertion"] : ""), + ["th", {}, "Message" ]]], + ["tbody", {}]]]); + + const tbody = section.querySelector("tbody"); + for (const test of tests) { + const status = test.format_status(); + const status_class_name = status_class(status); + tbody.appendChild(render( + ["tr", {"class":"overall-" + status_class_name}, + ["td", {"class":status_class_name}, status], + ["td", {}, test.name], + (assertions ? ["td", {}, get_assertion(test)] : ""), + ["td", {}, + test.message ?? "", + ["pre", {}, test.stack ?? ""]]])); if (!(test instanceof RemoteTest)) { - html += "
Asserts run" + get_asserts_output(test) + "
" + tbody.lastChild.lastChild.appendChild(get_asserts_output(test)); } - html += "
"; - try { - log.lastChild.innerHTML = html; - } catch (e) { - log.appendChild(document.createElementNS(xhtml_ns, "p")) - .textContent = "Setting innerHTML for the log threw an exception."; - log.appendChild(document.createElementNS(xhtml_ns, "pre")) - .textContent = html; } + log.appendChild(section); }; /* @@ -4410,13 +4659,20 @@ { var substitution_re = /\$\{([^ }]*)\}/g; - function do_substitution(input) { + function do_substitution(input) + { var components = input.split(substitution_re); var rv = []; - for (var i = 0; i < components.length; i += 2) { - rv.push(components[i]); - if (components[i + 1]) { - rv.push(String(substitutions[components[i + 1]])); + if (components.length === 1) { + rv = components; + } else if (substitutions) { + for (var i = 0; i < components.length; i += 2) { + if (components[i]) { + rv.push(components[i]); + } + if (substitutions[components[i + 1]]) { + rv.push(String(substitutions[components[i + 1]])); + } } } return rv; @@ -4532,7 +4788,7 @@ */ function AssertionError(message) { - if (typeof message == "string") { + if (typeof message === "string") { message = sanitize_unpaired_surrogates(message); } this.message = message; @@ -4578,7 +4834,7 @@ } return lines.slice(i).join("\n"); - } + }; function OptionalFeatureUnsupportedError(message) { @@ -4767,11 +5023,21 @@ return META_TITLE; } if ('location' in global_scope && 'pathname' in location) { - return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.')); + var filename = location.pathname.substring(location.pathname.lastIndexOf('/') + 1); + return filename.substring(0, filename.indexOf('.')); } return "Untitled"; } + /** Fetches a JSON resource and parses it */ + async function fetch_json(resource) { + const response = await fetch(resource); + return await response.json(); + } + if (!global_scope.GLOBAL || !global_scope.GLOBAL.isShadowRealm()) { + expose(fetch_json, 'fetch_json'); + } + /** * Setup globals */ diff --git a/test/fixtures/wpt/resources/testharnessreport.js b/test/fixtures/wpt/resources/testharnessreport.js index e5cb40fe0ef652..405a2d8b06f00f 100644 --- a/test/fixtures/wpt/resources/testharnessreport.js +++ b/test/fixtures/wpt/resources/testharnessreport.js @@ -14,31 +14,6 @@ * parameters they are called with see testharness.js */ -function dump_test_results(tests, status) { - var results_element = document.createElement("script"); - results_element.type = "text/json"; - results_element.id = "__testharness__results__"; - var test_results = tests.map(function(x) { - return {name:x.name, status:x.status, message:x.message, stack:x.stack} - }); - var data = {test:window.location.href, - tests:test_results, - status: status.status, - message: status.message, - stack: status.stack}; - results_element.textContent = JSON.stringify(data); - - // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element' - // is inserted at a location that results in a valid document. - var parent = document.body - ? document.body // is required in XHTML documents - : document.documentElement; // fallback for optional in HTML5, SVG, etc. - - parent.appendChild(results_element); -} - -add_completion_callback(dump_test_results); - /* If the parent window has a testharness_properties object, * we use this to provide the test settings. This is used by the * default in-browser runner to configure the timeout and the diff --git a/test/fixtures/wpt/resources/web-bluetooth-bidi-test.js b/test/fixtures/wpt/resources/web-bluetooth-bidi-test.js new file mode 100644 index 00000000000000..3283fef43fc7d2 --- /dev/null +++ b/test/fixtures/wpt/resources/web-bluetooth-bidi-test.js @@ -0,0 +1,408 @@ +'use strict' + +// Convert `manufacturerData` to an array of bluetooth.BluetoothManufacturerData +// defined in +// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-bidi-definitions. +function convertToBidiManufacturerData(manufacturerData) { + const bidiManufacturerData = []; + for (const key in manufacturerData) { + bidiManufacturerData.push({ + key: parseInt(key), + data: btoa(String.fromCharCode(...manufacturerData[key])) + }) + } + return bidiManufacturerData; +} + +function ArrayToMojoCharacteristicProperties(arr) { + const struct = {}; + arr.forEach(property => { + struct[property] = true; + }); + return struct; +} + +class FakeBluetooth { + constructor() { + this.fake_central_ = null; + } + + // Returns a promise that resolves with a FakeCentral that clients can use + // to simulate events that a device in the Central/Observer role would + // receive as well as monitor the operations performed by the device in the + // Central/Observer role. + // + // A "Central" object would allow its clients to receive advertising events + // and initiate connections to peripherals i.e. operations of two roles + // defined by the Bluetooth Spec: Observer and Central. + // See Bluetooth 4.2 Vol 3 Part C 2.2.2 "Roles when Operating over an + // LE Physical Transport". + async simulateCentral({state}) { + if (this.fake_central_) { + throw 'simulateCentral() should only be called once'; + } + + await test_driver.bidi.bluetooth.simulate_adapter({state: state}); + this.fake_central_ = new FakeCentral(); + return this.fake_central_; + } +} + +// FakeCentral allows clients to simulate events that a device in the +// Central/Observer role would receive as well as monitor the operations +// performed by the device in the Central/Observer role. +class FakeCentral { + constructor() { + this.peripherals_ = new Map(); + } + + // Simulates a peripheral with |address|, |name|, |manufacturerData| and + // |known_service_uuids| that has already been connected to the system. If the + // peripheral existed already it updates its name, manufacturer data, and + // known UUIDs. |known_service_uuids| should be an array of + // BluetoothServiceUUIDs + // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid + // + // Platforms offer methods to retrieve devices that have already been + // connected to the system or weren't connected through the UA e.g. a user + // connected a peripheral through the system's settings. This method is + // intended to simulate peripherals that those methods would return. + async simulatePreconnectedPeripheral( + {address, name, manufacturerData = {}, knownServiceUUIDs = []}) { + await test_driver.bidi.bluetooth.simulate_preconnected_peripheral({ + address: address, + name: name, + manufacturerData: convertToBidiManufacturerData(manufacturerData), + knownServiceUuids: + knownServiceUUIDs.map(uuid => BluetoothUUID.getService(uuid)) + }); + + return this.fetchOrCreatePeripheral_(address); + } + + // Create a fake_peripheral object from the given address. + fetchOrCreatePeripheral_(address) { + let peripheral = this.peripherals_.get(address); + if (peripheral === undefined) { + peripheral = new FakePeripheral(address); + this.peripherals_.set(address, peripheral); + } + return peripheral; + } +} + +class FakePeripheral { + constructor(address) { + this.address = address; + } + + // Adds a fake GATT Service with |uuid| to be discovered when discovering + // the peripheral's GATT Attributes. Returns a FakeRemoteGATTService + // corresponding to this service. |uuid| should be a BluetoothServiceUUIDs + // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid + async addFakeService({uuid}) { + const service_uuid = BluetoothUUID.getService(uuid); + await test_driver.bidi.bluetooth.simulate_service({ + address: this.address, + uuid: service_uuid, + type: 'add', + }); + return new FakeRemoteGATTService(service_uuid, this.address); + } + + // Sets the next GATT Connection request response to |code|. |code| could be + // an HCI Error Code from BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a + // number outside that range returned by specific platforms e.g. Android + // returns 0x101 to signal a GATT failure + // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE + async setNextGATTConnectionResponse({code}) { + const remove_handler = + test_driver.bidi.bluetooth.gatt_connection_attempted.on((event) => { + if (event.address != this.address) { + return; + } + remove_handler(); + test_driver.bidi.bluetooth.simulate_gatt_connection_response({ + address: event.address, + code, + }); + }); + } + + async setNextGATTDiscoveryResponse({code}) { + // No-op for Web Bluetooth Bidi test, it will be removed when migration + // completes. + return Promise.resolve(); + } + + // Simulates a GATT connection response with |code| from the peripheral. + async simulateGATTConnectionResponse(code) { + await test_driver.bidi.bluetooth.simulate_gatt_connection_response( + {address: this.address, code}); + } + + // Simulates a GATT disconnection in the peripheral. + async simulateGATTDisconnection() { + await test_driver.bidi.bluetooth.simulate_gatt_disconnection( + {address: this.address}); + } +} + +class FakeRemoteGATTService { + constructor(service_uuid, peripheral_address) { + this.service_uuid_ = service_uuid; + this.peripheral_address_ = peripheral_address; + } + + // Adds a fake GATT Characteristic with |uuid| and |properties| + // to this fake service. The characteristic will be found when discovering + // the peripheral's GATT Attributes. Returns a FakeRemoteGATTCharacteristic + // corresponding to the added characteristic. + async addFakeCharacteristic({uuid, properties}) { + const characteristic_uuid = BluetoothUUID.getCharacteristic(uuid); + await test_driver.bidi.bluetooth.simulate_characteristic({ + address: this.peripheral_address_, + serviceUuid: this.service_uuid_, + characteristicUuid: characteristic_uuid, + characteristicProperties: ArrayToMojoCharacteristicProperties(properties), + type: 'add' + }); + return new FakeRemoteGATTCharacteristic( + characteristic_uuid, this.service_uuid_, this.peripheral_address_); + } + + // Removes the fake GATT service from its fake peripheral. + async remove() { + await test_driver.bidi.bluetooth.simulate_service({ + address: this.peripheral_address_, + uuid: this.service_uuid_, + type: 'remove' + }); + } +} + +class FakeRemoteGATTCharacteristic { + constructor(characteristic_uuid, service_uuid, peripheral_address) { + this.characteristic_uuid_ = characteristic_uuid; + this.service_uuid_ = service_uuid; + this.peripheral_address_ = peripheral_address; + this.last_written_value_ = {lastValue: null, lastWriteType: 'none'}; + } + + // Adds a fake GATT Descriptor with |uuid| to be discovered when + // discovering the peripheral's GATT Attributes. Returns a + // FakeRemoteGATTDescriptor corresponding to this descriptor. |uuid| should + // be a BluetoothDescriptorUUID + // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothdescriptoruuid + async addFakeDescriptor({uuid}) { + const descriptor_uuid = BluetoothUUID.getDescriptor(uuid); + await test_driver.bidi.bluetooth.simulate_descriptor({ + address: this.peripheral_address_, + serviceUuid: this.service_uuid_, + characteristicUuid: this.characteristic_uuid_, + descriptorUuid: descriptor_uuid, + type: 'add' + }); + return new FakeRemoteGATTDescriptor( + descriptor_uuid, this.characteristic_uuid_, this.service_uuid_, + this.peripheral_address_); + } + + // Simulate a characteristic for operation |type| with response |code| and + // |data|. + async simulateResponse(type, code, data) { + await test_driver.bidi.bluetooth.simulate_characteristic_response({ + address: this.peripheral_address_, + serviceUuid: this.service_uuid_, + characteristicUuid: this.characteristic_uuid_, + type, + code, + data, + }); + } + + // Simulate a characteristic response for read operation with response |code| + // and |data|. + async simulateReadResponse(code, data) { + await this.simulateResponse('read', code, data); + } + + // Simulate a characteristic response for write operation with response + // |code|. + async simulateWriteResponse(code) { + await this.simulateResponse('write', code); + } + + // Sets the next read response for characteristic to |code| and |value|. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE + async setNextReadResponse(gatt_code, value = null) { + if (gatt_code === 0 && value === null) { + throw '|value| can\'t be null if read should success.'; + } + if (gatt_code !== 0 && value !== null) { + throw '|value| must be null if read should fail.'; + } + + const remove_handler = + test_driver.bidi.bluetooth.characteristic_event_generated.on( + (event) => { + if (event.address != this.peripheral_address_) { + return; + } + remove_handler(); + this.simulateReadResponse(gatt_code, value); + }); + } + + // Sets the next write response for this characteristic to |code|. If + // writing to a characteristic that only supports 'write-without-response' + // the set response will be ignored. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + async setNextWriteResponse(gatt_code) { + const remove_handler = + test_driver.bidi.bluetooth.characteristic_event_generated.on( + (event) => { + if (event.address != this.peripheral_address_) { + return; + } + this.last_written_value_ = { + lastValue: event.data, + lastWriteType: event.type + }; + remove_handler(); + if (event.type == 'write-with-response') { + this.simulateWriteResponse(gatt_code); + } + }); + } + + // Gets the last successfully written value to the characteristic and its + // write type. Write type is one of 'none', 'default-deprecated', + // 'with-response', 'without-response'. Returns {lastValue: null, + // lastWriteType: 'none'} if no value has yet been written to the + // characteristic. + async getLastWrittenValue() { + return this.last_written_value_; + } + + // Removes the fake GATT Characteristic from its fake service. + async remove() { + await test_driver.bidi.bluetooth.simulate_characteristic({ + address: this.peripheral_address_, + serviceUuid: this.service_uuid_, + characteristicUuid: this.characteristic_uuid_, + characteristicProperties: undefined, + type: 'remove' + }); + } +} + +class FakeRemoteGATTDescriptor { + constructor( + descriptor_uuid, characteristic_uuid, service_uuid, peripheral_address) { + this.descriptor_uuid_ = descriptor_uuid; + this.characteristic_uuid_ = characteristic_uuid; + this.service_uuid_ = service_uuid; + this.peripheral_address_ = peripheral_address; + this.last_written_value_ = null; + } + + // Simulate a descriptor for operation |type| with response |code| and + // |data|. + async simulateResponse(type, code, data) { + await test_driver.bidi.bluetooth.simulate_descriptor_response({ + address: this.peripheral_address_, + serviceUuid: this.service_uuid_, + characteristicUuid: this.characteristic_uuid_, + descriptorUuid: this.descriptor_uuid_, + type, + code, + data, + }); + } + + // Simulate a descriptor response for read operation with response |code| and + // |data|. + async simulateReadResponse(code, data) { + await this.simulateResponse('read', code, data); + } + + // Simulate a descriptor response for write operation with response |code|. + async simulateWriteResponse(code) { + await this.simulateResponse('write', code); + } + + // Sets the next read response for descriptor to |code| and |value|. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE + async setNextReadResponse(gatt_code, value = null) { + if (gatt_code === 0 && value === null) { + throw '|value| can\'t be null if read should success.'; + } + if (gatt_code !== 0 && value !== null) { + throw '|value| must be null if read should fail.'; + } + + const remove_handler = + test_driver.bidi.bluetooth.descriptor_event_generated.on((event) => { + if (event.address != this.peripheral_address_) { + return; + } + remove_handler(); + this.simulateReadResponse(gatt_code, value); + }); + } + + // Sets the next write response for this descriptor to |code|. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + async setNextWriteResponse(gatt_code) { + const remove_handler = + test_driver.bidi.bluetooth.descriptor_event_generated.on((event) => { + if (event.address != this.peripheral_address_) { + return; + } + this.last_written_value_ = { + lastValue: event.data, + lastWriteType: event.type + }; + remove_handler(); + if (event.type == 'write-with-response') { + this.simulateWriteResponse(gatt_code); + } + }); + } + + // Gets the last successfully written value to the descriptor. + // Returns null if no value has yet been written to the descriptor. + async getLastWrittenValue() { + return this.last_written_value_; + } + + // Removes the fake GATT Descriptor from its fake characteristic. + async remove() { + await test_driver.bidi.bluetooth.simulate_descriptor({ + address: this.peripheral_address_, + serviceUuid: this.service_uuid_, + characteristicUuid: this.characteristic_uuid_, + descriptorUuid: this.descriptor_uuid_, + type: 'remove' + }); + } +} + +function initializeBluetoothBidiResources() { + navigator.bluetooth.test = new FakeBluetooth(); +} diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 86b594dd5ef9a2..5c4c0305254155 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -64,7 +64,7 @@ "path": "resource-timing" }, "resources": { - "commit": "1e140d63ec885703ce24b3798abd81912696bb85", + "commit": "1d2c5fb36a6e477c8f915bde7eca027be6abe792", "path": "resources" }, "streams": { @@ -96,7 +96,7 @@ "path": "web-locks" }, "WebCryptoAPI": { - "commit": "ab08796857072c5a93e27e0a25effba2e07dad11", + "commit": "1d2c5fb36a6e477c8f915bde7eca027be6abe792", "path": "WebCryptoAPI" }, "webidl/ecmascript-binding/es-exceptions": { @@ -108,7 +108,7 @@ "path": "webmessaging/broadcastchannel" }, "webstorage": { - "commit": "1291340aaaa6e73db43b412e47401eca3830c556", + "commit": "1d2c5fb36a6e477c8f915bde7eca027be6abe792", "path": "webstorage" } } diff --git a/test/fixtures/wpt/webstorage/storage_local_setitem_quotaexceedederr.window.js b/test/fixtures/wpt/webstorage/storage_local_setitem_quotaexceedederr.window.js index fff7d6444a039e..f2f3c4d6887542 100644 --- a/test/fixtures/wpt/webstorage/storage_local_setitem_quotaexceedederr.window.js +++ b/test/fixtures/wpt/webstorage/storage_local_setitem_quotaexceedederr.window.js @@ -1,16 +1,18 @@ -test(function() { +test(t => { localStorage.clear(); var index = 0; var key = "name"; var val = "x".repeat(1024); - assert_throws_dom("QUOTA_EXCEEDED_ERR", function() { + t.add_cleanup(() => { + localStorage.clear(); + }); + + assert_throws_quotaexceedederror(() => { while (true) { index++; localStorage.setItem("" + key + index, "" + val + index); } - }); - - localStorage.clear(); + }, null, null); }, "Throws QuotaExceededError when the quota has been exceeded"); diff --git a/test/fixtures/wpt/webstorage/storage_session_setitem_quotaexceedederr.window.js b/test/fixtures/wpt/webstorage/storage_session_setitem_quotaexceedederr.window.js index 42a895470efa62..693c98d29f99fd 100644 --- a/test/fixtures/wpt/webstorage/storage_session_setitem_quotaexceedederr.window.js +++ b/test/fixtures/wpt/webstorage/storage_session_setitem_quotaexceedederr.window.js @@ -1,16 +1,18 @@ -test(function() { +test(t => { sessionStorage.clear(); var index = 0; var key = "name"; var val = "x".repeat(1024); - assert_throws_dom("QUOTA_EXCEEDED_ERR", function() { + t.add_cleanup(() => { + sessionStorage.clear(); + }); + + assert_throws_quotaexceedederror(() => { while (true) { index++; sessionStorage.setItem("" + key + index, "" + val + index); } - }); - - sessionStorage.clear(); + }, null, null); }, "Throws QuotaExceededError when the quota has been exceeded"); diff --git a/test/fixtures/wpt/webstorage/symbol-props.window.js b/test/fixtures/wpt/webstorage/symbol-props.window.js index 61dd8f83dc4f5b..8f598d7076909d 100644 --- a/test/fixtures/wpt/webstorage/symbol-props.window.js +++ b/test/fixtures/wpt/webstorage/symbol-props.window.js @@ -39,10 +39,10 @@ Object.defineProperty(storage, key, { "value": "test", "configurable": false }); assert_equals(storage[key], "test"); var desc = Object.getOwnPropertyDescriptor(storage, key); - assert_true(desc.configurable, "configurable"); + assert_false(desc.configurable, "configurable"); - assert_true(delete storage[key]); - assert_equals(storage[key], undefined); + assert_false(delete storage[key]); + assert_equals(storage[key], "test"); }, name + ": defineProperty not configurable"); test(function() { diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 709d34b8f47c40..7e4639ca497ba0 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -26,4 +26,20 @@ module.exports = { ], }, }, + 'getRandomValues.any.js': { + 'fail': { + 'note': 'https://github.com/nodejs/node/issues/58987', + 'expected': [ + 'Large length: Int8Array', + 'Large length: Int16Array', + 'Large length: Int32Array', + 'Large length: BigInt64Array', + 'Large length: Uint8Array', + 'Large length: Uint8ClampedArray', + 'Large length: Uint16Array', + 'Large length: Uint32Array', + 'Large length: BigUint64Array', + ], + }, + }, }; diff --git a/test/wpt/status/webstorage.json b/test/wpt/status/webstorage.json index 10171601480aad..1dad4e2dd48403 100644 --- a/test/wpt/status/webstorage.json +++ b/test/wpt/status/webstorage.json @@ -22,5 +22,30 @@ }, "storage_session_window_reopen.window.js": { "skip": "window.open() is not supported in Node.js." + }, + "storage_session_setitem_quotaexceedederr.window.js": { + "fail": { + "note": "https://github.com/nodejs/node/issues/58987", + "expected": [ + "Throws QuotaExceededError when the quota has been exceeded" + ] + } + }, + "storage_local_setitem_quotaexceedederr.window.js": { + "fail": { + "note": "https://github.com/nodejs/node/issues/58987", + "expected": [ + "Throws QuotaExceededError when the quota has been exceeded" + ] + } + }, + "symbol-props.window.js": { + "fail": { + "note": "https://github.com/nodejs/node/issues/59310", + "expected": [ + "localStorage: defineProperty not configurable", + "sessionStorage: defineProperty not configurable" + ] + } } } From 0eb5962f1e81a50dc871c8dc8ce5b1c1263c6ae5 Mon Sep 17 00:00:00 2001 From: Aditi <62544124+Aditi-1400@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:54:00 +0530 Subject: [PATCH 41/80] meta: add mailmap entry for aditi-1400 PR-URL: https://github.com/nodejs/node/pull/59316 Reviewed-By: Joyee Cheung Reviewed-By: Darshan Sen Reviewed-By: Luigi Pinca --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index c0aac7e9647a69..0422fba4472601 100644 --- a/.mailmap +++ b/.mailmap @@ -6,6 +6,7 @@ Abdirahim Musse <33973272+abmusse@users.noreply.github Abe Fettig Abhimanyu Vashisht Adam Langley +Aditi Singh Akhil Marsonya Akhil Marsonya <16393876+marsonya@users.noreply.github.com> Akito Ito From 1e5f6326660d025716afd423dda9bfaaf79997a5 Mon Sep 17 00:00:00 2001 From: iknoom Date: Fri, 1 Aug 2025 04:09:40 +0900 Subject: [PATCH 42/80] src: use C++20 `contains()` method Refactors several `v.find(...) == v.end()` and `v.find(...) != v.end()` to use more expressive and readable C++20 `contains()` method. PR-URL: https://github.com/nodejs/node/pull/59304 Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Franziska Hinkelmann --- src/inspector/io_agent.cc | 2 +- src/inspector_profiler.h | 4 +--- src/node_blob.cc | 4 ++-- src/node_builtins.cc | 2 +- src/node_env_var.cc | 2 +- src/node_messaging.cc | 2 +- src/signal_wrap.cc | 2 +- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/inspector/io_agent.cc b/src/inspector/io_agent.cc index 749774f5c3144f..c66c091d69b747 100644 --- a/src/inspector/io_agent.cc +++ b/src/inspector/io_agent.cc @@ -27,7 +27,7 @@ DispatchResponse IoAgent::read(const String& in_handle, if (in_offset.has_value()) { offset = *in_offset; offset_was_specified = true; - } else if (offset_map_.find(url) != offset_map_.end()) { + } else if (offset_map_.contains(url)) { offset = offset_map_[url]; } int size = 1 << 20; diff --git a/src/inspector_profiler.h b/src/inspector_profiler.h index fd741c1f1ff659..d07334c818d90e 100644 --- a/src/inspector_profiler.h +++ b/src/inspector_profiler.h @@ -65,9 +65,7 @@ class V8ProfilerConnection { simdjson::ondemand::object* result); virtual void WriteProfile(simdjson::ondemand::object* result); - bool HasProfileId(uint64_t id) const { - return profile_ids_.find(id) != profile_ids_.end(); - } + bool HasProfileId(uint64_t id) const { return profile_ids_.contains(id); } void RemoveProfileId(uint64_t id) { profile_ids_.erase(id); } diff --git a/src/node_blob.cc b/src/node_blob.cc index 0bc4eb7e9318b5..eceddfbba39144 100644 --- a/src/node_blob.cc +++ b/src/node_blob.cc @@ -537,11 +537,11 @@ void BlobBindingData::store_data_object( } void BlobBindingData::revoke_data_object(const std::string& uuid) { - if (data_objects_.find(uuid) == data_objects_.end()) { + if (!data_objects_.contains(uuid)) { return; } data_objects_.erase(uuid); - CHECK_EQ(data_objects_.find(uuid), data_objects_.end()); + CHECK(!data_objects_.contains(uuid)); } BlobBindingData::StoredDataObject BlobBindingData::get_data_object( diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 5555657cbe5b28..9e82a3f3cf9d16 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -306,7 +306,7 @@ MaybeLocal BuiltinLoader::LookupAndCompileInternal( if (should_eager_compile_) { options = ScriptCompiler::kEagerCompile; } else if (!to_eager_compile_.empty()) { - if (to_eager_compile_.find(id) != to_eager_compile_.end()) { + if (to_eager_compile_.contains(id)) { options = ScriptCompiler::kEagerCompile; } } diff --git a/src/node_env_var.cc b/src/node_env_var.cc index 307153c0e3781b..6aad252eb5681b 100644 --- a/src/node_env_var.cc +++ b/src/node_env_var.cc @@ -273,7 +273,7 @@ void MapKVStore::Set(Isolate* isolate, Local key, Local value) { int32_t MapKVStore::Query(const char* key) const { Mutex::ScopedLock lock(mutex_); - return map_.find(key) == map_.end() ? -1 : 0; + return map_.contains(key) ? 0 : -1; } int32_t MapKVStore::Query(Isolate* isolate, Local key) const { diff --git a/src/node_messaging.cc b/src/node_messaging.cc index cb321ff606f4fe..084ff03dbe8124 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -1505,7 +1505,7 @@ Maybe SiblingGroup::Dispatch( RwLock::ScopedReadLock lock(group_mutex_); // The source MessagePortData is not part of this group. - if (ports_.find(source) == ports_.end()) { + if (!ports_.contains(source)) { if (error != nullptr) *error = "Source MessagePort is not entangled with this group."; return Nothing(); diff --git a/src/signal_wrap.cc b/src/signal_wrap.cc index d4fe8a3bcdd9ff..cd4960a4ec9e28 100644 --- a/src/signal_wrap.cc +++ b/src/signal_wrap.cc @@ -170,7 +170,7 @@ void DecreaseSignalHandlerCount(int signum) { bool HasSignalJSHandler(int signum) { Mutex::ScopedLock lock(handled_signals_mutex); - return handled_signals.find(signum) != handled_signals.end(); + return handled_signals.contains(signum); } } // namespace node From e79c93a5d03b0a10712aed4ccd6f74fbb04dcda7 Mon Sep 17 00:00:00 2001 From: Haram Jeong <91401364+haramj@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:47:09 +0900 Subject: [PATCH 43/80] http: add server.keepAliveTimeoutBuffer option PR-URL: https://github.com/nodejs/node/pull/59243 Reviewed-By: Matteo Collina Reviewed-By: Jason Zhang --- doc/api/http.md | 29 ++++++++++++-- lib/_http_server.js | 23 +++++++++-- .../test-http-keep-alive-timeout-buffer.js | 39 +++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-http-keep-alive-timeout-buffer.js diff --git a/doc/api/http.md b/doc/api/http.md index 295c9e3642dad9..8aa2e45c8f1bb7 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -1942,18 +1942,39 @@ added: v8.0.0 The number of milliseconds of inactivity a server needs to wait for additional incoming data, after it has finished writing the last response, before a socket -will be destroyed. If the server receives new data before the keep-alive -timeout has fired, it will reset the regular inactivity timeout, i.e., -[`server.timeout`][]. +will be destroyed. + +This timeout value is combined with the +\[`server.keepAliveTimeoutBuffer`]\[] option to determine the actual socket +timeout, calculated as: +socketTimeout = keepAliveTimeout + keepAliveTimeoutBuffer +If the server receives new data before the keep-alive timeout has fired, it +will reset the regular inactivity timeout, i.e., [`server.timeout`][]. A value of `0` will disable the keep-alive timeout behavior on incoming connections. -A value of `0` makes the http server behave similarly to Node.js versions prior +A value of `0` makes the HTTP server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout. The socket timeout logic is set up on connection, so changing this value only affects new connections to the server, not any existing connections. +### `server.keepAliveTimeoutBuffer` + + + +* Type: {number} Timeout in milliseconds. **Default:** `1000` (1 second). + +An additional buffer time added to the +[`server.keepAliveTimeout`][] to extend the internal socket timeout. + +This buffer helps reduce connection reset (`ECONNRESET`) errors by increasing +the socket timeout slightly beyond the advertised keep-alive timeout. + +This option applies only to new incoming connections. + ### `server[Symbol.asyncDispose]()` + +* {string|null} + +A string identifier for the current thread or null if the thread is not running. +On the corresponding worker object (if there is any), it is available as [`worker.threadName`][]. + ## `worker.workerData` + +* {string|null} + +A string identifier for the referenced thread or null if the thread is not running. +Inside the worker thread, it is available as [`require('node:worker_threads').threadName`][]. + ### `worker.unref()` + +The `Assert` class allows creating independent assertion instances with custom options. + +### `new assert.Assert([options])` + +* `options` {Object} + * `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`. + Accepted values: `'simple'`, `'full'`. + * `strict` {boolean} If set to `true`, non-strict methods behave like their + corresponding strict methods. Defaults to `true`. + +Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages. + +```js +const { Assert } = require('node:assert'); +const assertInstance = new Assert({ diff: 'full' }); +assertInstance.deepStrictEqual({ a: 1 }, { a: 2 }); +// Shows a full diff in the error message. +``` + +**Important**: When destructuring assertion methods from an `Assert` instance, +the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings). +The destructured methods will fall back to default behavior instead. + +```js +const myAssert = new Assert({ diff: 'full' }); + +// This works as expected - uses 'full' diff +myAssert.strictEqual({ a: 1 }, { b: { c: 1 } }); + +// This loses the 'full' diff setting - falls back to default 'simple' diff +const { strictEqual } = myAssert; +strictEqual({ a: 1 }, { b: { c: 1 } }); +``` + +When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior +(diff: 'simple', non-strict mode). +To maintain custom options when using destructured methods, avoid +destructuring and call methods directly on the instance. + ## Class: `assert.CallTracker` * `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, - `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. + `'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35], + `'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35]. * `options` {Object} * `modulusLength` {number} Key size in bits (RSA, DSA). * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. @@ -3767,6 +3786,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties. * `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, - `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. + `'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35], + `'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35]. * `options` {Object} * `modulusLength` {number} Key size in bits (RSA, DSA). * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. @@ -3816,7 +3839,7 @@ changes: * `privateKey` {string | Buffer | KeyObject} Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC, -Ed25519, Ed448, X25519, X448, and DH are currently supported. +Ed25519, Ed448, X25519, X448, DH, and ML-DSA[^openssl35] are currently supported. If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function behaves as if [`keyObject.export()`][] had been called on its result. Otherwise, @@ -5416,6 +5439,9 @@ Throws an error if FIPS mode is not available. -#### `pbkdb2Params.hash` +#### `pbkdf2Params.hash` - -* Type: {string\[]} - -An array detailing the key extended usages for this certificate. - ### `x509.fingerprint` + +* Type: {string\[]} + +An array detailing the key extended usages for this certificate. + ### `x509.publicKey` + +Node.js uses the trusted CA certificates present in the system store along with +the `--use-bundled-ca` option and the `NODE_EXTRA_CA_CERTS` environment variable. + +This can also be enabled using the [`--use-system-ca`][] command-line flag. +When both are set, `--use-system-ca` takes precedence. + ### `NODE_V8_COVERAGE=dir` When set, Node.js will begin outputting [V8 JavaScript code coverage][] and @@ -3977,6 +3989,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [`--redirect-warnings`]: #--redirect-warningsfile [`--require`]: #-r---require-module [`--use-env-proxy`]: #--use-env-proxy +[`--use-system-ca`]: #--use-system-ca [`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage [`Buffer`]: buffer.md#class-buffer [`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html diff --git a/doc/node.1 b/doc/node.1 index b3fab50f377070..3e9b376a0739b2 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -841,6 +841,12 @@ This currently only affects requests sent over .Ar fetch() . Support for other built-in http and https methods is under way. . +.It Ev NODE_USE_SYSTEM_CA +Similar to +.Fl -use-system-ca . +Use the trusted CA certificates present in the system store, in addition to the certificates in the +bundled Mozilla CA store and certificates from `NODE_EXTRA_CA_CERTS`. +. .It Ev NODE_V8_COVERAGE Ar dir When set, Node.js writes JavaScript code coverage information to .Ar dir . diff --git a/src/node.cc b/src/node.cc index 4566414d68ef0e..95585e7b16a830 100644 --- a/src/node.cc +++ b/src/node.cc @@ -868,6 +868,15 @@ static ExitCode InitializeNodeWithArgsInternal( // default value. V8::SetFlagsFromString("--rehash-snapshot"); +#if HAVE_OPENSSL + // TODO(joyeecheung): make this a per-env option and move the normalization + // into HandleEnvOptions. + std::string use_system_ca; + if (credentials::SafeGetenv("NODE_USE_SYSTEM_CA", &use_system_ca) && + use_system_ca == "1") { + per_process::cli_options->use_system_ca = true; + } +#endif // HAVE_OPENSSL HandleEnvOptions(per_process::cli_options->per_isolate->per_env); std::string node_options; diff --git a/test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js b/test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js new file mode 100644 index 00000000000000..81a5cba4da77e2 --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js @@ -0,0 +1,29 @@ +'use strict'; +// This tests that NODE_USE_SYSTEM_CA environment variable works the same +// as --use-system-ca flag by comparing certificate counts. + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const tls = require('tls'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); + +const systemCerts = tls.getCACertificates('system'); +if (systemCerts.length === 0) { + common.skip('no system certificates available'); +} + +const { child: { stdout: expectedLength } } = spawnSyncAndExitWithoutError(process.execPath, [ + '--use-system-ca', + '-p', + `tls.getCACertificates('default').length`, +], { + env: { ...process.env, NODE_USE_SYSTEM_CA: '0' }, +}); + +spawnSyncAndExitWithoutError(process.execPath, [ + '-p', + `assert.strictEqual(tls.getCACertificates('default').length, ${expectedLength.toString()})`, +], { + env: { ...process.env, NODE_USE_SYSTEM_CA: '1' }, +}); diff --git a/test/system-ca/test-native-root-certs-env.mjs b/test/system-ca/test-native-root-certs-env.mjs new file mode 100644 index 00000000000000..bde7dfcd9610bc --- /dev/null +++ b/test/system-ca/test-native-root-certs-env.mjs @@ -0,0 +1,56 @@ +// Env: NODE_USE_SYSTEM_CA=1 +// Same as test-native-root-certs.mjs, just testing the environment variable instead of the flag. + +import * as common from '../common/index.mjs'; +import assert from 'node:assert/strict'; +import https from 'node:https'; +import fixtures from '../common/fixtures.js'; +import { it, beforeEach, afterEach, describe } from 'node:test'; +import { once } from 'events'; + +if (!common.hasCrypto) { + common.skip('requires crypto'); +} + +// To run this test, the system needs to be configured to trust +// the CA certificate first (which needs an interactive GUI approval, e.g. TouchID): +// see the README.md in this folder for instructions on how to do this. +const handleRequest = (req, res) => { + const path = req.url; + switch (path) { + case '/hello-world': + res.writeHead(200); + res.end('hello world\n'); + break; + default: + assert(false, `Unexpected path: ${path}`); + } +}; + +describe('use-system-ca', function() { + + async function setupServer(key, cert) { + const theServer = https.createServer({ + key: fixtures.readKey(key), + cert: fixtures.readKey(cert), + }, handleRequest); + theServer.listen(0); + await once(theServer, 'listening'); + + return theServer; + } + + let server; + + beforeEach(async function() { + server = await setupServer('agent8-key.pem', 'agent8-cert.pem'); + }); + + it('trusts a valid root certificate', async function() { + await fetch(`https://localhost:${server.address().port}/hello-world`); + }); + + afterEach(async function() { + server?.close(); + }); +}); From 7e10f95f13150e17aa2c296f1ef91388ba101258 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sun, 10 Aug 2025 21:35:42 +0200 Subject: [PATCH 74/80] test: split test-fs-cp.js This test previously squeezed 70+ test cases into one single file and has been constantly crashing on Windows with exit code 3221226505 and no stack trace. As it is already marked as flaky there is no way to understand which test case is failing and the Windows CI was constantly orange. This patch splits the test cases into different files so it's easier to find out which case is exactly failing and to be skipped. PR-URL: https://github.com/nodejs/node/pull/59408 Refs: https://github.com/nodejs/node/issues/56794 Reviewed-By: Luigi Pinca Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Daeyeon Jeong Reviewed-By: Filip Skokan --- test/common/fs.js | 49 + test/parallel/parallel.status | 10 +- ...test-fs-cp-async-async-filter-function.mjs | 32 + ...fs-cp-async-copy-non-directory-symlink.mjs | 22 + ...nc-dereference-force-false-silent-fail.mjs | 25 + .../test-fs-cp-async-dereference-symlink.mjs | 27 + ...async-dest-symlink-points-to-src-error.mjs | 21 + .../parallel/test-fs-cp-async-dir-to-file.mjs | 17 + .../test-fs-cp-async-error-on-exist.mjs | 22 + .../parallel/test-fs-cp-async-file-to-dir.mjs | 17 + .../test-fs-cp-async-file-to-file.mjs | 19 + test/parallel/test-fs-cp-async-file-url.mjs | 18 + .../test-fs-cp-async-filter-child-folder.mjs | 26 + .../test-fs-cp-async-filter-function.mjs | 31 + .../test-fs-cp-async-identical-src-dest.mjs | 14 + .../test-fs-cp-async-invalid-mode-range.mjs | 13 + .../test-fs-cp-async-invalid-options-type.mjs | 13 + .../test-fs-cp-async-nested-files-folders.mjs | 17 + ...test-fs-cp-async-no-errors-force-false.mjs | 28 + .../test-fs-cp-async-no-recursive.mjs | 16 + ...test-fs-cp-async-overwrites-force-true.mjs | 23 + ...sync-preserve-timestamps-readonly-file.mjs | 26 + .../test-fs-cp-async-preserve-timestamps.mjs | 24 + .../test-fs-cp-async-same-dir-twice.mjs | 25 + ...cp-async-skip-validation-when-filtered.mjs | 29 + test/parallel/test-fs-cp-async-socket.mjs | 34 + .../test-fs-cp-async-subdirectory-of-self.mjs | 12 + ...fs-cp-async-symlink-dest-points-to-src.mjs | 21 + .../test-fs-cp-async-symlink-over-file.mjs | 21 + ...est-fs-cp-async-symlink-points-to-dest.mjs | 20 + .../test-fs-cp-async-with-mode-flags.mjs | 31 + .../test-fs-cp-promises-async-error.mjs | 23 + .../parallel/test-fs-cp-promises-file-url.mjs | 21 + .../test-fs-cp-promises-invalid-mode.mjs | 15 + .../test-fs-cp-promises-mode-flags.mjs | 36 + ...fs-cp-promises-nested-folder-recursive.mjs | 16 + ...test-fs-cp-promises-options-validation.mjs | 13 + .../test-fs-cp-sync-apply-filter-function.mjs | 28 + .../test-fs-cp-sync-async-filter-error.mjs | 24 + ...s-cp-sync-copy-directory-to-file-error.mjs | 24 + ...copy-directory-without-recursive-error.mjs | 16 + ...s-cp-sync-copy-file-to-directory-error.mjs | 22 + ...test-fs-cp-sync-copy-file-to-file-path.mjs | 13 + .../test-fs-cp-sync-copy-socket-error.mjs | 34 + ...nc-copy-symlink-not-pointing-to-folder.mjs | 19 + ...s-cp-sync-copy-symlink-over-file-error.mjs | 21 + ...ync-copy-symlinks-to-existing-symlinks.mjs | 17 + ...-fs-cp-sync-copy-to-subdirectory-error.mjs | 14 + .../test-fs-cp-sync-dereference-directory.mjs | 23 + .../test-fs-cp-sync-dereference-file.mjs | 23 + .../test-fs-cp-sync-dereference-twice.mjs | 20 + ...test-fs-cp-sync-dest-name-prefix-match.mjs | 14 + ...-cp-sync-dest-parent-name-prefix-match.mjs | 16 + ...t-fs-cp-sync-directory-not-exist-error.mjs | 15 + .../test-fs-cp-sync-error-on-exist.mjs | 22 + test/parallel/test-fs-cp-sync-file-url.mjs | 14 + ...est-fs-cp-sync-filename-too-long-error.mjs | 17 + ...-fs-cp-sync-incompatible-options-error.mjs | 14 + test/parallel/test-fs-cp-sync-mode-flags.mjs | 30 + .../parallel/test-fs-cp-sync-mode-invalid.mjs | 12 + .../test-fs-cp-sync-nested-files-folders.mjs | 13 + ...st-fs-cp-sync-no-overwrite-force-false.mjs | 21 + ...-fs-cp-sync-options-invalid-type-error.mjs | 12 + .../test-fs-cp-sync-overwrite-force-true.mjs | 19 + ...arent-symlink-dest-points-to-src-error.mjs | 26 + ...s-cp-sync-preserve-timestamps-readonly.mjs | 24 + .../test-fs-cp-sync-preserve-timestamps.mjs | 18 + ...sync-resolve-relative-symlinks-default.mjs | 21 + ...p-sync-resolve-relative-symlinks-false.mjs | 21 + ...st-fs-cp-sync-src-dest-identical-error.mjs | 14 + ...st-fs-cp-sync-src-parent-of-dest-error.mjs | 25 + ...-sync-symlink-dest-points-to-src-error.mjs | 21 + ...s-cp-sync-symlink-points-to-dest-error.mjs | 22 + .../test-fs-cp-sync-unicode-folder-names.mjs | 13 + ...t-fs-cp-sync-verbatim-symlinks-invalid.mjs | 17 + ...test-fs-cp-sync-verbatim-symlinks-true.mjs | 21 + test/parallel/test-fs-cp.mjs | 1098 ----------------- 77 files changed, 1585 insertions(+), 1100 deletions(-) create mode 100644 test/common/fs.js create mode 100644 test/parallel/test-fs-cp-async-async-filter-function.mjs create mode 100644 test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs create mode 100644 test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs create mode 100644 test/parallel/test-fs-cp-async-dereference-symlink.mjs create mode 100644 test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs create mode 100644 test/parallel/test-fs-cp-async-dir-to-file.mjs create mode 100644 test/parallel/test-fs-cp-async-error-on-exist.mjs create mode 100644 test/parallel/test-fs-cp-async-file-to-dir.mjs create mode 100644 test/parallel/test-fs-cp-async-file-to-file.mjs create mode 100644 test/parallel/test-fs-cp-async-file-url.mjs create mode 100644 test/parallel/test-fs-cp-async-filter-child-folder.mjs create mode 100644 test/parallel/test-fs-cp-async-filter-function.mjs create mode 100644 test/parallel/test-fs-cp-async-identical-src-dest.mjs create mode 100644 test/parallel/test-fs-cp-async-invalid-mode-range.mjs create mode 100644 test/parallel/test-fs-cp-async-invalid-options-type.mjs create mode 100644 test/parallel/test-fs-cp-async-nested-files-folders.mjs create mode 100644 test/parallel/test-fs-cp-async-no-errors-force-false.mjs create mode 100644 test/parallel/test-fs-cp-async-no-recursive.mjs create mode 100644 test/parallel/test-fs-cp-async-overwrites-force-true.mjs create mode 100644 test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs create mode 100644 test/parallel/test-fs-cp-async-preserve-timestamps.mjs create mode 100644 test/parallel/test-fs-cp-async-same-dir-twice.mjs create mode 100644 test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs create mode 100644 test/parallel/test-fs-cp-async-socket.mjs create mode 100644 test/parallel/test-fs-cp-async-subdirectory-of-self.mjs create mode 100644 test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs create mode 100644 test/parallel/test-fs-cp-async-symlink-over-file.mjs create mode 100644 test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs create mode 100644 test/parallel/test-fs-cp-async-with-mode-flags.mjs create mode 100644 test/parallel/test-fs-cp-promises-async-error.mjs create mode 100644 test/parallel/test-fs-cp-promises-file-url.mjs create mode 100644 test/parallel/test-fs-cp-promises-invalid-mode.mjs create mode 100644 test/parallel/test-fs-cp-promises-mode-flags.mjs create mode 100644 test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs create mode 100644 test/parallel/test-fs-cp-promises-options-validation.mjs create mode 100644 test/parallel/test-fs-cp-sync-apply-filter-function.mjs create mode 100644 test/parallel/test-fs-cp-sync-async-filter-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-socket-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs create mode 100644 test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-dereference-directory.mjs create mode 100644 test/parallel/test-fs-cp-sync-dereference-file.mjs create mode 100644 test/parallel/test-fs-cp-sync-dereference-twice.mjs create mode 100644 test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs create mode 100644 test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs create mode 100644 test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-error-on-exist.mjs create mode 100644 test/parallel/test-fs-cp-sync-file-url.mjs create mode 100644 test/parallel/test-fs-cp-sync-filename-too-long-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-incompatible-options-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-mode-flags.mjs create mode 100644 test/parallel/test-fs-cp-sync-mode-invalid.mjs create mode 100644 test/parallel/test-fs-cp-sync-nested-files-folders.mjs create mode 100644 test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs create mode 100644 test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-overwrite-force-true.mjs create mode 100644 test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs create mode 100644 test/parallel/test-fs-cp-sync-preserve-timestamps.mjs create mode 100644 test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs create mode 100644 test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs create mode 100644 test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs create mode 100644 test/parallel/test-fs-cp-sync-unicode-folder-names.mjs create mode 100644 test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs create mode 100644 test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs delete mode 100644 test/parallel/test-fs-cp.mjs diff --git a/test/common/fs.js b/test/common/fs.js new file mode 100644 index 00000000000000..fdf7cbece5ceb9 --- /dev/null +++ b/test/common/fs.js @@ -0,0 +1,49 @@ +'use strict'; + +const { mustNotMutateObjectDeep } = require('.'); +const { readdirSync } = require('node:fs'); +const { join } = require('node:path'); +const assert = require('node:assert'); +const tmpdir = require('./tmpdir.js'); + +let dirc = 0; +function nextdir(dirname) { + return tmpdir.resolve(dirname || `copy_%${++dirc}`); +} + +function assertDirEquivalent(dir1, dir2) { + const dir1Entries = []; + collectEntries(dir1, dir1Entries); + const dir2Entries = []; + collectEntries(dir2, dir2Entries); + assert.strictEqual(dir1Entries.length, dir2Entries.length); + for (const entry1 of dir1Entries) { + const entry2 = dir2Entries.find((entry) => { + return entry.name === entry1.name; + }); + assert(entry2, `entry ${entry2.name} not copied`); + if (entry1.isFile()) { + assert(entry2.isFile(), `${entry2.name} was not file`); + } else if (entry1.isDirectory()) { + assert(entry2.isDirectory(), `${entry2.name} was not directory`); + } else if (entry1.isSymbolicLink()) { + assert(entry2.isSymbolicLink(), `${entry2.name} was not symlink`); + } + } +} + +function collectEntries(dir, dirEntries) { + const newEntries = readdirSync(dir, mustNotMutateObjectDeep({ withFileTypes: true })); + for (const entry of newEntries) { + if (entry.isDirectory()) { + collectEntries(join(dir, entry.name), dirEntries); + } + } + dirEntries.push(...newEntries); +} + +module.exports = { + nextdir, + assertDirEquivalent, + collectEntries, +}; diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status index 77682bc8498fbd..76e46b80719e07 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status @@ -24,8 +24,14 @@ test-snapshot-incompatible: SKIP test-async-context-frame: PASS, FLAKY # https://github.com/nodejs/node/issues/54534 test-runner-run-watch: PASS, FLAKY -# https://github.com/nodejs/node/issues/56794 -test-fs-cp: PASS, FLAKY +# https://github.com/nodejs/node/pull/59408#issuecomment-3170650933 +test-fs-cp-sync-error-on-exist: PASS, FLAKY +test-fs-cp-sync-copy-symlink-not-pointing-to-folder: PASS, FLAKY +test-fs-cp-sync-symlink-points-to-dest-error: PASS, FLAKY +test-fs-cp-sync-resolve-relative-symlinks-false: PASS, FLAKY +test-fs-cp-async-symlink-points-to-dest: PASS, FLAKY +test-fs-cp-sync-unicode-folder-names: PASS, FLAKY +test-fs-cp-sync-resolve-relative-symlinks-default: PASS, FLAKY # https://github.com/nodejs/node/issues/56751 test-without-async-context-frame: PASS, FLAKY diff --git a/test/parallel/test-fs-cp-async-async-filter-function.mjs b/test/parallel/test-fs-cp-async-async-filter-function.mjs new file mode 100644 index 00000000000000..91b17a0f8823de --- /dev/null +++ b/test/parallel/test-fs-cp-async-async-filter-function.mjs @@ -0,0 +1,32 @@ +// This tests that cp() supports async filter function. + +import { mustCall } from '../common/index.mjs'; +import { nextdir, collectEntries } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, statSync } from 'node:fs'; +import { setTimeout } from 'node:timers/promises'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cp(src, dest, { + filter: async (path) => { + await setTimeout(5, 'done'); + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, +}, mustCall((err) => { + assert.strictEqual(err, null); + const destEntries = []; + collectEntries(dest, destEntries); + for (const entry of destEntries) { + assert.strictEqual( + entry.isDirectory() || entry.name.endsWith('.js'), + true + ); + } +})); diff --git a/test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs b/test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs new file mode 100644 index 00000000000000..a5064a9a71a269 --- /dev/null +++ b/test/parallel/test-fs-cp-async-copy-non-directory-symlink.mjs @@ -0,0 +1,22 @@ +// This tests that cp() copies link if it does not point to folder in src. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, mkdirSync, readlinkSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(src, join(src, 'a', 'c')); +const dest = nextdir(); +mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(dest, join(dest, 'a', 'c')); +cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { + assert.strictEqual(err, null); + const link = readlinkSync(join(dest, 'a', 'c')); + assert.strictEqual(link, src); +})); diff --git a/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs b/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs new file mode 100644 index 00000000000000..0c92b3eb6276c5 --- /dev/null +++ b/test/parallel/test-fs-cp-async-dereference-force-false-silent-fail.mjs @@ -0,0 +1,25 @@ +// This tests that it does not fail if the same directory is copied to dest +// twice, when dereference is true, and force is false (fails silently). + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp, cpSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import { nextdir } from '../common/fs.js'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +const destFile = join(dest, 'a/b/README2.md'); +cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); +cp(src, dest, { + dereference: true, + recursive: true +}, mustCall((err) => { + assert.strictEqual(err, null); + const stat = lstatSync(destFile); + assert(stat.isFile()); +})); diff --git a/test/parallel/test-fs-cp-async-dereference-symlink.mjs b/test/parallel/test-fs-cp-async-dereference-symlink.mjs new file mode 100644 index 00000000000000..7edc8b8c66bbc6 --- /dev/null +++ b/test/parallel/test-fs-cp-async-dereference-symlink.mjs @@ -0,0 +1,27 @@ +// This tests that cp() copies file itself, rather than symlink, when dereference is true. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, lstatSync, mkdirSync, symlinkSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); +symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); + +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +const destFile = join(dest, 'foo.js'); + +cp(join(src, 'bar.js'), destFile, mustNotMutateObjectDeep({ dereference: true }), + mustCall((err) => { + assert.strictEqual(err, null); + const stat = lstatSync(destFile); + assert(stat.isFile()); + }) +); diff --git a/test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs b/test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs new file mode 100644 index 00000000000000..da8d606963bf53 --- /dev/null +++ b/test/parallel/test-fs-cp-async-dest-symlink-points-to-src-error.mjs @@ -0,0 +1,21 @@ +// This tests that cp() returns error if parent directory of symlink in dest points to src. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, mkdirSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a'), mustNotMutateObjectDeep({ recursive: true })); +const dest = nextdir(); +// Create symlink in dest pointing to src. +const destLink = join(dest, 'b'); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(src, destLink); +cp(src, join(dest, 'b', 'c'), mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); +})); diff --git a/test/parallel/test-fs-cp-async-dir-to-file.mjs b/test/parallel/test-fs-cp-async-dir-to-file.mjs new file mode 100644 index 00000000000000..16d9ce6f0c23f2 --- /dev/null +++ b/test/parallel/test-fs-cp-async-dir-to-file.mjs @@ -0,0 +1,17 @@ +// This tests that cp() returns error if attempt is made to copy directory to file. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, mkdirSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +const dest = fixtures.path('copy/kitchen-sink/README.md'); +cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_DIR_TO_NON_DIR'); +})); diff --git a/test/parallel/test-fs-cp-async-error-on-exist.mjs b/test/parallel/test-fs-cp-async-error-on-exist.mjs new file mode 100644 index 00000000000000..c6df45db7f3117 --- /dev/null +++ b/test/parallel/test-fs-cp-async-error-on-exist.mjs @@ -0,0 +1,22 @@ +// This tests that cp() returns error if errorOnExist is true, force is false, and file or folder copied over. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +cp(src, dest, { + dereference: true, + errorOnExist: true, + force: false, + recursive: true, +}, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EEXIST'); +})); diff --git a/test/parallel/test-fs-cp-async-file-to-dir.mjs b/test/parallel/test-fs-cp-async-file-to-dir.mjs new file mode 100644 index 00000000000000..94dc193f5c529a --- /dev/null +++ b/test/parallel/test-fs-cp-async-file-to-dir.mjs @@ -0,0 +1,17 @@ +// This tests that cp() returns error if attempt is made to copy file to directory. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, mkdirSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink/README.md'); +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_NON_DIR_TO_DIR'); +})); diff --git a/test/parallel/test-fs-cp-async-file-to-file.mjs b/test/parallel/test-fs-cp-async-file-to-file.mjs new file mode 100644 index 00000000000000..faff91e6a2bb8c --- /dev/null +++ b/test/parallel/test-fs-cp-async-file-to-file.mjs @@ -0,0 +1,19 @@ +// This tests that cp() allows file to be copied to a file path. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const srcFile = fixtures.path('copy/kitchen-sink/README.md'); +const destFile = join(nextdir(), 'index.js'); +cp(srcFile, destFile, mustNotMutateObjectDeep({ dereference: true }), mustCall((err) => { + assert.strictEqual(err, null); + const stat = lstatSync(destFile); + assert(stat.isFile()); +})); diff --git a/test/parallel/test-fs-cp-async-file-url.mjs b/test/parallel/test-fs-cp-async-file-url.mjs new file mode 100644 index 00000000000000..4c1ea1fdd56b8d --- /dev/null +++ b/test/parallel/test-fs-cp-async-file-url.mjs @@ -0,0 +1,18 @@ +// This tests that it accepts file URL as src and dest. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp } from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import tmpdir from '../common/tmpdir.js'; +import { assertDirEquivalent, nextdir } from '../common/fs.js'; + +tmpdir.refresh(); + +const src = './test/fixtures/copy/kitchen-sink'; +const dest = nextdir(); +cp(pathToFileURL(src), pathToFileURL(dest), mustNotMutateObjectDeep({ recursive: true }), + mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + })); diff --git a/test/parallel/test-fs-cp-async-filter-child-folder.mjs b/test/parallel/test-fs-cp-async-filter-child-folder.mjs new file mode 100644 index 00000000000000..b1ebeca514e438 --- /dev/null +++ b/test/parallel/test-fs-cp-async-filter-child-folder.mjs @@ -0,0 +1,26 @@ +// This tests that cp() should not throw exception if child folder is filtered out. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, cpSync, mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'test-cp'), mustNotMutateObjectDeep({ recursive: true })); + +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(dest, 'test-cp'), 'test-content', mustNotMutateObjectDeep({ mode: 0o444 })); + +const opts = { + filter: (path) => !path.includes('test-cp'), + recursive: true, +}; +cp(src, dest, opts, mustCall((err) => { + assert.strictEqual(err, null); +})); +cpSync(src, dest, opts); diff --git a/test/parallel/test-fs-cp-async-filter-function.mjs b/test/parallel/test-fs-cp-async-filter-function.mjs new file mode 100644 index 00000000000000..bb8145b035b881 --- /dev/null +++ b/test/parallel/test-fs-cp-async-filter-function.mjs @@ -0,0 +1,31 @@ +// This tests that cp() applies filter function. + +import { mustCall } from '../common/index.mjs'; +import { nextdir, collectEntries } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, statSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cp(src, dest, { + filter: (path) => { + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, +}, mustCall((err) => { + assert.strictEqual(err, null); + const destEntries = []; + collectEntries(dest, destEntries); + for (const entry of destEntries) { + assert.strictEqual( + entry.isDirectory() || entry.name.endsWith('.js'), + true + ); + } +})); diff --git a/test/parallel/test-fs-cp-async-identical-src-dest.mjs b/test/parallel/test-fs-cp-async-identical-src-dest.mjs new file mode 100644 index 00000000000000..20bed5f9268576 --- /dev/null +++ b/test/parallel/test-fs-cp-async-identical-src-dest.mjs @@ -0,0 +1,14 @@ +// This tests that cp() returns error when src and dest are identical. + +import { mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +cp(src, src, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); +})); diff --git a/test/parallel/test-fs-cp-async-invalid-mode-range.mjs b/test/parallel/test-fs-cp-async-invalid-mode-range.mjs new file mode 100644 index 00000000000000..12be6a6fadd284 --- /dev/null +++ b/test/parallel/test-fs-cp-async-invalid-mode-range.mjs @@ -0,0 +1,13 @@ +// This tests that cp() throws if mode is out of range. + +import '../common/index.mjs'; +import assert from 'node:assert'; +import { cp } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +assert.throws( + () => cp('a', 'b', { mode: -1 }, () => {}), + { code: 'ERR_OUT_OF_RANGE' } +); diff --git a/test/parallel/test-fs-cp-async-invalid-options-type.mjs b/test/parallel/test-fs-cp-async-invalid-options-type.mjs new file mode 100644 index 00000000000000..7c84d5f680132b --- /dev/null +++ b/test/parallel/test-fs-cp-async-invalid-options-type.mjs @@ -0,0 +1,13 @@ +// This tests that cp() throws if options is not object. + +import '../common/index.mjs'; +import assert from 'node:assert'; +import { cp } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +assert.throws( + () => cp('a', 'b', 'hello', () => {}), + { code: 'ERR_INVALID_ARG_TYPE' } +); diff --git a/test/parallel/test-fs-cp-async-nested-files-folders.mjs b/test/parallel/test-fs-cp-async-nested-files-folders.mjs new file mode 100644 index 00000000000000..9cc2115bc1fc7e --- /dev/null +++ b/test/parallel/test-fs-cp-async-nested-files-folders.mjs @@ -0,0 +1,17 @@ +// This tests that cp() copies a nested folder structure with files and folders. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); +})); diff --git a/test/parallel/test-fs-cp-async-no-errors-force-false.mjs b/test/parallel/test-fs-cp-async-no-errors-force-false.mjs new file mode 100644 index 00000000000000..609706a8306f7c --- /dev/null +++ b/test/parallel/test-fs-cp-async-no-errors-force-false.mjs @@ -0,0 +1,28 @@ +// This tests that it does not throw errors when directory is copied over and force is false. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp, cpSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import { assertDirEquivalent, nextdir } from '../common/fs.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); +const initialStat = lstatSync(join(dest, 'README.md')); +cp(src, dest, { + dereference: true, + force: false, + recursive: true, +}, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + // File should not have been copied over, so access times will be identical: + const finalStat = lstatSync(join(dest, 'README.md')); + assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); +})); diff --git a/test/parallel/test-fs-cp-async-no-recursive.mjs b/test/parallel/test-fs-cp-async-no-recursive.mjs new file mode 100644 index 00000000000000..fd58ee81d58eaa --- /dev/null +++ b/test/parallel/test-fs-cp-async-no-recursive.mjs @@ -0,0 +1,16 @@ +// This tests that cp() returns error if directory copied without recursive flag. + +import { mustCall } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_EISDIR'); +})); diff --git a/test/parallel/test-fs-cp-async-overwrites-force-true.mjs b/test/parallel/test-fs-cp-async-overwrites-force-true.mjs new file mode 100644 index 00000000000000..1d5006256a7a12 --- /dev/null +++ b/test/parallel/test-fs-cp-async-overwrites-force-true.mjs @@ -0,0 +1,23 @@ +// This tests that it overwrites existing files if force is true. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import { assertDirEquivalent, nextdir } from '../common/fs.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); + +cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + const content = readFileSync(join(dest, 'README.md'), 'utf8'); + assert.strictEqual(content.trim(), '# Hello'); +})); diff --git a/test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs b/test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs new file mode 100644 index 00000000000000..fff2e199fcd039 --- /dev/null +++ b/test/parallel/test-fs-cp-async-preserve-timestamps-readonly-file.mjs @@ -0,0 +1,26 @@ +// This tests that it makes file writeable when updating timestamp, if not writeable. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp, lstatSync, mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import { assertDirEquivalent, nextdir } from '../common/fs.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.txt'), 'foo', mustNotMutateObjectDeep({ mode: 0o444 })); +cp(src, dest, { + preserveTimestamps: true, + recursive: true, +}, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + const srcStat = lstatSync(join(src, 'foo.txt')); + const destStat = lstatSync(join(dest, 'foo.txt')); + assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); +})); diff --git a/test/parallel/test-fs-cp-async-preserve-timestamps.mjs b/test/parallel/test-fs-cp-async-preserve-timestamps.mjs new file mode 100644 index 00000000000000..8e55f07ca6024e --- /dev/null +++ b/test/parallel/test-fs-cp-async-preserve-timestamps.mjs @@ -0,0 +1,24 @@ +// This tests that cp() copies timestamps from src to dest if preserveTimestamps is true. + +import { mustCall } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cp(src, dest, { + preserveTimestamps: true, + recursive: true +}, mustCall((err) => { + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + const srcStat = lstatSync(join(src, 'index.js')); + const destStat = lstatSync(join(dest, 'index.js')); + assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); +})); diff --git a/test/parallel/test-fs-cp-async-same-dir-twice.mjs b/test/parallel/test-fs-cp-async-same-dir-twice.mjs new file mode 100644 index 00000000000000..0c92b3eb6276c5 --- /dev/null +++ b/test/parallel/test-fs-cp-async-same-dir-twice.mjs @@ -0,0 +1,25 @@ +// This tests that it does not fail if the same directory is copied to dest +// twice, when dereference is true, and force is false (fails silently). + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp, cpSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import { nextdir } from '../common/fs.js'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +const destFile = join(dest, 'a/b/README2.md'); +cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); +cp(src, dest, { + dereference: true, + recursive: true +}, mustCall((err) => { + assert.strictEqual(err, null); + const stat = lstatSync(destFile); + assert(stat.isFile()); +})); diff --git a/test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs b/test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs new file mode 100644 index 00000000000000..4778bc23319158 --- /dev/null +++ b/test/parallel/test-fs-cp-async-skip-validation-when-filtered.mjs @@ -0,0 +1,29 @@ +// This tests that cp() should not throw exception if dest is invalid but filtered out. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, cpSync, mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +// Create dest as a file. +// Expect: cp skips the copy logic entirely and won't throw any exception in path validation process. +const src = join(nextdir(), 'bar'); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); + +const destParent = nextdir(); +const dest = join(destParent, 'bar'); +mkdirSync(destParent, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(dest, 'test-content', mustNotMutateObjectDeep({ mode: 0o444 })); + +const opts = { + filter: (path) => !path.includes('bar'), + recursive: true, +}; +cp(src, dest, opts, mustCall((err) => { + assert.strictEqual(err, null); +})); +cpSync(src, dest, opts); diff --git a/test/parallel/test-fs-cp-async-socket.mjs b/test/parallel/test-fs-cp-async-socket.mjs new file mode 100644 index 00000000000000..4ebc56cc3f664f --- /dev/null +++ b/test/parallel/test-fs-cp-async-socket.mjs @@ -0,0 +1,34 @@ +// This tests that cp() returns an error if attempt is made to copy socket. + +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp, mkdirSync } from 'node:fs'; +import { createServer } from 'node:net'; +import { join } from 'node:path'; +import { nextdir } from '../common/fs.js'; +import tmpdir from '../common/tmpdir.js'; + +const isWindows = process.platform === 'win32'; +if (isWindows) { + common.skip('No socket support on Windows'); +} + +// See https://github.com/nodejs/node/pull/48409 +if (common.isInsideDirWithUnusualChars) { + common.skip('Test is borken in directories with unusual characters'); +} + +tmpdir.refresh(); + +{ + const src = nextdir(); + mkdirSync(src); + const dest = nextdir(); + const sock = join(src, `${process.pid}.sock`); + const server = createServer(); + server.listen(sock); + cp(sock, dest, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_SOCKET'); + server.close(); + })); +} diff --git a/test/parallel/test-fs-cp-async-subdirectory-of-self.mjs b/test/parallel/test-fs-cp-async-subdirectory-of-self.mjs new file mode 100644 index 00000000000000..6884b2ddcd6998 --- /dev/null +++ b/test/parallel/test-fs-cp-async-subdirectory-of-self.mjs @@ -0,0 +1,12 @@ +// This tests that cp() returns error if attempt is made to copy to subdirectory of self. + +import { mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp } from 'node:fs'; +import fixtures from '../common/fixtures.js'; + +const src = fixtures.path('copy/kitchen-sink'); +const dest = fixtures.path('copy/kitchen-sink/a'); +cp(src, dest, mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); +})); diff --git a/test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs b/test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs new file mode 100644 index 00000000000000..dab9c572f9d7d1 --- /dev/null +++ b/test/parallel/test-fs-cp-async-symlink-dest-points-to-src.mjs @@ -0,0 +1,21 @@ +// This tests that cp() returns error if symlink in dest points to location in src. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, mkdirSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + +const dest = nextdir(); +mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(src, join(dest, 'a', 'c')); +cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY'); +})); diff --git a/test/parallel/test-fs-cp-async-symlink-over-file.mjs b/test/parallel/test-fs-cp-async-symlink-over-file.mjs new file mode 100644 index 00000000000000..8685a3d611dcf2 --- /dev/null +++ b/test/parallel/test-fs-cp-async-symlink-over-file.mjs @@ -0,0 +1,21 @@ +// This tests that cp() returns EEXIST error if attempt is made to copy symlink over file. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, mkdirSync, symlinkSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + +const dest = nextdir(); +mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); +cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { + assert.strictEqual(err.code, 'EEXIST'); +})); diff --git a/test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs b/test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs new file mode 100644 index 00000000000000..f8e60fe1fa2b2e --- /dev/null +++ b/test/parallel/test-fs-cp-async-symlink-points-to-dest.mjs @@ -0,0 +1,20 @@ +// This tests that cp() returns error if symlink in src points to location in dest. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cp, cpSync, mkdirSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +const dest = nextdir(); +mkdirSync(dest); +symlinkSync(dest, join(src, 'link')); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { + assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); +})); diff --git a/test/parallel/test-fs-cp-async-with-mode-flags.mjs b/test/parallel/test-fs-cp-async-with-mode-flags.mjs new file mode 100644 index 00000000000000..99f6b5fe09d916 --- /dev/null +++ b/test/parallel/test-fs-cp-async-with-mode-flags.mjs @@ -0,0 +1,31 @@ +// This tests that it copies a nested folder structure with mode flags. + +import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cp, constants } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import { assertDirEquivalent, nextdir } from '../common/fs.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cp(src, dest, mustNotMutateObjectDeep({ + recursive: true, + mode: constants.COPYFILE_FICLONE_FORCE, +}), mustCall((err) => { + if (!err) { + // If the platform support `COPYFILE_FICLONE_FORCE` operation, + // it should reach to here. + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + return; + } + + // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, + // it should enter this path. + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); +})); diff --git a/test/parallel/test-fs-cp-promises-async-error.mjs b/test/parallel/test-fs-cp-promises-async-error.mjs new file mode 100644 index 00000000000000..b7817cfd8073de --- /dev/null +++ b/test/parallel/test-fs-cp-promises-async-error.mjs @@ -0,0 +1,23 @@ +// This tests that fs.promises.cp() allows async error to be caught. + +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ recursive: true })); +await assert.rejects( + fs.promises.cp(src, dest, { + dereference: true, + errorOnExist: true, + force: false, + recursive: true, + }), + { code: 'ERR_FS_CP_EEXIST' } +); diff --git a/test/parallel/test-fs-cp-promises-file-url.mjs b/test/parallel/test-fs-cp-promises-file-url.mjs new file mode 100644 index 00000000000000..552e9c0b272f9a --- /dev/null +++ b/test/parallel/test-fs-cp-promises-file-url.mjs @@ -0,0 +1,21 @@ +// This tests that fs.promises.cp() accepts file URL as src and dest. + +import '../common/index.mjs'; +import assert from 'node:assert'; +import { promises as fs } from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import { assertDirEquivalent, nextdir } from '../common/fs.js'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +const p = await fs.cp( + pathToFileURL(src), + pathToFileURL(dest), + { recursive: true } +); +assert.strictEqual(p, undefined); +assertDirEquivalent(src, dest); diff --git a/test/parallel/test-fs-cp-promises-invalid-mode.mjs b/test/parallel/test-fs-cp-promises-invalid-mode.mjs new file mode 100644 index 00000000000000..2e200e56afe0bd --- /dev/null +++ b/test/parallel/test-fs-cp-promises-invalid-mode.mjs @@ -0,0 +1,15 @@ +// This tests that fs.promises.cp() rejects if options.mode is invalid. + +import '../common/index.mjs'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +await assert.rejects( + fs.promises.cp('a', 'b', { + mode: -1, + }), + { code: 'ERR_OUT_OF_RANGE' } +); diff --git a/test/parallel/test-fs-cp-promises-mode-flags.mjs b/test/parallel/test-fs-cp-promises-mode-flags.mjs new file mode 100644 index 00000000000000..b5633ae5e7f409 --- /dev/null +++ b/test/parallel/test-fs-cp-promises-mode-flags.mjs @@ -0,0 +1,36 @@ +// This tests that fs.promises.cp() copies a nested folder structure with mode flags. +// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. + +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { promises as fs, constants } from 'node:fs'; +import { assertDirEquivalent, nextdir } from '../common/fs.js'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +let p = null; +let successFiClone = false; +try { + p = await fs.cp(src, dest, mustNotMutateObjectDeep({ + recursive: true, + mode: constants.COPYFILE_FICLONE_FORCE, + })); + successFiClone = true; +} catch (err) { + // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, + // it should enter this path. + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); +} + +if (successFiClone) { + // If the platform support `COPYFILE_FICLONE_FORCE` operation, + // it should reach to here. + assert.strictEqual(p, undefined); + assertDirEquivalent(src, dest); +} diff --git a/test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs b/test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs new file mode 100644 index 00000000000000..21cd1ba9d257a6 --- /dev/null +++ b/test/parallel/test-fs-cp-promises-nested-folder-recursive.mjs @@ -0,0 +1,16 @@ +// This tests that fs.promises.cp() copies a nested folder structure with files and folders. + +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +const p = await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ recursive: true })); +assert.strictEqual(p, undefined); +assertDirEquivalent(src, dest); diff --git a/test/parallel/test-fs-cp-promises-options-validation.mjs b/test/parallel/test-fs-cp-promises-options-validation.mjs new file mode 100644 index 00000000000000..a2cfad4552e429 --- /dev/null +++ b/test/parallel/test-fs-cp-promises-options-validation.mjs @@ -0,0 +1,13 @@ +// This tests that fs.promises.cp() rejects if options is not object. + +import '../common/index.mjs'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +await assert.rejects( + fs.promises.cp('a', 'b', () => {}), + { code: 'ERR_INVALID_ARG_TYPE' } +); diff --git a/test/parallel/test-fs-cp-sync-apply-filter-function.mjs b/test/parallel/test-fs-cp-sync-apply-filter-function.mjs new file mode 100644 index 00000000000000..02e7446c572ee9 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-apply-filter-function.mjs @@ -0,0 +1,28 @@ +// This tests that cpSync applies filter function. +import '../common/index.mjs'; +import { nextdir, collectEntries } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, statSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cpSync(src, dest, { + filter: (path) => { + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, +}); +const destEntries = []; +collectEntries(dest, destEntries); +for (const entry of destEntries) { + assert.strictEqual( + entry.isDirectory() || entry.name.endsWith('.js'), + true + ); +} diff --git a/test/parallel/test-fs-cp-sync-async-filter-error.mjs b/test/parallel/test-fs-cp-sync-async-filter-error.mjs new file mode 100644 index 00000000000000..0bb2e62e768bbe --- /dev/null +++ b/test/parallel/test-fs-cp-sync-async-filter-error.mjs @@ -0,0 +1,24 @@ +// This tests that cpSync throws error if filter function is asynchronous. +import '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, statSync } from 'node:fs'; +import { setTimeout } from 'node:timers/promises'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +assert.throws(() => { + cpSync(src, dest, { + filter: async (path) => { + await setTimeout(5, 'done'); + const pathStat = statSync(path); + return pathStat.isDirectory() || path.endsWith('.js'); + }, + dereference: true, + recursive: true, + }); +}, { code: 'ERR_INVALID_RETURN_VALUE' }); diff --git a/test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs b/test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs new file mode 100644 index 00000000000000..60438cbe98b8bb --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-directory-to-file-error.mjs @@ -0,0 +1,24 @@ +// This tests that cpSync throws error if attempt is made to copy directory to file. +import { isInsideDirWithUnusualChars, mustNotMutateObjectDeep, skip } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +// See https://github.com/nodejs/node/pull/48409 +if (isInsideDirWithUnusualChars) { + skip('Test is borken in directories with unusual characters'); +} + +tmpdir.refresh(); + +{ + const src = nextdir(); + mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); + const dest = fixtures.path('copy/kitchen-sink/README.md'); + assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_CP_DIR_TO_NON_DIR' } + ); +} diff --git a/test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs b/test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs new file mode 100644 index 00000000000000..114089977974f8 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-directory-without-recursive-error.mjs @@ -0,0 +1,16 @@ +// This tests that cpSync throws error if directory copied without recursive flag. +import '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_EISDIR' } +); diff --git a/test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs b/test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs new file mode 100644 index 00000000000000..96e1fa3b4e2ad1 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-file-to-directory-error.mjs @@ -0,0 +1,22 @@ +// This tests that cpSync throws error if attempt is made to copy file to directory. +import { mustNotMutateObjectDeep, isInsideDirWithUnusualChars, skip } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +// See https://github.com/nodejs/node/pull/48409 +if (isInsideDirWithUnusualChars) { + skip('Test is borken in directories with unusual characters'); +} + +const src = fixtures.path('copy/kitchen-sink/README.md'); +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_CP_NON_DIR_TO_DIR' } +); diff --git a/test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs b/test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs new file mode 100644 index 00000000000000..fa7ad45d82ac9f --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-file-to-file-path.mjs @@ -0,0 +1,13 @@ +// This tests that cpSync allows file to be copied to a file path. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import fixtures from '../common/fixtures.js'; + +const srcFile = fixtures.path('copy/kitchen-sink/index.js'); +const destFile = join(nextdir(), 'index.js'); +cpSync(srcFile, destFile, mustNotMutateObjectDeep({ dereference: true })); +const stat = lstatSync(destFile); +assert(stat.isFile()); diff --git a/test/parallel/test-fs-cp-sync-copy-socket-error.mjs b/test/parallel/test-fs-cp-sync-copy-socket-error.mjs new file mode 100644 index 00000000000000..81a06cc224aee1 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-socket-error.mjs @@ -0,0 +1,34 @@ +// This tests that cpSync throws an error if attempt is made to copy socket. +import * as common from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync } from 'node:fs'; +import { createServer } from 'node:net'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +const isWindows = process.platform === 'win32'; +if (isWindows) { + common.skip('No socket support on Windows'); +} + +// See https://github.com/nodejs/node/pull/48409 +if (common.isInsideDirWithUnusualChars) { + common.skip('Test is borken in directories with unusual characters'); +} + +tmpdir.refresh(); + +{ + const src = nextdir(); + mkdirSync(src); + const dest = nextdir(); + const sock = join(src, `${process.pid}.sock`); + const server = createServer(); + server.listen(sock); + assert.throws( + () => cpSync(sock, dest), + { code: 'ERR_FS_CP_SOCKET' } + ); + server.close(); +} diff --git a/test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs b/test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs new file mode 100644 index 00000000000000..d23a2c8be63e6c --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-symlink-not-pointing-to-folder.mjs @@ -0,0 +1,19 @@ +// This tests that cpSync copies link if it does not point to folder in src. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, symlinkSync, readlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(src, join(src, 'a', 'c')); +const dest = nextdir(); +mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(dest, join(dest, 'a', 'c')); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +const link = readlinkSync(join(dest, 'a', 'c')); +assert.strictEqual(link, src); diff --git a/test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs b/test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs new file mode 100644 index 00000000000000..cf8f055bc2422a --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-symlink-over-file-error.mjs @@ -0,0 +1,21 @@ +// This tests that cpSync throws EEXIST error if attempt is made to copy symlink over file. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, symlinkSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + +const dest = nextdir(); +mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); +assert.throws( + () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), + { code: 'EEXIST' } +); diff --git a/test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs b/test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs new file mode 100644 index 00000000000000..a84dc07873beee --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-symlinks-to-existing-symlinks.mjs @@ -0,0 +1,17 @@ +// This tests that cpSync allows copying symlinks in src to locations in dest with +// existing symlinks not pointing to a directory. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import { cpSync, mkdirSync, writeFileSync, symlinkSync } from 'node:fs'; +import { resolve, join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +const dest = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(`${src}/test.txt`, 'test'); +symlinkSync(resolve(`${src}/test.txt`), join(src, 'link.txt')); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); diff --git a/test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs b/test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs new file mode 100644 index 00000000000000..034c54efec25aa --- /dev/null +++ b/test/parallel/test-fs-cp-sync-copy-to-subdirectory-error.mjs @@ -0,0 +1,14 @@ +// This tests that cpSync throws error if attempt is made to copy to subdirectory of self. +import '../common/index.mjs'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = fixtures.path('copy/kitchen-sink/a'); +assert.throws( + () => cpSync(src, dest), + { code: 'ERR_FS_CP_EINVAL' } +); diff --git a/test/parallel/test-fs-cp-sync-dereference-directory.mjs b/test/parallel/test-fs-cp-sync-dereference-directory.mjs new file mode 100644 index 00000000000000..0fdb827301d1bf --- /dev/null +++ b/test/parallel/test-fs-cp-sync-dereference-directory.mjs @@ -0,0 +1,23 @@ +// This tests that cpSync overrides target directory with what symlink points to, when dereference is true. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, symlinkSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +const symlink = nextdir(); +const dest = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); +symlinkSync(src, symlink); + +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); + +cpSync(symlink, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); +const destStat = lstatSync(dest); +assert(!destStat.isSymbolicLink()); +assertDirEquivalent(src, dest); diff --git a/test/parallel/test-fs-cp-sync-dereference-file.mjs b/test/parallel/test-fs-cp-sync-dereference-file.mjs new file mode 100644 index 00000000000000..3615dde9aaadbb --- /dev/null +++ b/test/parallel/test-fs-cp-sync-dereference-file.mjs @@ -0,0 +1,23 @@ +// This tests that cpSync copies file itself, rather than symlink, when dereference is true. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'assert'; + +import { cpSync, mkdirSync, writeFileSync, symlinkSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); +symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); + +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +const destFile = join(dest, 'foo.js'); + +cpSync(join(src, 'bar.js'), destFile, mustNotMutateObjectDeep({ dereference: true, recursive: true })); +const stat = lstatSync(destFile); +assert(stat.isFile()); diff --git a/test/parallel/test-fs-cp-sync-dereference-twice.mjs b/test/parallel/test-fs-cp-sync-dereference-twice.mjs new file mode 100644 index 00000000000000..921b65902f1c83 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-dereference-twice.mjs @@ -0,0 +1,20 @@ +// This tests that cpSync does not fail if the same directory is copied to dest twice, +// when dereference is true, and force is false (fails silently). +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'assert'; + +import { cpSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +const destFile = join(dest, 'a/b/README2.md'); +cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); +cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); +const stat = lstatSync(destFile); +assert(stat.isFile()); diff --git a/test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs b/test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs new file mode 100644 index 00000000000000..5322188f34db10 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-dest-name-prefix-match.mjs @@ -0,0 +1,14 @@ +// This tests that cpSync must not throw error if attempt is made to copy to dest +// directory with same prefix as src directory. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import { cpSync, mkdirSync } from 'node:fs'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir('prefix', tmpdir); +const dest = nextdir('prefix-a', tmpdir); +mkdirSync(src); +mkdirSync(dest); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); diff --git a/test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs b/test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs new file mode 100644 index 00000000000000..4b136398e654c9 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-dest-parent-name-prefix-match.mjs @@ -0,0 +1,16 @@ +// This tests that cpSync must not throw error if attempt is made to copy to dest +// directory if the parent of dest has same prefix as src directory. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import { cpSync, mkdirSync } from 'node:fs'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir('aa', tmpdir); +const destParent = nextdir('aaa', tmpdir); +const dest = nextdir('aaa/aabb', tmpdir); +mkdirSync(src); +mkdirSync(destParent); +mkdirSync(dest); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); diff --git a/test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs b/test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs new file mode 100644 index 00000000000000..2ea2b0aaf63a98 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-directory-not-exist-error.mjs @@ -0,0 +1,15 @@ +// This tests that cpSync throws an error when attempting to copy a dir that does not exist. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +const dest = nextdir(); +assert.throws( + () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), + { code: 'ENOENT' } +); diff --git a/test/parallel/test-fs-cp-sync-error-on-exist.mjs b/test/parallel/test-fs-cp-sync-error-on-exist.mjs new file mode 100644 index 00000000000000..700e52e3b3c86e --- /dev/null +++ b/test/parallel/test-fs-cp-sync-error-on-exist.mjs @@ -0,0 +1,22 @@ +// This tests that cpSync throws error if errorOnExist is true, force is false, and file or folder copied over. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +assert.throws( + () => cpSync(src, dest, { + dereference: true, + errorOnExist: true, + force: false, + recursive: true, + }), + { code: 'ERR_FS_CP_EEXIST' } +); diff --git a/test/parallel/test-fs-cp-sync-file-url.mjs b/test/parallel/test-fs-cp-sync-file-url.mjs new file mode 100644 index 00000000000000..8da791b5c376c7 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-file-url.mjs @@ -0,0 +1,14 @@ +// This tests that cpSync accepts file URL as src and dest. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import { cpSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cpSync(pathToFileURL(src), pathToFileURL(dest), mustNotMutateObjectDeep({ recursive: true })); +assertDirEquivalent(src, dest); diff --git a/test/parallel/test-fs-cp-sync-filename-too-long-error.mjs b/test/parallel/test-fs-cp-sync-filename-too-long-error.mjs new file mode 100644 index 00000000000000..363a84b366f798 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-filename-too-long-error.mjs @@ -0,0 +1,17 @@ +// This tests that cpSync throws an error when attempting to copy a file with a name that is too long. +import '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; + +const isWindows = process.platform === 'win32'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = 'a'.repeat(5000); +const dest = nextdir(); +assert.throws( + () => cpSync(src, dest), + { code: isWindows ? 'ENOENT' : 'ENAMETOOLONG' } +); diff --git a/test/parallel/test-fs-cp-sync-incompatible-options-error.mjs b/test/parallel/test-fs-cp-sync-incompatible-options-error.mjs new file mode 100644 index 00000000000000..629da5d9dfd6bf --- /dev/null +++ b/test/parallel/test-fs-cp-sync-incompatible-options-error.mjs @@ -0,0 +1,14 @@ +// This tests that cpSync throws an error when both dereference and verbatimSymlinks are enabled. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +assert.throws( + () => cpSync(src, src, mustNotMutateObjectDeep({ dereference: true, verbatimSymlinks: true })), + { code: 'ERR_INCOMPATIBLE_OPTION_PAIR' } +); diff --git a/test/parallel/test-fs-cp-sync-mode-flags.mjs b/test/parallel/test-fs-cp-sync-mode-flags.mjs new file mode 100644 index 00000000000000..5471a6008520b1 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-mode-flags.mjs @@ -0,0 +1,30 @@ +// This tests that cpSync copies a nested folder structure with mode flags. +// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, constants } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +try { + cpSync(src, dest, mustNotMutateObjectDeep({ + recursive: true, + mode: constants.COPYFILE_FICLONE_FORCE, + })); +} catch (err) { + // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, + // it should enter this path. + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); + process.exit(0); +} + +// If the platform support `COPYFILE_FICLONE_FORCE` operation, +// it should reach to here. +assertDirEquivalent(src, dest); diff --git a/test/parallel/test-fs-cp-sync-mode-invalid.mjs b/test/parallel/test-fs-cp-sync-mode-invalid.mjs new file mode 100644 index 00000000000000..2ebbd2431238fc --- /dev/null +++ b/test/parallel/test-fs-cp-sync-mode-invalid.mjs @@ -0,0 +1,12 @@ +// This tests that cpSync rejects if options.mode is invalid. +import '../common/index.mjs'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +assert.throws( + () => cpSync('a', 'b', { mode: -1 }), + { code: 'ERR_OUT_OF_RANGE' } +); diff --git a/test/parallel/test-fs-cp-sync-nested-files-folders.mjs b/test/parallel/test-fs-cp-sync-nested-files-folders.mjs new file mode 100644 index 00000000000000..378bb538645594 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-nested-files-folders.mjs @@ -0,0 +1,13 @@ +// This tests that cpSync copies a nested folder structure with files and folders. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +assertDirEquivalent(src, dest); diff --git a/test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs b/test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs new file mode 100644 index 00000000000000..696d48d3edc5c5 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-no-overwrite-force-false.mjs @@ -0,0 +1,21 @@ +// This tests that cpSync does not throw errors when directory is copied over and force is false. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +const initialStat = lstatSync(join(dest, 'README.md')); +cpSync(src, dest, mustNotMutateObjectDeep({ force: false, recursive: true })); +// File should not have been copied over, so access times will be identical: +assertDirEquivalent(src, dest); +const finalStat = lstatSync(join(dest, 'README.md')); +assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); diff --git a/test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs b/test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs new file mode 100644 index 00000000000000..d7d8bea38fff3e --- /dev/null +++ b/test/parallel/test-fs-cp-sync-options-invalid-type-error.mjs @@ -0,0 +1,12 @@ +// This tests that cpSync throws if options is not object. +import '../common/index.mjs'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +assert.throws( + () => cpSync('a', 'b', () => {}), + { code: 'ERR_INVALID_ARG_TYPE' } +); diff --git a/test/parallel/test-fs-cp-sync-overwrite-force-true.mjs b/test/parallel/test-fs-cp-sync-overwrite-force-true.mjs new file mode 100644 index 00000000000000..cd9f8ab3e3ee9f --- /dev/null +++ b/test/parallel/test-fs-cp-sync-overwrite-force-true.mjs @@ -0,0 +1,19 @@ +// This tests that cpSync overwrites existing files if force is true. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +assertDirEquivalent(src, dest); +const content = readFileSync(join(dest, 'README.md'), 'utf8'); +assert.strictEqual(content.trim(), '# Hello'); diff --git a/test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs b/test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs new file mode 100644 index 00000000000000..52feaf8120165e --- /dev/null +++ b/test/parallel/test-fs-cp-sync-parent-symlink-dest-points-to-src-error.mjs @@ -0,0 +1,26 @@ +// This tests that cpSync throws error if parent directory of symlink in dest points to src. +import { mustNotMutateObjectDeep, isInsideDirWithUnusualChars, skip } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +// See https://github.com/nodejs/node/pull/48409 +if (isInsideDirWithUnusualChars) { + skip('Test is borken in directories with unusual characters'); +} + +const src = nextdir(); +mkdirSync(join(src, 'a'), mustNotMutateObjectDeep({ recursive: true })); +const dest = nextdir(); +// Create symlink in dest pointing to src. +const destLink = join(dest, 'b'); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(src, destLink); +assert.throws( + () => cpSync(src, join(dest, 'b', 'c')), + { code: 'ERR_FS_CP_EINVAL' } +); diff --git a/test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs b/test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs new file mode 100644 index 00000000000000..69a4de434f7408 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-preserve-timestamps-readonly.mjs @@ -0,0 +1,24 @@ +// This tests that cpSync makes file writeable when updating timestamp, if not writeable. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import { setTimeout } from 'node:timers/promises'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.txt'), 'foo', mustNotMutateObjectDeep({ mode: 0o444 })); +// Small wait to make sure that destStat.mtime.getTime() would produce a time +// different from srcStat.mtime.getTime() if preserveTimestamps wasn't set to true +await setTimeout(5); +cpSync(src, dest, mustNotMutateObjectDeep({ preserveTimestamps: true, recursive: true })); +assertDirEquivalent(src, dest); +const srcStat = lstatSync(join(src, 'foo.txt')); +const destStat = lstatSync(join(dest, 'foo.txt')); +assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); diff --git a/test/parallel/test-fs-cp-sync-preserve-timestamps.mjs b/test/parallel/test-fs-cp-sync-preserve-timestamps.mjs new file mode 100644 index 00000000000000..3fb35c2e6359b4 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-preserve-timestamps.mjs @@ -0,0 +1,18 @@ +// This tests that cpSync copies timestamps from src to dest if preserveTimestamps is true. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, lstatSync } from 'node:fs'; +import { join } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ preserveTimestamps: true, recursive: true })); +assertDirEquivalent(src, dest); +const srcStat = lstatSync(join(src, 'index.js')); +const destStat = lstatSync(join(dest, 'index.js')); +assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); diff --git a/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs b/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs new file mode 100644 index 00000000000000..070fabd438749e --- /dev/null +++ b/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-default.mjs @@ -0,0 +1,21 @@ +// This tests that cpSync resolves relative symlinks to their absolute path by default. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); +symlinkSync('foo.js', join(src, 'bar.js')); + +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); + +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +const link = readlinkSync(join(dest, 'bar.js')); +assert.strictEqual(link, join(src, 'foo.js')); diff --git a/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs b/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs new file mode 100644 index 00000000000000..e5f59d655f33f7 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-resolve-relative-symlinks-false.mjs @@ -0,0 +1,21 @@ +// This tests that cpSync resolves relative symlinks when verbatimSymlinks is false. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); +symlinkSync('foo.js', join(src, 'bar.js')); + +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); + +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, verbatimSymlinks: false })); +const link = readlinkSync(join(dest, 'bar.js')); +assert.strictEqual(link, join(src, 'foo.js')); diff --git a/test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs b/test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs new file mode 100644 index 00000000000000..81d7e1fcb63eb2 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-src-dest-identical-error.mjs @@ -0,0 +1,14 @@ +// This tests that cpSync throws error when src and dest are identical. +import '../common/index.mjs'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +assert.throws( + () => cpSync(src, src), + { code: 'ERR_FS_CP_EINVAL' } +); diff --git a/test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs b/test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs new file mode 100644 index 00000000000000..55e4c7014b7497 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-src-parent-of-dest-error.mjs @@ -0,0 +1,25 @@ +// This tests that cpSync throws error if attempt is made to copy src to dest when +// src is parent directory of the parent of dest. +import { mustNotMutateObjectDeep, isInsideDirWithUnusualChars, skip } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync } from 'node:fs'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +// See https://github.com/nodejs/node/pull/48409 +if (isInsideDirWithUnusualChars) { + skip('Test is borken in directories with unusual characters'); +} + +const src = nextdir('a', tmpdir); +const destParent = nextdir('a/b', tmpdir); +const dest = nextdir('a/b/c', tmpdir); +mkdirSync(src); +mkdirSync(destParent); +mkdirSync(dest); +assert.throws( + () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), + { code: 'ERR_FS_CP_EINVAL' }, +); diff --git a/test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs b/test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs new file mode 100644 index 00000000000000..580e2ada0e2780 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-symlink-dest-points-to-src-error.mjs @@ -0,0 +1,21 @@ +// This tests that cpSync throws error if symlink in dest points to location in src. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); + +const dest = nextdir(); +mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(src, join(dest, 'a', 'c')); +assert.throws( + () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), + { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' } +); diff --git a/test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs b/test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs new file mode 100644 index 00000000000000..141798f1d27d4e --- /dev/null +++ b/test/parallel/test-fs-cp-sync-symlink-points-to-dest-error.mjs @@ -0,0 +1,22 @@ +// This tests that cpSync throws error if symlink in src points to location in dest. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +const dest = nextdir(); +mkdirSync(dest); +symlinkSync(dest, join(src, 'link')); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +assert.throws( + () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), + { + code: 'ERR_FS_CP_EINVAL' + } +); diff --git a/test/parallel/test-fs-cp-sync-unicode-folder-names.mjs b/test/parallel/test-fs-cp-sync-unicode-folder-names.mjs new file mode 100644 index 00000000000000..6393aeb2c15859 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-unicode-folder-names.mjs @@ -0,0 +1,13 @@ +// This tests that cpSync copies a nested folder containing UTF characters. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir, assertDirEquivalent } from '../common/fs.js'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/utf/新建文件夹'); +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); +assertDirEquivalent(src, dest); diff --git a/test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs b/test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs new file mode 100644 index 00000000000000..3db176487f715d --- /dev/null +++ b/test/parallel/test-fs-cp-sync-verbatim-symlinks-invalid.mjs @@ -0,0 +1,17 @@ +// This tests that cpSync throws error when verbatimSymlinks is not a boolean. +import '../common/index.mjs'; +import assert from 'node:assert'; +import { cpSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +tmpdir.refresh(); + +const src = fixtures.path('copy/kitchen-sink'); +[1, [], {}, null, 1n, undefined, null, Symbol(), '', () => {}] + .forEach((verbatimSymlinks) => { + assert.throws( + () => cpSync(src, src, { verbatimSymlinks }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + }); diff --git a/test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs b/test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs new file mode 100644 index 00000000000000..e8d0010119fac5 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-verbatim-symlinks-true.mjs @@ -0,0 +1,21 @@ +// This tests that cpSync does not resolve relative symlinks when verbatimSymlinks is true. +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +const src = nextdir(); +mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); +symlinkSync('foo.js', join(src, 'bar.js')); + +const dest = nextdir(); +mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); + +cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, verbatimSymlinks: true })); +const link = readlinkSync(join(dest, 'bar.js')); +assert.strictEqual(link, 'foo.js'); diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs deleted file mode 100644 index 589d407d756b51..00000000000000 --- a/test/parallel/test-fs-cp.mjs +++ /dev/null @@ -1,1098 +0,0 @@ -import { mustCall, mustNotMutateObjectDeep, isInsideDirWithUnusualChars } from '../common/index.mjs'; - -import assert from 'assert'; -import fs from 'fs'; -const { - cp, - cpSync, - lstatSync, - mkdirSync, - readdirSync, - readFileSync, - readlinkSync, - symlinkSync, - statSync, - writeFileSync, -} = fs; -import net from 'net'; -import { join, resolve } from 'path'; -import { pathToFileURL } from 'url'; -import { setTimeout } from 'timers/promises'; - -const isWindows = process.platform === 'win32'; -import tmpdir from '../common/tmpdir.js'; -tmpdir.refresh(); - -let dirc = 0; -function nextdir(dirname) { - return tmpdir.resolve(dirname || `copy_%${++dirc}`); -} - -// Synchronous implementation of copy. - -// It copies a nested folder containing UTF characters. -{ - const src = './test/fixtures/copy/utf/新建文件夹'; - const dest = nextdir(); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - assertDirEquivalent(src, dest); -} - -// It copies a nested folder structure with files and folders. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - assertDirEquivalent(src, dest); -} - -// It copies a nested folder structure with mode flags. -// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. -(() => { - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - try { - cpSync(src, dest, mustNotMutateObjectDeep({ - recursive: true, - mode: fs.constants.COPYFILE_FICLONE_FORCE, - })); - } catch (err) { - // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, - // it should enter this path. - assert.strictEqual(err.syscall, 'copyfile'); - assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || - err.code === 'ENOSYS' || err.code === 'EXDEV'); - return; - } - - // If the platform support `COPYFILE_FICLONE_FORCE` operation, - // it should reach to here. - assertDirEquivalent(src, dest); -})(); - -// It does not throw errors when directory is copied over and force is false. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); - const dest = nextdir(); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - const initialStat = lstatSync(join(dest, 'README.md')); - cpSync(src, dest, mustNotMutateObjectDeep({ force: false, recursive: true })); - // File should not have been copied over, so access times will be identical: - assertDirEquivalent(src, dest); - const finalStat = lstatSync(join(dest, 'README.md')); - assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); -} - -// It overwrites existing files if force is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - assertDirEquivalent(src, dest); - const content = readFileSync(join(dest, 'README.md'), 'utf8'); - assert.strictEqual(content.trim(), '# Hello'); -} - -// It does not fail if the same directory is copied to dest twice, -// when dereference is true, and force is false (fails silently). -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - const destFile = join(dest, 'a/b/README2.md'); - cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); - cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); - const stat = lstatSync(destFile); - assert(stat.isFile()); -} - - -// It copies file itself, rather than symlink, when dereference is true. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); - - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - const destFile = join(dest, 'foo.js'); - - cpSync(join(src, 'bar.js'), destFile, mustNotMutateObjectDeep({ dereference: true, recursive: true })); - const stat = lstatSync(destFile); - assert(stat.isFile()); -} - - -// It overrides target directory with what symlink points to, when dereference is true. -{ - const src = nextdir(); - const symlink = nextdir(); - const dest = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync(src, symlink); - - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - - cpSync(symlink, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); - const destStat = lstatSync(dest); - assert(!destStat.isSymbolicLink()); - assertDirEquivalent(src, dest); -} - -// It throws error when verbatimSymlinks is not a boolean. -{ - const src = './test/fixtures/copy/kitchen-sink'; - [1, [], {}, null, 1n, undefined, null, Symbol(), '', () => {}] - .forEach((verbatimSymlinks) => { - assert.throws( - () => cpSync(src, src, { verbatimSymlinks }), - { code: 'ERR_INVALID_ARG_TYPE' } - ); - }); -} - -// It rejects if options.mode is invalid. -{ - assert.throws( - () => cpSync('a', 'b', { mode: -1 }), - { code: 'ERR_OUT_OF_RANGE' } - ); -} - - -// It throws an error when both dereference and verbatimSymlinks are enabled. -{ - const src = './test/fixtures/copy/kitchen-sink'; - assert.throws( - () => cpSync(src, src, mustNotMutateObjectDeep({ dereference: true, verbatimSymlinks: true })), - { code: 'ERR_INCOMPATIBLE_OPTION_PAIR' } - ); -} - - -// It resolves relative symlinks to their absolute path by default. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync('foo.js', join(src, 'bar.js')); - - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - const link = readlinkSync(join(dest, 'bar.js')); - assert.strictEqual(link, join(src, 'foo.js')); -} - - -// It resolves relative symlinks when verbatimSymlinks is false. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync('foo.js', join(src, 'bar.js')); - - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, verbatimSymlinks: false })); - const link = readlinkSync(join(dest, 'bar.js')); - assert.strictEqual(link, join(src, 'foo.js')); -} - - -// It does not resolve relative symlinks when verbatimSymlinks is true. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync('foo.js', join(src, 'bar.js')); - - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, verbatimSymlinks: true })); - const link = readlinkSync(join(dest, 'bar.js')); - assert.strictEqual(link, 'foo.js'); -} - - -// It throws error when src and dest are identical. -{ - const src = './test/fixtures/copy/kitchen-sink'; - assert.throws( - () => cpSync(src, src), - { code: 'ERR_FS_CP_EINVAL' } - ); -} - -// It throws error if symlink in src points to location in dest. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - const dest = nextdir(); - mkdirSync(dest); - symlinkSync(dest, join(src, 'link')); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - assert.throws( - () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), - { - code: 'ERR_FS_CP_EINVAL' - } - ); -} - -// It allows cpSync copying symlinks in src to locations in dest with existing synlinks not pointing to a directory. -{ - const src = nextdir(); - const dest = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(`${src}/test.txt`, 'test'); - symlinkSync(resolve(`${src}/test.txt`), join(src, 'link.txt')); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); -} - -// It allows cp copying symlinks in src to locations in dest with existing synlinks not pointing to a directory. -{ - const src = nextdir(); - const dest = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(`${src}/test.txt`, 'test'); - symlinkSync(resolve(`${src}/test.txt`), join(src, 'link.txt')); - cp(src, dest, { recursive: true }, - mustCall((err) => { - assert.strictEqual(err, null); - - cp(src, dest, { recursive: true }, mustCall((err) => { - assert.strictEqual(err, null); - })); - })); -} - -// It throws error if symlink in dest points to location in src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); - - const dest = nextdir(); - mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(src, join(dest, 'a', 'c')); - assert.throws( - () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), - { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' } - ); -} - -// It throws error if parent directory of symlink in dest points to src. -if (!isInsideDirWithUnusualChars) { - const src = nextdir(); - mkdirSync(join(src, 'a'), mustNotMutateObjectDeep({ recursive: true })); - const dest = nextdir(); - // Create symlink in dest pointing to src. - const destLink = join(dest, 'b'); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(src, destLink); - assert.throws( - () => cpSync(src, join(dest, 'b', 'c')), - { code: 'ERR_FS_CP_EINVAL' } - ); -} - -// It throws error if attempt is made to copy directory to file. -if (!isInsideDirWithUnusualChars) { - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - const dest = './test/fixtures/copy/kitchen-sink/README.md'; - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_CP_DIR_TO_NON_DIR' } - ); -} - -// It allows file to be copied to a file path. -{ - const srcFile = './test/fixtures/copy/kitchen-sink/index.js'; - const destFile = join(nextdir(), 'index.js'); - cpSync(srcFile, destFile, mustNotMutateObjectDeep({ dereference: true })); - const stat = lstatSync(destFile); - assert(stat.isFile()); -} - -// It throws error if directory copied without recursive flag. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_EISDIR' } - ); -} - - -// It throws error if attempt is made to copy file to directory. -if (!isInsideDirWithUnusualChars) { - const src = './test/fixtures/copy/kitchen-sink/README.md'; - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_CP_NON_DIR_TO_DIR' } - ); -} - -// It must not throw error if attempt is made to copy to dest -// directory with same prefix as src directory -// regression test for https://github.com/nodejs/node/issues/54285 -{ - const src = nextdir('prefix'); - const dest = nextdir('prefix-a'); - mkdirSync(src); - mkdirSync(dest); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); -} - -// It must not throw error if attempt is made to copy to dest -// directory if the parent of dest has same prefix as src directory -// regression test for https://github.com/nodejs/node/issues/54285 -{ - const src = nextdir('aa'); - const destParent = nextdir('aaa'); - const dest = nextdir('aaa/aabb'); - mkdirSync(src); - mkdirSync(destParent); - mkdirSync(dest); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); -} - -// It throws error if attempt is made to copy src to dest -// when src is parent directory of the parent of dest -if (!isInsideDirWithUnusualChars) { - const src = nextdir('a'); - const destParent = nextdir('a/b'); - const dest = nextdir('a/b/c'); - mkdirSync(src); - mkdirSync(destParent); - mkdirSync(dest); - assert.throws( - () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), - { code: 'ERR_FS_CP_EINVAL' }, - ); -} - -// It throws error if attempt is made to copy to subdirectory of self. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = './test/fixtures/copy/kitchen-sink/a'; - assert.throws( - () => cpSync(src, dest), - { code: 'ERR_FS_CP_EINVAL' } - ); -} - -// It throws an error if attempt is made to copy socket. -if (!isWindows && !isInsideDirWithUnusualChars) { - const src = nextdir(); - mkdirSync(src); - const dest = nextdir(); - const sock = join(src, `${process.pid}.sock`); - const server = net.createServer(); - server.listen(sock); - assert.throws( - () => cpSync(sock, dest), - { code: 'ERR_FS_CP_SOCKET' } - ); - server.close(); -} - -// It copies timestamps from src to dest if preserveTimestamps is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(src, dest, mustNotMutateObjectDeep({ preserveTimestamps: true, recursive: true })); - assertDirEquivalent(src, dest); - const srcStat = lstatSync(join(src, 'index.js')); - const destStat = lstatSync(join(dest, 'index.js')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); -} - -// It applies filter function. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(src, dest, { - filter: (path) => { - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); - }, - dereference: true, - recursive: true, - }); - const destEntries = []; - collectEntries(dest, destEntries); - for (const entry of destEntries) { - assert.strictEqual( - entry.isDirectory() || entry.name.endsWith('.js'), - true - ); - } -} - -// It throws error if filter function is asynchronous. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - assert.throws(() => { - cpSync(src, dest, { - filter: async (path) => { - await setTimeout(5, 'done'); - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); - }, - dereference: true, - recursive: true, - }); - }, { code: 'ERR_INVALID_RETURN_VALUE' }); -} - -// It throws error if errorOnExist is true, force is false, and file or folder -// copied over. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - assert.throws( - () => cpSync(src, dest, { - dereference: true, - errorOnExist: true, - force: false, - recursive: true, - }), - { code: 'ERR_FS_CP_EEXIST' } - ); -} - -// It throws EEXIST error if attempt is made to copy symlink over file. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); - - const dest = nextdir(); - mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); - assert.throws( - () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), - { code: 'EEXIST' } - ); -} - -// It throws an error when attempting to copy a file with a name that is too long. -{ - const src = 'a'.repeat(5000); - const dest = nextdir(); - assert.throws( - () => cpSync(src, dest), - { code: isWindows ? 'ENOENT' : 'ENAMETOOLONG' } - ); -} - -// It throws an error when attempting to copy a dir that does not exist. -{ - const src = nextdir(); - const dest = nextdir(); - assert.throws( - () => cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })), - { code: 'ENOENT' } - ); -} - -// It makes file writeable when updating timestamp, if not writeable. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.txt'), 'foo', mustNotMutateObjectDeep({ mode: 0o444 })); - // Small wait to make sure that destStat.mtime.getTime() would produce a time - // different from srcStat.mtime.getTime() if preserveTimestamps wasn't set to true - await setTimeout(5); - cpSync(src, dest, mustNotMutateObjectDeep({ preserveTimestamps: true, recursive: true })); - assertDirEquivalent(src, dest); - const srcStat = lstatSync(join(src, 'foo.txt')); - const destStat = lstatSync(join(dest, 'foo.txt')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); -} - -// It copies link if it does not point to folder in src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(src, join(src, 'a', 'c')); - const dest = nextdir(); - mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(dest, join(dest, 'a', 'c')); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - const link = readlinkSync(join(dest, 'a', 'c')); - assert.strictEqual(link, src); -} - -// It accepts file URL as src and dest. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(pathToFileURL(src), pathToFileURL(dest), mustNotMutateObjectDeep({ recursive: true })); - assertDirEquivalent(src, dest); -} - -// It throws if options is not object. -{ - assert.throws( - () => cpSync('a', 'b', () => {}), - { code: 'ERR_INVALID_ARG_TYPE' } - ); -} - -// Callback implementation of copy. - -// It copies a nested folder structure with files and folders. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - })); -} - -// It copies a nested folder structure with mode flags. -// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, mustNotMutateObjectDeep({ - recursive: true, - mode: fs.constants.COPYFILE_FICLONE_FORCE, - }), mustCall((err) => { - if (!err) { - // If the platform support `COPYFILE_FICLONE_FORCE` operation, - // it should reach to here. - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - return; - } - - // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, - // it should enter this path. - assert.strictEqual(err.syscall, 'copyfile'); - assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || - err.code === 'ENOSYS' || err.code === 'EXDEV'); - })); -} - -// It does not throw errors when directory is copied over and force is false. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'README.md'), 'hello world', 'utf8'); - const dest = nextdir(); - cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); - const initialStat = lstatSync(join(dest, 'README.md')); - cp(src, dest, { - dereference: true, - force: false, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - // File should not have been copied over, so access times will be identical: - const finalStat = lstatSync(join(dest, 'README.md')); - assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime()); - })); -} - -// It overwrites existing files if force is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8'); - - cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - const content = readFileSync(join(dest, 'README.md'), 'utf8'); - assert.strictEqual(content.trim(), '# Hello'); - })); -} - -// It does not fail if the same directory is copied to dest twice, -// when dereference is true, and force is false (fails silently). -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - const destFile = join(dest, 'a/b/README2.md'); - cpSync(src, dest, mustNotMutateObjectDeep({ dereference: true, recursive: true })); - cp(src, dest, { - dereference: true, - recursive: true - }, mustCall((err) => { - assert.strictEqual(err, null); - const stat = lstatSync(destFile); - assert(stat.isFile()); - })); -} - -// It copies file itself, rather than symlink, when dereference is true. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); - symlinkSync(join(src, 'foo.js'), join(src, 'bar.js')); - - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - const destFile = join(dest, 'foo.js'); - - cp(join(src, 'bar.js'), destFile, mustNotMutateObjectDeep({ dereference: true }), - mustCall((err) => { - assert.strictEqual(err, null); - const stat = lstatSync(destFile); - assert(stat.isFile()); - }) - ); -} - -// It returns error when src and dest are identical. -{ - const src = './test/fixtures/copy/kitchen-sink'; - cp(src, src, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns error if symlink in src points to location in dest. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - const dest = nextdir(); - mkdirSync(dest); - symlinkSync(dest, join(src, 'link')); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns error if symlink in dest points to location in src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); - - const dest = nextdir(); - mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(src, join(dest, 'a', 'c')); - cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY'); - })); -} - -// It returns error if parent directory of symlink in dest points to src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a'), mustNotMutateObjectDeep({ recursive: true })); - const dest = nextdir(); - // Create symlink in dest pointing to src. - const destLink = join(dest, 'b'); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(src, destLink); - cp(src, join(dest, 'b', 'c'), mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns error if attempt is made to copy directory to file. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - const dest = './test/fixtures/copy/kitchen-sink/README.md'; - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_DIR_TO_NON_DIR'); - })); -} - -// It allows file to be copied to a file path. -{ - const srcFile = './test/fixtures/copy/kitchen-sink/README.md'; - const destFile = join(nextdir(), 'index.js'); - cp(srcFile, destFile, mustNotMutateObjectDeep({ dereference: true }), mustCall((err) => { - assert.strictEqual(err, null); - const stat = lstatSync(destFile); - assert(stat.isFile()); - })); -} - -// It returns error if directory copied without recursive flag. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_EISDIR'); - })); -} - -// It returns error if attempt is made to copy file to directory. -{ - const src = './test/fixtures/copy/kitchen-sink/README.md'; - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_NON_DIR_TO_DIR'); - })); -} - -// It returns error if attempt is made to copy to subdirectory of self. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = './test/fixtures/copy/kitchen-sink/a'; - cp(src, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL'); - })); -} - -// It returns an error if attempt is made to copy socket. -if (!isWindows && !isInsideDirWithUnusualChars) { - const src = nextdir(); - mkdirSync(src); - const dest = nextdir(); - const sock = join(src, `${process.pid}.sock`); - const server = net.createServer(); - server.listen(sock); - cp(sock, dest, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_SOCKET'); - server.close(); - })); -} - -// It copies timestamps from src to dest if preserveTimestamps is true. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, { - preserveTimestamps: true, - recursive: true - }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - const srcStat = lstatSync(join(src, 'index.js')); - const destStat = lstatSync(join(dest, 'index.js')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); - })); -} - -// It applies filter function. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, { - filter: (path) => { - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); - }, - dereference: true, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - const destEntries = []; - collectEntries(dest, destEntries); - for (const entry of destEntries) { - assert.strictEqual( - entry.isDirectory() || entry.name.endsWith('.js'), - true - ); - } - })); -} - -// It supports async filter function. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(src, dest, { - filter: async (path) => { - await setTimeout(5, 'done'); - const pathStat = statSync(path); - return pathStat.isDirectory() || path.endsWith('.js'); - }, - dereference: true, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - const destEntries = []; - collectEntries(dest, destEntries); - for (const entry of destEntries) { - assert.strictEqual( - entry.isDirectory() || entry.name.endsWith('.js'), - true - ); - } - })); -} - -// It returns error if errorOnExist is true, force is false, and file or folder -// copied over. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true })); - cp(src, dest, { - dereference: true, - errorOnExist: true, - force: false, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err.code, 'ERR_FS_CP_EEXIST'); - })); -} - -// It returns EEXIST error if attempt is made to copy symlink over file. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c')); - - const dest = nextdir(); - mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8'); - cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { - assert.strictEqual(err.code, 'EEXIST'); - })); -} - -// It makes file writeable when updating timestamp, if not writeable. -{ - const src = nextdir(); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(src, 'foo.txt'), 'foo', mustNotMutateObjectDeep({ mode: 0o444 })); - cp(src, dest, { - preserveTimestamps: true, - recursive: true, - }, mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - const srcStat = lstatSync(join(src, 'foo.txt')); - const destStat = lstatSync(join(dest, 'foo.txt')); - assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime()); - })); -} - -// It copies link if it does not point to folder in src. -{ - const src = nextdir(); - mkdirSync(join(src, 'a', 'b'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(src, join(src, 'a', 'c')); - const dest = nextdir(); - mkdirSync(join(dest, 'a'), mustNotMutateObjectDeep({ recursive: true })); - symlinkSync(dest, join(dest, 'a', 'c')); - cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => { - assert.strictEqual(err, null); - const link = readlinkSync(join(dest, 'a', 'c')); - assert.strictEqual(link, src); - })); -} - -// It accepts file URL as src and dest. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - cp(pathToFileURL(src), pathToFileURL(dest), mustNotMutateObjectDeep({ recursive: true }), - mustCall((err) => { - assert.strictEqual(err, null); - assertDirEquivalent(src, dest); - })); -} - -// Copy should not throw exception if child folder is filtered out. -{ - const src = nextdir(); - mkdirSync(join(src, 'test-cp'), mustNotMutateObjectDeep({ recursive: true })); - - const dest = nextdir(); - mkdirSync(dest, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(join(dest, 'test-cp'), 'test-content', mustNotMutateObjectDeep({ mode: 0o444 })); - - const opts = { - filter: (path) => !path.includes('test-cp'), - recursive: true, - }; - cp(src, dest, opts, mustCall((err) => { - assert.strictEqual(err, null); - })); - cpSync(src, dest, opts); -} - -// Copy should not throw exception if dest is invalid but filtered out. -{ - // Create dest as a file. - // Expect: cp skips the copy logic entirely and won't throw any exception in path validation process. - const src = join(nextdir(), 'bar'); - mkdirSync(src, mustNotMutateObjectDeep({ recursive: true })); - - const destParent = nextdir(); - const dest = join(destParent, 'bar'); - mkdirSync(destParent, mustNotMutateObjectDeep({ recursive: true })); - writeFileSync(dest, 'test-content', mustNotMutateObjectDeep({ mode: 0o444 })); - - const opts = { - filter: (path) => !path.includes('bar'), - recursive: true, - }; - cp(src, dest, opts, mustCall((err) => { - assert.strictEqual(err, null); - })); - cpSync(src, dest, opts); -} - -// It throws if options is not object. -{ - assert.throws( - () => cp('a', 'b', 'hello', () => {}), - { code: 'ERR_INVALID_ARG_TYPE' } - ); -} - -// It throws if options is not object. -{ - assert.throws( - () => cp('a', 'b', { mode: -1 }, () => {}), - { code: 'ERR_OUT_OF_RANGE' } - ); -} - -// Promises implementation of copy. - -// It copies a nested folder structure with files and folders. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - const p = await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ recursive: true })); - assert.strictEqual(p, undefined); - assertDirEquivalent(src, dest); -} - -// It copies a nested folder structure with mode flags. -// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - let p = null; - let successFiClone = false; - try { - p = await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ - recursive: true, - mode: fs.constants.COPYFILE_FICLONE_FORCE, - })); - successFiClone = true; - } catch (err) { - // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, - // it should enter this path. - assert.strictEqual(err.syscall, 'copyfile'); - assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || - err.code === 'ENOSYS' || err.code === 'EXDEV'); - } - - if (successFiClone) { - // If the platform support `COPYFILE_FICLONE_FORCE` operation, - // it should reach to here. - assert.strictEqual(p, undefined); - assertDirEquivalent(src, dest); - } -} - -// It accepts file URL as src and dest. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - const p = await fs.promises.cp( - pathToFileURL(src), - pathToFileURL(dest), - { recursive: true } - ); - assert.strictEqual(p, undefined); - assertDirEquivalent(src, dest); -} - -// It allows async error to be caught. -{ - const src = './test/fixtures/copy/kitchen-sink'; - const dest = nextdir(); - await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ recursive: true })); - await assert.rejects( - fs.promises.cp(src, dest, { - dereference: true, - errorOnExist: true, - force: false, - recursive: true, - }), - { code: 'ERR_FS_CP_EEXIST' } - ); -} - -// It rejects if options is not object. -{ - await assert.rejects( - fs.promises.cp('a', 'b', () => {}), - { code: 'ERR_INVALID_ARG_TYPE' } - ); -} - -// It rejects if options.mode is invalid. -{ - await assert.rejects( - fs.promises.cp('a', 'b', { - mode: -1, - }), - { code: 'ERR_OUT_OF_RANGE' } - ); -} - -function assertDirEquivalent(dir1, dir2) { - const dir1Entries = []; - collectEntries(dir1, dir1Entries); - const dir2Entries = []; - collectEntries(dir2, dir2Entries); - assert.strictEqual(dir1Entries.length, dir2Entries.length); - for (const entry1 of dir1Entries) { - const entry2 = dir2Entries.find((entry) => { - return entry.name === entry1.name; - }); - assert(entry2, `entry ${entry2.name} not copied`); - if (entry1.isFile()) { - assert(entry2.isFile(), `${entry2.name} was not file`); - } else if (entry1.isDirectory()) { - assert(entry2.isDirectory(), `${entry2.name} was not directory`); - } else if (entry1.isSymbolicLink()) { - assert(entry2.isSymbolicLink(), `${entry2.name} was not symlink`); - } - } -} - -function collectEntries(dir, dirEntries) { - const newEntries = readdirSync(dir, mustNotMutateObjectDeep({ withFileTypes: true })); - for (const entry of newEntries) { - if (entry.isDirectory()) { - collectEntries(join(dir, entry.name), dirEntries); - } - } - dirEntries.push(...newEntries); -} From d2183d860a5fd46b328ecc16e8d2e4d2d49aef7b Mon Sep 17 00:00:00 2001 From: Yoo <121166835+ri7116@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:29:01 +0900 Subject: [PATCH 75/80] lib: optimize writable stream buffer clearing Improved the `clearBuffer` function by replacing `buffered.splice` with `ArrayPrototypeSlice`. - Eliminates O(N) shifting overhead. - Improves CPU utilization and reduces GC overhead. PR-URL: https://github.com/nodejs/node/pull/59406 Reviewed-By: Matteo Collina Reviewed-By: Ethan Arrowood Reviewed-By: Ruben Bridgewater Reviewed-By: Anna Henningsen --- lib/internal/streams/writable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js index 1081db9ffcf661..67beef44142d44 100644 --- a/lib/internal/streams/writable.js +++ b/lib/internal/streams/writable.js @@ -784,7 +784,7 @@ function clearBuffer(stream, state) { if (i === buffered.length) { resetBuffer(state); } else if (i > 256) { - buffered.splice(0, i); + state[kBufferedValue] = ArrayPrototypeSlice(buffered, i); state.bufferedIndex = 0; } else { state.bufferedIndex = i; From fb0a6fb57f1513d41e130baa6fdb0d308292e9f7 Mon Sep 17 00:00:00 2001 From: Shima Ryuhei <65934663+islandryu@users.noreply.github.com> Date: Mon, 11 Aug 2025 14:49:53 +0900 Subject: [PATCH 76/80] test: exclude mock from coverage Fixes: https://github.com/nodejs/node/issues/59112 PR-URL: https://github.com/nodejs/node/pull/59348 Reviewed-By: Matteo Collina Reviewed-By: Pietro Marchini --- lib/internal/test_runner/coverage.js | 8 +++- .../test-runner/coverage-with-mock/module.js | 5 +++ .../test-runner/coverage-with-mock/sum.js | 9 +++++ .../test-runner/output/coverage-with-mock.mjs | 17 +++++++++ .../output/coverage-with-mock.snapshot | 38 +++++++++++++++++++ .../output/typescript-coverage.snapshot | 4 +- test/parallel/test-runner-output.mjs | 9 +++++ 7 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/test-runner/coverage-with-mock/module.js create mode 100644 test/fixtures/test-runner/coverage-with-mock/sum.js create mode 100644 test/fixtures/test-runner/output/coverage-with-mock.mjs create mode 100644 test/fixtures/test-runner/output/coverage-with-mock.snapshot diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index d8b4cfd8b5e7e0..80811dffb1e1d0 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -28,7 +28,7 @@ const { const { setupCoverageHooks } = require('internal/util'); const { tmpdir } = require('os'); const { join, resolve, relative } = require('path'); -const { fileURLToPath } = require('internal/url'); +const { fileURLToPath, URL } = require('internal/url'); const { kMappings, SourceMap } = require('internal/source_map/source_map'); const { codes: { @@ -37,6 +37,7 @@ const { }, } = require('internal/errors'); const { matchGlobPattern } = require('internal/fs/glob'); +const { kMockSearchParam } = require('internal/test_runner/mock/mock'); const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/; const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; @@ -497,6 +498,11 @@ class TestCoverage { return true; } + const searchParams = new URL(url).searchParams; + if (searchParams.get(kMockSearchParam)) { + return true; + } + // This check filters out the node_modules/ directory, unless it is explicitly included. return StringPrototypeIncludes(url, '/node_modules/'); } diff --git a/test/fixtures/test-runner/coverage-with-mock/module.js b/test/fixtures/test-runner/coverage-with-mock/module.js new file mode 100644 index 00000000000000..aa80ca1950dfc6 --- /dev/null +++ b/test/fixtures/test-runner/coverage-with-mock/module.js @@ -0,0 +1,5 @@ +import {getData, sum} from './sum.js' + +export const theModuleSum = (a, b) => sum(a,b) + +export const theModuleGetData = () => getData() diff --git a/test/fixtures/test-runner/coverage-with-mock/sum.js b/test/fixtures/test-runner/coverage-with-mock/sum.js new file mode 100644 index 00000000000000..3227f83209e23c --- /dev/null +++ b/test/fixtures/test-runner/coverage-with-mock/sum.js @@ -0,0 +1,9 @@ +// console.log('imported') + +const data = { type: 'object' } + +// console.log(data) + +export const sum = (a, b) => a + b + +export const getData = () => data diff --git a/test/fixtures/test-runner/output/coverage-with-mock.mjs b/test/fixtures/test-runner/output/coverage-with-mock.mjs new file mode 100644 index 00000000000000..5d5b2b14f66a95 --- /dev/null +++ b/test/fixtures/test-runner/output/coverage-with-mock.mjs @@ -0,0 +1,17 @@ +import { describe, it, mock } from 'node:test'; + +describe('module test with mock', async () => { + mock.module('../coverage-with-mock/sum.js', { + namedExports: { + sum: (a, b) => 1, + getData: () => ({}), + }, + }); + + const { theModuleSum, theModuleGetData } = await import('../coverage-with-mock/module.js'); + + it('tests correct thing', (t) => { + t.assert.strictEqual(theModuleSum(1, 2), 1); + t.assert.deepStrictEqual(theModuleGetData(), {}); + }); +}); diff --git a/test/fixtures/test-runner/output/coverage-with-mock.snapshot b/test/fixtures/test-runner/output/coverage-with-mock.snapshot new file mode 100644 index 00000000000000..2dd3443265b7b7 --- /dev/null +++ b/test/fixtures/test-runner/output/coverage-with-mock.snapshot @@ -0,0 +1,38 @@ +TAP version 13 +# Subtest: module test with mock + # Subtest: tests correct thing + ok 1 - tests correct thing + --- + duration_ms: * + type: 'test' + ... + 1..1 +ok 1 - module test with mock + --- + duration_ms: * + type: 'suite' + ... +1..1 +# tests 1 +# suites 1 +# pass 1 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * +# start of coverage report +# --------------------------------------------------------------------------- +# file | line % | branch % | funcs % | uncovered lines +# --------------------------------------------------------------------------- +# test | | | | +# fixtures | | | | +# test-runner | | | | +# coverage-with-mock | | | | +# module.js | 100.00 | 100.00 | 100.00 | +# output | | | | +# coverage-with-mock.mjs | 100.00 | 100.00 | 100.00 | +# --------------------------------------------------------------------------- +# all files | 100.00 | 100.00 | 100.00 | +# --------------------------------------------------------------------------- +# end of coverage report diff --git a/test/fixtures/test-runner/output/typescript-coverage.snapshot b/test/fixtures/test-runner/output/typescript-coverage.snapshot index eaeba2bfa11b12..5d628e2b0854d8 100644 --- a/test/fixtures/test-runner/output/typescript-coverage.snapshot +++ b/test/fixtures/test-runner/output/typescript-coverage.snapshot @@ -29,11 +29,11 @@ ok 1 - foo # fixtures | | | | # test-runner | | | | # coverage | | | | -# bar.mts | 0.00 | 100.00 | 100.00 | 1-3 +# bar.mts | 33.33 | 100.00 | 0.00 | 2-3 # foo.mts | 100.00 | 100.00 | 100.00 | # output | | | | # typescript-coverage.mts | 100.00 | 100.00 | 100.00 | # ---------------------------------------------------------------------------- -# all files | 85.29 | 100.00 | 85.71 | +# all files | 93.55 | 100.00 | 85.71 | # ---------------------------------------------------------------------------- # end of coverage report diff --git a/test/parallel/test-runner-output.mjs b/test/parallel/test-runner-output.mjs index 8adac4dfa72b94..ccfca45e502fd5 100644 --- a/test/parallel/test-runner-output.mjs +++ b/test/parallel/test-runner-output.mjs @@ -329,6 +329,15 @@ const tests = [ '--experimental-test-coverage', '--test-coverage-exclude=!test/**'] } : false, + process.features.inspector ? { + name: 'test-runner/output/coverage-with-mock.mjs', + flags: ['--disable-warning=ExperimentalWarning', + '--test-reporter=tap', + '--experimental-transform-types', + '--experimental-test-module-mocks', + '--experimental-test-coverage', + '--test-coverage-exclude=!test/**'] + } : false, ] .filter(Boolean) .map(({ flags, name, tty, transform, cwd }) => ({ From e0555396042bd8e19ad81d8fc694ae2330543c86 Mon Sep 17 00:00:00 2001 From: theanarkh Date: Mon, 11 Aug 2025 17:14:44 +0800 Subject: [PATCH 77/80] lib: add trace-sigint APIs PR-URL: https://github.com/nodejs/node/pull/59040 Reviewed-By: Chengzhong Wu --- doc/api/util.md | 10 ++++++ lib/internal/process/pre_execution.js | 5 +-- lib/internal/util/trace_sigint.js | 29 ++++++++++++++++++ lib/util.js | 6 ++++ test/parallel/test-trace-sigint-in-worker.js | 20 ++++++++++++ test/pseudo-tty/test-start-trace-sigint.js | 32 ++++++++++++++++++++ test/pseudo-tty/test-start-trace-sigint.out | 11 +++++++ test/pseudo-tty/test-stop-trace-sigint.js | 32 ++++++++++++++++++++ test/pseudo-tty/test-stop-trace-sigint.out | 0 9 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 lib/internal/util/trace_sigint.js create mode 100644 test/parallel/test-trace-sigint-in-worker.js create mode 100644 test/pseudo-tty/test-start-trace-sigint.js create mode 100644 test/pseudo-tty/test-start-trace-sigint.out create mode 100644 test/pseudo-tty/test-stop-trace-sigint.js create mode 100644 test/pseudo-tty/test-stop-trace-sigint.out diff --git a/doc/api/util.md b/doc/api/util.md index b2df99241dccd3..68c2e297ee7e3a 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -739,6 +739,16 @@ fs.access('file/that/does/not/exist', (err) => { }); ``` +## `util.setTraceSigInt(enable)` + + + +* `enable` {boolean} + +Enable or disable printing a stack trace on `SIGINT`. The API is only available on the main thread. + ## `util.inherits(constructor, superConstructor)` The `Assert` class allows creating independent assertion instances with custom options. diff --git a/doc/api/cli.md b/doc/api/cli.md index 1cbccd8e13e64d..ba1356650c2383 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -3652,7 +3652,7 @@ When both are set, `--use-env-proxy` takes precedence. ### `NODE_USE_SYSTEM_CA=1` Node.js uses the trusted CA certificates present in the system store along with diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 53c0c3ba2bdd80..9333db6941abfe 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1916,7 +1916,7 @@ This can be called many times with new data as it is streamed. diff --git a/doc/api/fs.md b/doc/api/fs.md index 6df73592f12d33..57ce4c0bae3a10 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -7704,7 +7704,7 @@ Type of file system. ### Class: `fs.Utf8Stream` > Stability: 1 - Experimental diff --git a/doc/api/http.md b/doc/api/http.md index 4a3e6ef8d2aa9c..64bd5a519a75f1 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -1962,7 +1962,7 @@ affects new connections to the server, not any existing connections. ### `server.keepAliveTimeoutBuffer` * Type: {number} Timeout in milliseconds. **Default:** `1000` (1 second). diff --git a/doc/api/util.md b/doc/api/util.md index 68c2e297ee7e3a..f1dce7e65d3670 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -742,7 +742,7 @@ fs.access('file/that/does/not/exist', (err) => { ## `util.setTraceSigInt(enable)` * `enable` {boolean} diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index daee26f8286711..80949039875db2 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -724,7 +724,7 @@ This value is unique for each [`Worker`][] instance inside a single process. ## `worker.threadName` * {string|null} @@ -1773,7 +1773,7 @@ JavaScript code. * Returns: {Promise} @@ -2025,7 +2025,7 @@ This value is unique for each `Worker` instance inside a single process. ### `worker.threadName` * {string|null} diff --git a/doc/changelogs/CHANGELOG_V24.md b/doc/changelogs/CHANGELOG_V24.md index 0994f7cd51f566..eb6c549d0b8191 100644 --- a/doc/changelogs/CHANGELOG_V24.md +++ b/doc/changelogs/CHANGELOG_V24.md @@ -8,6 +8,7 @@ +24.6.0
24.5.0
24.4.1
24.4.0
@@ -47,6 +48,101 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2025-08-14, Version 24.6.0 (Current), @RafaelGSS + +### Notable Changes + +* \[[`471fe712b3`](https://github.com/nodejs/node/commit/471fe712b3)] - **(SEMVER-MINOR)** **cli**: add NODE\_USE\_SYSTEM\_CA=1 (Joyee Cheung) [#59276](https://github.com/nodejs/node/pull/59276) +* \[[`38aedfbf73`](https://github.com/nodejs/node/commit/38aedfbf73)] - **(SEMVER-MINOR)** **crypto**: support ML-DSA KeyObject, sign, and verify (Filip Skokan) [#59259](https://github.com/nodejs/node/pull/59259) +* \[[`201304537e`](https://github.com/nodejs/node/commit/201304537e)] - **(SEMVER-MINOR)** **zlib**: add dictionary support to zstdCompress and zstdDecompress (lluisemper) [#59240](https://github.com/nodejs/node/pull/59240) +* \[[`e79c93a5d0`](https://github.com/nodejs/node/commit/e79c93a5d0)] - **(SEMVER-MINOR)** **http**: add server.keepAliveTimeoutBuffer option (Haram Jeong) [#59243](https://github.com/nodejs/node/pull/59243) +* \[[`c144d69efc`](https://github.com/nodejs/node/commit/c144d69efc)] - **lib**: docs deprecate \_http\_\* (Sebastian Beltran) [#59293](https://github.com/nodejs/node/pull/59293) +* \[[`aeb4de55a7`](https://github.com/nodejs/node/commit/aeb4de55a7)] - **(SEMVER-MINOR)** **fs**: port SonicBoom module to fs module as Utf8Stream (James M Snell) [#58897](https://github.com/nodejs/node/pull/58897) + +### Commits + +* \[[`f7484575ff`](https://github.com/nodejs/node/commit/f7484575ff)] - **assert**: change utils to use index instead of for...of (방진혁) [#59278](https://github.com/nodejs/node/pull/59278) +* \[[`269cd16185`](https://github.com/nodejs/node/commit/269cd16185)] - **benchmark**: remove deprecated \_extend from benchmark (Rafael Gonzaga) [#59228](https://github.com/nodejs/node/pull/59228) +* \[[`848e49c20b`](https://github.com/nodejs/node/commit/848e49c20b)] - **benchmark**: add fs warmup to writefile-promises (Bruno Rodrigues) [#59215](https://github.com/nodejs/node/pull/59215) +* \[[`8c609be1b1`](https://github.com/nodejs/node/commit/8c609be1b1)] - **benchmark**: add calibrate-n script (Rafael Gonzaga) [#59186](https://github.com/nodejs/node/pull/59186) +* \[[`6a3bf772d8`](https://github.com/nodejs/node/commit/6a3bf772d8)] - **build**: fix node\_use\_sqlite for GN builds (Shelley Vohr) [#59017](https://github.com/nodejs/node/pull/59017) +* \[[`471fe712b3`](https://github.com/nodejs/node/commit/471fe712b3)] - **(SEMVER-MINOR)** **cli**: add NODE\_USE\_SYSTEM\_CA=1 (Joyee Cheung) [#59276](https://github.com/nodejs/node/pull/59276) +* \[[`38aedfbf73`](https://github.com/nodejs/node/commit/38aedfbf73)] - **(SEMVER-MINOR)** **crypto**: support ML-DSA KeyObject, sign, and verify (Filip Skokan) [#59259](https://github.com/nodejs/node/pull/59259) +* \[[`a312e706cf`](https://github.com/nodejs/node/commit/a312e706cf)] - **crypto**: prepare webcrypto key import/export for modern algorithms (Filip Skokan) [#59284](https://github.com/nodejs/node/pull/59284) +* \[[`3a7c2c3a47`](https://github.com/nodejs/node/commit/3a7c2c3a47)] - **deps**: update ada to 3.2.7 (Node.js GitHub Bot) [#59336](https://github.com/nodejs/node/pull/59336) +* \[[`8d9ceeaf6a`](https://github.com/nodejs/node/commit/8d9ceeaf6a)] - **deps**: update archs files for openssl-3.5.2 (Node.js GitHub Bot) [#59371](https://github.com/nodejs/node/pull/59371) +* \[[`33b06df354`](https://github.com/nodejs/node/commit/33b06df354)] - **deps**: upgrade openssl sources to openssl-3.5.2 (Node.js GitHub Bot) [#59371](https://github.com/nodejs/node/pull/59371) +* \[[`fa70f1af77`](https://github.com/nodejs/node/commit/fa70f1af77)] - **deps**: support madvise(3C) across ALL illumos revisions (Dan McDonald) [#58237](https://github.com/nodejs/node/pull/58237) +* \[[`f834a6be59`](https://github.com/nodejs/node/commit/f834a6be59)] - **deps**: update undici to 7.13.0 (Node.js GitHub Bot) [#59338](https://github.com/nodejs/node/pull/59338) +* \[[`db2417487e`](https://github.com/nodejs/node/commit/db2417487e)] - **deps**: update sqlite to 3.50.4 (Node.js GitHub Bot) [#59337](https://github.com/nodejs/node/pull/59337) +* \[[`41978adb08`](https://github.com/nodejs/node/commit/41978adb08)] - **deps**: V8: backport 493cb53691be (Chengzhong Wu) [#59238](https://github.com/nodejs/node/pull/59238) +* \[[`05667991ca`](https://github.com/nodejs/node/commit/05667991ca)] - **deps**: V8: backport 1c3e018e7d48 (Renegade334) [#58818](https://github.com/nodejs/node/pull/58818) +* \[[`fd61588bb4`](https://github.com/nodejs/node/commit/fd61588bb4)] - **doc**: rename x509.extKeyUsage to x509.keyUsage (Filip Skokan) [#59332](https://github.com/nodejs/node/pull/59332) +* \[[`a271ae4360`](https://github.com/nodejs/node/commit/a271ae4360)] - **doc**: fix Pbkdf2Params hash attribute heading (Filip Skokan) [#59395](https://github.com/nodejs/node/pull/59395) +* \[[`72cfff165b`](https://github.com/nodejs/node/commit/72cfff165b)] - **doc**: fix missing reference links for server.keepAliveTimeoutBuffer (Lee Jiho) [#59356](https://github.com/nodejs/node/pull/59356) +* \[[`8341916772`](https://github.com/nodejs/node/commit/8341916772)] - **doc**: fix grammar in global dispatcher usage (Eng Zer Jun) [#59344](https://github.com/nodejs/node/pull/59344) +* \[[`e3e489706b`](https://github.com/nodejs/node/commit/e3e489706b)] - **doc**: run license-builder (github-actions\[bot]) [#59343](https://github.com/nodejs/node/pull/59343) +* \[[`46527e8cea`](https://github.com/nodejs/node/commit/46527e8cea)] - **doc**: correct orthography `eg.` → `e.g.` (Jacob Smith) [#59329](https://github.com/nodejs/node/pull/59329) +* \[[`d140c3713e`](https://github.com/nodejs/node/commit/d140c3713e)] - **doc**: clarify the need of compiler compatible with c++20 (Rafael Gonzaga) [#59297](https://github.com/nodejs/node/pull/59297) +* \[[`95e9cabf9d`](https://github.com/nodejs/node/commit/95e9cabf9d)] - **doc**: clarify release candidate stability index (Filip Skokan) [#59295](https://github.com/nodejs/node/pull/59295) +* \[[`a056dd36d2`](https://github.com/nodejs/node/commit/a056dd36d2)] - **doc**: add WDYT to glossary (btea) [#59280](https://github.com/nodejs/node/pull/59280) +* \[[`1e2c52f5c4`](https://github.com/nodejs/node/commit/1e2c52f5c4)] - **doc**: add manpage entry for --use-system-ca (Joyee Cheung) [#59273](https://github.com/nodejs/node/pull/59273) +* \[[`31a46fdeb4`](https://github.com/nodejs/node/commit/31a46fdeb4)] - **doc**: add path.join and path.normalize clarification (Rafael Gonzaga) [#59262](https://github.com/nodejs/node/pull/59262) +* \[[`cff3725ff9`](https://github.com/nodejs/node/commit/cff3725ff9)] - **doc**: fix typo in `test/common/README.md` (Yoo) [#59180](https://github.com/nodejs/node/pull/59180) +* \[[`31a9283591`](https://github.com/nodejs/node/commit/31a9283591)] - **doc**: add note on process memoryUsage (fengmk2) [#59026](https://github.com/nodejs/node/pull/59026) +* \[[`5a98bff6b8`](https://github.com/nodejs/node/commit/5a98bff6b8)] - **doc**: format safely for `doc-kit` (Aviv Keller) [#59229](https://github.com/nodejs/node/pull/59229) +* \[[`95b8b7ea5c`](https://github.com/nodejs/node/commit/95b8b7ea5c)] - **domain**: remove deprecated API call (Alex Yang) [#59339](https://github.com/nodejs/node/pull/59339) +* \[[`2990f178bd`](https://github.com/nodejs/node/commit/2990f178bd)] - **fs**: fix glob TypeError on restricted dirs (Sylphy-0xd3ac) [#58674](https://github.com/nodejs/node/pull/58674) +* \[[`e2fb4caf9c`](https://github.com/nodejs/node/commit/e2fb4caf9c)] - **fs**: correct error message when FileHandle is transferred (Alex Yang) [#59156](https://github.com/nodejs/node/pull/59156) +* \[[`aeb4de55a7`](https://github.com/nodejs/node/commit/aeb4de55a7)] - **(SEMVER-MINOR)** **fs**: port SonicBoom module to fs module as Utf8Stream (James M Snell) [#58897](https://github.com/nodejs/node/pull/58897) +* \[[`e79c93a5d0`](https://github.com/nodejs/node/commit/e79c93a5d0)] - **(SEMVER-MINOR)** **http**: add server.keepAliveTimeoutBuffer option (Haram Jeong) [#59243](https://github.com/nodejs/node/pull/59243) +* \[[`0fb005a53f`](https://github.com/nodejs/node/commit/0fb005a53f)] - **http2**: set Http2Stream#sentHeaders for raw headers (Darshan Sen) [#59244](https://github.com/nodejs/node/pull/59244) +* \[[`e055539604`](https://github.com/nodejs/node/commit/e055539604)] - **lib**: add trace-sigint APIs (theanarkh) [#59040](https://github.com/nodejs/node/pull/59040) +* \[[`d2183d860a`](https://github.com/nodejs/node/commit/d2183d860a)] - **lib**: optimize writable stream buffer clearing (Yoo) [#59406](https://github.com/nodejs/node/pull/59406) +* \[[`47543a7e17`](https://github.com/nodejs/node/commit/47543a7e17)] - **lib**: handle windows reserved device names on UNC (Rafael Gonzaga) [#59286](https://github.com/nodejs/node/pull/59286) +* \[[`c6911f0717`](https://github.com/nodejs/node/commit/c6911f0717)] - **lib**: do not modify prototype deprecated asyncResource (RafaelGSS) [#59195](https://github.com/nodejs/node/pull/59195) +* \[[`3c88b769bb`](https://github.com/nodejs/node/commit/3c88b769bb)] - **lib**: restructure assert to become a class (Miguel Marcondes Filho) [#58253](https://github.com/nodejs/node/pull/58253) +* \[[`e91b54df59`](https://github.com/nodejs/node/commit/e91b54df59)] - **lib**: handle superscript variants on windows device (Rafael Gonzaga) [#59261](https://github.com/nodejs/node/pull/59261) +* \[[`4ee467905d`](https://github.com/nodejs/node/commit/4ee467905d)] - **lib**: use validateString (hotpineapple) [#59296](https://github.com/nodejs/node/pull/59296) +* \[[`c144d69efc`](https://github.com/nodejs/node/commit/c144d69efc)] - **lib**: docs deprecate \_http\_\* (Sebastian Beltran) [#59293](https://github.com/nodejs/node/pull/59293) +* \[[`c89b67e681`](https://github.com/nodejs/node/commit/c89b67e681)] - **lib**: add type names in source mapped stack traces (Chengzhong Wu) [#58976](https://github.com/nodejs/node/pull/58976) +* \[[`5b2363be8d`](https://github.com/nodejs/node/commit/5b2363be8d)] - **lib**: prefer AsyncIteratorPrototype primordial (René) [#59097](https://github.com/nodejs/node/pull/59097) +* \[[`41b4f4d694`](https://github.com/nodejs/node/commit/41b4f4d694)] - **meta**: clarify pr objection process further (James M Snell) [#59096](https://github.com/nodejs/node/pull/59096) +* \[[`0eb5962f1e`](https://github.com/nodejs/node/commit/0eb5962f1e)] - **meta**: add mailmap entry for aditi-1400 (Aditi) [#59316](https://github.com/nodejs/node/pull/59316) +* \[[`a2b72c2304`](https://github.com/nodejs/node/commit/a2b72c2304)] - **meta**: add tsc and build team as codeowners building.md (Rafael Gonzaga) [#59298](https://github.com/nodejs/node/pull/59298) +* \[[`d69f3ee1e0`](https://github.com/nodejs/node/commit/d69f3ee1e0)] - **meta**: add nodejs/path to path files (Rafael Gonzaga) [#59289](https://github.com/nodejs/node/pull/59289) +* \[[`1e37eab865`](https://github.com/nodejs/node/commit/1e37eab865)] - **node-api**: reword "implementation in an alternative VM" as implementable (Chengzhong Wu) [#59036](https://github.com/nodejs/node/pull/59036) +* \[[`64add6302a`](https://github.com/nodejs/node/commit/64add6302a)] - **src**: use simdjson to parse SEA configuration (Joyee Cheung) [#59323](https://github.com/nodejs/node/pull/59323) +* \[[`e9c6636585`](https://github.com/nodejs/node/commit/e9c6636585)] - **src**: mark realm leaf classes final (Anna Henningsen) [#59355](https://github.com/nodejs/node/pull/59355) +* \[[`42ef8147d1`](https://github.com/nodejs/node/commit/42ef8147d1)] - **src**: warn about FastOneByteString invalidation (James M Snell) [#59275](https://github.com/nodejs/node/pull/59275) +* \[[`8686b8037a`](https://github.com/nodejs/node/commit/8686b8037a)] - **src**: remove unused DSAKeyExportJob (Filip Skokan) [#59291](https://github.com/nodejs/node/pull/59291) +* \[[`1e5f632666`](https://github.com/nodejs/node/commit/1e5f632666)] - **src**: use C++20 `contains()` method (iknoom) [#59304](https://github.com/nodejs/node/pull/59304) +* \[[`22d4683cfe`](https://github.com/nodejs/node/commit/22d4683cfe)] - **src**: added CHECK\_NOT\_NULL check for multiple eq\_wrap\_async (F3lixTheCat) [#59267](https://github.com/nodejs/node/pull/59267) +* \[[`6a47ff4943`](https://github.com/nodejs/node/commit/6a47ff4943)] - **src**: clear all linked module caches once instantiated (Chengzhong Wu) [#59117](https://github.com/nodejs/node/pull/59117) +* \[[`33728cb4ca`](https://github.com/nodejs/node/commit/33728cb4ca)] - **src**: add nullptr checks in `StreamPipe::New` (Burkov Egor) [#57613](https://github.com/nodejs/node/pull/57613) +* \[[`4a907bdad1`](https://github.com/nodejs/node/commit/4a907bdad1)] - **src**: add percentage support to --max-old-space-size (Asaf Federman) [#59082](https://github.com/nodejs/node/pull/59082) +* \[[`7c189d4f55`](https://github.com/nodejs/node/commit/7c189d4f55)] - **test**: deflake sequential/test-tls-session-timeout (Joyee Cheung) [#59423](https://github.com/nodejs/node/pull/59423) +* \[[`fb0a6fb57f`](https://github.com/nodejs/node/commit/fb0a6fb57f)] - **test**: exclude mock from coverage (Shima Ryuhei) [#59348](https://github.com/nodejs/node/pull/59348) +* \[[`7e10f95f13`](https://github.com/nodejs/node/commit/7e10f95f13)] - **test**: split test-fs-cp.js (Joyee Cheung) [#59408](https://github.com/nodejs/node/pull/59408) +* \[[`41bcf5f659`](https://github.com/nodejs/node/commit/41bcf5f659)] - **test**: update WPT resources,WebCryptoAPI,webstorage (Filip Skokan) [#59311](https://github.com/nodejs/node/pull/59311) +* \[[`f9f3dc94cb`](https://github.com/nodejs/node/commit/f9f3dc94cb)] - **test**: add known issue test for fs.cpSync dereference bug (James M Snell) [#58941](https://github.com/nodejs/node/pull/58941) +* \[[`244d0c38a8`](https://github.com/nodejs/node/commit/244d0c38a8)] - **test**: deflake stream-readable-to-web test (Ethan Arrowood) [#58948](https://github.com/nodejs/node/pull/58948) +* \[[`564e604a1a`](https://github.com/nodejs/node/commit/564e604a1a)] - **test**: make test-inspector-network-resource sequential (Shima Ryuhei) [#59104](https://github.com/nodejs/node/pull/59104) +* \[[`7ab13b7477`](https://github.com/nodejs/node/commit/7ab13b7477)] - **test**: don't use expose internals in test-http-outgoing-buffer.js (Meghan Denny) [#59219](https://github.com/nodejs/node/pull/59219) +* \[[`319df3859a`](https://github.com/nodejs/node/commit/319df3859a)] - **test,crypto**: skip unsupported ciphers (Shelley Vohr) [#59388](https://github.com/nodejs/node/pull/59388) +* \[[`713c70c32a`](https://github.com/nodejs/node/commit/713c70c32a)] - **test\_runner**: remove unused callee convertion (Alex Yang) [#59221](https://github.com/nodejs/node/pull/59221) +* \[[`e4ca30e115`](https://github.com/nodejs/node/commit/e4ca30e115)] - **tools**: disable nullability-completeness warnings (Michaël Zasso) [#59392](https://github.com/nodejs/node/pull/59392) +* \[[`dab7f6b542`](https://github.com/nodejs/node/commit/dab7f6b542)] - **tools**: check for std::vector\ in lint (Aditi) [#58497](https://github.com/nodejs/node/pull/58497) +* \[[`7b94982eb0`](https://github.com/nodejs/node/commit/7b94982eb0)] - **tools**: allow selecting test subsystems with numbers in their names (Darshan Sen) [#59242](https://github.com/nodejs/node/pull/59242) +* \[[`16bbcd8881`](https://github.com/nodejs/node/commit/16bbcd8881)] - **typings**: improve internal binding types (Nam Yooseong) [#59351](https://github.com/nodejs/node/pull/59351) +* \[[`76bc4d659b`](https://github.com/nodejs/node/commit/76bc4d659b)] - **typings**: improve internal binding types (Michaël Zasso) [#59176](https://github.com/nodejs/node/pull/59176) +* \[[`eecd3272a6`](https://github.com/nodejs/node/commit/eecd3272a6)] - **worker**: add name for worker (theanarkh) [#59213](https://github.com/nodejs/node/pull/59213) +* \[[`84c3513ce2`](https://github.com/nodejs/node/commit/84c3513ce2)] - **worker**: implements nits in Web Locks code (Antoine du Hamel) [#59270](https://github.com/nodejs/node/pull/59270) +* \[[`bd68fbd753`](https://github.com/nodejs/node/commit/bd68fbd753)] - **worker**: add cpuUsage for worker (theanarkh) [#59177](https://github.com/nodejs/node/pull/59177) +* \[[`201304537e`](https://github.com/nodejs/node/commit/201304537e)] - **(SEMVER-MINOR)** **zlib**: add dictionary support to zstdCompress and zstdDecompress (lluisemper) [#59240](https://github.com/nodejs/node/pull/59240) + ## 2025-07-31, Version 24.5.0 (Current), @aduh95 diff --git a/src/node_version.h b/src/node_version.h index 0b023173eb46e4..9238ca24804757 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -23,13 +23,13 @@ #define SRC_NODE_VERSION_H_ #define NODE_MAJOR_VERSION 24 -#define NODE_MINOR_VERSION 5 -#define NODE_PATCH_VERSION 1 +#define NODE_MINOR_VERSION 6 +#define NODE_PATCH_VERSION 0 #define NODE_VERSION_IS_LTS 0 #define NODE_VERSION_LTS_CODENAME "" -#define NODE_VERSION_IS_RELEASE 0 +#define NODE_VERSION_IS_RELEASE 1 #ifndef NODE_STRINGIFY #define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n)