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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,24 @@ each other in ways that are not possible when isolation is enabled. For example,
if a test relies on global state, it is possible for that state to be modified
by a test originating from another file.

#### Child process option inheritance

When running tests in process isolation mode (the default), spawned child processes
inherit Node.js options from the parent process, including those specified in
[configuration files][]. However, certain flags are filtered out to enable proper
test runner functionality:

* `--test` - Prevented to avoid recursive test execution
* `--experimental-test-coverage` - Managed by the test runner
* `--watch` - Watch mode is handled at the parent level
* `--experimental-default-config-file` - Config file loading is handled by the parent
* `--test-reporter` - Reporting is managed by the parent process
* `--test-reporter-destination` - Output destinations are controlled by the parent
* `--experimental-config-file` - Config file paths are managed by the parent

All other Node.js options from command line arguments, environment variables,
and configuration files are inherited by the child processes.

## Collecting code coverage

> Stability: 1 - Experimental
Expand Down Expand Up @@ -3950,6 +3968,7 @@ Can be used to abort test subtasks when the test has been aborted.
[`suite()`]: #suitename-options-fn
[`test()`]: #testname-options-fn
[code coverage]: #collecting-code-coverage
[configuration files]: cli.md#--experimental-config-fileconfig
[describe options]: #describename-options-fn
[it options]: #testname-options-fn
[running tests from the command line]: #running-tests-from-the-command-line
Expand Down
7 changes: 7 additions & 0 deletions lib/internal/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
const {
getCLIOptionsValues,
getCLIOptionsInfo,
getOptionsAsFlags,
getEmbedderOptions: getEmbedderOptionsFromBinding,
getEnvOptionsInputType,
getNamespaceOptionsInputType,
Expand All @@ -21,6 +22,7 @@ let warnOnAllowUnauthorized = true;
let optionsDict;
let cliInfo;
let embedderOptions;
let optionsFlags;

// getCLIOptionsValues() would serialize the option values from C++ land.
// It would error if the values are queried before bootstrap is
Expand All @@ -34,6 +36,10 @@ function getCLIOptionsInfoFromBinding() {
return cliInfo ??= getCLIOptionsInfo();
}

function getOptionsAsFlagsFromBinding() {
return optionsFlags ??= getOptionsAsFlags();
}

function getEmbedderOptions() {
return embedderOptions ??= getEmbedderOptionsFromBinding();
}
Expand Down Expand Up @@ -156,6 +162,7 @@ function getAllowUnauthorized() {
module.exports = {
getCLIOptionsInfo: getCLIOptionsInfoFromBinding,
getOptionValue,
getOptionsAsFlagsFromBinding,
getAllowUnauthorized,
getEmbedderOptions,
generateConfigJsonSchema,
Expand Down
29 changes: 15 additions & 14 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const { spawn } = require('child_process');
const { finished } = require('internal/streams/end-of-stream');
const { resolve, sep, isAbsolute } = require('path');
const { DefaultDeserializer, DefaultSerializer } = require('v8');
const { getOptionValue } = require('internal/options');
const { getOptionValue, getOptionsAsFlagsFromBinding } = require('internal/options');
const { Interface } = require('internal/readline/interface');
const { deserializeError } = require('internal/error_serdes');
const { Buffer } = require('buffer');
Expand Down Expand Up @@ -150,38 +150,39 @@ function getRunArgs(path, { forceExit,
execArgv,
root: { timeout },
cwd }) {
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
const processNodeOptions = getOptionsAsFlagsFromBinding();
const runArgs = ArrayPrototypeFilter(processNodeOptions, filterExecArgv);
if (forceExit === true) {
ArrayPrototypePush(argv, '--test-force-exit');
ArrayPrototypePush(runArgs, '--test-force-exit');
}
if (isUsingInspector()) {
ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`);
ArrayPrototypePush(runArgs, `--inspect-port=${getInspectPort(inspectPort)}`);
}
if (testNamePatterns != null) {
ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`));
ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(runArgs, `--test-name-pattern=${pattern}`));
}
if (testSkipPatterns != null) {
ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(argv, `--test-skip-pattern=${pattern}`));
ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(runArgs, `--test-skip-pattern=${pattern}`));
}
if (only === true) {
ArrayPrototypePush(argv, '--test-only');
ArrayPrototypePush(runArgs, '--test-only');
}
if (timeout != null) {
ArrayPrototypePush(argv, `--test-timeout=${timeout}`);
ArrayPrototypePush(runArgs, `--test-timeout=${timeout}`);
}

ArrayPrototypePushApply(argv, execArgv);
ArrayPrototypePushApply(runArgs, execArgv);

if (path === kIsolatedProcessName) {
ArrayPrototypePush(argv, '--test');
ArrayPrototypePushApply(argv, ArrayPrototypeSlice(process.argv, 1));
ArrayPrototypePush(runArgs, '--test');
ArrayPrototypePushApply(runArgs, ArrayPrototypeSlice(process.argv, 1));
} else {
ArrayPrototypePush(argv, path);
ArrayPrototypePush(runArgs, path);
}

ArrayPrototypePushApply(argv, suppliedArgs);
ArrayPrototypePushApply(runArgs, suppliedArgs);

return argv;
return runArgs;
}

const serializer = new DefaultSerializer();
Expand Down
114 changes: 114 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1867,6 +1867,117 @@ void GetNamespaceOptionsInputType(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(namespaces_map);
}

// Return an array containing all currently active options as flag
// strings from all sources (command line, NODE_OPTIONS, config file)
void GetOptionsAsFlags(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);

if (!env->has_run_bootstrapping_code()) {
// No code because this is an assertion.
THROW_ERR_OPTIONS_BEFORE_BOOTSTRAPPING(
isolate, "Should not query options before bootstrapping is done");
}
env->set_has_serialized_options(true);

Mutex::ScopedLock lock(per_process::cli_options_mutex);
IterateCLIOptionsScope s(env);

std::vector<std::string> flags;
PerProcessOptions* opts = per_process::cli_options.get();

for (const auto& item : _ppop_instance.options_) {
const std::string& option_name = item.first;
const auto& option_info = item.second;
auto field = option_info.field;

// TODO(pmarchini): Skip internal options for the moment as probably not
// required
if (option_name.empty() || option_name.starts_with('[')) {
continue;
}

// Skip V8 options and NoOp options - only Node.js-specific options
if (option_info.type == kNoOp || option_info.type == kV8Option) {
continue;
}

switch (option_info.type) {
case kBoolean: {
bool current_value = *_ppop_instance.Lookup<bool>(field, opts);
// For boolean options with default_is_true, we want the opposite logic
if (option_info.default_is_true) {
if (!current_value) {
// If default is true and current is false, add --no-* flag
flags.push_back("--no-" + option_name.substr(2));
}
} else {
if (current_value) {
// If default is false and current is true, add --flag
flags.push_back(option_name);
}
}
break;
}
case kInteger: {
int64_t current_value = *_ppop_instance.Lookup<int64_t>(field, opts);
flags.push_back(option_name + "=" + std::to_string(current_value));
break;
}
case kUInteger: {
uint64_t current_value = *_ppop_instance.Lookup<uint64_t>(field, opts);
flags.push_back(option_name + "=" + std::to_string(current_value));
break;
}
case kString: {
const std::string& current_value =
*_ppop_instance.Lookup<std::string>(field, opts);
// Only include if not empty
if (!current_value.empty()) {
flags.push_back(option_name + "=" + current_value);
}
break;
}
case kStringList: {
const std::vector<std::string>& current_values =
*_ppop_instance.Lookup<StringVector>(field, opts);
// Add each string in the list as a separate flag
for (const std::string& value : current_values) {
flags.push_back(option_name + "=" + value);
}
break;
}
case kHostPort: {
const HostPort& host_port =
*_ppop_instance.Lookup<HostPort>(field, opts);
// Only include if host is not empty or port is not default
if (!host_port.host().empty() || host_port.port() != 0) {
std::string host_port_str = host_port.host();
if (host_port.port() != 0) {
if (!host_port_str.empty()) {
host_port_str += ":";
}
host_port_str += std::to_string(host_port.port());
}
if (!host_port_str.empty()) {
flags.push_back(option_name + "=" + host_port_str);
}
}
break;
}
default:
// Skip unknown types
break;
}
}

Local<Value> result;
CHECK(ToV8Value(context, flags).ToLocal(&result));

args.GetReturnValue().Set(result);
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand All @@ -1877,6 +1988,8 @@ void Initialize(Local<Object> target,
context, target, "getCLIOptionsValues", GetCLIOptionsValues);
SetMethodNoSideEffect(
context, target, "getCLIOptionsInfo", GetCLIOptionsInfo);
SetMethodNoSideEffect(
context, target, "getOptionsAsFlags", GetOptionsAsFlags);
SetMethodNoSideEffect(
context, target, "getEmbedderOptions", GetEmbedderOptions);
SetMethodNoSideEffect(
Expand Down Expand Up @@ -1909,6 +2022,7 @@ void Initialize(Local<Object> target,
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetCLIOptionsValues);
registry->Register(GetCLIOptionsInfo);
registry->Register(GetOptionsAsFlags);
registry->Register(GetEmbedderOptions);
registry->Register(GetEnvOptionsInputType);
registry->Register(GetNamespaceOptionsInputType);
Expand Down
2 changes: 2 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,8 @@ class OptionsParser {
friend std::vector<std::string> MapAvailableNamespaces();
friend void GetEnvOptionsInputType(
const v8::FunctionCallbackInfo<v8::Value>& args);
friend void GetOptionsAsFlags(
const v8::FunctionCallbackInfo<v8::Value>& args);
};

using StringVector = std::vector<std::string>;
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/options-as-flags/.test.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NODE_OPTIONS=--v8-pool-size=8
4 changes: 4 additions & 0 deletions test/fixtures/options-as-flags/fixture.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { getOptionsAsFlagsFromBinding } = require('internal/options');

const flags = getOptionsAsFlagsFromBinding();
console.log(JSON.stringify(flags.sort()));
9 changes: 9 additions & 0 deletions test/fixtures/options-as-flags/test-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"nodeOptions": {
"experimental-transform-types": true,
"max-http-header-size": 8192
},
"testRunner": {
"test-isolation": "none"
}
}
Empty file.
1 change: 1 addition & 0 deletions test/fixtures/test-runner/flag-propagation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Empty file used by test/parallel/test-runner-flag-propagation.js
5 changes: 5 additions & 0 deletions test/fixtures/test-runner/flag-propagation/node.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"nodeOptions": {
"max-http-header-size": 10
}
}
33 changes: 33 additions & 0 deletions test/fixtures/test-runner/flag-propagation/runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { run } from 'node:test';
import { tap } from 'node:test/reporters';
import { parseArgs } from 'node:util';

const options = {
flag: {
type: 'string',
default: '',
},
expected: {
type: 'string',
default: '',
},
description: {
type: 'string',
default: 'flag propagation test',
},
};

const { values } = parseArgs({ args: process.argv.slice(2), options });

const argv = [
`--flag=${values.flag}`,
`--expected=${values.expected}`,
`--description="${values.description}"`,
].filter(Boolean);

run({
files: ['./test.mjs'],
cwd: process.cwd(),
argv,
isolation: 'process',
}).compose(tap).pipe(process.stdout);
46 changes: 46 additions & 0 deletions test/fixtures/test-runner/flag-propagation/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test } from 'node:test';
import { deepStrictEqual } from 'node:assert';
import internal from 'internal/options';
import { parseArgs } from 'node:util';

const options = {
flag: {
type: 'string',
default: '',
},
expected: {
type: 'string',
default: '',
},
description: {
type: 'string',
default: 'flag propagation test',
},
};


const { values } = parseArgs({ args: process.argv.slice(2), options });

let { flag, expected, description } = values;

test(description, () => {
const optionValue = internal.getOptionValue(flag);
const isArrayOption = Array.isArray(optionValue);

if (isArrayOption) {
expected = [expected];
}

console.error(`testing flag: ${flag}, found value: ${optionValue}, expected: ${expected}`);

const isNumber = !isNaN(Number(expected));
const booleanValue = expected === 'true' || expected === 'false';
if (booleanValue) {
deepStrictEqual(optionValue, expected === 'true');
return;
} else if (isNumber) {
deepStrictEqual(Number(optionValue), Number(expected));
} else{
deepStrictEqual(optionValue, expected);
}
});
Loading
Loading