From 1ec6270efa3430e26a8616be1044585cf436c404 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Mon, 9 Jan 2023 20:15:49 +0100 Subject: [PATCH 01/72] http: res.setHeaders first implementation Backport-PR-URL: https://github.com/nodejs/node/pull/46272 PR-URL: https://github.com/nodejs/node/pull/46109 Reviewed-By: Matteo Collina Reviewed-By: Paolo Insogna Reviewed-By: Yagiz Nizipli Reviewed-By: Rafael Gonzaga --- doc/api/http.md | 44 ++++++ lib/_http_outgoing.js | 22 +++ .../parallel/test-http-response-setheaders.js | 131 ++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 test/parallel/test-http-response-setheaders.js diff --git a/doc/api/http.md b/doc/api/http.md index dcb39cef32f030..6c9e21ab806dee 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -2969,6 +2969,48 @@ Sets a single header value. If the header already exists in the to-be-sent headers, its value will be replaced. Use an array of strings to send multiple headers with the same name. +### `outgoingMessage.setHeaders(headers)` + + + +* `headers` {Headers|Map} +* Returns: {http.ServerResponse} + +Returns the response object. + +Sets multiple header values for implicit headers. +`headers` must be an instance of [`Headers`][] or `Map`, +if a header already exists in the to-be-sent headers, +its value will be replaced. + +```js +const headers = new Headers({ foo: 'bar' }); +response.setHeaders(headers); +``` + +or + +```js +const headers = new Map([['foo', 'bar']]); +res.setHeaders(headers); +``` + +When headers have been set with [`outgoingMessage.setHeaders()`][], +they will be merged with any headers passed to [`response.writeHead()`][], +with the headers passed to [`response.writeHead()`][] given precedence. + +```js +// Returns content-type = text/plain +const server = http.createServer((req, res) => { + const headers = new Headers({ 'Content-Type': 'text/html' }); + res.setHeaders(headers); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + ### `outgoingMessage.setTimeout(msesc[, callback])` + +A test reporter to use when running tests. See the documentation on +[test reporters][] for more details. + +### `--test-reporter-destination` + + + +The destination for the corresponding test reporter. See the documentation on +[test reporters][] for more details. + ### `--test-only` -The `node:test` module facilitates the creation of JavaScript tests that -report results in [TAP][] format. To access it: +The `node:test` module facilitates the creation of JavaScript tests. +To access it: ```mjs import test from 'node:test'; @@ -91,9 +91,7 @@ test('callback failing test', (t, done) => { }); ``` -As a test file executes, TAP is written to the standard output of the Node.js -process. This output can be interpreted by any test harness that understands -the TAP format. If any tests fail, the process exit code is set to `1`. +If any tests fail, the process exit code is set to `1`. ## Subtests @@ -122,8 +120,7 @@ test to fail. ## Skipping tests Individual tests can be skipped by passing the `skip` option to the test, or by -calling the test context's `skip()` method. Both of these options support -including a message that is displayed in the TAP output as shown in the +calling the test context's `skip()` method as shown in the following example. ```js @@ -258,7 +255,7 @@ Test name patterns do not change the set of files that the test runner executes. ## Extraneous asynchronous activity -Once a test function finishes executing, the TAP results are output as quickly +Once a test function finishes executing, the results are reported as quickly as possible while maintaining the order of the tests. However, it is possible for the test function to generate asynchronous activity that outlives the test itself. The test runner handles this type of activity, but does not delay the @@ -267,13 +264,13 @@ reporting of test results in order to accommodate it. In the following example, a test completes with two `setImmediate()` operations still outstanding. The first `setImmediate()` attempts to create a new subtest. Because the parent test has already finished and output its -results, the new subtest is immediately marked as failed, and reported in the -top level of the file's TAP output. +results, the new subtest is immediately marked as failed, and reported later +to the {TestsStream}. The second `setImmediate()` creates an `uncaughtException` event. `uncaughtException` and `unhandledRejection` events originating from a completed test are marked as failed by the `test` module and reported as diagnostic -warnings in the top level of the file's TAP output. +warnings at the top level by the {TestsStream}. ```js test('a test that creates asynchronous activity', (t) => { @@ -454,6 +451,166 @@ test('spies on an object method', (t) => { }); ``` +## Test reporters + + + +The `node:test` module supports passing [`--test-reporter`][] +flags for the test runner to use a specific reporter. + +The following built-reporters are supported: + +* `tap` + The `tap` reporter is the default reporter used by the test runner. It outputs + the test results in the [TAP][] format. + +* `spec` + The `spec` reporter outputs the test results in a human-readable format. + +* `dot` + The `dot` reporter outputs the test results in a comact format, + where each passing test is represented by a `.`, + and each failing test is represented by a `X`. + +### Custom reporters + +[`--test-reporter`][] can be used to specify a path to custom reporter. +a custom reporter is a module that exports a value +accepted by [stream.compose][]. +Reporters should transform events emitted by a {TestsStream} + +Example of a custom reporter using {stream.Transform}: + +```mjs +import { Transform } from 'node:stream'; + +const customReporter = new Transform({ + writableObjectMode: true, + transform(event, encoding, callback) { + switch (event.type) { + case 'test:start': + callback(null, `test ${event.data.name} started`); + break; + case 'test:pass': + callback(null, `test ${event.data.name} passed`); + break; + case 'test:fail': + callback(null, `test ${event.data.name} failed`); + break; + case 'test:plan': + callback(null, 'test plan'); + break; + case 'test:diagnostic': + callback(null, event.data.message); + break; + } + }, +}); + +export default customReporter; +``` + +```cjs +const { Transform } = require('node:stream'); + +const customReporter = new Transform({ + writableObjectMode: true, + transform(event, encoding, callback) { + switch (event.type) { + case 'test:start': + callback(null, `test ${event.data.name} started`); + break; + case 'test:pass': + callback(null, `test ${event.data.name} passed`); + break; + case 'test:fail': + callback(null, `test ${event.data.name} failed`); + break; + case 'test:plan': + callback(null, 'test plan'); + break; + case 'test:diagnostic': + callback(null, event.data.message); + break; + } + }, +}); + +module.exports = customReporter; +``` + +Example of a custom reporter using a generator function: + +```mjs +export default async function * customReporter(source) { + for await (const event of source) { + switch (event.type) { + case 'test:start': + yield `test ${event.data.name} started\n`; + break; + case 'test:pass': + yield `test ${event.data.name} passed\n`; + break; + case 'test:fail': + yield `test ${event.data.name} failed\n`; + break; + case 'test:plan': + yield 'test plan'; + break; + case 'test:diagnostic': + yield `${event.data.message}\n`; + break; + } + } +} +``` + +```cjs +module.exports = async function * customReporter(source) { + for await (const event of source) { + switch (event.type) { + case 'test:start': + yield `test ${event.data.name} started\n`; + break; + case 'test:pass': + yield `test ${event.data.name} passed\n`; + break; + case 'test:fail': + yield `test ${event.data.name} failed\n`; + break; + case 'test:plan': + yield 'test plan\n'; + break; + case 'test:diagnostic': + yield `${event.data.message}\n`; + break; + } + } +}; +``` + +### Multiple reporters + +The [`--test-reporter`][] flag can be specified multiple times to report test +results in several formats. In this situation +it is required to specify a destination for each reporter +using [`--test-reporter-destination`][]. +Destination can be `stdout`, `stderr`, or a file path. +Reporters and destinations are paired according +to the order they were specified. + +In the following example, the `spec` reporter will output to `stdout`, +and the `dot` reporter will output to `file.txt`: + +```bash +node --test-reporter=spec --test-reporter=dot --test-reporter-destination=stdout --test-reporter-destination=file.txt +``` + +When a single reporter is specified, the destination will default to `stdout`, +unless a destination is explicitly provided. + ## `run([options])` -* `message` {string} Message to be displayed as a TAP diagnostic. +* `message` {string} Message to be reported. -This function is used to write TAP diagnostics to the output. Any diagnostic +This function is used to write diagnostics to the output. Any diagnostic information is included at the end of the test's results. This function does not return a value. @@ -1275,10 +1451,10 @@ added: - v16.17.0 --> -* `message` {string} Optional skip message to be displayed in TAP output. +* `message` {string} Optional skip message. This function causes the test's output to indicate the test as skipped. If -`message` is provided, it is included in the TAP output. Calling `skip()` does +`message` is provided, it is included in the output. Calling `skip()` does not terminate execution of the test function. This function does not return a value. @@ -1297,10 +1473,10 @@ added: - v16.17.0 --> -* `message` {string} Optional `TODO` message to be displayed in TAP output. +* `message` {string} Optional `TODO` message. This function adds a `TODO` directive to the test's output. If `message` is -provided, it is included in the TAP output. Calling `todo()` does not terminate +provided, it is included in the output. Calling `todo()` does not terminate execution of the test function. This function does not return a value. ```js @@ -1407,6 +1583,8 @@ added: [TAP]: https://testanything.org/ [`--test-name-pattern`]: cli.md#--test-name-pattern [`--test-only`]: cli.md#--test-only +[`--test-reporter-destination`]: cli.md#--test-reporter-destination +[`--test-reporter`]: cli.md#--test-reporter [`--test`]: cli.md#--test [`MockFunctionContext`]: #class-mockfunctioncontext [`MockTracker.method`]: #mockmethodobject-methodname-implementation-options @@ -1420,4 +1598,5 @@ added: [`test()`]: #testname-options-fn [describe options]: #describename-options-fn [it options]: #testname-options-fn +[stream.compose]: stream.md#streamcomposestreams [test runner execution model]: #test-runner-execution-model diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index f7165a0288cf9e..658aab03323a24 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -6,6 +6,7 @@ const { const { getOptionValue } = require('internal/options'); const { isUsingInspector } = require('internal/util/inspector'); const { run } = require('internal/test_runner/runner'); +const { setupTestReporters } = require('internal/test_runner/utils'); const { exitCodes: { kGenericUserError } } = internalBinding('errors'); prepareMainThreadExecution(false); @@ -21,8 +22,8 @@ if (isUsingInspector()) { inspectPort = process.debugPort; } -const tapStream = run({ concurrency, inspectPort, watch: getOptionValue('--watch') }); -tapStream.pipe(process.stdout); -tapStream.once('test:fail', () => { +const testsStream = run({ concurrency, inspectPort, watch: getOptionValue('--watch') }); +testsStream.once('test:fail', () => { process.exitCode = kGenericUserError; }); +setupTestReporters(testsStream); diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 0bfe7b11241416..e5dc867882dbde 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -1,11 +1,8 @@ 'use strict'; -const { - StringPrototypeEndsWith, -} = primordials; - const { getOptionValue } = require('internal/options'); const path = require('path'); +const { shouldUseESMLoader } = require('internal/modules/utils'); function resolveMainPath(main) { // Note extension resolution for the main entry point can be deprecated in a @@ -23,29 +20,6 @@ function resolveMainPath(main) { return mainPath; } -function shouldUseESMLoader(mainPath) { - /** - * @type {string[]} userLoaders A list of custom loaders registered by the user - * (or an empty list when none have been registered). - */ - const userLoaders = getOptionValue('--experimental-loader'); - /** - * @type {string[]} userImports A list of preloaded modules registered by the user - * (or an empty list when none have been registered). - */ - const userImports = getOptionValue('--import'); - if (userLoaders.length > 0 || userImports.length > 0) - return true; - const { readPackageScope } = require('internal/modules/cjs/loader'); - // Determine the module format of the main - if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) - return true; - if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) - return false; - const pkg = readPackageScope(mainPath); - return pkg && pkg.data.type === 'module'; -} - function runMainESM(mainPath) { const { loadESM } = require('internal/process/esm_loader'); const { pathToFileURL } = require('internal/url'); diff --git a/lib/internal/modules/utils.js b/lib/internal/modules/utils.js new file mode 100644 index 00000000000000..1e2d0b3edcf499 --- /dev/null +++ b/lib/internal/modules/utils.js @@ -0,0 +1,54 @@ +'use strict'; + +const { + StringPrototypeEndsWith, +} = primordials; +const { getOptionValue } = require('internal/options'); + + +function shouldUseESMLoader(filePath) { + /** + * @type {string[]} userLoaders A list of custom loaders registered by the user + * (or an empty list when none have been registered). + */ + const userLoaders = getOptionValue('--experimental-loader'); + /** + * @type {string[]} userImports A list of preloaded modules registered by the user + * (or an empty list when none have been registered). + */ + const userImports = getOptionValue('--import'); + if (userLoaders.length > 0 || userImports.length > 0) + return true; + // Determine the module format of the main + if (filePath && StringPrototypeEndsWith(filePath, '.mjs')) + return true; + if (!filePath || StringPrototypeEndsWith(filePath, '.cjs')) + return false; + const { readPackageScope } = require('internal/modules/cjs/loader'); + const pkg = readPackageScope(filePath); + return pkg?.data?.type === 'module'; +} + +/** + * @param {string} filePath + * @returns {any} + * requireOrImport imports a module if the file is an ES module, otherwise it requires it. + */ +function requireOrImport(filePath) { + const useESMLoader = shouldUseESMLoader(filePath); + if (useESMLoader) { + const { esmLoader } = require('internal/process/esm_loader'); + const { pathToFileURL } = require('internal/url'); + const { isAbsolute } = require('path'); + const file = isAbsolute(filePath) ? pathToFileURL(filePath).href : filePath; + return esmLoader.import(file, undefined, { __proto__: null }); + } + const { Module } = require('internal/modules/cjs/loader'); + + return new Module._load(filePath, null, false); +} + +module.exports = { + shouldUseESMLoader, + requireOrImport, +}; diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 0a6be080e8b7f1..33c0bb5ae8c962 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -18,6 +18,7 @@ const { exitCodes: { kGenericUserError } } = internalBinding('errors'); const { kEmptyObject } = require('internal/util'); const { getOptionValue } = require('internal/options'); const { kCancelledByParent, Test, ItTest, Suite } = require('internal/test_runner/test'); +const { setupTestReporters } = require('internal/test_runner/utils'); const { bigint: hrtime } = process.hrtime; const isTestRunnerCli = getOptionValue('--test'); @@ -109,7 +110,6 @@ function setup(root) { } root.startTime = hrtime(); - root.reporter.version(); wasRootSetup.add(root); return root; @@ -119,10 +119,10 @@ let globalRoot; function getGlobalRoot() { if (!globalRoot) { globalRoot = createTestTree(); - globalRoot.reporter.pipe(process.stdout); globalRoot.reporter.once('test:fail', () => { process.exitCode = kGenericUserError; }); + setupTestReporters(globalRoot.reporter); } return globalRoot; } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index e303e8f050e0b7..ff91993ce9df29 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -6,6 +6,7 @@ const { ArrayPrototypeIncludes, ArrayPrototypePush, ArrayPrototypeSlice, + ArrayPrototypeSome, ArrayPrototypeSort, ObjectAssign, PromisePrototypeThen, @@ -14,7 +15,7 @@ const { SafePromiseAllSettledReturnVoid, SafeMap, SafeSet, - StringPrototypeRepeat, + StringPrototypeStartsWith, } = primordials; const { spawn } = require('child_process'); @@ -32,9 +33,9 @@ const { validateArray, validateBoolean } = require('internal/validators'); const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector'); const { kEmptyObject } = require('internal/util'); const { createTestTree } = require('internal/test_runner/harness'); -const { kDefaultIndent, kSubtestsFailed, Test } = require('internal/test_runner/test'); +const { kSubtestsFailed, Test } = require('internal/test_runner/test'); const { TapParser } = require('internal/test_runner/tap_parser'); -const { YAMLToJs } = require('internal/test_runner/yaml_parser'); +const { YAMLToJs } = require('internal/test_runner/yaml_to_js'); const { TokenKind } = require('internal/test_runner/tap_lexer'); const { @@ -49,6 +50,7 @@ const { } = internalBinding('errors'); const kFilterArgs = ['--test', '--watch']; +const kFilterArgValues = ['--test-reporter', '--test-reporter-destination']; // TODO(cjihrig): Replace this with recursive readdir once it lands. function processPath(path, testFiles, options) { @@ -112,8 +114,9 @@ function createTestFileList() { return ArrayPrototypeSort(ArrayFrom(testFiles)); } -function filterExecArgv(arg) { - return !ArrayPrototypeIncludes(kFilterArgs, arg); +function filterExecArgv(arg, i, arr) { + return !ArrayPrototypeIncludes(kFilterArgs, arg) && + !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)); } function getRunArgs({ path, inspectPort }) { @@ -128,7 +131,7 @@ function getRunArgs({ path, inspectPort }) { class FileTest extends Test { #buffer = []; #handleReportItem({ kind, node, nesting = 0 }) { - const indent = StringPrototypeRepeat(kDefaultIndent, nesting + 1); + nesting += 1; switch (kind) { case TokenKind.TAP_VERSION: @@ -137,11 +140,11 @@ class FileTest extends Test { break; case TokenKind.TAP_PLAN: - this.reporter.plan(indent, node.end - node.start + 1); + this.reporter.plan(nesting, node.end - node.start + 1); break; case TokenKind.TAP_SUBTEST_POINT: - this.reporter.subtest(indent, node.name); + this.reporter.start(nesting, node.name); break; case TokenKind.TAP_TEST_POINT: @@ -160,7 +163,7 @@ class FileTest extends Test { if (pass) { this.reporter.ok( - indent, + nesting, node.id, node.description, YAMLToJs(node.diagnostics), @@ -168,7 +171,7 @@ class FileTest extends Test { ); } else { this.reporter.fail( - indent, + nesting, node.id, node.description, YAMLToJs(node.diagnostics), @@ -178,15 +181,15 @@ class FileTest extends Test { break; case TokenKind.COMMENT: - if (indent === kDefaultIndent) { + if (nesting === 1) { // Ignore file top level diagnostics break; } - this.reporter.diagnostic(indent, node.comment); + this.reporter.diagnostic(nesting, node.comment); break; case TokenKind.UNKNOWN: - this.reporter.diagnostic(indent, node.value); + this.reporter.diagnostic(nesting, node.value); break; } } @@ -195,11 +198,11 @@ class FileTest extends Test { ArrayPrototypePush(this.#buffer, ast); return; } - this.reportSubtest(); + this.reportStarted(); this.#handleReportItem(ast); } report() { - this.reportSubtest(); + this.reportStarted(); ArrayPrototypeForEach(this.#buffer, (ast) => this.#handleReportItem(ast)); super.report(); } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 6f1c29d1def6a0..b7267491d50c3f 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -33,7 +33,7 @@ const { } = require('internal/errors'); const { getOptionValue } = require('internal/options'); const { MockTracker } = require('internal/test_runner/mock'); -const { TapStream } = require('internal/test_runner/tap_stream'); +const { TestsStream } = require('internal/test_runner/tests_stream'); const { convertStringToRegExp, createDeferredCallback, @@ -63,7 +63,6 @@ const kTestCodeFailure = 'testCodeFailure'; const kTestTimeoutFailure = 'testTimeoutFailure'; const kHookFailure = 'hookFailed'; const kDefaultTimeout = null; -const kDefaultIndent = ' '; // 4 spaces const noop = FunctionPrototype; const isTestRunner = getOptionValue('--test'); const testOnlyFlag = !isTestRunner && getOptionValue('--test-only'); @@ -190,18 +189,18 @@ class Test extends AsyncResource { if (parent === null) { this.concurrency = 1; - this.indent = ''; + this.nesting = 0; this.only = testOnlyFlag; - this.reporter = new TapStream(); + this.reporter = new TestsStream(); this.runOnlySubtests = this.only; this.testNumber = 0; this.timeout = kDefaultTimeout; } else { - const indent = parent.parent === null ? parent.indent : - parent.indent + kDefaultIndent; + const nesting = parent.parent === null ? parent.nesting : + parent.nesting + 1; this.concurrency = parent.concurrency; - this.indent = indent; + this.nesting = nesting; this.only = only ?? !parent.runOnlySubtests; this.reporter = parent.reporter; this.runOnlySubtests = !this.only; @@ -334,7 +333,7 @@ class Test extends AsyncResource { } if (i === 1 && this.parent !== null) { - this.reportSubtest(); + this.reportStarted(); } // Report the subtest's results and remove it from the ready map. @@ -633,19 +632,19 @@ class Test extends AsyncResource { this.parent.processPendingSubtests(); } else if (!this.reported) { this.reported = true; - this.reporter.plan(this.indent, this.subtests.length); + this.reporter.plan(this.nesting, this.subtests.length); for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.indent, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, this.diagnostics[i]); } - this.reporter.diagnostic(this.indent, `tests ${this.subtests.length}`); - this.reporter.diagnostic(this.indent, `pass ${counters.passed}`); - this.reporter.diagnostic(this.indent, `fail ${counters.failed}`); - this.reporter.diagnostic(this.indent, `cancelled ${counters.cancelled}`); - this.reporter.diagnostic(this.indent, `skipped ${counters.skipped}`); - this.reporter.diagnostic(this.indent, `todo ${counters.todo}`); - this.reporter.diagnostic(this.indent, `duration_ms ${this.#duration()}`); + this.reporter.diagnostic(this.nesting, `tests ${this.subtests.length}`); + this.reporter.diagnostic(this.nesting, `pass ${counters.passed}`); + this.reporter.diagnostic(this.nesting, `fail ${counters.failed}`); + this.reporter.diagnostic(this.nesting, `cancelled ${counters.cancelled}`); + this.reporter.diagnostic(this.nesting, `skipped ${counters.skipped}`); + this.reporter.diagnostic(this.nesting, `todo ${counters.todo}`); + this.reporter.diagnostic(this.nesting, `duration_ms ${this.#duration()}`); this.reporter.push(null); } } @@ -681,9 +680,9 @@ class Test extends AsyncResource { report() { if (this.subtests.length > 0) { - this.reporter.plan(this.subtests[0].indent, this.subtests.length); + this.reporter.plan(this.subtests[0].nesting, this.subtests.length); } else { - this.reportSubtest(); + this.reportStarted(); } let directive; const details = { __proto__: null, duration_ms: this.#duration() }; @@ -695,24 +694,24 @@ class Test extends AsyncResource { } if (this.passed) { - this.reporter.ok(this.indent, this.testNumber, this.name, details, directive); + this.reporter.ok(this.nesting, this.testNumber, this.name, details, directive); } else { details.error = this.error; - this.reporter.fail(this.indent, this.testNumber, this.name, details, directive); + this.reporter.fail(this.nesting, this.testNumber, this.name, details, directive); } for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.indent, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, this.diagnostics[i]); } } - reportSubtest() { + reportStarted() { if (this.#reportedSubtest || this.parent === null) { return; } this.#reportedSubtest = true; - this.parent.reportSubtest(); - this.reporter.subtest(this.indent, this.name); + this.parent.reportStarted(); + this.reporter.start(this.nesting, this.name); } } @@ -817,7 +816,6 @@ class Suite extends Test { module.exports = { ItTest, kCancelledByParent, - kDefaultIndent, kSubtestsFailed, kTestCodeFailure, kUnwrapErrors, diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js new file mode 100644 index 00000000000000..b016d316154807 --- /dev/null +++ b/lib/internal/test_runner/tests_stream.js @@ -0,0 +1,74 @@ +'use strict'; +const { + ArrayPrototypePush, + ArrayPrototypeShift, +} = primordials; +const Readable = require('internal/streams/readable'); + +class TestsStream extends Readable { + #buffer; + #canPush; + + constructor() { + super({ objectMode: true }); + this.#buffer = []; + this.#canPush = true; + } + + _read() { + this.#canPush = true; + + while (this.#buffer.length > 0) { + const obj = ArrayPrototypeShift(this.#buffer); + + if (!this.#tryPush(obj)) { + return; + } + } + } + + fail(nesting, testNumber, name, details, directive) { + this.#emit('test:fail', { __proto__: null, name, nesting, testNumber, details, ...directive }); + } + + ok(nesting, testNumber, name, details, directive) { + this.#emit('test:pass', { __proto__: null, name, nesting, testNumber, details, ...directive }); + } + + plan(nesting, count) { + this.#emit('test:plan', { __proto__: null, nesting, count }); + } + + getSkip(reason = undefined) { + return { __proto__: null, skip: reason ?? true }; + } + + getTodo(reason = undefined) { + return { __proto__: null, todo: reason ?? true }; + } + + start(nesting, name) { + this.#emit('test:start', { __proto__: null, nesting, name }); + } + + diagnostic(nesting, message) { + this.#emit('test:diagnostic', { __proto__: null, nesting, message }); + } + + #emit(type, data) { + this.emit(type, data); + this.#tryPush({ type, data }); + } + + #tryPush(message) { + if (this.#canPush) { + this.#canPush = this.push(message); + } else { + ArrayPrototypePush(this.#buffer, message); + } + + return this.#canPush; + } +} + +module.exports = { TestsStream }; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index ad040f010250e2..9dba00de25719e 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -1,7 +1,18 @@ 'use strict'; -const { RegExp, RegExpPrototypeExec } = primordials; +const { + ArrayPrototypePush, + ObjectGetOwnPropertyDescriptor, + SafePromiseAllReturnArrayLike, + RegExp, + RegExpPrototypeExec, + SafeMap, +} = primordials; const { basename } = require('path'); +const { createWriteStream } = require('fs'); const { createDeferredPromise } = require('internal/util'); +const { getOptionValue } = require('internal/options'); +const { requireOrImport } = require('internal/modules/utils'); + const { codes: { ERR_INVALID_ARG_VALUE, @@ -9,6 +20,7 @@ const { }, kIsNodeError, } = require('internal/errors'); +const { compose } = require('stream'); const kMultipleCallbackInvocations = 'multipleCallbackInvocations'; const kRegExpPattern = /^\/(.*)\/([a-z]*)$/; @@ -74,10 +86,71 @@ function convertStringToRegExp(str, name) { } } +const kBuiltinDestinations = new SafeMap([ + ['stdout', process.stdout], + ['stderr', process.stderr], +]); + +const kBuiltinReporters = new SafeMap([ + ['spec', 'node:test/reporter/spec'], + ['dot', 'node:test/reporter/dot'], + ['tap', 'node:test/reporter/tap'], +]); + +const kDefaultReporter = 'tap'; +const kDefaultDestination = 'stdout'; + +async function getReportersMap(reporters, destinations) { + return SafePromiseAllReturnArrayLike(reporters, async (name, i) => { + const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]); + let reporter = await requireOrImport(kBuiltinReporters.get(name) ?? name); + + if (reporter?.default) { + reporter = reporter.default; + } + + if (reporter?.prototype && ObjectGetOwnPropertyDescriptor(reporter.prototype, 'constructor')) { + reporter = new reporter(); + } + + if (!reporter) { + throw new ERR_INVALID_ARG_VALUE('Reporter', name, 'is not a valid reporter'); + } + + return { __proto__: null, reporter, destination }; + }); +} + + +async function setupTestReporters(testsStream) { + const destinations = getOptionValue('--test-reporter-destination'); + const reporters = getOptionValue('--test-reporter'); + + if (reporters.length === 0 && destinations.length === 0) { + ArrayPrototypePush(reporters, kDefaultReporter); + } + + if (reporters.length === 1 && destinations.length === 0) { + ArrayPrototypePush(destinations, kDefaultDestination); + } + + if (destinations.length !== reporters.length) { + throw new ERR_INVALID_ARG_VALUE('--test-reporter', reporters, + 'must match the number of specified \'--test-reporter-destination\''); + } + + const reportersMap = await getReportersMap(reporters, destinations); + for (let i = 0; i < reportersMap.length; i++) { + const { reporter, destination } = reportersMap[i]; + compose(testsStream, reporter).pipe(destination); + } +} + module.exports = { convertStringToRegExp, createDeferredCallback, doesPathMatchFilter, isSupportedFileType, isTestFailureError, + setupTestReporters, }; diff --git a/lib/internal/test_runner/yaml_parser.js b/lib/internal/test_runner/yaml_to_js.js similarity index 100% rename from lib/internal/test_runner/yaml_parser.js rename to lib/internal/test_runner/yaml_to_js.js diff --git a/lib/internal/util/colors.js b/lib/internal/util/colors.js index 5622a88467d038..79021a2bd9825d 100644 --- a/lib/internal/util/colors.js +++ b/lib/internal/util/colors.js @@ -5,6 +5,7 @@ module.exports = { green: '', white: '', red: '', + gray: '', clear: '', hasColors: false, refresh() { @@ -14,6 +15,7 @@ module.exports = { module.exports.green = hasColors ? '\u001b[32m' : ''; module.exports.white = hasColors ? '\u001b[39m' : ''; module.exports.red = hasColors ? '\u001b[31m' : ''; + module.exports.gray = hasColors ? '\u001b[90m' : ''; module.exports.clear = hasColors ? '\u001bc' : ''; module.exports.hasColors = hasColors; } diff --git a/lib/test/reporter/dot.js b/lib/test/reporter/dot.js new file mode 100644 index 00000000000000..7dbba5a957894e --- /dev/null +++ b/lib/test/reporter/dot.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = async function* dot(source) { + let count = 0; + for await (const { type } of source) { + if (type === 'test:pass') { + yield '.'; + } + if (type === 'test:fail') { + yield 'X'; + } + if ((type === 'test:fail' || type === 'test:pass') && ++count % 20 === 0) { + yield '\n'; + } + } + yield '\n'; +}; diff --git a/lib/test/reporter/spec.js b/lib/test/reporter/spec.js new file mode 100644 index 00000000000000..c19d5568d1c5ca --- /dev/null +++ b/lib/test/reporter/spec.js @@ -0,0 +1,107 @@ +'use strict'; + +const { + ArrayPrototypeJoin, + ArrayPrototypePop, + ArrayPrototypeShift, + ArrayPrototypeUnshift, + hardenRegExp, + RegExpPrototypeSymbolSplit, + SafeMap, + StringPrototypeRepeat, +} = primordials; +const assert = require('assert'); +const Transform = require('internal/streams/transform'); +const { inspectWithNoCustomRetry } = require('internal/errors'); +const { green, blue, red, white, gray } = require('internal/util/colors'); + + +const inspectOptions = { __proto__: null, colors: true, breakLength: Infinity }; + +const colors = { + '__proto__': null, + 'test:fail': red, + 'test:pass': green, + 'test:diagnostic': blue, +}; +const symbols = { + '__proto__': null, + 'test:fail': '\u2716 ', + 'test:pass': '\u2714 ', + 'test:diagnostic': '\u2139 ', + 'arrow:right': '\u25B6 ', +}; +class SpecReporter extends Transform { + #stack = []; + #reported = []; + #indentMemo = new SafeMap(); + + constructor() { + super({ writableObjectMode: true }); + } + + #indent(nesting) { + let value = this.#indentMemo.get(nesting); + if (value === undefined) { + value = StringPrototypeRepeat(' ', nesting); + this.#indentMemo.set(nesting, value); + } + + return value; + } + #formatError(error, indent) { + if (!error) return ''; + const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error; + const message = ArrayPrototypeJoin( + RegExpPrototypeSymbolSplit( + hardenRegExp(/\r?\n/), + inspectWithNoCustomRetry(err, inspectOptions), + ), `\n${indent} `); + return `\n${indent} ${message}\n`; + } + #handleEvent({ type, data }) { + const color = colors[type] ?? white; + const symbol = symbols[type] ?? ' '; + + switch (type) { + case 'test:fail': + case 'test:pass': { + const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event + if (subtest) { + assert(subtest.type === 'test:start'); + assert(subtest.data.nesting === data.nesting); + assert(subtest.data.name === data.name); + } + let prefix = ''; + while (this.#stack.length) { + // Report all the parent `test:start` events + const parent = ArrayPrototypePop(this.#stack); + assert(parent.type === 'test:start'); + const msg = parent.data; + ArrayPrototypeUnshift(this.#reported, msg); + prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`; + } + const indent = this.#indent(data.nesting); + const duration_ms = data.details?.duration_ms ? ` ${gray}(${data.details.duration_ms}ms)${white}` : ''; + const title = `${data.name}${duration_ms}`; + if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) { + // If this test has had children - it was already reporter, so slightly modify the output + ArrayPrototypeShift(this.#reported); + return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n\n`; + } + const error = this.#formatError(data.details?.error, indent); + return `${prefix}${indent}${color}${symbol}${title}${error}${white}\n`; + } + case 'test:start': + ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type }); + break; + case 'test:diagnostic': + return `${color}${this.#indent(data.nesting)}${symbol}${data.message}${white}\n`; + } + } + _transform({ type, data }, encoding, callback) { + callback(null, this.#handleEvent({ type, data })); + } +} + +module.exports = SpecReporter; diff --git a/lib/internal/test_runner/tap_stream.js b/lib/test/reporter/tap.js similarity index 63% rename from lib/internal/test_runner/tap_stream.js rename to lib/test/reporter/tap.js index 052f8284c8d931..fa5d4684fbb9e3 100644 --- a/lib/internal/test_runner/tap_stream.js +++ b/lib/test/reporter/tap.js @@ -2,18 +2,17 @@ const { ArrayPrototypeForEach, ArrayPrototypeJoin, - ArrayPrototypeMap, ArrayPrototypePush, - ArrayPrototypeShift, ObjectEntries, + RegExpPrototypeSymbolReplace, + SafeMap, StringPrototypeReplaceAll, - StringPrototypeToUpperCase, StringPrototypeSplit, - RegExpPrototypeSymbolReplace, + StringPrototypeRepeat, } = primordials; const { inspectWithNoCustomRetry } = require('internal/errors'); -const Readable = require('internal/streams/readable'); const { isError, kEmptyObject } = require('internal/util'); +const kDefaultIndent = ' '; // 4 spaces const kFrameStartRegExp = /^ {4}at /; const kLineBreakRegExp = /\n|\r\n/; const kDefaultTAPVersion = 13; @@ -22,112 +21,77 @@ let testModule; // Lazy loaded due to circular dependency. function lazyLoadTest() { testModule ??= require('internal/test_runner/test'); - return testModule; } -class TapStream extends Readable { - #buffer; - #canPush; - - constructor() { - super(); - this.#buffer = []; - this.#canPush = true; - } - - _read() { - this.#canPush = true; - while (this.#buffer.length > 0) { - const line = ArrayPrototypeShift(this.#buffer); - - if (!this.#tryPush(line)) { - return; - } +async function * tapReporter(source) { + yield `TAP version ${kDefaultTAPVersion}\n`; + for await (const { type, data } of source) { + switch (type) { + case 'test:fail': + yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo); + yield reportDetails(data.nesting, data.details); + break; + case 'test:pass': + yield reportTest(data.nesting, data.testNumber, 'ok', data.name, data.skip, data.todo); + yield reportDetails(data.nesting, data.details); + break; + case 'test:plan': + yield `${indent(data.nesting)}1..${data.count}\n`; + break; + case 'test:start': + yield `${indent(data.nesting)}# Subtest: ${tapEscape(data.name)}\n`; + break; + case 'test:diagnostic': + yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`; + break; } } +} - bail(message) { - this.#tryPush(`Bail out!${message ? ` ${tapEscape(message)}` : ''}\n`); - } - - fail(indent, testNumber, name, details, directive) { - this.emit('test:fail', { __proto__: null, name, testNumber, details, ...directive }); - this.#test(indent, testNumber, 'not ok', name, directive); - this.#details(indent, details); - } - - ok(indent, testNumber, name, details, directive) { - this.emit('test:pass', { __proto__: null, name, testNumber, details, ...directive }); - this.#test(indent, testNumber, 'ok', name, directive); - this.#details(indent, details); - } - - plan(indent, count, explanation) { - const exp = `${explanation ? ` # ${tapEscape(explanation)}` : ''}`; - - this.#tryPush(`${indent}1..${count}${exp}\n`); - } - - getSkip(reason) { - return { __proto__: null, skip: reason }; - } - - getTodo(reason) { - return { __proto__: null, todo: reason }; - } - - subtest(indent, name) { - this.#tryPush(`${indent}# Subtest: ${tapEscape(name)}\n`); - } - - #details(indent, data = kEmptyObject) { - const { error, duration_ms } = data; - let details = `${indent} ---\n`; +function reportTest(nesting, testNumber, status, name, skip, todo) { + let line = `${indent(nesting)}${status} ${testNumber}`; - details += jsToYaml(indent, 'duration_ms', duration_ms); - details += jsToYaml(indent, null, error); - details += `${indent} ...\n`; - this.#tryPush(details); + if (name) { + line += ` ${tapEscape(`- ${name}`)}`; } - diagnostic(indent, message) { - this.emit('test:diagnostic', message); - this.#tryPush(`${indent}# ${tapEscape(message)}\n`); + if (skip !== undefined) { + line += ` # SKIP${typeof skip === 'string' && skip.length ? ` ${tapEscape(skip)}` : ''}`; + } else if (todo !== undefined) { + line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}`; } - version(spec = kDefaultTAPVersion) { - this.#tryPush(`TAP version ${spec}\n`); - } + line += '\n'; - #test(indent, testNumber, status, name, directive = kEmptyObject) { - let line = `${indent}${status} ${testNumber}`; + return line; +} - if (name) { - line += ` ${tapEscape(`- ${name}`)}`; - } - line += ArrayPrototypeJoin(ArrayPrototypeMap(ObjectEntries(directive), ({ 0: key, 1: value }) => ( - ` # ${StringPrototypeToUpperCase(key)}${value ? ` ${tapEscape(value)}` : ''}` - )), ''); +function reportDetails(nesting, data = kEmptyObject) { + const { error, duration_ms } = data; + const _indent = indent(nesting); + let details = `${_indent} ---\n`; - line += '\n'; + details += jsToYaml(_indent, 'duration_ms', duration_ms); + details += jsToYaml(_indent, null, error); + details += `${_indent} ...\n`; + return details; +} - this.#tryPush(line); +const memo = new SafeMap(); +function indent(nesting) { + let value = memo.get(nesting); + if (value === undefined) { + value = StringPrototypeRepeat(kDefaultIndent, nesting); + memo.set(nesting, value); } - #tryPush(message) { - if (this.#canPush) { - this.#canPush = this.push(message); - } else { - ArrayPrototypePush(this.#buffer, message); - } - - return this.#canPush; - } + return value; } + // In certain places, # and \ need to be escaped as \# and \\. function tapEscape(input) { let result = StringPrototypeReplaceAll(input, '\\', '\\\\'); @@ -266,4 +230,4 @@ function isAssertionLike(value) { return value && typeof value === 'object' && 'expected' in value && 'actual' in value; } -module.exports = { TapStream }; +module.exports = tapReporter; diff --git a/src/node_options.cc b/src/node_options.cc index e5f814c4b594e0..aaaae751f0ad79 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -552,6 +552,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--test-name-pattern", "run tests whose name matches this regular expression", &EnvironmentOptions::test_name_pattern); + AddOption("--test-reporter", + "report test output using the given reporter", + &EnvironmentOptions::test_reporter); + AddOption("--test-reporter-destination", + "report given reporter to the given destination", + &EnvironmentOptions::test_reporter_destination); AddOption("--test-only", "run tests with 'only' option set", &EnvironmentOptions::test_only, diff --git a/src/node_options.h b/src/node_options.h index 2bb58e8f86102e..7a9993a5fac2c7 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -155,6 +155,8 @@ class EnvironmentOptions : public Options { std::string diagnostic_dir; bool test_runner = false; std::vector test_name_pattern; + std::vector test_reporter; + std::vector test_reporter_destination; bool test_only = false; bool test_udp_no_try_send = false; bool throw_deprecation = false; diff --git a/test/fixtures/test-runner/custom_reporters/custom.cjs b/test/fixtures/test-runner/custom_reporters/custom.cjs new file mode 100644 index 00000000000000..a3f653d11bb981 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/custom.cjs @@ -0,0 +1,17 @@ +const { Transform } = require('node:stream'); + +const customReporter = new Transform({ + writableObjectMode: true, + transform(event, encoding, callback) { + this.counters ??= {}; + this.counters[event.type] = (this.counters[event.type] ?? 0) + 1; + callback(); + }, + flush(callback) { + this.push('custom.cjs ') + this.push(JSON.stringify(this.counters)); + callback(); + } +}); + +module.exports = customReporter; diff --git a/test/fixtures/test-runner/custom_reporters/custom.js b/test/fixtures/test-runner/custom_reporters/custom.js new file mode 100644 index 00000000000000..62690f115b7ae1 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/custom.js @@ -0,0 +1,8 @@ +module.exports = async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "custom.js "; + yield JSON.stringify(counters); +}; diff --git a/test/fixtures/test-runner/custom_reporters/custom.mjs b/test/fixtures/test-runner/custom_reporters/custom.mjs new file mode 100644 index 00000000000000..b202d770c6bf19 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/custom.mjs @@ -0,0 +1,8 @@ +export default async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "custom.mjs "; + yield JSON.stringify(counters); +} diff --git a/test/fixtures/test-runner/reporters.js b/test/fixtures/test-runner/reporters.js new file mode 100644 index 00000000000000..ed7066023d1299 --- /dev/null +++ b/test/fixtures/test-runner/reporters.js @@ -0,0 +1,11 @@ +'use strict'; +const test = require('node:test'); + +test('nested', { concurrency: 4 }, async (t) => { + t.test('ok', () => {}); + t.test('failing', () => { + throw new Error('error'); + }); +}); + +test('top level', () => {}); diff --git a/test/message/test_runner_describe_it.out b/test/message/test_runner_describe_it.out index 199e834d6f65ae..87207aca71fafa 100644 --- a/test/message/test_runner_describe_it.out +++ b/test/message/test_runner_describe_it.out @@ -24,7 +24,6 @@ not ok 3 - sync fail todo # TODO * * * - * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -41,7 +40,6 @@ not ok 4 - sync fail todo with message # TODO this is a failing todo * * * - * ... # Subtest: sync skip pass ok 5 - sync skip pass # SKIP @@ -73,7 +71,6 @@ not ok 8 - sync throw fail * * * - * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -100,7 +97,6 @@ not ok 11 - async throw fail * * * - * ... # Subtest: async skip fail not ok 12 - async skip fail @@ -132,7 +128,6 @@ not ok 13 - async assertion fail * * * - * ... # Subtest: resolve pass ok 14 - resolve pass @@ -154,7 +149,6 @@ not ok 15 - reject fail * * * - * ... # Subtest: unhandled rejection - passes but warns ok 16 - unhandled rejection - passes but warns @@ -620,7 +614,6 @@ not ok 58 - rejected thenable code: 'ERR_TEST_FAILURE' stack: |- * - * ... # Subtest: invalid subtest fail not ok 59 - invalid subtest fail diff --git a/test/message/test_runner_hooks.out b/test/message/test_runner_hooks.out index 6bb1705967d043..7c82e9ff292ad5 100644 --- a/test/message/test_runner_hooks.out +++ b/test/message/test_runner_hooks.out @@ -64,7 +64,6 @@ not ok 2 - before throws * * * - * ... # Subtest: after throws # Subtest: 1 @@ -93,7 +92,6 @@ not ok 3 - after throws * * * - * ... # Subtest: beforeEach throws # Subtest: 1 @@ -490,7 +488,6 @@ not ok 13 - t.after() is called if test body throws * * * - * ... # - after() called 1..13 diff --git a/test/message/test_runner_output.js b/test/message/test_runner_output.js index 47087303a715ed..c29402ad33521d 100644 --- a/test/message/test_runner_output.js +++ b/test/message/test_runner_output.js @@ -113,7 +113,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p1a = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 1000); + }, 100); }); return p1a; @@ -131,7 +131,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p1c = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 2000); + }, 200); }); return p1c; @@ -141,7 +141,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p1c = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 1500); + }, 150); }); return p1c; @@ -150,7 +150,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p0a = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 3000); + }, 300); }); return p0a; @@ -159,7 +159,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { test('top level', { concurrency: 2 }, async (t) => { t.test('+long running', async (t) => { return new Promise((resolve, reject) => { - setTimeout(resolve, 3000).unref(); + setTimeout(resolve, 300).unref(); }); }); @@ -331,12 +331,12 @@ test('subtest sync throw fails', async (t) => { test('timed out async test', { timeout: 5 }, async (t) => { return new Promise((resolve) => { - setTimeout(resolve, 1000); + setTimeout(resolve, 100); }); }); test('timed out callback test', { timeout: 5 }, (t, done) => { - setTimeout(done, 1000); + setTimeout(done, 100); }); diff --git a/test/message/test_runner_output.out b/test/message/test_runner_output.out index 14479c773bbc86..42eae979daf6dd 100644 --- a/test/message/test_runner_output.out +++ b/test/message/test_runner_output.out @@ -24,7 +24,6 @@ not ok 3 - sync fail todo # TODO * * * - * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -41,7 +40,6 @@ not ok 4 - sync fail todo with message # TODO this is a failing todo * * * - * ... # Subtest: sync skip pass ok 5 - sync skip pass # SKIP @@ -74,7 +72,6 @@ not ok 8 - sync throw fail * * * - * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -101,7 +98,6 @@ not ok 11 - async throw fail * * * - * ... # Subtest: async skip fail not ok 12 - async skip fail # SKIP @@ -118,7 +114,6 @@ not ok 12 - async skip fail # SKIP * * * - * ... # Subtest: async assertion fail not ok 13 - async assertion fail @@ -142,7 +137,6 @@ not ok 13 - async assertion fail * * * - * ... # Subtest: resolve pass ok 14 - resolve pass @@ -164,7 +158,6 @@ not ok 15 - reject fail * * * - * ... # Subtest: unhandled rejection - passes but warns ok 16 - unhandled rejection - passes but warns diff --git a/test/message/test_runner_output_cli.js b/test/message/test_runner_output_cli.js index 1058d903c5fee4..5645f1afb1f3a2 100644 --- a/test/message/test_runner_output_cli.js +++ b/test/message/test_runner_output_cli.js @@ -3,4 +3,5 @@ require('../common'); const spawn = require('node:child_process').spawn; spawn(process.execPath, - ['--no-warnings', '--test', 'test/message/test_runner_output.js'], { stdio: 'inherit' }); + ['--no-warnings', '--test', '--test-reporter', 'tap', 'test/message/test_runner_output.js'], + { stdio: 'inherit' }); diff --git a/test/message/test_runner_output_cli.out b/test/message/test_runner_output_cli.out index b33d3e0fbf50b1..044610905755ca 100644 --- a/test/message/test_runner_output_cli.out +++ b/test/message/test_runner_output_cli.out @@ -25,7 +25,6 @@ TAP version 13 * * * - * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -42,7 +41,6 @@ TAP version 13 * * * - * ... # Subtest: sync skip pass ok 5 - sync skip pass # SKIP @@ -74,7 +72,6 @@ TAP version 13 * * * - * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -101,7 +98,6 @@ TAP version 13 * * * - * ... # Subtest: async skip fail not ok 12 - async skip fail # SKIP @@ -118,7 +114,6 @@ TAP version 13 * * * - * ... # Subtest: async assertion fail not ok 13 - async assertion fail @@ -142,7 +137,6 @@ TAP version 13 * * * - * ... # Subtest: resolve pass ok 14 - resolve pass @@ -164,7 +158,6 @@ TAP version 13 * * * - * ... # Subtest: unhandled rejection - passes but warns ok 16 - unhandled rejection - passes but warns diff --git a/test/message/test_runner_output_dot_reporter.js b/test/message/test_runner_output_dot_reporter.js new file mode 100644 index 00000000000000..8c36b9ba245425 --- /dev/null +++ b/test/message/test_runner_output_dot_reporter.js @@ -0,0 +1,6 @@ +// Flags: --no-warnings +'use strict'; +require('../common'); +const spawn = require('node:child_process').spawn; +spawn(process.execPath, + ['--no-warnings', '--test-reporter', 'dot', 'test/message/test_runner_output.js'], { stdio: 'inherit' }); diff --git a/test/message/test_runner_output_dot_reporter.out b/test/message/test_runner_output_dot_reporter.out new file mode 100644 index 00000000000000..823ecfb146b991 --- /dev/null +++ b/test/message/test_runner_output_dot_reporter.out @@ -0,0 +1,4 @@ +..XX...X..XXX.X..... +XXX.....X..X...X.... +.........X...XXX.XX. +.....XXXXXXX...XXXX diff --git a/test/message/test_runner_output_spec_reporter.js b/test/message/test_runner_output_spec_reporter.js new file mode 100644 index 00000000000000..49d8d3f2293da1 --- /dev/null +++ b/test/message/test_runner_output_spec_reporter.js @@ -0,0 +1,10 @@ +// Flags: --no-warnings +'use strict'; +require('../common'); +const spawn = require('node:child_process').spawn; +const child = spawn(process.execPath, + ['--no-warnings', '--test-reporter', 'spec', 'test/message/test_runner_output.js'], + { stdio: 'pipe' }); +// eslint-disable-next-line no-control-regex +child.stdout.on('data', (d) => process.stdout.write(d.toString().replace(/[^\x00-\x7F]/g, '').replace(/\u001b\[\d+m/g, ''))); +child.stderr.pipe(process.stderr); diff --git a/test/message/test_runner_output_spec_reporter.out b/test/message/test_runner_output_spec_reporter.out new file mode 100644 index 00000000000000..f7e2b7e66d800a --- /dev/null +++ b/test/message/test_runner_output_spec_reporter.out @@ -0,0 +1,280 @@ + sync pass todo (*ms) + sync pass todo with message (*ms) + sync fail todo (*ms) + Error: thrown from sync fail todo + * + * + * + * + * + * + * + + sync fail todo with message (*ms) + Error: thrown from sync fail todo with message + * + * + * + * + * + * + * + + sync skip pass (*ms) + sync skip pass with message (*ms) + sync pass (*ms) + this test should pass + sync throw fail (*ms) + Error: thrown from sync throw fail + * + * + * + * + * + * + * + + async skip pass (*ms) + async pass (*ms) + async throw fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async skip fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async assertion fail (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + * + * + * + * + * + * + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual' + } + + resolve pass (*ms) + reject fail (*ms) + Error: rejected from reject fail + * + * + * + * + * + * + * + + unhandled rejection - passes but warns (*ms) + async unhandled rejection - passes but warns (*ms) + immediate throw - passes but warns (*ms) + immediate reject - passes but warns (*ms) + immediate resolve pass (*ms) + subtest sync throw fail + +sync throw fail (*ms) + Error: thrown from subtest sync throw fail + * + * + * + * + * + * + * + * + * + * + + this subtest should make its parent test fail + subtest sync throw fail (*ms) + + sync throw non-error fail (*ms) + Symbol(thrown symbol from sync throw non-error fail) + + level 0a + level 1a (*ms) + level 1b (*ms) + level 1c (*ms) + level 1d (*ms) + level 0a (*ms) + + top level + +long running (*ms) + 'test did not finish before its parent and was cancelled' + + +short running + ++short running (*ms) + +short running (*ms) + + top level (*ms) + + invalid subtest - pass but subtest fails (*ms) + sync skip option (*ms) + sync skip option with message (*ms) + sync skip option is false fail (*ms) + Error: this should be executed + * + * + * + * + * + * + * + + (*ms) + functionOnly (*ms) + (*ms) + test with only a name provided (*ms) + (*ms) + (*ms) + test with a name and options provided (*ms) + functionAndOptions (*ms) + escaped description \ # * + * + (*ms) + escaped skip message (*ms) + escaped todo message (*ms) + escaped diagnostic (*ms) + #diagnostic + callback pass (*ms) + callback fail (*ms) + Error: callback failure + * + * + + sync t is this in test (*ms) + async t is this in test (*ms) + callback t is this in test (*ms) + callback also returns a Promise (*ms) + 'passed a callback but also returned a Promise' + + callback throw (*ms) + Error: thrown from callback throw + * + * + * + * + * + * + * + + callback called twice (*ms) + 'callback invoked multiple times' + + callback called twice in different ticks (*ms) + callback called twice in future tick (*ms) + Error [ERR_TEST_FAILURE]: callback invoked multiple times + * + failureType: 'multipleCallbackInvocations', + cause: 'callback invoked multiple times', + code: 'ERR_TEST_FAILURE' + } + + callback async throw (*ms) + Error: thrown from callback async throw + * + * + + callback async throw after done (*ms) + only is set but not in only mode + running subtest 1 (*ms) + running subtest 2 (*ms) + running subtest 3 (*ms) + running subtest 4 (*ms) + only is set but not in only mode (*ms) + + custom inspect symbol fail (*ms) + customized + + custom inspect symbol that throws fail (*ms) + { foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] } + + subtest sync throw fails + sync throw fails at first (*ms) + Error: thrown from subtest sync throw fails at first + * + * + * + * + * + * + * + * + * + * + + sync throw fails at second (*ms) + Error: thrown from subtest sync throw fails at second + * + * + * + * + * + * + * + * + * + * + + subtest sync throw fails (*ms) + + timed out async test (*ms) + 'test timed out after *ms' + + timed out callback test (*ms) + 'test timed out after *ms' + + large timeout async test is ok (*ms) + large timeout callback test is ok (*ms) + successful thenable (*ms) + rejected thenable (*ms) + 'custom error' + + unfinished test with uncaughtException (*ms) + Error: foo + * + * + * + + unfinished test with unhandledRejection (*ms) + Error: bar + * + * + * + + invalid subtest fail (*ms) + 'test could not be started because its parent finished' + + Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. + Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. + Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. + tests 65 + pass 27 + fail 21 + cancelled 2 + skipped 10 + todo 5 + duration_ms * diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 693fa9efb4111b..44f850915a2b9d 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -53,6 +53,7 @@ const expectedModules = new Set([ 'NativeModule internal/idna', 'NativeModule internal/linkedlist', 'NativeModule internal/modules/cjs/loader', + 'NativeModule internal/modules/utils', 'NativeModule internal/modules/esm/utils', 'NativeModule internal/modules/helpers', 'NativeModule internal/modules/package_json_reader', diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js new file mode 100644 index 00000000000000..74cae3401e2843 --- /dev/null +++ b/test/parallel/test-runner-reporters.js @@ -0,0 +1,95 @@ +'use strict'; + +require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { describe, it } = require('node:test'); +const { spawnSync } = require('node:child_process'); +const assert = require('node:assert'); +const path = require('node:path'); +const fs = require('node:fs'); + +const testFile = fixtures.path('test-runner/reporters.js'); +tmpdir.refresh(); + +let tmpFiles = 0; +describe('node:test reporters', { concurrency: true }, () => { + it('should default to outputing TAP to stdout', async () => { + const child = spawnSync(process.execPath, ['--test', testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.match(child.stdout.toString(), /TAP version 13/); + assert.match(child.stdout.toString(), /ok 1 - ok/); + assert.match(child.stdout.toString(), /not ok 2 - failing/); + assert.match(child.stdout.toString(), /ok 2 - top level/); + }); + + it('should default destination to stdout when passing a single reporter', async () => { + const child = spawnSync(process.execPath, ['--test', '--test-reporter', 'dot', testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), '.XX.X\n'); + }); + + it('should throw when passing reporters without a destination', async () => { + const child = spawnSync(process.execPath, ['--test', '--test-reporter', 'dot', '--test-reporter', 'tap', testFile]); + assert.match(child.stderr.toString(), /The argument '--test-reporter' must match the number of specified '--test-reporter-destination'\. Received \[ 'dot', 'tap' \]/); + assert.strictEqual(child.stdout.toString(), ''); + }); + + it('should throw when passing a destination without a reporter', async () => { + const child = spawnSync(process.execPath, ['--test', '--test-reporter-destination', 'tap', testFile]); + assert.match(child.stderr.toString(), /The argument '--test-reporter' must match the number of specified '--test-reporter-destination'\. Received \[\]/); + assert.strictEqual(child.stdout.toString(), ''); + }); + + it('should support stdout as a destination', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'dot', '--test-reporter-destination', 'stdout', testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), '.XX.X\n'); + }); + + it('should support stderr as a destination', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'dot', '--test-reporter-destination', 'stderr', testFile]); + assert.strictEqual(child.stderr.toString(), '.XX.X\n'); + assert.strictEqual(child.stdout.toString(), ''); + }); + + it('should support a file as a destination', async () => { + const file = path.join(tmpdir.path, `${tmpFiles++}.out`); + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'dot', '--test-reporter-destination', file, testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), ''); + assert.strictEqual(fs.readFileSync(file, 'utf8'), '.XX.X\n'); + }); + + it('should support multiple reporters', async () => { + const file = path.join(tmpdir.path, `${tmpFiles++}.out`); + const file2 = path.join(tmpdir.path, `${tmpFiles++}.out`); + const child = spawnSync(process.execPath, + ['--test', + '--test-reporter', 'dot', '--test-reporter-destination', file, + '--test-reporter', 'spec', '--test-reporter-destination', file2, + '--test-reporter', 'tap', '--test-reporter-destination', 'stdout', + testFile]); + assert.match(child.stdout.toString(), /TAP version 13/); + assert.match(child.stdout.toString(), /# duration_ms/); + assert.strictEqual(fs.readFileSync(file, 'utf8'), '.XX.X\n'); + const file2Contents = fs.readFileSync(file2, 'utf8'); + assert.match(file2Contents, /▶ nested/); + assert.match(file2Contents, /✔ ok/); + assert.match(file2Contents, /✖ failing/); + }); + + ['js', 'cjs', 'mjs'].forEach((ext) => { + it(`should support a '${ext}' file as a custom reporter`, async () => { + const filename = `custom.${ext}`; + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', fixtures.path('test-runner/custom_reporters/', filename), + testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), `${filename} {"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":7}`); + }); + }); +}); diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index 6ce7c8f5d97c5d..b6e0eee3b259ad 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -10,7 +10,6 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { it('should run with no tests', async () => { const stream = run({ files: [] }); - stream.setEncoding('utf8'); stream.on('test:fail', common.mustNotCall()); stream.on('test:pass', common.mustNotCall()); // eslint-disable-next-line no-unused-vars diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 02194e1e29128f..93e098bfd99639 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -207,7 +207,7 @@ const customTypesMap = { 'Timeout': 'timers.html#class-timeout', 'Timer': 'timers.html#timers', - 'TapStream': 'test.html#class-tapstream', + 'TestsStream': 'test.html#class-testsstream', 'tls.SecureContext': 'tls.html#tlscreatesecurecontextoptions', 'tls.Server': 'tls.html#class-tlsserver', From 3a3a6d87f1d241084344ca5886c9144cc3d0f7f0 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 22 Dec 2022 13:40:32 -0800 Subject: [PATCH 03/72] module: move test reporter loading Move the logic for handling --test-reporter out of the general module loader and into the test_runner subsystem. Backport-PR-URL: https://github.com/nodejs/node/pull/46361 PR-URL: https://github.com/nodejs/node/pull/45923 Reviewed-By: Moshe Atlow Reviewed-By: Benjamin Gruenbaum Reviewed-By: Colin Ihrig Reviewed-By: Jacob Smith Reviewed-By: Antoine du Hamel --- doc/api/test.md | 6 ++- lib/internal/modules/run_main.js | 28 +++++++++- lib/internal/modules/utils.js | 54 ------------------- lib/internal/test_runner/utils.js | 14 ++++- .../node_modules/reporter-cjs/index.js | 8 +++ .../node_modules/reporter-cjs/package.json | 4 ++ .../node_modules/reporter-esm/index.mjs | 8 +++ .../node_modules/reporter-esm/package.json | 4 ++ test/parallel/test-bootstrap-modules.js | 1 - test/parallel/test-runner-reporters.js | 24 ++++++++- 10 files changed, 91 insertions(+), 60 deletions(-) delete mode 100644 lib/internal/modules/utils.js create mode 100644 test/fixtures/test-runner/node_modules/reporter-cjs/index.js create mode 100644 test/fixtures/test-runner/node_modules/reporter-cjs/package.json create mode 100644 test/fixtures/test-runner/node_modules/reporter-esm/index.mjs create mode 100644 test/fixtures/test-runner/node_modules/reporter-esm/package.json diff --git a/doc/api/test.md b/doc/api/test.md index 97e160ae09eebd..345c2fb6fd0153 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -470,7 +470,7 @@ The following built-reporters are supported: The `spec` reporter outputs the test results in a human-readable format. * `dot` - The `dot` reporter outputs the test results in a comact format, + The `dot` reporter outputs the test results in a compact format, where each passing test is represented by a `.`, and each failing test is represented by a `X`. @@ -591,6 +591,9 @@ module.exports = async function * customReporter(source) { }; ``` +The value provided to `--test-reporter` should be a string like one used in an +`import()` in JavaScript code, or a value provided for [`--import`][]. + ### Multiple reporters The [`--test-reporter`][] flag can be specified multiple times to report test @@ -1581,6 +1584,7 @@ added: aborted. [TAP]: https://testanything.org/ +[`--import`]: cli.md#--importmodule [`--test-name-pattern`]: cli.md#--test-name-pattern [`--test-only`]: cli.md#--test-only [`--test-reporter-destination`]: cli.md#--test-reporter-destination diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index e5dc867882dbde..0bfe7b11241416 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -1,8 +1,11 @@ 'use strict'; +const { + StringPrototypeEndsWith, +} = primordials; + const { getOptionValue } = require('internal/options'); const path = require('path'); -const { shouldUseESMLoader } = require('internal/modules/utils'); function resolveMainPath(main) { // Note extension resolution for the main entry point can be deprecated in a @@ -20,6 +23,29 @@ function resolveMainPath(main) { return mainPath; } +function shouldUseESMLoader(mainPath) { + /** + * @type {string[]} userLoaders A list of custom loaders registered by the user + * (or an empty list when none have been registered). + */ + const userLoaders = getOptionValue('--experimental-loader'); + /** + * @type {string[]} userImports A list of preloaded modules registered by the user + * (or an empty list when none have been registered). + */ + const userImports = getOptionValue('--import'); + if (userLoaders.length > 0 || userImports.length > 0) + return true; + const { readPackageScope } = require('internal/modules/cjs/loader'); + // Determine the module format of the main + if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) + return true; + if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) + return false; + const pkg = readPackageScope(mainPath); + return pkg && pkg.data.type === 'module'; +} + function runMainESM(mainPath) { const { loadESM } = require('internal/process/esm_loader'); const { pathToFileURL } = require('internal/url'); diff --git a/lib/internal/modules/utils.js b/lib/internal/modules/utils.js deleted file mode 100644 index 1e2d0b3edcf499..00000000000000 --- a/lib/internal/modules/utils.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const { - StringPrototypeEndsWith, -} = primordials; -const { getOptionValue } = require('internal/options'); - - -function shouldUseESMLoader(filePath) { - /** - * @type {string[]} userLoaders A list of custom loaders registered by the user - * (or an empty list when none have been registered). - */ - const userLoaders = getOptionValue('--experimental-loader'); - /** - * @type {string[]} userImports A list of preloaded modules registered by the user - * (or an empty list when none have been registered). - */ - const userImports = getOptionValue('--import'); - if (userLoaders.length > 0 || userImports.length > 0) - return true; - // Determine the module format of the main - if (filePath && StringPrototypeEndsWith(filePath, '.mjs')) - return true; - if (!filePath || StringPrototypeEndsWith(filePath, '.cjs')) - return false; - const { readPackageScope } = require('internal/modules/cjs/loader'); - const pkg = readPackageScope(filePath); - return pkg?.data?.type === 'module'; -} - -/** - * @param {string} filePath - * @returns {any} - * requireOrImport imports a module if the file is an ES module, otherwise it requires it. - */ -function requireOrImport(filePath) { - const useESMLoader = shouldUseESMLoader(filePath); - if (useESMLoader) { - const { esmLoader } = require('internal/process/esm_loader'); - const { pathToFileURL } = require('internal/url'); - const { isAbsolute } = require('path'); - const file = isAbsolute(filePath) ? pathToFileURL(filePath).href : filePath; - return esmLoader.import(file, undefined, { __proto__: null }); - } - const { Module } = require('internal/modules/cjs/loader'); - - return new Module._load(filePath, null, false); -} - -module.exports = { - shouldUseESMLoader, - requireOrImport, -}; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 9dba00de25719e..b9c9b4b677fef4 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -9,9 +9,9 @@ const { } = primordials; const { basename } = require('path'); const { createWriteStream } = require('fs'); +const { pathToFileURL } = require('internal/url'); const { createDeferredPromise } = require('internal/util'); const { getOptionValue } = require('internal/options'); -const { requireOrImport } = require('internal/modules/utils'); const { codes: { @@ -103,7 +103,17 @@ const kDefaultDestination = 'stdout'; async function getReportersMap(reporters, destinations) { return SafePromiseAllReturnArrayLike(reporters, async (name, i) => { const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]); - let reporter = await requireOrImport(kBuiltinReporters.get(name) ?? name); + + // Load the test reporter passed to --test-reporter + const reporterSpecifier = kBuiltinReporters.get(name) ?? name; + let parentURL; + try { + parentURL = pathToFileURL(process.cwd() + '/').href; + } catch { + parentURL = 'file:///'; + } + const { esmLoader } = require('internal/process/esm_loader'); + let reporter = await esmLoader.import(reporterSpecifier, parentURL, { __proto__: null }); if (reporter?.default) { reporter = reporter.default; diff --git a/test/fixtures/test-runner/node_modules/reporter-cjs/index.js b/test/fixtures/test-runner/node_modules/reporter-cjs/index.js new file mode 100644 index 00000000000000..d99cd29926e86e --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-cjs/index.js @@ -0,0 +1,8 @@ +module.exports = async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "package: reporter-cjs"; + yield JSON.stringify(counters); +}; diff --git a/test/fixtures/test-runner/node_modules/reporter-cjs/package.json b/test/fixtures/test-runner/node_modules/reporter-cjs/package.json new file mode 100644 index 00000000000000..cf7db2b7eca767 --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-cjs/package.json @@ -0,0 +1,4 @@ +{ + "name": "reporter-cjs", + "main": "index.js" +} diff --git a/test/fixtures/test-runner/node_modules/reporter-esm/index.mjs b/test/fixtures/test-runner/node_modules/reporter-esm/index.mjs new file mode 100644 index 00000000000000..0eb82dfe4502d8 --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-esm/index.mjs @@ -0,0 +1,8 @@ +export default async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "package: reporter-esm"; + yield JSON.stringify(counters); +}; diff --git a/test/fixtures/test-runner/node_modules/reporter-esm/package.json b/test/fixtures/test-runner/node_modules/reporter-esm/package.json new file mode 100644 index 00000000000000..60d6b3a97fd186 --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-esm/package.json @@ -0,0 +1,4 @@ +{ + "name": "reporter-esm", + "exports": "./index.mjs" +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 44f850915a2b9d..693fa9efb4111b 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -53,7 +53,6 @@ const expectedModules = new Set([ 'NativeModule internal/idna', 'NativeModule internal/linkedlist', 'NativeModule internal/modules/cjs/loader', - 'NativeModule internal/modules/utils', 'NativeModule internal/modules/esm/utils', 'NativeModule internal/modules/helpers', 'NativeModule internal/modules/package_json_reader', diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js index 74cae3401e2843..671b6ac4432167 100644 --- a/test/parallel/test-runner-reporters.js +++ b/test/parallel/test-runner-reporters.js @@ -86,10 +86,32 @@ describe('node:test reporters', { concurrency: true }, () => { it(`should support a '${ext}' file as a custom reporter`, async () => { const filename = `custom.${ext}`; const child = spawnSync(process.execPath, - ['--test', '--test-reporter', fixtures.path('test-runner/custom_reporters/', filename), + ['--test', '--test-reporter', fixtures.fileURL('test-runner/custom_reporters/', filename), testFile]); assert.strictEqual(child.stderr.toString(), ''); assert.strictEqual(child.stdout.toString(), `${filename} {"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":7}`); }); }); + + it('should support a custom reporter from node_modules', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'reporter-cjs', 'reporters.js'], + { cwd: fixtures.path('test-runner') }); + assert.strictEqual(child.stderr.toString(), ''); + assert.match( + child.stdout.toString(), + /^package: reporter-cjs{"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":\d+}$/, + ); + }); + + it('should support a custom ESM reporter from node_modules', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'reporter-esm', 'reporters.js'], + { cwd: fixtures.path('test-runner') }); + assert.strictEqual(child.stderr.toString(), ''); + assert.match( + child.stdout.toString(), + /^package: reporter-esm{"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":\d+}$/, + ); + }); }); From a49e17e22bb5f2422897fb5a80df1d43cbd18ea2 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Mon, 2 Jan 2023 23:22:54 +0200 Subject: [PATCH 04/72] test_runner: report `file` in test runner events Backport-PR-URL: https://github.com/nodejs/node/pull/46361 PR-URL: https://github.com/nodejs/node/pull/46030 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Antoine du Hamel Reviewed-By: Colin Ihrig --- doc/api/test.md | 10 +++++++ lib/internal/test_runner/runner.js | 10 ++++--- lib/internal/test_runner/test.js | 29 ++++++++++--------- lib/internal/test_runner/tests_stream.js | 20 ++++++------- .../test-runner/custom_reporters/custom.js | 6 ++++ 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 345c2fb6fd0153..106b1eeb4682c1 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1218,6 +1218,8 @@ object, streaming a series of events representing the execution of the tests. ### Event: `'test:diagnostic'` * `data` {Object} + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `message` {string} The diagnostic message. * `nesting` {number} The nesting level of the test. @@ -1229,6 +1231,8 @@ Emitted when [`context.diagnostic`][] is called. * `details` {Object} Additional execution metadata. * `duration` {number} The duration of the test in milliseconds. * `error` {Error} The error thrown by the test. + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. * `testNumber` {number} The ordinal number of the test. @@ -1242,6 +1246,8 @@ Emitted when a test fails. * `data` {Object} * `details` {Object} Additional execution metadata. * `duration` {number} The duration of the test in milliseconds. + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. * `testNumber` {number} The ordinal number of the test. @@ -1253,6 +1259,8 @@ Emitted when a test passes. ### Event: `'test:plan'` * `data` {Object} + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `nesting` {number} The nesting level of the test. * `count` {number} The number of subtests that have ran. @@ -1261,6 +1269,8 @@ Emitted when all subtests have completed for a given test. ### Event: `'test:start'` * `data` {Object} + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index ff91993ce9df29..3b5ce187b8c1b7 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -140,11 +140,11 @@ class FileTest extends Test { break; case TokenKind.TAP_PLAN: - this.reporter.plan(nesting, node.end - node.start + 1); + this.reporter.plan(nesting, this.name, node.end - node.start + 1); break; case TokenKind.TAP_SUBTEST_POINT: - this.reporter.start(nesting, node.name); + this.reporter.start(nesting, this.name, node.name); break; case TokenKind.TAP_TEST_POINT: @@ -164,6 +164,7 @@ class FileTest extends Test { if (pass) { this.reporter.ok( nesting, + this.name, node.id, node.description, YAMLToJs(node.diagnostics), @@ -172,6 +173,7 @@ class FileTest extends Test { } else { this.reporter.fail( nesting, + this.name, node.id, node.description, YAMLToJs(node.diagnostics), @@ -185,11 +187,11 @@ class FileTest extends Test { // Ignore file top level diagnostics break; } - this.reporter.diagnostic(nesting, node.comment); + this.reporter.diagnostic(nesting, this.name, node.comment); break; case TokenKind.UNKNOWN: - this.reporter.diagnostic(nesting, node.value); + this.reporter.diagnostic(nesting, this.name, node.value); break; } } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index b7267491d50c3f..267ccb072adcb8 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -74,6 +74,7 @@ const testNamePatterns = testNamePatternFlag?.length > 0 ? (re) => convertStringToRegExp(re, '--test-name-pattern') ) : null; const kShouldAbort = Symbol('kShouldAbort'); +const kFilename = process.argv?.[1]; const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']); const kUnwrapErrors = new SafeSet() .add(kTestCodeFailure).add(kHookFailure) @@ -632,19 +633,19 @@ class Test extends AsyncResource { this.parent.processPendingSubtests(); } else if (!this.reported) { this.reported = true; - this.reporter.plan(this.nesting, this.subtests.length); + this.reporter.plan(this.nesting, kFilename, this.subtests.length); for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.nesting, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, kFilename, this.diagnostics[i]); } - this.reporter.diagnostic(this.nesting, `tests ${this.subtests.length}`); - this.reporter.diagnostic(this.nesting, `pass ${counters.passed}`); - this.reporter.diagnostic(this.nesting, `fail ${counters.failed}`); - this.reporter.diagnostic(this.nesting, `cancelled ${counters.cancelled}`); - this.reporter.diagnostic(this.nesting, `skipped ${counters.skipped}`); - this.reporter.diagnostic(this.nesting, `todo ${counters.todo}`); - this.reporter.diagnostic(this.nesting, `duration_ms ${this.#duration()}`); + this.reporter.diagnostic(this.nesting, kFilename, `tests ${this.subtests.length}`); + this.reporter.diagnostic(this.nesting, kFilename, `pass ${counters.passed}`); + this.reporter.diagnostic(this.nesting, kFilename, `fail ${counters.failed}`); + this.reporter.diagnostic(this.nesting, kFilename, `cancelled ${counters.cancelled}`); + this.reporter.diagnostic(this.nesting, kFilename, `skipped ${counters.skipped}`); + this.reporter.diagnostic(this.nesting, kFilename, `todo ${counters.todo}`); + this.reporter.diagnostic(this.nesting, kFilename, `duration_ms ${this.#duration()}`); this.reporter.push(null); } } @@ -680,7 +681,7 @@ class Test extends AsyncResource { report() { if (this.subtests.length > 0) { - this.reporter.plan(this.subtests[0].nesting, this.subtests.length); + this.reporter.plan(this.subtests[0].nesting, kFilename, this.subtests.length); } else { this.reportStarted(); } @@ -694,14 +695,14 @@ class Test extends AsyncResource { } if (this.passed) { - this.reporter.ok(this.nesting, this.testNumber, this.name, details, directive); + this.reporter.ok(this.nesting, kFilename, this.testNumber, this.name, details, directive); } else { details.error = this.error; - this.reporter.fail(this.nesting, this.testNumber, this.name, details, directive); + this.reporter.fail(this.nesting, kFilename, this.testNumber, this.name, details, directive); } for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.nesting, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, kFilename, this.diagnostics[i]); } } @@ -711,7 +712,7 @@ class Test extends AsyncResource { } this.#reportedSubtest = true; this.parent.reportStarted(); - this.reporter.start(this.nesting, this.name); + this.reporter.start(this.nesting, kFilename, this.name); } } diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index b016d316154807..2614cab16daf64 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -27,16 +27,16 @@ class TestsStream extends Readable { } } - fail(nesting, testNumber, name, details, directive) { - this.#emit('test:fail', { __proto__: null, name, nesting, testNumber, details, ...directive }); + fail(nesting, file, testNumber, name, details, directive) { + this.#emit('test:fail', { __proto__: null, name, nesting, file, testNumber, details, ...directive }); } - ok(nesting, testNumber, name, details, directive) { - this.#emit('test:pass', { __proto__: null, name, nesting, testNumber, details, ...directive }); + ok(nesting, file, testNumber, name, details, directive) { + this.#emit('test:pass', { __proto__: null, name, nesting, file, testNumber, details, ...directive }); } - plan(nesting, count) { - this.#emit('test:plan', { __proto__: null, nesting, count }); + plan(nesting, file, count) { + this.#emit('test:plan', { __proto__: null, nesting, file, count }); } getSkip(reason = undefined) { @@ -47,12 +47,12 @@ class TestsStream extends Readable { return { __proto__: null, todo: reason ?? true }; } - start(nesting, name) { - this.#emit('test:start', { __proto__: null, nesting, name }); + start(nesting, file, name) { + this.#emit('test:start', { __proto__: null, nesting, file, name }); } - diagnostic(nesting, message) { - this.#emit('test:diagnostic', { __proto__: null, nesting, message }); + diagnostic(nesting, file, message) { + this.#emit('test:diagnostic', { __proto__: null, nesting, file, message }); } #emit(type, data) { diff --git a/test/fixtures/test-runner/custom_reporters/custom.js b/test/fixtures/test-runner/custom_reporters/custom.js index 62690f115b7ae1..aa85eab14acff4 100644 --- a/test/fixtures/test-runner/custom_reporters/custom.js +++ b/test/fixtures/test-runner/custom_reporters/custom.js @@ -1,6 +1,12 @@ +const assert = require('assert'); +const path = require('path'); + module.exports = async function * customReporter(source) { const counters = {}; for await (const event of source) { + if (event.data.file) { + assert.strictEqual(event.data.file, path.resolve(__dirname, '../reporters.js')); + } counters[event.type] = (counters[event.type] ?? 0) + 1; } yield "custom.js "; From 8b473affe89a78f993636fbb2e0c5f2001a43da1 Mon Sep 17 00:00:00 2001 From: Colin Ihrig Date: Fri, 6 Jan 2023 14:34:12 -0500 Subject: [PATCH 05/72] test_runner: make built in reporters internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates the test runner to make the built in test reporters internal modules. Backport-PR-URL: https://github.com/nodejs/node/pull/46361 PR-URL: https://github.com/nodejs/node/pull/46092 Reviewed-By: Tobias Nießen Reviewed-By: Antoine du Hamel Reviewed-By: Moshe Atlow --- .../test_runner}/reporter/dot.js | 0 .../test_runner}/reporter/spec.js | 0 .../test_runner}/reporter/tap.js | 0 lib/internal/test_runner/utils.js | 37 +++++++++++++------ 4 files changed, 26 insertions(+), 11 deletions(-) rename lib/{test => internal/test_runner}/reporter/dot.js (100%) rename lib/{test => internal/test_runner}/reporter/spec.js (100%) rename lib/{test => internal/test_runner}/reporter/tap.js (100%) diff --git a/lib/test/reporter/dot.js b/lib/internal/test_runner/reporter/dot.js similarity index 100% rename from lib/test/reporter/dot.js rename to lib/internal/test_runner/reporter/dot.js diff --git a/lib/test/reporter/spec.js b/lib/internal/test_runner/reporter/spec.js similarity index 100% rename from lib/test/reporter/spec.js rename to lib/internal/test_runner/reporter/spec.js diff --git a/lib/test/reporter/tap.js b/lib/internal/test_runner/reporter/tap.js similarity index 100% rename from lib/test/reporter/tap.js rename to lib/internal/test_runner/reporter/tap.js diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index b9c9b4b677fef4..a212e4ad37cc93 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -92,28 +92,43 @@ const kBuiltinDestinations = new SafeMap([ ]); const kBuiltinReporters = new SafeMap([ - ['spec', 'node:test/reporter/spec'], - ['dot', 'node:test/reporter/dot'], - ['tap', 'node:test/reporter/tap'], + ['spec', 'internal/test_runner/reporter/spec'], + ['dot', 'internal/test_runner/reporter/dot'], + ['tap', 'internal/test_runner/reporter/tap'], ]); const kDefaultReporter = 'tap'; const kDefaultDestination = 'stdout'; +function tryBuiltinReporter(name) { + const builtinPath = kBuiltinReporters.get(name); + + if (builtinPath === undefined) { + return; + } + + return require(builtinPath); +} + async function getReportersMap(reporters, destinations) { return SafePromiseAllReturnArrayLike(reporters, async (name, i) => { const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]); // Load the test reporter passed to --test-reporter - const reporterSpecifier = kBuiltinReporters.get(name) ?? name; - let parentURL; - try { - parentURL = pathToFileURL(process.cwd() + '/').href; - } catch { - parentURL = 'file:///'; + let reporter = tryBuiltinReporter(name); + + if (reporter === undefined) { + let parentURL; + + try { + parentURL = pathToFileURL(process.cwd() + '/').href; + } catch { + parentURL = 'file:///'; + } + + const { esmLoader } = require('internal/process/esm_loader'); + reporter = await esmLoader.import(name, parentURL, { __proto__: null }); } - const { esmLoader } = require('internal/process/esm_loader'); - let reporter = await esmLoader.import(reporterSpecifier, parentURL, { __proto__: null }); if (reporter?.default) { reporter = reporter.default; From 7aac21e90a34260646980a6bc1a5515a9ebdfc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Sat, 17 Dec 2022 05:23:11 +0100 Subject: [PATCH 06/72] esm: leverage loaders when resolving subsequent loaders PR-URL: https://github.com/nodejs/node/pull/43772 Reviewed-By: James M Snell Reviewed-By: Jacob Smith Reviewed-By: Geoffrey Booth --- doc/api/esm.md | 5 +- lib/internal/modules/esm/loader.js | 4 +- lib/internal/process/esm_loader.js | 48 ++++++++++++++----- test/es-module/test-esm-loader-chaining.mjs | 18 ++++++- .../loader-load-foo-or-42.mjs | 10 +++- .../loader-load-incomplete.mjs | 10 +++- .../loader-load-passthru.mjs | 8 ++++ .../es-module-loaders/loader-resolve-42.mjs | 8 ++++ .../es-module-loaders/loader-resolve-foo.mjs | 8 ++++ .../loader-resolve-incomplete.mjs | 10 +++- .../loader-resolve-next-modified.mjs | 8 ++++ .../loader-resolve-passthru.mjs | 8 ++++ .../loader-resolve-shortcircuit.mjs | 10 +++- .../loader-resolve-strip-xxx.mjs | 4 ++ .../loader-resolve-strip-yyy.mjs | 4 ++ 15 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 test/fixtures/es-module-loaders/loader-resolve-strip-xxx.mjs create mode 100644 test/fixtures/es-module-loaders/loader-resolve-strip-yyy.mjs diff --git a/doc/api/esm.md b/doc/api/esm.md index d67cb47abe526e..9a4273940a7ce0 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -701,8 +701,9 @@ changes: To customize the default module resolution, loader hooks can optionally be provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js. -When hooks are used they apply to the entry point and all `import` calls. They -won't apply to `require` calls; those still follow [CommonJS][] rules. +When hooks are used they apply to each subsequent loader, the entry point, and +all `import` calls. They won't apply to `require` calls; those still follow +[CommonJS][] rules. Loaders follow the pattern of `--require`: diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 38cd2777dab9c5..a02619818ec78b 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -309,7 +309,7 @@ class ESMLoader { /** * Collect custom/user-defined hook(s). After all hooks have been collected, - * calls global preload hook(s). + * the global preload hook(s) must be called. * @param {KeyedExports} customLoaders * A list of exports from user-defined loaders (as returned by * ESMLoader.import()). @@ -356,8 +356,6 @@ class ESMLoader { ); } } - - this.preload(); } async eval( diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 975cc1f1cca4f5..d040d8e068ecfa 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -2,6 +2,7 @@ const { ArrayIsArray, + ArrayPrototypePushApply, } = primordials; const { ESMLoader } = require('internal/modules/esm/loader'); @@ -9,6 +10,7 @@ const { hasUncaughtExceptionCaptureCallback, } = require('internal/process/execution'); const { pathToFileURL } = require('internal/url'); +const { kEmptyObject } = require('internal/util'); const esmLoader = new ESMLoader(); exports.esmLoader = esmLoader; @@ -28,41 +30,61 @@ async function initializeLoader() { const { getOptionValue } = require('internal/options'); const customLoaders = getOptionValue('--experimental-loader'); const preloadModules = getOptionValue('--import'); - const loaders = await loadModulesInIsolation(customLoaders); + + let cwd; + try { + cwd = process.cwd() + '/'; + } catch { + cwd = '/'; + } + + const internalEsmLoader = new ESMLoader(); + const allLoaders = []; + + const parentURL = pathToFileURL(cwd).href; + + for (let i = 0; i < customLoaders.length; i++) { + const customLoader = customLoaders[i]; + + // Importation must be handled by internal loader to avoid polluting user-land + const keyedExportsSublist = await internalEsmLoader.import( + [customLoader], + parentURL, + kEmptyObject, + ); + + internalEsmLoader.addCustomLoaders(keyedExportsSublist); + ArrayPrototypePushApply(allLoaders, keyedExportsSublist); + } // Hooks must then be added to external/public loader // (so they're triggered in userland) - esmLoader.addCustomLoaders(loaders); + esmLoader.addCustomLoaders(allLoaders); + esmLoader.preload(); // Preload after loaders are added so they can be used if (preloadModules?.length) { - await loadModulesInIsolation(preloadModules, loaders); + await loadModulesInIsolation(parentURL, preloadModules, allLoaders); } isESMInitialized = true; } -function loadModulesInIsolation(specifiers, loaders = []) { +function loadModulesInIsolation(parentURL, specifiers, loaders = []) { if (!ArrayIsArray(specifiers) || specifiers.length === 0) { return; } - let cwd; - try { - cwd = process.cwd() + '/'; - } catch { - cwd = 'file:///'; - } - // A separate loader instance is necessary to avoid cross-contamination // between internal Node.js and userland. For example, a module with internal // state (such as a counter) should be independent. const internalEsmLoader = new ESMLoader(); internalEsmLoader.addCustomLoaders(loaders); + internalEsmLoader.preload(); // Importation must be handled by internal loader to avoid poluting userland return internalEsmLoader.import( specifiers, - pathToFileURL(cwd).href, - { __proto__: null }, + parentURL, + kEmptyObject, ); } diff --git a/test/es-module/test-esm-loader-chaining.mjs b/test/es-module/test-esm-loader-chaining.mjs index b04dbe4ddd6c1a..a42020ef42f8dd 100644 --- a/test/es-module/test-esm-loader-chaining.mjs +++ b/test/es-module/test-esm-loader-chaining.mjs @@ -4,7 +4,6 @@ import assert from 'node:assert'; import { execPath } from 'node:process'; import { describe, it } from 'node:test'; - const setupArgs = [ '--no-warnings', '--input-type=module', @@ -253,6 +252,23 @@ describe('ESM: loader chaining', { concurrency: true }, () => { assert.strictEqual(code, 0); }); + it('should allow loaders to influence subsequent loader resolutions', async () => { + const { code, stderr } = await spawnPromisified( + execPath, + [ + '--loader', + fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'), + '--loader', + 'xxx/loader-resolve-strip-yyy.mjs', + ...commonArgs, + ], + { encoding: 'utf8', cwd: fixtures.path('es-module-loaders') }, + ); + + assert.strictEqual(stderr, ''); + assert.strictEqual(code, 0); + }); + it('should throw when the resolve chain is broken', async () => { const { code, stderr, stdout } = await spawnPromisified( execPath, diff --git a/test/fixtures/es-module-loaders/loader-load-foo-or-42.mjs b/test/fixtures/es-module-loaders/loader-load-foo-or-42.mjs index 8d408223e66a0a..8f850e82bef54b 100644 --- a/test/fixtures/es-module-loaders/loader-load-foo-or-42.mjs +++ b/test/fixtures/es-module-loaders/loader-load-foo-or-42.mjs @@ -1,4 +1,12 @@ -export async function load(url) { +export async function load(url, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (url.includes('loader')) { + return next(url); + } + const val = url.includes('42') ? '42' : '"foo"'; diff --git a/test/fixtures/es-module-loaders/loader-load-incomplete.mjs b/test/fixtures/es-module-loaders/loader-load-incomplete.mjs index d6242488e5738e..a4bf3531f3d225 100644 --- a/test/fixtures/es-module-loaders/loader-load-incomplete.mjs +++ b/test/fixtures/es-module-loaders/loader-load-incomplete.mjs @@ -1,4 +1,12 @@ -export async function load() { +export async function load(url, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (url.includes('loader')) { + return next(url); + } + return { format: 'module', source: 'export default 42', diff --git a/test/fixtures/es-module-loaders/loader-load-passthru.mjs b/test/fixtures/es-module-loaders/loader-load-passthru.mjs index 0de06142007562..1c2f2ea4487cdf 100644 --- a/test/fixtures/es-module-loaders/loader-load-passthru.mjs +++ b/test/fixtures/es-module-loaders/loader-load-passthru.mjs @@ -1,4 +1,12 @@ export async function load(url, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (url.includes('loader')) { + return next(url); + } + console.log('load passthru'); // This log is deliberate return next(url); } diff --git a/test/fixtures/es-module-loaders/loader-resolve-42.mjs b/test/fixtures/es-module-loaders/loader-resolve-42.mjs index eaca111998ae23..05ef2f68390bd1 100644 --- a/test/fixtures/es-module-loaders/loader-resolve-42.mjs +++ b/test/fixtures/es-module-loaders/loader-resolve-42.mjs @@ -1,4 +1,12 @@ export async function resolve(specifier, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (specifier.includes('loader')) { + return next(specifier); + } + console.log('resolve 42'); // This log is deliberate console.log('next:', next.name); // This log is deliberate diff --git a/test/fixtures/es-module-loaders/loader-resolve-foo.mjs b/test/fixtures/es-module-loaders/loader-resolve-foo.mjs index 7d23d6c49088c9..09d30e952a2a92 100644 --- a/test/fixtures/es-module-loaders/loader-resolve-foo.mjs +++ b/test/fixtures/es-module-loaders/loader-resolve-foo.mjs @@ -1,4 +1,12 @@ export async function resolve(specifier, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (specifier.includes('loader')) { + return next(specifier); + } + console.log('resolve foo'); // This log is deliberate return next('file:///foo.mjs'); } diff --git a/test/fixtures/es-module-loaders/loader-resolve-incomplete.mjs b/test/fixtures/es-module-loaders/loader-resolve-incomplete.mjs index 9eb1617f30130e..d00b8fc50134e1 100644 --- a/test/fixtures/es-module-loaders/loader-resolve-incomplete.mjs +++ b/test/fixtures/es-module-loaders/loader-resolve-incomplete.mjs @@ -1,4 +1,12 @@ -export async function resolve() { +export async function resolve(specifier, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (specifier.includes('loader')) { + return next(specifier); + } + return { url: 'file:///incomplete-resolve-chain.js', }; diff --git a/test/fixtures/es-module-loaders/loader-resolve-next-modified.mjs b/test/fixtures/es-module-loaders/loader-resolve-next-modified.mjs index a973345a82ff21..ac5ad5b1fe020d 100644 --- a/test/fixtures/es-module-loaders/loader-resolve-next-modified.mjs +++ b/test/fixtures/es-module-loaders/loader-resolve-next-modified.mjs @@ -1,4 +1,12 @@ export async function resolve(url, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (url.includes('loader')) { + return next(url); + } + const { format, url: nextUrl, diff --git a/test/fixtures/es-module-loaders/loader-resolve-passthru.mjs b/test/fixtures/es-module-loaders/loader-resolve-passthru.mjs index 3db5b21bb98793..d4845acc9f5f71 100644 --- a/test/fixtures/es-module-loaders/loader-resolve-passthru.mjs +++ b/test/fixtures/es-module-loaders/loader-resolve-passthru.mjs @@ -1,4 +1,12 @@ export async function resolve(specifier, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (specifier.includes('loader')) { + return next(specifier); + } + console.log('resolve passthru'); // This log is deliberate return next(specifier); } diff --git a/test/fixtures/es-module-loaders/loader-resolve-shortcircuit.mjs b/test/fixtures/es-module-loaders/loader-resolve-shortcircuit.mjs index d886b3dfcbf237..e1a357b4ab48f9 100644 --- a/test/fixtures/es-module-loaders/loader-resolve-shortcircuit.mjs +++ b/test/fixtures/es-module-loaders/loader-resolve-shortcircuit.mjs @@ -1,4 +1,12 @@ -export async function resolve(specifier) { +export async function resolve(specifier, context, next) { + // This check is needed to make sure that we don't prevent the + // resolution from follow-up loaders. It wouldn't be a problem + // in real life because loaders aren't supposed to break the + // resolution, but the ones used in our tests do, for convenience. + if (specifier.includes('loader')) { + return next(specifier); + } + return { shortCircuit: true, url: specifier, diff --git a/test/fixtures/es-module-loaders/loader-resolve-strip-xxx.mjs b/test/fixtures/es-module-loaders/loader-resolve-strip-xxx.mjs new file mode 100644 index 00000000000000..58d7a1107994fe --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-resolve-strip-xxx.mjs @@ -0,0 +1,4 @@ +export async function resolve(specifier, context, nextResolve) { + console.log(`loader-a`, {specifier}); + return nextResolve(specifier.replace(/^xxx\//, `./`)); +} diff --git a/test/fixtures/es-module-loaders/loader-resolve-strip-yyy.mjs b/test/fixtures/es-module-loaders/loader-resolve-strip-yyy.mjs new file mode 100644 index 00000000000000..3615926143c4c7 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-resolve-strip-yyy.mjs @@ -0,0 +1,4 @@ +export async function resolve(specifier, context, nextResolve) { + console.log(`loader-b`, {specifier}); + return nextResolve(specifier.replace(/^yyy\//, `./`)); +} From 5ad6c2088e2f753e6169fa50fe909513e8555a04 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Thu, 19 Jan 2023 22:24:40 -0500 Subject: [PATCH 07/72] buffer: add isAscii method PR-URL: https://github.com/nodejs/node/pull/46046 Reviewed-By: Antoine du Hamel Reviewed-By: Matteo Collina --- doc/api/buffer.md | 14 ++++++++++ lib/buffer.js | 10 +++++++ src/node_buffer.cc | 17 +++++++++++ test/parallel/test-buffer-isascii.js | 42 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 test/parallel/test-buffer-isascii.js diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 7a741f4f6c0de2..6d165b280e7209 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -5130,6 +5130,20 @@ For code running using Node.js APIs, converting between base64-encoded strings and binary data should be performed using `Buffer.from(str, 'base64')` and `buf.toString('base64')`.** +### `buffer.isAscii(input)` + + + +* input {Buffer | ArrayBuffer | TypedArray} The input to validate. +* Returns: {boolean} + +This function returns `true` if `input` contains only valid ASCII-encoded data, +including the case in which `input` is empty. + +Throws if the `input` is a detached array buffer. + ### `buffer.isUtf8(input)` + +> Stability: 1 - Experimental + +* {number|undefined} + +Gets the amount of memory available to the process (in bytes) based on +limits imposed by the OS. If there is no such constraint, or the constraint +is unknown, `undefined` is returned. + +See [`uv_get_constrained_memory`][uv_get_constrained_memory] for more +information. + ## `process.cpuUsage([previousValue])` + +This API collects GC data in current thread. + +### `new v8.GCProfiler()` + + + +Create a new instance of the `v8.GCProfiler` class. + +### `profiler.start()` + + + +Start collecting GC data. + +### `profiler.stop()` + + + +Stop collecting GC data and return a object.The content of object +is as follows. + +```json +{ + "version": 1, + "startTime": 1674059033862, + "statistics": [ + { + "gcType": "Scavenge", + "beforeGC": { + "heapStatistics": { + "totalHeapSize": 5005312, + "totalHeapSizeExecutable": 524288, + "totalPhysicalSize": 5226496, + "totalAvailableSize": 4341325216, + "totalGlobalHandlesSize": 8192, + "usedGlobalHandlesSize": 2112, + "usedHeapSize": 4883840, + "heapSizeLimit": 4345298944, + "mallocedMemory": 254128, + "externalMemory": 225138, + "peakMallocedMemory": 181760 + }, + "heapSpaceStatistics": [ + { + "spaceName": "read_only_space", + "spaceSize": 0, + "spaceUsedSize": 0, + "spaceAvailableSize": 0, + "physicalSpaceSize": 0 + } + ] + }, + "cost": 1574.14, + "afterGC": { + "heapStatistics": { + "totalHeapSize": 6053888, + "totalHeapSizeExecutable": 524288, + "totalPhysicalSize": 5500928, + "totalAvailableSize": 4341101384, + "totalGlobalHandlesSize": 8192, + "usedGlobalHandlesSize": 2112, + "usedHeapSize": 4059096, + "heapSizeLimit": 4345298944, + "mallocedMemory": 254128, + "externalMemory": 225138, + "peakMallocedMemory": 181760 + }, + "heapSpaceStatistics": [ + { + "spaceName": "read_only_space", + "spaceSize": 0, + "spaceUsedSize": 0, + "spaceAvailableSize": 0, + "physicalSpaceSize": 0 + } + ] + } + } + ], + "endTime": 1674059036865 +} +``` + +Here's an example. + +```js +const { GCProfiler } = require('v8'); +const profiler = new GCProfiler(); +profiler.start(); +setTimeout(() => { + console.log(profiler.stop()); +}, 1000); +``` + [HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm [Hook Callbacks]: #hook-callbacks [V8]: https://developers.google.com/v8/ diff --git a/lib/v8.js b/lib/v8.js index 70956192d7d34f..a10966992147a3 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -63,7 +63,7 @@ const { } = require('internal/heap_utils'); const promiseHooks = require('internal/promise_hooks'); const { getOptionValue } = require('internal/options'); - +const { JSONParse } = primordials; /** * Generates a snapshot of the current V8 heap * and writes it to a JSON file. @@ -397,6 +397,25 @@ function deserialize(buffer) { return der.readValue(); } +class GCProfiler { + #profiler = null; + + start() { + if (!this.#profiler) { + this.#profiler = new binding.GCProfiler(); + this.#profiler.start(); + } + } + + stop() { + if (this.#profiler) { + const data = this.#profiler.stop(); + this.#profiler = null; + return JSONParse(data); + } + } +} + module.exports = { cachedDataVersionTag, getHeapSnapshot, @@ -416,4 +435,5 @@ module.exports = { promiseHooks, startupSnapshot, setHeapSnapshotNearHeapLimit, + GCProfiler, }; diff --git a/src/node_v8.cc b/src/node_v8.cc index 91a3dbb93e0155..890f59eea673c5 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -33,6 +33,7 @@ namespace v8_utils { using v8::Array; using v8::Context; using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; using v8::HandleScope; using v8::HeapCodeStatistics; using v8::HeapSpaceStatistics; @@ -210,6 +211,184 @@ void SetFlagsFromString(const FunctionCallbackInfo& args) { V8::SetFlagsFromString(*flags, static_cast(flags.length())); } +static const char* GetGCTypeName(v8::GCType gc_type) { + switch (gc_type) { + case v8::GCType::kGCTypeScavenge: + return "Scavenge"; + case v8::GCType::kGCTypeMarkSweepCompact: + return "MarkSweepCompact"; + case v8::GCType::kGCTypeIncrementalMarking: + return "IncrementalMarking"; + case v8::GCType::kGCTypeProcessWeakCallbacks: + return "ProcessWeakCallbacks"; + default: + return "Unknown"; + } +} + +static void SetHeapStatistics(JSONWriter* writer, Isolate* isolate) { + HeapStatistics heap_statistics; + isolate->GetHeapStatistics(&heap_statistics); + writer->json_objectstart("heapStatistics"); + writer->json_keyvalue("totalHeapSize", heap_statistics.total_heap_size()); + writer->json_keyvalue("totalHeapSizeExecutable", + heap_statistics.total_heap_size_executable()); + writer->json_keyvalue("totalPhysicalSize", + heap_statistics.total_physical_size()); + writer->json_keyvalue("totalAvailableSize", + heap_statistics.total_available_size()); + writer->json_keyvalue("totalGlobalHandlesSize", + heap_statistics.total_global_handles_size()); + writer->json_keyvalue("usedGlobalHandlesSize", + heap_statistics.used_global_handles_size()); + writer->json_keyvalue("usedHeapSize", heap_statistics.used_heap_size()); + writer->json_keyvalue("heapSizeLimit", heap_statistics.heap_size_limit()); + writer->json_keyvalue("mallocedMemory", heap_statistics.malloced_memory()); + writer->json_keyvalue("externalMemory", heap_statistics.external_memory()); + writer->json_keyvalue("peakMallocedMemory", + heap_statistics.peak_malloced_memory()); + writer->json_objectend(); + + int space_count = isolate->NumberOfHeapSpaces(); + writer->json_arraystart("heapSpaceStatistics"); + for (int i = 0; i < space_count; i++) { + HeapSpaceStatistics heap_space_statistics; + isolate->GetHeapSpaceStatistics(&heap_space_statistics, i); + writer->json_start(); + writer->json_keyvalue("spaceName", heap_space_statistics.space_name()); + writer->json_keyvalue("spaceSize", heap_space_statistics.space_size()); + writer->json_keyvalue("spaceUsedSize", + heap_space_statistics.space_used_size()); + writer->json_keyvalue("spaceAvailableSize", + heap_space_statistics.space_available_size()); + writer->json_keyvalue("physicalSpaceSize", + heap_space_statistics.physical_space_size()); + writer->json_end(); + } + writer->json_arrayend(); +} + +static void BeforeGCCallback(Isolate* isolate, + v8::GCType gc_type, + v8::GCCallbackFlags flags, + void* data) { + GCProfiler* profiler = static_cast(data); + if (profiler->current_gc_type != 0) { + return; + } + JSONWriter* writer = profiler->writer(); + writer->json_start(); + writer->json_keyvalue("gcType", GetGCTypeName(gc_type)); + writer->json_objectstart("beforeGC"); + SetHeapStatistics(writer, isolate); + writer->json_objectend(); + profiler->current_gc_type = gc_type; + profiler->start_time = uv_hrtime(); +} + +static void AfterGCCallback(Isolate* isolate, + v8::GCType gc_type, + v8::GCCallbackFlags flags, + void* data) { + GCProfiler* profiler = static_cast(data); + if (profiler->current_gc_type != gc_type) { + return; + } + JSONWriter* writer = profiler->writer(); + profiler->current_gc_type = 0; + writer->json_keyvalue("cost", (uv_hrtime() - profiler->start_time) / 1e3); + profiler->start_time = 0; + writer->json_objectstart("afterGC"); + SetHeapStatistics(writer, isolate); + writer->json_objectend(); + writer->json_end(); +} + +GCProfiler::GCProfiler(Environment* env, Local object) + : BaseObject(env, object), + start_time(0), + current_gc_type(0), + state(GCProfilerState::kInitialized), + writer_(out_stream_, false) { + MakeWeak(); +} + +// This function will be called when +// 1. StartGCProfile and StopGCProfile are called and +// JS land does not keep the object anymore. +// 2. StartGCProfile is called then the env exits before +// StopGCProfile is called. +GCProfiler::~GCProfiler() { + if (state != GCProfiler::GCProfilerState::kInitialized) { + env()->isolate()->RemoveGCPrologueCallback(BeforeGCCallback, this); + env()->isolate()->RemoveGCEpilogueCallback(AfterGCCallback, this); + } +} + +JSONWriter* GCProfiler::writer() { + return &writer_; +} + +std::ostringstream* GCProfiler::out_stream() { + return &out_stream_; +} + +void GCProfiler::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new GCProfiler(env, args.This()); +} + +void GCProfiler::Start(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + GCProfiler* profiler; + ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder()); + if (profiler->state != GCProfiler::GCProfilerState::kInitialized) { + return; + } + profiler->writer()->json_start(); + profiler->writer()->json_keyvalue("version", 1); + + uv_timeval64_t ts; + if (uv_gettimeofday(&ts) == 0) { + profiler->writer()->json_keyvalue("startTime", + ts.tv_sec * 1000 + ts.tv_usec / 1000); + } else { + profiler->writer()->json_keyvalue("startTime", 0); + } + profiler->writer()->json_arraystart("statistics"); + env->isolate()->AddGCPrologueCallback(BeforeGCCallback, + static_cast(profiler)); + env->isolate()->AddGCEpilogueCallback(AfterGCCallback, + static_cast(profiler)); + profiler->state = GCProfiler::GCProfilerState::kStarted; +} + +void GCProfiler::Stop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + GCProfiler* profiler; + ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder()); + if (profiler->state != GCProfiler::GCProfilerState::kStarted) { + return; + } + profiler->writer()->json_arrayend(); + uv_timeval64_t ts; + if (uv_gettimeofday(&ts) == 0) { + profiler->writer()->json_keyvalue("endTime", + ts.tv_sec * 1000 + ts.tv_usec / 1000); + } else { + profiler->writer()->json_keyvalue("endTime", 0); + } + profiler->writer()->json_end(); + profiler->state = GCProfiler::GCProfilerState::kStopped; + auto string = profiler->out_stream()->str(); + args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(), + string.data(), + v8::NewStringType::kNormal, + string.size()) + .ToLocalChecked()); +} + void Initialize(Local target, Local unused, Local context, @@ -272,6 +451,14 @@ void Initialize(Local target, // Export symbols used by v8.setFlagsFromString() SetMethod(context, target, "setFlagsFromString", SetFlagsFromString); + + // GCProfiler + Local t = + NewFunctionTemplate(env->isolate(), GCProfiler::New); + t->InstanceTemplate()->SetInternalFieldCount(BaseObject::kInternalFieldCount); + SetProtoMethod(env->isolate(), t, "start", GCProfiler::Start); + SetProtoMethod(env->isolate(), t, "stop", GCProfiler::Stop); + SetConstructorFunction(context, target, "GCProfiler", t); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -281,6 +468,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(UpdateHeapSpaceStatisticsBuffer); registry->Register(SetFlagsFromString); registry->Register(SetHeapSnapshotNearHeapLimit); + registry->Register(GCProfiler::New); + registry->Register(GCProfiler::Start); + registry->Register(GCProfiler::Stop); } } // namespace v8_utils diff --git a/src/node_v8.h b/src/node_v8.h index 18b3621a2a5d6a..ecab454603b36b 100644 --- a/src/node_v8.h +++ b/src/node_v8.h @@ -3,8 +3,10 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include #include "aliased_buffer.h" #include "base_object.h" +#include "json_utils.h" #include "node_snapshotable.h" #include "util.h" #include "v8.h" @@ -34,6 +36,32 @@ class BindingData : public SnapshotableObject { SET_MEMORY_INFO_NAME(BindingData) }; +class GCProfiler : public BaseObject { + public: + enum class GCProfilerState { kInitialized, kStarted, kStopped }; + GCProfiler(Environment* env, v8::Local object); + inline ~GCProfiler() override; + static void New(const v8::FunctionCallbackInfo& args); + static void Start(const v8::FunctionCallbackInfo& args); + static void Stop(const v8::FunctionCallbackInfo& args); + + JSONWriter* writer(); + + std::ostringstream* out_stream(); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(GCProfiler) + SET_SELF_SIZE(GCProfiler) + + uint64_t start_time; + uint8_t current_gc_type; + GCProfilerState state; + + private: + std::ostringstream out_stream_; + JSONWriter writer_; +}; + } // namespace v8_utils } // namespace node diff --git a/test/common/v8.js b/test/common/v8.js new file mode 100644 index 00000000000000..9c247bde50eb8c --- /dev/null +++ b/test/common/v8.js @@ -0,0 +1,70 @@ +'use strict'; +const assert = require('assert'); +const { GCProfiler } = require('v8'); + +function collectGCProfile({ duration }) { + return new Promise((resolve) => { + const profiler = new GCProfiler(); + profiler.start(); + setTimeout(() => { + resolve(profiler.stop()); + }, duration); + }); +} + +function checkGCProfile(data) { + assert.ok(data.version > 0); + assert.ok(data.startTime >= 0); + assert.ok(data.endTime >= 0); + assert.ok(Array.isArray(data.statistics)); + // If the array is not empty, check it + if (data.statistics.length) { + // Just check the first one + const item = data.statistics[0]; + assert.ok(typeof item.gcType === 'string'); + assert.ok(item.cost >= 0); + assert.ok(typeof item.beforeGC === 'object'); + assert.ok(typeof item.afterGC === 'object'); + // The content of beforeGC and afterGC is same, so we just check afterGC + assert.ok(typeof item.afterGC.heapStatistics === 'object'); + const heapStatisticsKeys = [ + 'externalMemory', + 'heapSizeLimit', + 'mallocedMemory', + 'peakMallocedMemory', + 'totalAvailableSize', + 'totalGlobalHandlesSize', + 'totalHeapSize', + 'totalHeapSizeExecutable', + 'totalPhysicalSize', + 'usedGlobalHandlesSize', + 'usedHeapSize', + ]; + heapStatisticsKeys.forEach((key) => { + assert.ok(item.afterGC.heapStatistics[key] >= 0); + }); + assert.ok(typeof item.afterGC.heapSpaceStatistics === 'object'); + const heapSpaceStatisticsKeys = [ + 'physicalSpaceSize', + 'spaceAvailableSize', + 'spaceName', + 'spaceSize', + 'spaceUsedSize', + ]; + heapSpaceStatisticsKeys.forEach((key) => { + const value = item.afterGC.heapSpaceStatistics[0][key]; + assert.ok(key === 'spaceName' ? typeof value === 'string' : value >= 0); + }); + } +} + +async function testGCProfiler() { + const data = await collectGCProfile({ duration: 5000 }); + checkGCProfile(data); +} + +module.exports = { + collectGCProfile, + checkGCProfile, + testGCProfiler, +}; diff --git a/test/parallel/test-v8-collect-gc-profile-exit-before-stop.js b/test/parallel/test-v8-collect-gc-profile-exit-before-stop.js new file mode 100644 index 00000000000000..4ef44001236209 --- /dev/null +++ b/test/parallel/test-v8-collect-gc-profile-exit-before-stop.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); +const { GCProfiler } = require('v8'); + +// Test if it makes the process crash. +{ + const profiler = new GCProfiler(); + profiler.start(); + profiler.stop(); + profiler.start(); + profiler.stop(); +} +{ + const profiler = new GCProfiler(); + profiler.start(); + profiler.stop(); +} diff --git a/test/parallel/test-v8-collect-gc-profile-in-worker.js b/test/parallel/test-v8-collect-gc-profile-in-worker.js new file mode 100644 index 00000000000000..49762c50fb1c73 --- /dev/null +++ b/test/parallel/test-v8-collect-gc-profile-in-worker.js @@ -0,0 +1,16 @@ +// Flags: --expose-gc +'use strict'; +require('../common'); +const { Worker } = require('worker_threads'); +const { testGCProfiler } = require('../common/v8'); + +if (process.env.isWorker) { + process.env.isWorker = 1; + new Worker(__filename); +} else { + testGCProfiler(); + for (let i = 0; i < 100; i++) { + new Array(100); + } + global?.gc(); +} diff --git a/test/parallel/test-v8-collect-gc-profile.js b/test/parallel/test-v8-collect-gc-profile.js new file mode 100644 index 00000000000000..70a8a0d842ef9e --- /dev/null +++ b/test/parallel/test-v8-collect-gc-profile.js @@ -0,0 +1,12 @@ +// Flags: --expose-gc +'use strict'; +require('../common'); +const { testGCProfiler } = require('../common/v8'); + +testGCProfiler(); + +for (let i = 0; i < 100; i++) { + new Array(100); +} + +global?.gc(); From e7b507a8cf23d12206554891bcac1161ff98a655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Wed, 25 Jan 2023 15:00:21 +0100 Subject: [PATCH 38/72] src: use UNREACHABLE instead of CHECK(falsy) Also remove some dead code following such statements. PR-URL: https://github.com/nodejs/node/pull/46317 Reviewed-By: Darshan Sen Reviewed-By: Anna Henningsen Reviewed-By: Yagiz Nizipli Reviewed-By: Luigi Pinca Reviewed-By: Franziska Hinkelmann --- src/cares_wrap.cc | 7 +++---- src/crypto/crypto_bio.cc | 7 ++----- src/crypto/crypto_keys.cc | 2 +- src/debug_utils.cc | 2 +- src/fs_event_wrap.cc | 2 +- src/node_zlib.cc | 4 ++-- src/spawn_sync.cc | 3 +-- src/string_bytes.cc | 6 ++---- src/udp_wrap.cc | 2 +- 9 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 2dafdf7a650436..997c3ba00d6150 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -233,8 +233,7 @@ int ParseGeneralReply( status = ares_parse_ptr_reply(buf, len, nullptr, 0, AF_INET, &host); break; default: - CHECK(0 && "Bad NS type"); - break; + UNREACHABLE("Bad NS type"); } if (status != ARES_SUCCESS) @@ -1578,7 +1577,7 @@ void GetAddrInfo(const FunctionCallbackInfo& args) { family = AF_INET6; break; default: - CHECK(0 && "bad address family"); + UNREACHABLE("bad address family"); } auto req_wrap = std::make_unique(env, @@ -1736,7 +1735,7 @@ void SetServers(const FunctionCallbackInfo& args) { err = uv_inet_pton(AF_INET6, *ip, &cur->addr); break; default: - CHECK(0 && "Bad address family."); + UNREACHABLE("Bad address family"); } if (err) diff --git a/src/crypto/crypto_bio.cc b/src/crypto/crypto_bio.cc index 099b11ee72520d..47045365ceaf81 100644 --- a/src/crypto/crypto_bio.cc +++ b/src/crypto/crypto_bio.cc @@ -190,12 +190,9 @@ long NodeBIO::Ctrl(BIO* bio, int cmd, long num, // NOLINT(runtime/int) *reinterpret_cast(ptr) = nullptr; break; case BIO_C_SET_BUF_MEM: - CHECK(0 && "Can't use SET_BUF_MEM_PTR with NodeBIO"); - break; + UNREACHABLE("Can't use SET_BUF_MEM_PTR with NodeBIO"); case BIO_C_GET_BUF_MEM_PTR: - CHECK(0 && "Can't use GET_BUF_MEM_PTR with NodeBIO"); - ret = 0; - break; + UNREACHABLE("Can't use GET_BUF_MEM_PTR with NodeBIO"); case BIO_CTRL_GET_CLOSE: ret = BIO_get_shutdown(bio); break; diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index f5661ccedad4f7..cc2ff2e631ff77 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1393,7 +1393,7 @@ BaseObjectPtr NativeKeyObject::KeyObjectTransferData::Deserialize( key_ctor = env->crypto_key_object_private_constructor(); break; default: - CHECK(false); + UNREACHABLE(); } Local key; diff --git a/src/debug_utils.cc b/src/debug_utils.cc index 69ef383ed22ca8..fadf321c3d925f 100644 --- a/src/debug_utils.cc +++ b/src/debug_utils.cc @@ -321,7 +321,7 @@ void CheckedUvLoopClose(uv_loop_t* loop) { fflush(stderr); // Finally, abort. - CHECK(0 && "uv_loop_close() while having open handles"); + UNREACHABLE("uv_loop_close() while having open handles"); } void PrintLibuvHandleInformation(uv_loop_t* loop, FILE* stream) { diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index dafcc065ac612b..048b72666c0570 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -204,7 +204,7 @@ void FSEventWrap::OnEvent(uv_fs_event_t* handle, const char* filename, } else if (events & UV_CHANGE) { event_string = env->change_string(); } else { - CHECK(0 && "bad fs events flag"); + UNREACHABLE("bad fs events flag"); } Local argv[] = { diff --git a/src/node_zlib.cc b/src/node_zlib.cc index 3d130e8eb64859..fac116f9e6b3e2 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -313,7 +313,7 @@ class CompressionStream : public AsyncWrap, public ThreadPoolWork { flush != Z_FULL_FLUSH && flush != Z_FINISH && flush != Z_BLOCK) { - CHECK(0 && "Invalid flush value"); + UNREACHABLE("Invalid flush value"); } if (args[1]->IsNull()) { @@ -814,7 +814,7 @@ void ZlibContext::DoThreadPoolWork() { break; default: - CHECK(0 && "invalid number of gzip magic number bytes read"); + UNREACHABLE("invalid number of gzip magic number bytes read"); } [[fallthrough]]; diff --git a/src/spawn_sync.cc b/src/spawn_sync.cc index a602795e54fdca..ae4a85a42d6166 100644 --- a/src/spawn_sync.cc +++ b/src/spawn_sync.cc @@ -931,8 +931,7 @@ int SyncProcessRunner::ParseStdioOption(int child_fd, return AddStdioInheritFD(child_fd, inherit_fd); } else { - CHECK(0 && "invalid child stdio type"); - return UV_EINVAL; + UNREACHABLE("invalid child stdio type"); } } diff --git a/src/string_bytes.cc b/src/string_bytes.cc index 0e6b8b842141c5..98da55a9d71f21 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -368,8 +368,7 @@ size_t StringBytes::Write(Isolate* isolate, break; default: - CHECK(0 && "unknown encoding"); - break; + UNREACHABLE("unknown encoding"); } return nbytes; @@ -423,8 +422,7 @@ Maybe StringBytes::StorageSize(Isolate* isolate, break; default: - CHECK(0 && "unknown encoding"); - break; + UNREACHABLE("unknown encoding"); } return Just(data_size); diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index 772021ff653089..cad50fec409730 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -233,7 +233,7 @@ int sockaddr_for_family(int address_family, case AF_INET6: return uv_ip6_addr(address, port, reinterpret_cast(addr)); default: - CHECK(0 && "unexpected address family"); + UNREACHABLE("unexpected address family"); } } From 88b904cf2463de704df849935b082c6bb9a97b0c Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 25 Jan 2023 19:07:53 +0100 Subject: [PATCH 39/72] tools: require more trailing commas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All of the import and export statements in the codebase already use trailing commas, this commit adds a linter rule to enforce that. PR-URL: https://github.com/nodejs/node/pull/46346 Reviewed-By: Michaël Zasso Reviewed-By: Filip Skokan Reviewed-By: Darshan Sen Reviewed-By: Colin Ihrig --- benchmark/.eslintrc.yaml | 4 ++-- lib/.eslintrc.yaml | 4 ++-- test/.eslintrc.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benchmark/.eslintrc.yaml b/benchmark/.eslintrc.yaml index 821b2381d3e215..7802df7c2f97cb 100644 --- a/benchmark/.eslintrc.yaml +++ b/benchmark/.eslintrc.yaml @@ -7,9 +7,9 @@ env: rules: comma-dangle: [error, { arrays: always-multiline, - exports: only-multiline, + exports: always-multiline, functions: only-multiline, - imports: only-multiline, + imports: always-multiline, objects: only-multiline, }] prefer-arrow-callback: error diff --git a/lib/.eslintrc.yaml b/lib/.eslintrc.yaml index 2cc6ca904ee76f..ff790e49821d28 100644 --- a/lib/.eslintrc.yaml +++ b/lib/.eslintrc.yaml @@ -4,9 +4,9 @@ env: rules: comma-dangle: [error, { arrays: always-multiline, - exports: only-multiline, + exports: always-multiline, functions: only-multiline, - imports: only-multiline, + imports: always-multiline, objects: only-multiline, }] prefer-object-spread: error diff --git a/test/.eslintrc.yaml b/test/.eslintrc.yaml index b3e39ecf45b797..3c5ecdb3e44fec 100644 --- a/test/.eslintrc.yaml +++ b/test/.eslintrc.yaml @@ -90,8 +90,8 @@ overrides: rules: comma-dangle: [error, { arrays: always-multiline, - exports: only-multiline, + exports: always-multiline, functions: only-multiline, - imports: only-multiline, + imports: always-multiline, objects: only-multiline, }] From 34d70ce615109e12f231e730040ac554b2349da7 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 25 Jan 2023 21:06:55 +0100 Subject: [PATCH 40/72] vm: expose cachedDataRejected for vm.compileFunction Having this information available is useful for functions just as it is for scripts. Therefore, expose it in the same way that other information related to code caching is reported. As part of this, de-duplify the code for setting the properties on the C++ side and add proper exception handling to it. PR-URL: https://github.com/nodejs/node/pull/46320 Reviewed-By: Gus Caplan Reviewed-By: Chengzhong Wu --- doc/api/vm.md | 6 ++ lib/internal/vm.js | 4 ++ src/node_contextify.cc | 125 ++++++++++++++++++++------------- src/node_contextify.h | 8 +++ test/parallel/test-vm-basic.js | 26 +++++-- 5 files changed, 114 insertions(+), 55 deletions(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index 35a5d4f4bafd67..10c1cbefee828c 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -962,6 +962,12 @@ const vm = require('node:vm'); + $(IntDir)%(FileName)%(Extension).pp + + + diff --git a/tools/v8_gypfiles/v8.gyp b/tools/v8_gypfiles/v8.gyp index 4419fba73626b8..89358ad626921e 100644 --- a/tools/v8_gypfiles/v8.gyp +++ b/tools/v8_gypfiles/v8.gyp @@ -1433,6 +1433,14 @@ ['want_separate_host_toolset', { 'toolsets': ['host'], }], + ['OS=="win"', { + 'msvs_precompiled_header': '<(V8_ROOT)/../../tools/msvs/pch/v8_pch.h', + 'msvs_precompiled_source': '<(V8_ROOT)/../../tools/msvs/pch/v8_pch.cc', + 'sources': [ + '<(_msvs_precompiled_header)', + '<(_msvs_precompiled_source)', + ], + }], ], }, # mksnapshot { From a64d7f4e31bf40265d566d84d6ec2efb3c06f5f7 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 28 Jan 2023 07:00:45 +0100 Subject: [PATCH 46/72] doc: add documentation for socket.destroySoon() PR-URL: https://github.com/nodejs/node/pull/46337 Reviewed-By: Paolo Insogna Reviewed-By: Matteo Collina --- doc/api/net.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/api/net.md b/doc/api/net.md index 5fb6246b8d8f92..f328320a5c8b2d 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -1049,6 +1049,16 @@ See [`writable.destroy()`][] for further details. See [`writable.destroyed`][] for further details. +### `socket.destroySoon()` + + + +Destroys the socket after all data is written. If the `'finish'` event was +already emitted the socket is destroyed immediately. If the socket is still +writable it implicitly calls `socket.end()`. + ### `socket.end([data[, encoding]][, callback])` -Stop collecting GC data and return a object.The content of object +Stop collecting GC data and return an object.The content of object is as follows. ```json From a7c9daa4972b31b2af777b49d9e0c0b8fda69d62 Mon Sep 17 00:00:00 2001 From: Colin Ihrig Date: Sun, 29 Jan 2023 12:43:20 -0500 Subject: [PATCH 53/72] fs: add statfs() functions This commit adds statfs() and statfsSync() to the fs module, and statfs() to the fsPromises module. Co-authored-by: cjihrig Fixes: https://github.com/nodejs/node/issues/10745 Refs: https://github.com/nodejs/node/pull/31351 PR-URL: https://github.com/nodejs/node/pull/46358 Reviewed-By: Matteo Collina Reviewed-By: James M Snell --- doc/api/fs.md | 159 ++++++++++++++++++++++++++++++ lib/fs.js | 30 ++++++ lib/internal/fs/promises.js | 9 ++ lib/internal/fs/utils.js | 19 ++++ src/node_file-inl.h | 61 +++++++++--- src/node_file.cc | 76 +++++++++++++- src/node_file.h | 29 ++++++ test/parallel/test-fs-promises.js | 26 +++++ test/parallel/test-fs-statfs.js | 59 +++++++++++ tools/doc/type-parser.mjs | 1 + 10 files changed, 457 insertions(+), 12 deletions(-) create mode 100644 test/parallel/test-fs-statfs.js diff --git a/doc/api/fs.md b/doc/api/fs.md index 6c7099b265f454..ee67a125b43b66 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1531,6 +1531,19 @@ changes: * Returns: {Promise} Fulfills with the {fs.Stats} object for the given `path`. +### `fsPromises.statfs(path[, options])` + + + +* `path` {string|Buffer|URL} +* `options` {Object} + * `bigint` {boolean} Whether the numeric values in the returned + {fs.StatFs} object should be `bigint`. **Default:** `false`. +* Returns: {Promise} Fulfills with the {fs.StatFs} object for the + given `path`. + ### `fsPromises.symlink(target, path[, type])` + +* `path` {string|Buffer|URL} +* `options` {Object} + * `bigint` {boolean} Whether the numeric values in the returned + {fs.StatFs} object should be `bigint`. **Default:** `false`. +* `callback` {Function} + * `err` {Error} + * `stats` {fs.StatFs} + +Asynchronous statfs(2). Returns information about the mounted file system which +contains `path`. The callback gets two arguments `(err, stats)` where `stats` +is an {fs.StatFs} object. + +In case of an error, the `err.code` will be one of [Common System Errors][]. + ### `fs.symlink(target, path[, type], callback)` + +* `path` {string|Buffer|URL} +* `options` {Object} + * `bigint` {boolean} Whether the numeric values in the returned + {fs.StatFs} object should be `bigint`. **Default:** `false`. +* Returns: {fs.StatFs} + +Synchronous statfs(2). Returns information about the mounted file system which +contains `path`. + +In case of an error, the `err.code` will be one of [Common System Errors][]. + ### `fs.symlinkSync(target, path[, type])` + +Provides information about a mounted file system. + +Objects returned from [`fs.statfs()`][] and its synchronous counterpart are of +this type. If `bigint` in the `options` passed to those methods is `true`, the +numeric values will be `bigint` instead of `number`. + +```console +StatFs { + type: 1397114950, + bsize: 4096, + blocks: 121938943, + bfree: 61058895, + bavail: 61058895, + files: 999, + ffree: 1000000 +} +``` + +`bigint` version: + +```console +StatFs { + type: 1397114950n, + bsize: 4096n, + blocks: 121938943n, + bfree: 61058895n, + bavail: 61058895n, + files: 999n, + ffree: 1000000n +} +``` + +#### `statfs.bavail` + + + +* {number|bigint} + +Free blocks available to unprivileged users. + +#### `statfs.bfree` + + + +* {number|bigint} + +Free blocks in file system. + +#### `statfs.blocks` + + + +* {number|bigint} + +Total data blocks in file system. + +#### `statfs.bsize` + + + +* {number|bigint} + +Optimal transfer block size. + +#### `statfs.ffree` + + + +* {number|bigint} + +Free file nodes in file system. + +#### `statfs.files` + + + +* {number|bigint} + +Total file nodes in file system. + +#### `statfs.type` + + + +* {number|bigint} + +Type of file system. + ### Class: `fs.WriteStream` -* `stream` {Stream} A readable and/or writable stream. +* `stream` {Stream|ReadableStream|WritableStream} + +A readable and/or writable stream/webstream. * `options` {Object} * `error` {boolean} If set to `false`, then a call to `emit('error', err)` is @@ -3029,10 +3034,16 @@ added: v17.0.0 * `src` {Stream|Blob|ArrayBuffer|string|Iterable|AsyncIterable| - AsyncGeneratorFunction|AsyncFunction|Promise|Object} + AsyncGeneratorFunction|AsyncFunction|Promise|Object| + ReadableStream|WritableStream} A utility method for creating duplex streams. @@ -3052,6 +3063,8 @@ A utility method for creating duplex streams. `writable` into `Stream` and then combines them into `Duplex` where the `Duplex` will write to the `writable` and read from the `readable`. * `Promise` converts into readable `Duplex`. Value `null` is ignored. +* `ReadableStream` converts into readable `Duplex`. +* `WritableStream` converts into writable `Duplex`. * Returns: {stream.Duplex} If an `Iterable` object containing promises is passed as an argument, From c7024eec16762a816942e01d3559823df864dde3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9?= Date: Tue, 31 Jan 2023 13:58:12 -0500 Subject: [PATCH 71/72] doc: correct the `sed` command for macOS in release process docs PR-URL: https://github.com/nodejs/node/pull/46397 Reviewed-By: Antoine du Hamel Reviewed-By: Rafael Gonzaga --- doc/contributing/releases.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/contributing/releases.md b/doc/contributing/releases.md index c2f26eebe884f3..5c3d372aaed5fb 100644 --- a/doc/contributing/releases.md +++ b/doc/contributing/releases.md @@ -428,6 +428,12 @@ and substitute this node version with sed -i "s/REPLACEME/$VERSION/g" doc/api/*.md ``` +For macOS requires the extension to be specified. + +```bash +sed -i "" "s/REPLACEME/$VERSION/g" doc/api/*.md +``` + or ```console From 3ecd96f0636c9e36c2baf4606e57931c08dc24e2 Mon Sep 17 00:00:00 2001 From: "ruyadorno@google.com" Date: Tue, 31 Jan 2023 22:15:28 -0500 Subject: [PATCH 72/72] 2023-02-02, Version 19.6.0 (Current) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notable changes: buffer: * (SEMVER-MINOR) add isAscii method (Yagiz Nizipli) https://github.com/nodejs/node/pull/46046 deps: * upgrade npm to 9.4.0 (npm team) https://github.com/nodejs/node/pull/46353 esm: * leverage loaders when resolving subsequent loaders (Maël Nison) https://github.com/nodejs/node/pull/43772 fs: * (SEMVER-MINOR) add statfs() functions (Colin Ihrig) https://github.com/nodejs/node/pull/46358 src,lib: * (SEMVER-MINOR) add constrainedMemory API for process (theanarkh) https://github.com/nodejs/node/pull/46218 test_runner: * (SEMVER-MINOR) add reporters (Moshe Atlow) https://github.com/nodejs/node/pull/45712 v8: * (SEMVER-MINOR) support gc profile (theanarkh) https://github.com/nodejs/node/pull/46255 vm: * (SEMVER-MINOR) expose cachedDataRejected for vm.compileFunction (Anna Henningsen) https://github.com/nodejs/node/pull/46320 PR-URL: https://github.com/nodejs/node/pull/46455 --- CHANGELOG.md | 3 +- doc/api/buffer.md | 2 +- doc/api/cli.md | 4 +- doc/api/fs.md | 22 +++---- doc/api/http.md | 2 +- doc/api/process.md | 2 +- doc/api/test.md | 2 +- doc/api/v8.md | 8 +-- doc/api/vm.md | 2 +- doc/changelogs/CHANGELOG_V19.md | 100 +++++++++++++++++++++++++++++++- src/node_version.h | 6 +- 11 files changed, 126 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa7e6ef379676..9a91979a928504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,8 @@ release. -19.5.0
+19.6.0
+19.5.0
19.4.0
19.3.0
19.2.0
diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 6d165b280e7209..0540bccc3851d8 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -5133,7 +5133,7 @@ and binary data should be performed using `Buffer.from(str, 'base64')` and ### `buffer.isAscii(input)` * input {Buffer | ArrayBuffer | TypedArray} The input to validate. diff --git a/doc/api/cli.md b/doc/api/cli.md index ade3584b75170b..a5f168a0b4f86d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1241,7 +1241,7 @@ whose name matches the provided pattern. See the documentation on ### `--test-reporter` A test reporter to use when running tests. See the documentation on @@ -1250,7 +1250,7 @@ A test reporter to use when running tests. See the documentation on ### `--test-reporter-destination` The destination for the corresponding test reporter. See the documentation on diff --git a/doc/api/fs.md b/doc/api/fs.md index ee67a125b43b66..865813069e16b1 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1534,7 +1534,7 @@ changes: ### `fsPromises.statfs(path[, options])` * `path` {string|Buffer|URL} @@ -4117,7 +4117,7 @@ Stats { ### `fs.statfs(path[, options], callback)` * `path` {string|Buffer|URL} @@ -5888,7 +5888,7 @@ Retrieves the {fs.Stats} for the path. ### `fs.statfsSync(path[, options])` * `path` {string|Buffer|URL} @@ -6997,7 +6997,7 @@ of 0.12, `ctime` is not "creation time", and on Unix systems, it never was. ### Class: `fs.StatFs` Provides information about a mounted file system. @@ -7035,7 +7035,7 @@ StatFs { #### `statfs.bavail` * {number|bigint} @@ -7045,7 +7045,7 @@ Free blocks available to unprivileged users. #### `statfs.bfree` * {number|bigint} @@ -7055,7 +7055,7 @@ Free blocks in file system. #### `statfs.blocks` * {number|bigint} @@ -7065,7 +7065,7 @@ Total data blocks in file system. #### `statfs.bsize` * {number|bigint} @@ -7075,7 +7075,7 @@ Optimal transfer block size. #### `statfs.ffree` * {number|bigint} @@ -7085,7 +7085,7 @@ Free file nodes in file system. #### `statfs.files` * {number|bigint} @@ -7095,7 +7095,7 @@ Total file nodes in file system. #### `statfs.type` * {number|bigint} diff --git a/doc/api/http.md b/doc/api/http.md index 6c9e21ab806dee..b9b243ae4fc7da 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -2972,7 +2972,7 @@ headers with the same name. ### `outgoingMessage.setHeaders(headers)` * `headers` {Headers|Map} diff --git a/doc/api/process.md b/doc/api/process.md index c772cfe900d0df..3a0b27b625e416 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1106,7 +1106,7 @@ over the IPC channel using `process.send()`. ## `process.constrainedMemory()` > Stability: 1 - Experimental diff --git a/doc/api/test.md b/doc/api/test.md index 106b1eeb4682c1..99b3009be20382 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -454,7 +454,7 @@ test('spies on an object method', (t) => { ## Test reporters The `node:test` module supports passing [`--test-reporter`][] diff --git a/doc/api/v8.md b/doc/api/v8.md index bd56168b49ad09..061c525d63318d 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1054,7 +1054,7 @@ Returns true if the Node.js instance is run to build a snapshot. ## Class: `v8.GCProfiler` This API collects GC data in current thread. @@ -1062,7 +1062,7 @@ This API collects GC data in current thread. ### `new v8.GCProfiler()` Create a new instance of the `v8.GCProfiler` class. @@ -1070,7 +1070,7 @@ Create a new instance of the `v8.GCProfiler` class. ### `profiler.start()` Start collecting GC data. @@ -1078,7 +1078,7 @@ Start collecting GC data. ### `profiler.stop()` Stop collecting GC data and return an object.The content of object diff --git a/doc/api/vm.md b/doc/api/vm.md index 10c1cbefee828c..d7e72ab42e3de6 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -963,7 +963,7 @@ const vm = require('node:vm'); added: v10.10.0 changes: - version: - - REPLACEME + - v19.6.0 pr-url: https://github.com/nodejs/node/pull/46320 description: The return value now includes `cachedDataRejected` with the same semantics as the `vm.Script` version diff --git a/doc/changelogs/CHANGELOG_V19.md b/doc/changelogs/CHANGELOG_V19.md index fa69fdbb68d5c1..3df0483a66b4b8 100644 --- a/doc/changelogs/CHANGELOG_V19.md +++ b/doc/changelogs/CHANGELOG_V19.md @@ -8,7 +8,8 @@ -19.5.0
+19.6.0
+19.5.0
19.4.0
19.3.0
19.2.0
@@ -40,6 +41,103 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2023-02-02, Version 19.6.0 (Current), @ruyadorno + +### Notable changes + +#### ESM: Leverage loaders when resolving subsequent loaders + +Loaders now apply to subsequent loaders, for example: `--experimental-loader ts-node --experimental-loader loader-written-in-typescript`. + +#### Upgrade npm to 9.4.0 + +Added `--install-strategy=linked` option for installations similar to pnpm. + +#### Other notable changes + +* \[[`a7c9daa497`](https://github.com/nodejs/node/commit/a7c9daa497)] - **(SEMVER-MINOR)** **fs**: add statfs() functions (Colin Ihrig) [#46358](https://github.com/nodejs/node/pull/46358) +* \[[`34d70ce615`](https://github.com/nodejs/node/commit/34d70ce615)] - **(SEMVER-MINOR)** **vm**: expose cachedDataRejected for vm.compileFunction (Anna Henningsen) [#46320](https://github.com/nodejs/node/pull/46320) +* \[[`b4ac794923`](https://github.com/nodejs/node/commit/b4ac794923)] - **(SEMVER-MINOR)** **v8**: support gc profile (theanarkh) [#46255](https://github.com/nodejs/node/pull/46255) +* \[[`d52f60009a`](https://github.com/nodejs/node/commit/d52f60009a)] - **(SEMVER-MINOR)** **src,lib**: add constrainedMemory API for process (theanarkh) [#46218](https://github.com/nodejs/node/pull/46218) +* \[[`5ad6c2088e`](https://github.com/nodejs/node/commit/5ad6c2088e)] - **(SEMVER-MINOR)** **buffer**: add isAscii method (Yagiz Nizipli) [#46046](https://github.com/nodejs/node/pull/46046) +* \[[`fbdc3f7316`](https://github.com/nodejs/node/commit/fbdc3f7316)] - **(SEMVER-MINOR)** **test\_runner**: add reporters (Moshe Atlow) [#45712](https://github.com/nodejs/node/pull/45712) + +### Commits + +* \[[`524eec70e2`](https://github.com/nodejs/node/commit/524eec70e2)] - **benchmark**: add trailing commas (Antoine du Hamel) [#46370](https://github.com/nodejs/node/pull/46370) +* \[[`f318a85408`](https://github.com/nodejs/node/commit/f318a85408)] - **benchmark**: remove buffer benchmarks redundancy (Brian White) [#45735](https://github.com/nodejs/node/pull/45735) +* \[[`6186b3ea14`](https://github.com/nodejs/node/commit/6186b3ea14)] - **benchmark**: introduce benchmark combination filtering (Brian White) [#45735](https://github.com/nodejs/node/pull/45735) +* \[[`5ad6c2088e`](https://github.com/nodejs/node/commit/5ad6c2088e)] - **(SEMVER-MINOR)** **buffer**: add isAscii method (Yagiz Nizipli) [#46046](https://github.com/nodejs/node/pull/46046) +* \[[`8c6c4338a6`](https://github.com/nodejs/node/commit/8c6c4338a6)] - **build**: export more OpenSSL symbols on Windows (Mohamed Akram) [#45486](https://github.com/nodejs/node/pull/45486) +* \[[`d795d93901`](https://github.com/nodejs/node/commit/d795d93901)] - **build**: fix MSVC 2022 Release compilation (Vladimir Morozov (REDMOND)) [#46228](https://github.com/nodejs/node/pull/46228) +* \[[`8e363cf8e8`](https://github.com/nodejs/node/commit/8e363cf8e8)] - **crypto**: include `hmac.h` in `crypto_util.h` (Adam Langley) [#46279](https://github.com/nodejs/node/pull/46279) +* \[[`c1f3e13c65`](https://github.com/nodejs/node/commit/c1f3e13c65)] - **deps**: update acorn to 8.8.2 (Node.js GitHub Bot) [#46363](https://github.com/nodejs/node/pull/46363) +* \[[`813b160bd7`](https://github.com/nodejs/node/commit/813b160bd7)] - **deps**: upgrade npm to 9.4.0 (npm team) [#46353](https://github.com/nodejs/node/pull/46353) +* \[[`9c2f3cea70`](https://github.com/nodejs/node/commit/9c2f3cea70)] - **deps**: update undici to 5.15.0 (Node.js GitHub Bot) [#46213](https://github.com/nodejs/node/pull/46213) +* \[[`312e10c1e3`](https://github.com/nodejs/node/commit/312e10c1e3)] - **deps**: update to uvwasi 0.0.15 (Colin Ihrig) [#46253](https://github.com/nodejs/node/pull/46253) +* \[[`c7024eec16`](https://github.com/nodejs/node/commit/c7024eec16)] - **doc**: correct the `sed` command for macOS in release process docs (Juan José) [#46397](https://github.com/nodejs/node/pull/46397) +* \[[`996bac044b`](https://github.com/nodejs/node/commit/996bac044b)] - **doc**: include webstreams in finished() and Duplex.from() parameters (Debadree Chatterjee) [#46312](https://github.com/nodejs/node/pull/46312) +* \[[`891d18d55c`](https://github.com/nodejs/node/commit/891d18d55c)] - **doc**: pass string to `textEncoder.encode` as input (Deokjin Kim) [#46421](https://github.com/nodejs/node/pull/46421) +* \[[`968db213f8`](https://github.com/nodejs/node/commit/968db213f8)] - **doc**: add tip for session.post function (theanarkh) [#46354](https://github.com/nodejs/node/pull/46354) +* \[[`a64d7f4e31`](https://github.com/nodejs/node/commit/a64d7f4e31)] - **doc**: add documentation for socket.destroySoon() (Luigi Pinca) [#46337](https://github.com/nodejs/node/pull/46337) +* \[[`975788899f`](https://github.com/nodejs/node/commit/975788899f)] - **doc**: fix commit message using test instead of deps (Tony Gorez) [#46313](https://github.com/nodejs/node/pull/46313) +* \[[`1d44017f52`](https://github.com/nodejs/node/commit/1d44017f52)] - **doc**: add v8 fast api contribution guidelines (Yagiz Nizipli) [#46199](https://github.com/nodejs/node/pull/46199) +* \[[`e2698c05fb`](https://github.com/nodejs/node/commit/e2698c05fb)] - **doc**: fix small typo error (0xflotus) [#46186](https://github.com/nodejs/node/pull/46186) +* \[[`f39fb8c001`](https://github.com/nodejs/node/commit/f39fb8c001)] - **doc**: mark some parameters as optional in webstreams (Deokjin Kim) [#46269](https://github.com/nodejs/node/pull/46269) +* \[[`7a9af38128`](https://github.com/nodejs/node/commit/7a9af38128)] - **doc**: update output of example in `events.getEventListeners` (Deokjin Kim) [#46268](https://github.com/nodejs/node/pull/46268) +* \[[`729642f30b`](https://github.com/nodejs/node/commit/729642f30b)] - **esm**: delete preload mock test (Geoffrey Booth) [#46402](https://github.com/nodejs/node/pull/46402) +* \[[`7aac21e90a`](https://github.com/nodejs/node/commit/7aac21e90a)] - **esm**: leverage loaders when resolving subsequent loaders (Maël Nison) [#43772](https://github.com/nodejs/node/pull/43772) +* \[[`a7c9daa497`](https://github.com/nodejs/node/commit/a7c9daa497)] - **(SEMVER-MINOR)** **fs**: add statfs() functions (Colin Ihrig) [#46358](https://github.com/nodejs/node/pull/46358) +* \[[`1ec6270efa`](https://github.com/nodejs/node/commit/1ec6270efa)] - **http**: res.setHeaders first implementation (Marco Ippolito) [#46109](https://github.com/nodejs/node/pull/46109) +* \[[`d4370259e9`](https://github.com/nodejs/node/commit/d4370259e9)] - **inspector**: allow opening inspector when `NODE_V8_COVERAGE` is set (Moshe Atlow) [#46113](https://github.com/nodejs/node/pull/46113) +* \[[`b966ef9a42`](https://github.com/nodejs/node/commit/b966ef9a42)] - **lib**: remove unnecessary ObjectGetValueSafe (Chengzhong Wu) [#46335](https://github.com/nodejs/node/pull/46335) +* \[[`2b06d66289`](https://github.com/nodejs/node/commit/2b06d66289)] - **lib**: cache parsed source maps to reduce memory footprint (Chengzhong Wu) [#46225](https://github.com/nodejs/node/pull/46225) +* \[[`c38673df91`](https://github.com/nodejs/node/commit/c38673df91)] - **meta**: update AUTHORS (Node.js GitHub Bot) [#46399](https://github.com/nodejs/node/pull/46399) +* \[[`c10e602547`](https://github.com/nodejs/node/commit/c10e602547)] - **meta**: update AUTHORS (Node.js GitHub Bot) [#46303](https://github.com/nodejs/node/pull/46303) +* \[[`9dc026b14a`](https://github.com/nodejs/node/commit/9dc026b14a)] - **meta**: add .mailmap entry (Rich Trott) [#46303](https://github.com/nodejs/node/pull/46303) +* \[[`7c514574f7`](https://github.com/nodejs/node/commit/7c514574f7)] - **meta**: move evanlucas to emeritus (Evan Lucas) [#46274](https://github.com/nodejs/node/pull/46274) +* \[[`3a3a6d87f1`](https://github.com/nodejs/node/commit/3a3a6d87f1)] - **module**: move test reporter loading (Geoffrey Booth) [#45923](https://github.com/nodejs/node/pull/45923) +* \[[`4ae2492a33`](https://github.com/nodejs/node/commit/4ae2492a33)] - **readline**: fix detection of carriage return (Antoine du Hamel) [#46306](https://github.com/nodejs/node/pull/46306) +* \[[`43cad78b7a`](https://github.com/nodejs/node/commit/43cad78b7a)] - **src**: stop tracing agent before shutting down libuv (Santiago Gimeno) [#46380](https://github.com/nodejs/node/pull/46380) +* \[[`360a3f3094`](https://github.com/nodejs/node/commit/360a3f3094)] - **src**: get rid of fp arithmetic in ParseIPv4Host (Tobias Nießen) [#46326](https://github.com/nodejs/node/pull/46326) +* \[[`e7b507a8cf`](https://github.com/nodejs/node/commit/e7b507a8cf)] - **src**: use UNREACHABLE instead of CHECK(falsy) (Tobias Nießen) [#46317](https://github.com/nodejs/node/pull/46317) +* \[[`4c59b60ee8`](https://github.com/nodejs/node/commit/4c59b60ee8)] - **src**: add support for ETW stack walking (José Dapena Paz) [#46203](https://github.com/nodejs/node/pull/46203) +* \[[`640d111f95`](https://github.com/nodejs/node/commit/640d111f95)] - **src**: refactor EndsInANumber in node\_url.cc and adds IsIPv4NumberValid (Miguel Teixeira) [#46227](https://github.com/nodejs/node/pull/46227) +* \[[`fb7bee2b6e`](https://github.com/nodejs/node/commit/fb7bee2b6e)] - **src**: fix c++ exception on bad command line arg (Ben Noordhuis) [#46290](https://github.com/nodejs/node/pull/46290) +* \[[`18c95ec4bd`](https://github.com/nodejs/node/commit/18c95ec4bd)] - **src**: remove unreachable UNREACHABLE (Tobias Nießen) [#46281](https://github.com/nodejs/node/pull/46281) +* \[[`35bf93b01a`](https://github.com/nodejs/node/commit/35bf93b01a)] - **src**: replace custom ASCII validation with simdutf one (Anna Henningsen) [#46271](https://github.com/nodejs/node/pull/46271) +* \[[`8307a4bbcd`](https://github.com/nodejs/node/commit/8307a4bbcd)] - **src**: replace unreachable code with static\_assert (Tobias Nießen) [#46250](https://github.com/nodejs/node/pull/46250) +* \[[`7cf0da020a`](https://github.com/nodejs/node/commit/7cf0da020a)] - **src**: use explicit C++17 fallthrough (Tobias Nießen) [#46251](https://github.com/nodejs/node/pull/46251) +* \[[`d52f60009a`](https://github.com/nodejs/node/commit/d52f60009a)] - **(SEMVER-MINOR)** **src,lib**: add constrainedMemory API for process (theanarkh) [#46218](https://github.com/nodejs/node/pull/46218) +* \[[`2e5e7a9261`](https://github.com/nodejs/node/commit/2e5e7a9261)] - **stream**: remove brandchecks from stream duplexify (Debadree Chatterjee) [#46315](https://github.com/nodejs/node/pull/46315) +* \[[`9675863461`](https://github.com/nodejs/node/commit/9675863461)] - **stream**: fix readable stream as async iterator function (Erick Wendel) [#46147](https://github.com/nodejs/node/pull/46147) +* \[[`232bdd5d16`](https://github.com/nodejs/node/commit/232bdd5d16)] - **test**: add trailing commas in `test/node-api` (Antoine du Hamel) [#46384](https://github.com/nodejs/node/pull/46384) +* \[[`4cc081815d`](https://github.com/nodejs/node/commit/4cc081815d)] - **test**: add trailing commas in `test/message` (Antoine du Hamel) [#46372](https://github.com/nodejs/node/pull/46372) +* \[[`b83c5d9deb`](https://github.com/nodejs/node/commit/b83c5d9deb)] - **test**: add trailing commas in `test/pseudo-tty` (Antoine du Hamel) [#46371](https://github.com/nodejs/node/pull/46371) +* \[[`8a45c9d231`](https://github.com/nodejs/node/commit/8a45c9d231)] - **test**: fix tap escaping with and without --test (Pulkit Gupta) [#46311](https://github.com/nodejs/node/pull/46311) +* \[[`367dc41299`](https://github.com/nodejs/node/commit/367dc41299)] - **test**: set common.bits to 64 for loong64 (Shi Pujin) [#45383](https://github.com/nodejs/node/pull/45383) +* \[[`7385edc7d0`](https://github.com/nodejs/node/commit/7385edc7d0)] - **test**: s390x zlib test case fixes (Adam Majer) [#46367](https://github.com/nodejs/node/pull/46367) +* \[[`d5d837bdee`](https://github.com/nodejs/node/commit/d5d837bdee)] - **test**: fix logInTimeout is not function (theanarkh) [#46348](https://github.com/nodejs/node/pull/46348) +* \[[`a1d79546ac`](https://github.com/nodejs/node/commit/a1d79546ac)] - **test**: avoid trying to call sysctl directly (Adam Majer) [#46366](https://github.com/nodejs/node/pull/46366) +* \[[`747f3689e0`](https://github.com/nodejs/node/commit/747f3689e0)] - **test**: avoid left behind child processes (Richard Lau) [#46276](https://github.com/nodejs/node/pull/46276) +* \[[`940484b7aa`](https://github.com/nodejs/node/commit/940484b7aa)] - **test**: add failing test for readline with carriage return (Alec Mev) [#46075](https://github.com/nodejs/node/pull/46075) +* \[[`d13116a719`](https://github.com/nodejs/node/commit/d13116a719)] - **test,crypto**: add CFRG curve vectors to wrap/unwrap tests (Filip Skokan) [#46406](https://github.com/nodejs/node/pull/46406) +* \[[`398a7477b3`](https://github.com/nodejs/node/commit/398a7477b3)] - **test,crypto**: update WebCryptoAPI WPT (Filip Skokan) [#46267](https://github.com/nodejs/node/pull/46267) +* \[[`8b473affe8`](https://github.com/nodejs/node/commit/8b473affe8)] - **test\_runner**: make built in reporters internal (Colin Ihrig) [#46092](https://github.com/nodejs/node/pull/46092) +* \[[`a49e17e22b`](https://github.com/nodejs/node/commit/a49e17e22b)] - **test\_runner**: report `file` in test runner events (Moshe Atlow) [#46030](https://github.com/nodejs/node/pull/46030) +* \[[`fbdc3f7316`](https://github.com/nodejs/node/commit/fbdc3f7316)] - **test\_runner**: add reporters (Moshe Atlow) [#45712](https://github.com/nodejs/node/pull/45712) +* \[[`6579de8c47`](https://github.com/nodejs/node/commit/6579de8c47)] - **tools**: update eslint to 8.33.0 (Node.js GitHub Bot) [#46400](https://github.com/nodejs/node/pull/46400) +* \[[`bf62da55ad`](https://github.com/nodejs/node/commit/bf62da55ad)] - **tools**: update doc to unist-util-select\@4.0.3 unist-util-visit\@4.1.2 (Node.js GitHub Bot) [#46364](https://github.com/nodejs/node/pull/46364) +* \[[`b0acf55197`](https://github.com/nodejs/node/commit/b0acf55197)] - **tools**: update lint-md-dependencies to rollup\@3.12.0 (Node.js GitHub Bot) [#46398](https://github.com/nodejs/node/pull/46398) +* \[[`88b904cf24`](https://github.com/nodejs/node/commit/88b904cf24)] - **tools**: require more trailing commas (Antoine du Hamel) [#46346](https://github.com/nodejs/node/pull/46346) +* \[[`4440b3ef87`](https://github.com/nodejs/node/commit/4440b3ef87)] - **tools**: update lint-md-dependencies (Node.js GitHub Bot) [#46302](https://github.com/nodejs/node/pull/46302) +* \[[`e75faff4bd`](https://github.com/nodejs/node/commit/e75faff4bd)] - **tools**: allow icutrim.py to run on python2 (Michael Dawson) [#46263](https://github.com/nodejs/node/pull/46263) +* \[[`e460d16d73`](https://github.com/nodejs/node/commit/e460d16d73)] - **url**: refactor to use more primordials (Antoine du Hamel) [#45966](https://github.com/nodejs/node/pull/45966) +* \[[`b4ac794923`](https://github.com/nodejs/node/commit/b4ac794923)] - **(SEMVER-MINOR)** **v8**: support gc profile (theanarkh) [#46255](https://github.com/nodejs/node/pull/46255) +* \[[`34d70ce615`](https://github.com/nodejs/node/commit/34d70ce615)] - **(SEMVER-MINOR)** **vm**: expose cachedDataRejected for vm.compileFunction (Anna Henningsen) [#46320](https://github.com/nodejs/node/pull/46320) + ## 2023-01-24, Version 19.5.0 (Current), @RafaelGSS diff --git a/src/node_version.h b/src/node_version.h index 695ce5fc09ce21..d848bb0004a54e 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -23,13 +23,13 @@ #define SRC_NODE_VERSION_H_ #define NODE_MAJOR_VERSION 19 -#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)