Skip to content

Commit c022c1f

Browse files
pmarchinitargos
authored andcommitted
src: add internal GetOptionsAsFlags
PR-URL: #59138 Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent 7880978 commit c022c1f

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
@@ -3949,6 +3967,7 @@ Can be used to abort test subtasks when the test has been aborted.
39493967
[`suite()`]: #suitename-options-fn
39503968
[`test()`]: #testname-options-fn
39513969
[code coverage]: #collecting-code-coverage
3970+
[configuration files]: cli.md#--experimental-config-fileconfig
39523971
[describe options]: #describename-options-fn
39533972
[it options]: #testname-options-fn
39543973
[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
@@ -1864,6 +1864,117 @@ void GetNamespaceOptionsInputType(const FunctionCallbackInfo<Value>& args) {
18641864
args.GetReturnValue().Set(namespaces_map);
18651865
}
18661866

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

src/node_options.h

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

653655
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)