Skip to content

Commit abb1f92

Browse files
authored
src: add internal GetOptionsAsFlags
PR-URL: #59138 Reviewed-By: Marco Ippolito <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Chemi Atlow <[email protected]>
1 parent 360f7cc commit abb1f92

File tree

15 files changed

+495
-14
lines changed

15 files changed

+495
-14
lines changed

β€Ždoc/api/test.mdβ€Ž

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,24 @@ each other in ways that are not possible when isolation is enabled. For example,
520520
if a test relies on global state, it is possible for that state to be modified
521521
by a test originating from another file.
522522

523+
#### Child process option inheritance
524+
525+
When running tests in process isolation mode (the default), spawned child processes
526+
inherit Node.js options from the parent process, including those specified in
527+
[configuration files][]. However, certain flags are filtered out to enable proper
528+
test runner functionality:
529+
530+
* `--test` - Prevented to avoid recursive test execution
531+
* `--experimental-test-coverage` - Managed by the test runner
532+
* `--watch` - Watch mode is handled at the parent level
533+
* `--experimental-default-config-file` - Config file loading is handled by the parent
534+
* `--test-reporter` - Reporting is managed by the parent process
535+
* `--test-reporter-destination` - Output destinations are controlled by the parent
536+
* `--experimental-config-file` - Config file paths are managed by the parent
537+
538+
All other Node.js options from command line arguments, environment variables,
539+
and configuration files are inherited by the child processes.
540+
523541
## Collecting code coverage
524542

525543
> Stability: 1 - Experimental
@@ -3950,6 +3968,7 @@ Can be used to abort test subtasks when the test has been aborted.
39503968
[`suite()`]: #suitename-options-fn
39513969
[`test()`]: #testname-options-fn
39523970
[code coverage]: #collecting-code-coverage
3971+
[configuration files]: cli.md#--experimental-config-fileconfig
39533972
[describe options]: #describename-options-fn
39543973
[it options]: #testname-options-fn
39553974
[running tests from the command line]: #running-tests-from-the-command-line

β€Žlib/internal/options.jsβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
const {
1212
getCLIOptionsValues,
1313
getCLIOptionsInfo,
14+
getOptionsAsFlags,
1415
getEmbedderOptions: getEmbedderOptionsFromBinding,
1516
getEnvOptionsInputType,
1617
getNamespaceOptionsInputType,
@@ -21,6 +22,7 @@ let warnOnAllowUnauthorized = true;
2122
let optionsDict;
2223
let cliInfo;
2324
let embedderOptions;
25+
let optionsFlags;
2426

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

39+
function getOptionsAsFlagsFromBinding() {
40+
return optionsFlags ??= getOptionsAsFlags();
41+
}
42+
3743
function getEmbedderOptions() {
3844
return embedderOptions ??= getEmbedderOptionsFromBinding();
3945
}
@@ -156,6 +162,7 @@ function getAllowUnauthorized() {
156162
module.exports = {
157163
getCLIOptionsInfo: getCLIOptionsInfoFromBinding,
158164
getOptionValue,
165+
getOptionsAsFlagsFromBinding,
159166
getAllowUnauthorized,
160167
getEmbedderOptions,
161168
generateConfigJsonSchema,

β€Žlib/internal/test_runner/runner.jsβ€Ž

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const { spawn } = require('child_process');
3535
const { finished } = require('internal/streams/end-of-stream');
3636
const { resolve, sep, isAbsolute } = require('path');
3737
const { DefaultDeserializer, DefaultSerializer } = require('v8');
38-
const { getOptionValue } = require('internal/options');
38+
const { getOptionValue, getOptionsAsFlagsFromBinding } = require('internal/options');
3939
const { Interface } = require('internal/readline/interface');
4040
const { deserializeError } = require('internal/error_serdes');
4141
const { Buffer } = require('buffer');
@@ -150,38 +150,39 @@ function getRunArgs(path, { forceExit,
150150
execArgv,
151151
root: { timeout },
152152
cwd }) {
153-
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
153+
const processNodeOptions = getOptionsAsFlagsFromBinding();
154+
const runArgs = ArrayPrototypeFilter(processNodeOptions, filterExecArgv);
154155
if (forceExit === true) {
155-
ArrayPrototypePush(argv, '--test-force-exit');
156+
ArrayPrototypePush(runArgs, '--test-force-exit');
156157
}
157158
if (isUsingInspector()) {
158-
ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`);
159+
ArrayPrototypePush(runArgs, `--inspect-port=${getInspectPort(inspectPort)}`);
159160
}
160161
if (testNamePatterns != null) {
161-
ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`));
162+
ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(runArgs, `--test-name-pattern=${pattern}`));
162163
}
163164
if (testSkipPatterns != null) {
164-
ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(argv, `--test-skip-pattern=${pattern}`));
165+
ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(runArgs, `--test-skip-pattern=${pattern}`));
165166
}
166167
if (only === true) {
167-
ArrayPrototypePush(argv, '--test-only');
168+
ArrayPrototypePush(runArgs, '--test-only');
168169
}
169170
if (timeout != null) {
170-
ArrayPrototypePush(argv, `--test-timeout=${timeout}`);
171+
ArrayPrototypePush(runArgs, `--test-timeout=${timeout}`);
171172
}
172173

173-
ArrayPrototypePushApply(argv, execArgv);
174+
ArrayPrototypePushApply(runArgs, execArgv);
174175

175176
if (path === kIsolatedProcessName) {
176-
ArrayPrototypePush(argv, '--test');
177-
ArrayPrototypePushApply(argv, ArrayPrototypeSlice(process.argv, 1));
177+
ArrayPrototypePush(runArgs, '--test');
178+
ArrayPrototypePushApply(runArgs, ArrayPrototypeSlice(process.argv, 1));
178179
} else {
179-
ArrayPrototypePush(argv, path);
180+
ArrayPrototypePush(runArgs, path);
180181
}
181182

182-
ArrayPrototypePushApply(argv, suppliedArgs);
183+
ArrayPrototypePushApply(runArgs, suppliedArgs);
183184

184-
return argv;
185+
return runArgs;
185186
}
186187

187188
const serializer = new DefaultSerializer();

β€Žsrc/node_options.ccβ€Ž

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1867,6 +1867,117 @@ void GetNamespaceOptionsInputType(const FunctionCallbackInfo<Value>& args) {
18671867
args.GetReturnValue().Set(namespaces_map);
18681868
}
18691869

1870+
// Return an array containing all currently active options as flag
1871+
// strings from all sources (command line, NODE_OPTIONS, config file)
1872+
void GetOptionsAsFlags(const FunctionCallbackInfo<Value>& args) {
1873+
Isolate* isolate = args.GetIsolate();
1874+
Local<Context> context = isolate->GetCurrentContext();
1875+
Environment* env = Environment::GetCurrent(context);
1876+
1877+
if (!env->has_run_bootstrapping_code()) {
1878+
// No code because this is an assertion.
1879+
THROW_ERR_OPTIONS_BEFORE_BOOTSTRAPPING(
1880+
isolate, "Should not query options before bootstrapping is done");
1881+
}
1882+
env->set_has_serialized_options(true);
1883+
1884+
Mutex::ScopedLock lock(per_process::cli_options_mutex);
1885+
IterateCLIOptionsScope s(env);
1886+
1887+
std::vector<std::string> flags;
1888+
PerProcessOptions* opts = per_process::cli_options.get();
1889+
1890+
for (const auto& item : _ppop_instance.options_) {
1891+
const std::string& option_name = item.first;
1892+
const auto& option_info = item.second;
1893+
auto field = option_info.field;
1894+
1895+
// TODO(pmarchini): Skip internal options for the moment as probably not
1896+
// required
1897+
if (option_name.empty() || option_name.starts_with('[')) {
1898+
continue;
1899+
}
1900+
1901+
// Skip V8 options and NoOp options - only Node.js-specific options
1902+
if (option_info.type == kNoOp || option_info.type == kV8Option) {
1903+
continue;
1904+
}
1905+
1906+
switch (option_info.type) {
1907+
case kBoolean: {
1908+
bool current_value = *_ppop_instance.Lookup<bool>(field, opts);
1909+
// For boolean options with default_is_true, we want the opposite logic
1910+
if (option_info.default_is_true) {
1911+
if (!current_value) {
1912+
// If default is true and current is false, add --no-* flag
1913+
flags.push_back("--no-" + option_name.substr(2));
1914+
}
1915+
} else {
1916+
if (current_value) {
1917+
// If default is false and current is true, add --flag
1918+
flags.push_back(option_name);
1919+
}
1920+
}
1921+
break;
1922+
}
1923+
case kInteger: {
1924+
int64_t current_value = *_ppop_instance.Lookup<int64_t>(field, opts);
1925+
flags.push_back(option_name + "=" + std::to_string(current_value));
1926+
break;
1927+
}
1928+
case kUInteger: {
1929+
uint64_t current_value = *_ppop_instance.Lookup<uint64_t>(field, opts);
1930+
flags.push_back(option_name + "=" + std::to_string(current_value));
1931+
break;
1932+
}
1933+
case kString: {
1934+
const std::string& current_value =
1935+
*_ppop_instance.Lookup<std::string>(field, opts);
1936+
// Only include if not empty
1937+
if (!current_value.empty()) {
1938+
flags.push_back(option_name + "=" + current_value);
1939+
}
1940+
break;
1941+
}
1942+
case kStringList: {
1943+
const std::vector<std::string>& current_values =
1944+
*_ppop_instance.Lookup<StringVector>(field, opts);
1945+
// Add each string in the list as a separate flag
1946+
for (const std::string& value : current_values) {
1947+
flags.push_back(option_name + "=" + value);
1948+
}
1949+
break;
1950+
}
1951+
case kHostPort: {
1952+
const HostPort& host_port =
1953+
*_ppop_instance.Lookup<HostPort>(field, opts);
1954+
// Only include if host is not empty or port is not default
1955+
if (!host_port.host().empty() || host_port.port() != 0) {
1956+
std::string host_port_str = host_port.host();
1957+
if (host_port.port() != 0) {
1958+
if (!host_port_str.empty()) {
1959+
host_port_str += ":";
1960+
}
1961+
host_port_str += std::to_string(host_port.port());
1962+
}
1963+
if (!host_port_str.empty()) {
1964+
flags.push_back(option_name + "=" + host_port_str);
1965+
}
1966+
}
1967+
break;
1968+
}
1969+
default:
1970+
// Skip unknown types
1971+
break;
1972+
}
1973+
}
1974+
1975+
Local<Value> result;
1976+
CHECK(ToV8Value(context, flags).ToLocal(&result));
1977+
1978+
args.GetReturnValue().Set(result);
1979+
}
1980+
18701981
void Initialize(Local<Object> target,
18711982
Local<Value> unused,
18721983
Local<Context> context,
@@ -1877,6 +1988,8 @@ void Initialize(Local<Object> target,
18771988
context, target, "getCLIOptionsValues", GetCLIOptionsValues);
18781989
SetMethodNoSideEffect(
18791990
context, target, "getCLIOptionsInfo", GetCLIOptionsInfo);
1991+
SetMethodNoSideEffect(
1992+
context, target, "getOptionsAsFlags", GetOptionsAsFlags);
18801993
SetMethodNoSideEffect(
18811994
context, target, "getEmbedderOptions", GetEmbedderOptions);
18821995
SetMethodNoSideEffect(
@@ -1909,6 +2022,7 @@ void Initialize(Local<Object> target,
19092022
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
19102023
registry->Register(GetCLIOptionsValues);
19112024
registry->Register(GetCLIOptionsInfo);
2025+
registry->Register(GetOptionsAsFlags);
19122026
registry->Register(GetEmbedderOptions);
19132027
registry->Register(GetEnvOptionsInputType);
19142028
registry->Register(GetNamespaceOptionsInputType);

β€Žsrc/node_options.hβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,8 @@ class OptionsParser {
649649
friend std::vector<std::string> MapAvailableNamespaces();
650650
friend void GetEnvOptionsInputType(
651651
const v8::FunctionCallbackInfo<v8::Value>& args);
652+
friend void GetOptionsAsFlags(
653+
const v8::FunctionCallbackInfo<v8::Value>& args);
652654
};
653655

654656
using StringVector = std::vector<std::string>;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NODE_OPTIONS=--v8-pool-size=8
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const { getOptionsAsFlagsFromBinding } = require('internal/options');
2+
3+
const flags = getOptionsAsFlagsFromBinding();
4+
console.log(JSON.stringify(flags.sort()));
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"nodeOptions": {
3+
"experimental-transform-types": true,
4+
"max-http-header-size": 8192
5+
},
6+
"testRunner": {
7+
"test-isolation": "none"
8+
}
9+
}

β€Žtest/fixtures/test-runner/flag-propagation/.envβ€Ž

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Empty file used by test/parallel/test-runner-flag-propagation.js

0 commit comments

Comments
Β (0)