Commit cd189650411 for nodejs
commit cd18965041118eeac069a4f886385ad6c4956c5f
Author: RafaelGSS <rafael.nunu@hotmail.com>
Date: Mon Oct 5 10:30:53 2026 -0300
lib: set exit code on failed node:bench run()
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
PR-URL: https://github.com/nodejs/node/pull/66534
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Bryan English <bryan@bryanenglish.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
diff --git a/doc/api/bench.md b/doc/api/bench.md
index c04ae971523..86644916553 100644
--- a/doc/api/bench.md
+++ b/doc/api/bench.md
@@ -62,8 +62,10 @@ node --experimental-bench --bench benchmark.mjs
Benchmarks are executed serially in declaration order. Declared benchmarks are
scheduled automatically. Call `run()` during the same turn as the declarations
to consume the event stream or configure filtering.
-If an automatically scheduled run fails and `run()` was not called, the process
-exit code is set to `1`.
+If a benchmark run started automatically or through the module-level `run()`
+fails, the process exit code is set to `1` unless `process.exitCode` has
+already been set. Runners created with [`createRunner()`][] leave the exit code
+to their caller.
## Measurement model
@@ -310,7 +312,8 @@ runner and obtain its {BenchmarksStream}.
Each runner can be started once. Its `run()` function accepts the same options
as the module-level [`run()`][]. `run({ yieldBetweenSamples })` overrides the
-value passed to `createRunner()`.
+value passed to `createRunner()`. Unlike the module-level runner, a failed
+benchmark in an explicit runner does not change the process exit code.
## `bench([name][, options], fn)`
@@ -504,6 +507,9 @@ execution begins. Calling `run()` is optional when the returned stream is not
needed. An explicit runner created by `createRunner()` does not run
automatically, so its `run()` function may be called later.
+If any benchmark fails, the process exit code is set to `1` once the run
+finishes, unless `process.exitCode` has already been set.
+
```mjs
import { bench, run } from 'node:bench';
@@ -849,6 +855,7 @@ A completed benchmark result contains:
* `skewness` {number} The skewness of the scaled rate histogram.
[`context.record()`]: #contextrecordsample
+[`createRunner()`]: #createrunneroptions
[`run()`]: #runoptions
[benchmark result]: #benchmark-result
[command-line options documentation]: cli.md#--bench
diff --git a/lib/internal/bench_runner/harness.js b/lib/internal/bench_runner/harness.js
index 98544063421..46d35a0a235 100644
--- a/lib/internal/bench_runner/harness.js
+++ b/lib/internal/bench_runner/harness.js
@@ -104,6 +104,7 @@ class Harness {
#abortReason = null;
#autoRun;
#buildPromises = [];
+ #cliRun = false;
#duplicateErrors = new SafeMap();
#explicitRun = false;
#fileScopeStorage = new AsyncLocalStorage();
@@ -318,6 +319,7 @@ class Harness {
try {
this.configure(options);
this.#explicitRun = true;
+ this.#cliRun = force;
} finally {
this.#starting = false;
}
@@ -1035,8 +1037,10 @@ class Harness {
this.root.completion.resolve();
this.root.emitDestroy();
this.#storage.disable();
- if (!this.#explicitRun && !this.success) {
- process.exitCode = kGenericUserError;
+ // Runners created with createRunner() leave the exit code to their
+ // caller, and the CLI runner derives it from the aggregated summary.
+ if (this.#autoRun && !this.#cliRun && !this.success) {
+ process.exitCode ??= kGenericUserError;
}
}
}
diff --git a/test/parallel/test-bench-clock-precision.js b/test/parallel/test-bench-clock-precision.js
index 7f7fc800969..3c991da8709 100644
--- a/test/parallel/test-bench-clock-precision.js
+++ b/test/parallel/test-bench-clock-precision.js
@@ -19,4 +19,7 @@ const completion = bench('zero duration', { samples: 1 }, (b) => {
const result = await completion;
assert.strictEqual(result.error.code, 'ERR_INVALID_STATE');
assert.match(result.error.message, /insufficient clock precision/);
+ // The failed run sets the exit code; reset it so the test itself passes.
+ assert.strictEqual(process.exitCode, 1);
+ process.exitCode = 0;
})().then(common.mustCall());
diff --git a/test/parallel/test-bench-errors.js b/test/parallel/test-bench-errors.js
index 5363efd6b14..f388cc44965 100644
--- a/test/parallel/test-bench-errors.js
+++ b/test/parallel/test-bench-errors.js
@@ -81,6 +81,9 @@ stream.on('end', common.mustCall(() => {
total: 17,
});
assert.strictEqual(summary.success, false);
+ // The failed run sets the exit code; reset it so the test itself passes.
+ assert.strictEqual(process.exitCode, 1);
+ process.exitCode = 0;
const byName = new Map();
for (const result of completions) {
diff --git a/test/parallel/test-bench-hook-errors.js b/test/parallel/test-bench-hook-errors.js
index 498406010c4..f45712c70a7 100644
--- a/test/parallel/test-bench-hook-errors.js
+++ b/test/parallel/test-bench-hook-errors.js
@@ -57,6 +57,9 @@ stream.on('end', common.mustCall(() => {
total: 4,
});
assert.strictEqual(summary.success, false);
+ // The failed run sets the exit code; reset it so the test itself passes.
+ assert.strictEqual(process.exitCode, 1);
+ process.exitCode = 0;
const byName = new Map(completions.map((result) => [result.name, result]));
assert.strictEqual(byName.get('blocked by before').error.message,
diff --git a/test/parallel/test-bench-reporters.js b/test/parallel/test-bench-reporters.js
index 7a3ed8bff7c..b5078a40de6 100644
--- a/test/parallel/test-bench-reporters.js
+++ b/test/parallel/test-bench-reporters.js
@@ -55,6 +55,9 @@ bench('json failed', { samples: 1 }, () => {
skipped: 0,
total: 2,
});
+ // The failed run sets the exit code; reset it so the test itself passes.
+ assert.strictEqual(process.exitCode, 1);
+ process.exitCode = 0;
const synthetic = [
{
diff --git a/test/parallel/test-bench-run-exit-code.js b/test/parallel/test-bench-run-exit-code.js
new file mode 100644
index 00000000000..8f03e862d0f
--- /dev/null
+++ b/test/parallel/test-bench-run-exit-code.js
@@ -0,0 +1,68 @@
+// Flags: --experimental-bench --no-warnings
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+
+function runScript(script) {
+ return spawnSync(process.execPath, [
+ '--no-warnings',
+ '--experimental-bench',
+ '-e',
+ script,
+ ]);
+}
+
+// A failing benchmark consumed through the module-level run() sets the exit
+// code.
+{
+ const child = runScript(`
+ const { bench, run } = require('node:bench');
+ bench('failure', () => { throw new Error('boom'); });
+ (async () => { for await (const record of run()); })();
+ `);
+ assert.strictEqual(child.status, 1, child.stderr.toString());
+}
+
+// A successful run through the module-level run() keeps the default exit code.
+{
+ const child = runScript(`
+ const { bench, run } = require('node:bench');
+ bench('success', { samples: 1 }, (b) => {
+ b.record({ duration_ns: 1n, operations: 1 });
+ });
+ (async () => { for await (const record of run()); })();
+ `);
+ assert.strictEqual(child.status, 0, child.stderr.toString());
+}
+
+// An exit code set by the user is preserved when the explicit run fails.
+{
+ const child = runScript(`
+ const { bench, run } = require('node:bench');
+ process.exitCode = 3;
+ bench('failure', () => { throw new Error('boom'); });
+ (async () => { for await (const record of run()); })();
+ `);
+ assert.strictEqual(child.status, 3, child.stderr.toString());
+}
+
+// An exit code set by the user is preserved when an automatic run fails.
+{
+ const child = runScript(`
+ process.exitCode = 3;
+ require('node:bench').bench('failure', () => { throw new Error(); });
+ `);
+ assert.strictEqual(child.status, 3, child.stderr.toString());
+}
+
+// Runners created with createRunner() leave the exit code to the caller.
+{
+ const child = runScript(`
+ const runner = require('node:bench').createRunner();
+ runner.bench('failure', () => { throw new Error('boom'); });
+ (async () => { for await (const record of runner.run()); })();
+ `);
+ assert.strictEqual(child.status, 0, child.stderr.toString());
+}