src: support namespace options in configuration file
PR-URL: https://github.com/nodejs/node/pull/58073 Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Giovanni Bucci <github@puskin.it> Reviewed-By: Daniel Lemire <daniel@lemire.me>
This commit is contained in:
parent
708fd1945b
commit
c1f090dc76
@ -933,11 +933,19 @@ in the `$schema` must be replaced with the version of Node.js you are using.
|
||||
],
|
||||
"watch-path": "src",
|
||||
"watch-preserve-output": true
|
||||
},
|
||||
"testRunner": {
|
||||
"test-isolation": "process"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the `nodeOptions` field, only flags that are allowed in [`NODE_OPTIONS`][] are supported.
|
||||
The configuration file supports namespace-specific options:
|
||||
|
||||
* The `nodeOptions` field contains CLI flags that are allowed in [`NODE_OPTIONS`][].
|
||||
|
||||
* Namespace fields like `testRunner` contain configuration specific to that subsystem.
|
||||
|
||||
No-op flags are not supported.
|
||||
Not all V8 flags are currently supported.
|
||||
|
||||
@ -951,7 +959,7 @@ For example, the configuration file above is equivalent to
|
||||
the following command-line arguments:
|
||||
|
||||
```bash
|
||||
node --import amaro/strip --watch-path=src --watch-preserve-output
|
||||
node --import amaro/strip --watch-path=src --watch-preserve-output --test-isolation=process
|
||||
```
|
||||
|
||||
The priority in configuration is as follows:
|
||||
@ -964,11 +972,10 @@ Values in the configuration file will not override the values in the environment
|
||||
variables and command-line options, but will override the values in the `NODE_OPTIONS`
|
||||
env file parsed by the `--env-file` flag.
|
||||
|
||||
If duplicate keys are present in the configuration file, only
|
||||
the first key will be used.
|
||||
Keys cannot be duplicated within the same or different namespaces.
|
||||
|
||||
The configuration parser will throw an error if the configuration file contains
|
||||
unknown keys or keys that cannot used in `NODE_OPTIONS`.
|
||||
unknown keys or keys that cannot be used in a namespace.
|
||||
|
||||
Node.js will not sanitize or perform validation on the user-provided configuration,
|
||||
so **NEVER** use untrusted configuration files.
|
||||
|
@ -584,6 +584,135 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"testRunner": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"experimental-test-coverage": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"experimental-test-module-mocks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"test-concurrency": {
|
||||
"type": "number"
|
||||
},
|
||||
"test-coverage-branches": {
|
||||
"type": "number"
|
||||
},
|
||||
"test-coverage-exclude": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minItems": 1
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test-coverage-functions": {
|
||||
"type": "number"
|
||||
},
|
||||
"test-coverage-include": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minItems": 1
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test-coverage-lines": {
|
||||
"type": "number"
|
||||
},
|
||||
"test-force-exit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"test-global-setup": {
|
||||
"type": "string"
|
||||
},
|
||||
"test-isolation": {
|
||||
"type": "string"
|
||||
},
|
||||
"test-name-pattern": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minItems": 1
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test-only": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"test-reporter": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minItems": 1
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test-reporter-destination": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minItems": 1
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test-shard": {
|
||||
"type": "string"
|
||||
},
|
||||
"test-skip-pattern": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minItems": 1
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test-timeout": {
|
||||
"type": "number"
|
||||
},
|
||||
"test-update-snapshots": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
@ -13,6 +13,7 @@ const {
|
||||
getCLIOptionsInfo,
|
||||
getEmbedderOptions: getEmbedderOptionsFromBinding,
|
||||
getEnvOptionsInputType,
|
||||
getNamespaceOptionsInputType,
|
||||
} = internalBinding('options');
|
||||
|
||||
let warnOnAllowUnauthorized = true;
|
||||
@ -38,7 +39,22 @@ function getEmbedderOptions() {
|
||||
}
|
||||
|
||||
function generateConfigJsonSchema() {
|
||||
const map = getEnvOptionsInputType();
|
||||
const envOptionsMap = getEnvOptionsInputType();
|
||||
const namespaceOptionsMap = getNamespaceOptionsInputType();
|
||||
|
||||
function createPropertyForType(type) {
|
||||
if (type === 'array') {
|
||||
return {
|
||||
__proto__: null,
|
||||
oneOf: [
|
||||
{ __proto__: null, type: 'string' },
|
||||
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return { __proto__: null, type };
|
||||
}
|
||||
|
||||
const schema = {
|
||||
__proto__: null,
|
||||
@ -60,24 +76,43 @@ function generateConfigJsonSchema() {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
const nodeOptions = schema.properties.nodeOptions.properties;
|
||||
// Get the root properties object for adding namespaces
|
||||
const rootProperties = schema.properties;
|
||||
const nodeOptions = rootProperties.nodeOptions.properties;
|
||||
|
||||
for (const { 0: key, 1: type } of map) {
|
||||
// Add env options to nodeOptions (backward compatibility)
|
||||
for (const { 0: key, 1: type } of envOptionsMap) {
|
||||
const keyWithoutPrefix = StringPrototypeReplace(key, '--', '');
|
||||
if (type === 'array') {
|
||||
nodeOptions[keyWithoutPrefix] = {
|
||||
__proto__: null,
|
||||
oneOf: [
|
||||
{ __proto__: null, type: 'string' },
|
||||
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
nodeOptions[keyWithoutPrefix] = { __proto__: null, type };
|
||||
}
|
||||
nodeOptions[keyWithoutPrefix] = createPropertyForType(type);
|
||||
}
|
||||
|
||||
// Sort the proerties by key alphabetically.
|
||||
// Add namespace properties at the root level
|
||||
for (const { 0: namespace, 1: optionsMap } of namespaceOptionsMap) {
|
||||
// Create namespace object at the root level
|
||||
rootProperties[namespace] = {
|
||||
__proto__: null,
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: { __proto__: null },
|
||||
};
|
||||
|
||||
const namespaceProperties = rootProperties[namespace].properties;
|
||||
|
||||
// Add all options for this namespace
|
||||
for (const { 0: optionName, 1: optionType } of optionsMap) {
|
||||
const keyWithoutPrefix = StringPrototypeReplace(optionName, '--', '');
|
||||
namespaceProperties[keyWithoutPrefix] = createPropertyForType(optionType);
|
||||
}
|
||||
|
||||
// Sort the namespace properties alphabetically
|
||||
const sortedNamespaceKeys = ArrayPrototypeSort(ObjectKeys(namespaceProperties));
|
||||
const sortedNamespaceProperties = ObjectFromEntries(
|
||||
ArrayPrototypeMap(sortedNamespaceKeys, (key) => [key, namespaceProperties[key]]),
|
||||
);
|
||||
rootProperties[namespace].properties = sortedNamespaceProperties;
|
||||
}
|
||||
|
||||
// Sort the top-level properties by key alphabetically
|
||||
const sortedKeys = ArrayPrototypeSort(ObjectKeys(nodeOptions));
|
||||
const sortedProperties = ObjectFromEntries(
|
||||
ArrayPrototypeMap(sortedKeys, (key) => [key, nodeOptions[key]]),
|
||||
@ -85,6 +120,14 @@ function generateConfigJsonSchema() {
|
||||
|
||||
schema.properties.nodeOptions.properties = sortedProperties;
|
||||
|
||||
// Also sort the root level properties
|
||||
const sortedRootKeys = ArrayPrototypeSort(ObjectKeys(rootProperties));
|
||||
const sortedRootProperties = ObjectFromEntries(
|
||||
ArrayPrototypeMap(sortedRootKeys, (key) => [key, rootProperties[key]]),
|
||||
);
|
||||
|
||||
schema.properties = sortedRootProperties;
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,13 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
|
||||
});
|
||||
|
||||
const kIsolatedProcessName = Symbol('kIsolatedProcessName');
|
||||
const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch'];
|
||||
const kFilterArgs = [
|
||||
'--test',
|
||||
'--experimental-test-coverage',
|
||||
'--watch',
|
||||
'--experimental-default-config-file',
|
||||
'--experimental-config-file',
|
||||
];
|
||||
const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
|
||||
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];
|
||||
|
||||
|
16
src/node.cc
16
src/node.cc
@ -910,7 +910,7 @@ static ExitCode InitializeNodeWithArgsInternal(
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
node_options_from_config = per_process::config_reader.AssignNodeOptions();
|
||||
node_options_from_config = per_process::config_reader.GetNodeOptions();
|
||||
// (@marco-ippolito) Avoid reparsing the env options again
|
||||
std::vector<std::string> env_argv_from_config =
|
||||
ParseNodeOptionsEnvVar(node_options_from_config, errors);
|
||||
@ -956,7 +956,19 @@ static ExitCode InitializeNodeWithArgsInternal(
|
||||
#endif
|
||||
|
||||
if (!(flags & ProcessInitializationFlags::kDisableCLIOptions)) {
|
||||
const ExitCode exit_code =
|
||||
// Parse the options coming from the config file.
|
||||
// This is done before parsing the command line options
|
||||
// as the cli flags are expected to override the config file ones.
|
||||
std::vector<std::string> extra_argv =
|
||||
per_process::config_reader.GetNamespaceFlags();
|
||||
// [0] is expected to be the program name, fill it in from the real argv.
|
||||
extra_argv.insert(extra_argv.begin(), argv->at(0));
|
||||
// Parse the extra argv coming from the config file
|
||||
ExitCode exit_code = ProcessGlobalArgsInternal(
|
||||
&extra_argv, nullptr, errors, kDisallowedInEnvvar);
|
||||
if (exit_code != ExitCode::kNoFailure) return exit_code;
|
||||
// Parse options coming from the command line.
|
||||
exit_code =
|
||||
ProcessGlobalArgsInternal(argv, exec_argv, errors, kDisallowedInEnvvar);
|
||||
if (exit_code != ExitCode::kNoFailure) return exit_code;
|
||||
}
|
||||
|
@ -37,121 +37,171 @@ std::optional<std::string_view> ConfigReader::GetDataFromArgs(
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ParseResult ConfigReader::ParseNodeOptions(
|
||||
simdjson::ondemand::object* node_options_object) {
|
||||
auto env_options_map = options_parser::MapEnvOptionsFlagInputType();
|
||||
simdjson::ondemand::value ondemand_value;
|
||||
std::string_view key;
|
||||
ParseResult ConfigReader::ProcessOptionValue(
|
||||
const std::pair<std::string, options_parser::OptionType>& option_info,
|
||||
simdjson::ondemand::value* option_value,
|
||||
std::vector<std::string>* output) {
|
||||
const std::string& option_name = option_info.first;
|
||||
const options_parser::OptionType option_type = option_info.second;
|
||||
|
||||
for (auto field : *node_options_object) {
|
||||
if (field.unescaped_key().get(key) || field.value().get(ondemand_value)) {
|
||||
switch (option_type) {
|
||||
case options_parser::OptionType::kBoolean: {
|
||||
bool result;
|
||||
if (option_value->get_bool().get(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
// If the value is true, we need to set the flag
|
||||
output->push_back(option_name);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// String array can allow both string and array types
|
||||
case options_parser::OptionType::kStringList: {
|
||||
simdjson::ondemand::json_type field_type;
|
||||
if (option_value->type().get(field_type)) {
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
switch (field_type) {
|
||||
case simdjson::ondemand::json_type::array: {
|
||||
std::vector<std::string> result;
|
||||
simdjson::ondemand::array raw_imports;
|
||||
if (option_value->get_array().get(raw_imports)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
for (auto raw_import : raw_imports) {
|
||||
std::string_view import;
|
||||
if (raw_import.get_string(import)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
output->push_back(option_name + "=" + std::string(import));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case simdjson::ondemand::json_type::string: {
|
||||
std::string result;
|
||||
if (option_value->get_string(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
output->push_back(option_name + "=" + result);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kString: {
|
||||
std::string result;
|
||||
if (option_value->get_string(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
output->push_back(option_name + "=" + result);
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kInteger: {
|
||||
int64_t result;
|
||||
if (option_value->get_int64().get(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
output->push_back(option_name + "=" + std::to_string(result));
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kHostPort:
|
||||
case options_parser::OptionType::kUInteger: {
|
||||
uint64_t result;
|
||||
if (option_value->get_uint64().get(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
output->push_back(option_name + "=" + std::to_string(result));
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kNoOp: {
|
||||
FPrintF(stderr,
|
||||
"No-op flag %s is currently not supported\n",
|
||||
option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kV8Option: {
|
||||
FPrintF(stderr,
|
||||
"V8 flag %s is currently not supported\n",
|
||||
option_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
return ParseResult::Valid;
|
||||
}
|
||||
|
||||
ParseResult ConfigReader::ParseOptions(
|
||||
simdjson::ondemand::object* options_object,
|
||||
std::unordered_set<std::string>* unique_options,
|
||||
const std::string& namespace_name) {
|
||||
// Determine which options map to use and output vector
|
||||
std::unordered_map<std::string, options_parser::OptionType> options_map;
|
||||
std::vector<std::string>* output_vector;
|
||||
|
||||
if (namespace_name == "nodeOptions") {
|
||||
// Special case for backward compatibility: handle nodeOptions with env
|
||||
// options map
|
||||
options_map = options_parser::MapEnvOptionsFlagInputType();
|
||||
output_vector = &node_options_;
|
||||
} else {
|
||||
// Handle other namespaces
|
||||
options_map = options_parser::MapOptionsByNamespace(namespace_name);
|
||||
output_vector = &namespace_options_;
|
||||
|
||||
if (!env_options_initialized_) {
|
||||
env_options_map_ = options_parser::MapEnvOptionsFlagInputType();
|
||||
env_options_initialized_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
simdjson::ondemand::value option_value;
|
||||
std::string_view option_key;
|
||||
|
||||
for (auto field : *options_object) {
|
||||
if (field.unescaped_key().get(option_key) ||
|
||||
field.value().get(option_value)) {
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
|
||||
// The key needs to match the CLI option
|
||||
std::string prefix = "--";
|
||||
auto it = env_options_map.find(prefix.append(key));
|
||||
if (it != env_options_map.end()) {
|
||||
switch (it->second) {
|
||||
case options_parser::OptionType::kBoolean: {
|
||||
bool result;
|
||||
if (ondemand_value.get_bool().get(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
// If the value is true, we need to set the flag
|
||||
node_options_.push_back(it->first);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// String array can allow both string and array types
|
||||
case options_parser::OptionType::kStringList: {
|
||||
simdjson::ondemand::json_type field_type;
|
||||
if (ondemand_value.type().get(field_type)) {
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
switch (field_type) {
|
||||
case simdjson::ondemand::json_type::array: {
|
||||
std::vector<std::string> result;
|
||||
simdjson::ondemand::array raw_imports;
|
||||
if (ondemand_value.get_array().get(raw_imports)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
for (auto raw_import : raw_imports) {
|
||||
std::string_view import;
|
||||
if (raw_import.get_string(import)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
node_options_.push_back(it->first + "=" + std::string(import));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case simdjson::ondemand::json_type::string: {
|
||||
std::string result;
|
||||
if (ondemand_value.get_string(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
node_options_.push_back(it->first + "=" + result);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kString: {
|
||||
std::string result;
|
||||
if (ondemand_value.get_string(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
node_options_.push_back(it->first + "=" + result);
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kInteger: {
|
||||
int64_t result;
|
||||
if (ondemand_value.get_int64().get(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
node_options_.push_back(it->first + "=" + std::to_string(result));
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kHostPort:
|
||||
case options_parser::OptionType::kUInteger: {
|
||||
uint64_t result;
|
||||
if (ondemand_value.get_uint64().get(result)) {
|
||||
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
node_options_.push_back(it->first + "=" + std::to_string(result));
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kNoOp: {
|
||||
FPrintF(stderr,
|
||||
"No-op flag %s is currently not supported\n",
|
||||
it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
break;
|
||||
}
|
||||
case options_parser::OptionType::kV8Option: {
|
||||
FPrintF(stderr,
|
||||
"V8 flag %s is currently not supported\n",
|
||||
it->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE();
|
||||
auto option = options_map.find(prefix.append(option_key));
|
||||
if (option != options_map.end()) {
|
||||
// If the option has already been set, return an error
|
||||
if (unique_options->contains(option->first)) {
|
||||
FPrintF(
|
||||
stderr, "Option %s is already defined\n", option->first.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
// Add the option to the unique set to prevent duplicates
|
||||
// on future iterations
|
||||
unique_options->insert(option->first);
|
||||
// Process the option value based on its type
|
||||
ParseResult result =
|
||||
ProcessOptionValue(*option, &option_value, output_vector);
|
||||
if (result != ParseResult::Valid) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
FPrintF(stderr, "Unknown or not allowed option %s\n", key.data());
|
||||
FPrintF(stderr,
|
||||
"Unknown or not allowed option %s for namespace %s\n",
|
||||
option_key.data(),
|
||||
namespace_name.c_str());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
}
|
||||
@ -177,9 +227,10 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
|
||||
// Validate config is an object
|
||||
simdjson::ondemand::object main_object;
|
||||
// If document is not an object, throw an error.
|
||||
if (auto root_error = document.get_object().get(main_object)) {
|
||||
auto root_error = document.get_object().get(main_object);
|
||||
if (root_error) {
|
||||
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
|
||||
FPrintF(stderr,
|
||||
"Root value unexpected not an object for %s\n\n",
|
||||
@ -190,36 +241,67 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
|
||||
simdjson::ondemand::object node_options_object;
|
||||
// If "nodeOptions" is an object, parse it
|
||||
if (auto node_options_error =
|
||||
main_object["nodeOptions"].get_object().get(node_options_object)) {
|
||||
if (node_options_error != simdjson::error_code::NO_SUCH_FIELD) {
|
||||
// Get all available namespaces for validation
|
||||
std::vector<std::string> available_namespaces =
|
||||
options_parser::MapAvailableNamespaces();
|
||||
// Add "nodeOptions" as a special case for backward compatibility
|
||||
available_namespaces.emplace_back("nodeOptions");
|
||||
|
||||
// Create a set for faster lookup of valid namespaces
|
||||
std::unordered_set<std::string> valid_namespaces(available_namespaces.begin(),
|
||||
available_namespaces.end());
|
||||
// Create a set to track unique options
|
||||
std::unordered_set<std::string> unique_options;
|
||||
// Iterate through the main object to find all namespaces
|
||||
for (auto field : main_object) {
|
||||
std::string_view field_name;
|
||||
if (field.unescaped_key().get(field_name)) {
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
|
||||
// Check if this field is a valid namespace
|
||||
std::string namespace_name(field_name);
|
||||
if (!valid_namespaces.contains(namespace_name)) {
|
||||
// If not, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the namespace object
|
||||
simdjson::ondemand::object namespace_object;
|
||||
auto field_error = field.value().get_object().get(namespace_object);
|
||||
|
||||
// If namespace value is not an object
|
||||
if (field_error) {
|
||||
FPrintF(stderr,
|
||||
"\"nodeOptions\" value unexpected for %s\n\n",
|
||||
"\"%s\" value unexpected for %s (should be an object)\n",
|
||||
namespace_name.c_str(),
|
||||
config_path.data());
|
||||
return ParseResult::InvalidContent;
|
||||
}
|
||||
} else {
|
||||
return ParseNodeOptions(&node_options_object);
|
||||
|
||||
// Process options for this namespace using the unified method
|
||||
ParseResult result =
|
||||
ParseOptions(&namespace_object, &unique_options, namespace_name);
|
||||
if (result != ParseResult::Valid) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return ParseResult::Valid;
|
||||
}
|
||||
|
||||
std::string ConfigReader::AssignNodeOptions() {
|
||||
if (node_options_.empty()) {
|
||||
return "";
|
||||
} else {
|
||||
DCHECK_GT(node_options_.size(), 0);
|
||||
std::string acc;
|
||||
acc.reserve(node_options_.size() * 2);
|
||||
for (size_t i = 0; i < node_options_.size(); ++i) {
|
||||
// The space is necessary at the beginning of the string
|
||||
acc += " " + node_options_[i];
|
||||
}
|
||||
return acc;
|
||||
std::string ConfigReader::GetNodeOptions() {
|
||||
std::string acc = "";
|
||||
const size_t total_options = node_options_.size();
|
||||
acc.reserve(total_options * 2);
|
||||
for (auto& opt : node_options_) {
|
||||
acc += " " + opt;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& ConfigReader::GetNamespaceFlags() const {
|
||||
return namespace_options_;
|
||||
}
|
||||
|
||||
size_t ConfigReader::GetFlagsSize() {
|
||||
|
@ -5,7 +5,10 @@
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include "node_internals.h"
|
||||
#include "simdjson.h"
|
||||
#include "util-inl.h"
|
||||
@ -29,14 +32,30 @@ class ConfigReader {
|
||||
std::optional<std::string_view> GetDataFromArgs(
|
||||
const std::vector<std::string>& args);
|
||||
|
||||
std::string AssignNodeOptions();
|
||||
std::string GetNodeOptions();
|
||||
const std::vector<std::string>& GetNamespaceFlags() const;
|
||||
|
||||
size_t GetFlagsSize();
|
||||
|
||||
private:
|
||||
ParseResult ParseNodeOptions(simdjson::ondemand::object* node_options_object);
|
||||
// Parse options for a specific namespace (including nodeOptions for backward
|
||||
// compatibility)
|
||||
ParseResult ParseOptions(simdjson::ondemand::object* options_object,
|
||||
std::unordered_set<std::string>* unique_options,
|
||||
const std::string& namespace_name);
|
||||
|
||||
// Process a single option value based on its type
|
||||
ParseResult ProcessOptionValue(
|
||||
const std::pair<std::string, options_parser::OptionType>& option_info,
|
||||
simdjson::ondemand::value* option_value,
|
||||
std::vector<std::string>* output);
|
||||
|
||||
std::vector<std::string> node_options_;
|
||||
std::vector<std::string> namespace_options_;
|
||||
|
||||
// Cache for fast lookup of environment options
|
||||
std::unordered_map<std::string, options_parser::OptionType> env_options_map_;
|
||||
bool env_options_initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace node
|
||||
|
@ -31,98 +31,128 @@ namespace options_parser {
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
bool Options::* field,
|
||||
bool Options::*field,
|
||||
OptionEnvvarSettings env_setting,
|
||||
bool default_is_true) {
|
||||
bool default_is_true,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(name,
|
||||
OptionInfo{kBoolean,
|
||||
std::make_shared<SimpleOptionField<bool>>(field),
|
||||
env_setting,
|
||||
help_text,
|
||||
default_is_true});
|
||||
default_is_true,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
uint64_t Options::* field,
|
||||
OptionEnvvarSettings env_setting) {
|
||||
uint64_t Options::*field,
|
||||
OptionEnvvarSettings env_setting,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(
|
||||
name,
|
||||
OptionInfo{kUInteger,
|
||||
std::make_shared<SimpleOptionField<uint64_t>>(field),
|
||||
env_setting,
|
||||
help_text});
|
||||
help_text,
|
||||
false,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
int64_t Options::* field,
|
||||
OptionEnvvarSettings env_setting) {
|
||||
int64_t Options::*field,
|
||||
OptionEnvvarSettings env_setting,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(
|
||||
name,
|
||||
OptionInfo{kInteger,
|
||||
std::make_shared<SimpleOptionField<int64_t>>(field),
|
||||
env_setting,
|
||||
help_text});
|
||||
help_text,
|
||||
false,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
std::string Options::* field,
|
||||
OptionEnvvarSettings env_setting) {
|
||||
std::string Options::*field,
|
||||
OptionEnvvarSettings env_setting,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(
|
||||
name,
|
||||
OptionInfo{kString,
|
||||
std::make_shared<SimpleOptionField<std::string>>(field),
|
||||
env_setting,
|
||||
help_text});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
std::vector<std::string> Options::* field,
|
||||
OptionEnvvarSettings env_setting) {
|
||||
options_.emplace(name, OptionInfo {
|
||||
kStringList,
|
||||
std::make_shared<SimpleOptionField<std::vector<std::string>>>(field),
|
||||
env_setting,
|
||||
help_text
|
||||
});
|
||||
help_text,
|
||||
false,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
HostPort Options::* field,
|
||||
OptionEnvvarSettings env_setting) {
|
||||
std::vector<std::string> Options::*field,
|
||||
OptionEnvvarSettings env_setting,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(
|
||||
name,
|
||||
OptionInfo{
|
||||
kStringList,
|
||||
std::make_shared<SimpleOptionField<std::vector<std::string>>>(field),
|
||||
env_setting,
|
||||
help_text,
|
||||
false,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
HostPort Options::*field,
|
||||
OptionEnvvarSettings env_setting,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(
|
||||
name,
|
||||
OptionInfo{kHostPort,
|
||||
std::make_shared<SimpleOptionField<HostPort>>(field),
|
||||
env_setting,
|
||||
help_text});
|
||||
help_text,
|
||||
false,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
NoOp no_op_tag,
|
||||
OptionEnvvarSettings env_setting) {
|
||||
options_.emplace(name, OptionInfo{kNoOp, nullptr, env_setting, help_text});
|
||||
OptionEnvvarSettings env_setting,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(name,
|
||||
OptionInfo{kNoOp,
|
||||
nullptr,
|
||||
env_setting,
|
||||
help_text,
|
||||
false,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
void OptionsParser<Options>::AddOption(const char* name,
|
||||
const char* help_text,
|
||||
V8Option v8_option_tag,
|
||||
OptionEnvvarSettings env_setting) {
|
||||
OptionEnvvarSettings env_setting,
|
||||
OptionNamespaces namespace_id) {
|
||||
options_.emplace(name,
|
||||
OptionInfo{kV8Option, nullptr, env_setting, help_text});
|
||||
OptionInfo{kV8Option,
|
||||
nullptr,
|
||||
env_setting,
|
||||
help_text,
|
||||
false,
|
||||
NamespaceEnumToString(namespace_id)});
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
@ -198,7 +228,8 @@ auto OptionsParser<Options>::Convert(
|
||||
Convert(original.field, get_child),
|
||||
original.env_setting,
|
||||
original.help_text,
|
||||
original.default_is_true};
|
||||
original.default_is_true,
|
||||
original.namespace_id};
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
|
@ -17,6 +17,7 @@
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using v8::Boolean;
|
||||
using v8::Context;
|
||||
@ -244,6 +245,64 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
|
||||
|
||||
namespace options_parser {
|
||||
|
||||
// Helper function to convert option types to their string representation
|
||||
// and add them to a V8 Map
|
||||
static bool AddOptionTypeToMap(Isolate* isolate,
|
||||
Local<Context> context,
|
||||
Local<Map> map,
|
||||
const std::string& option_name,
|
||||
const OptionType& option_type) {
|
||||
std::string type;
|
||||
switch (static_cast<int>(option_type)) {
|
||||
case 0: // No-op
|
||||
case 1: // V8 flags
|
||||
break; // V8 and NoOp flags are not supported
|
||||
|
||||
case 2:
|
||||
type = "boolean";
|
||||
break;
|
||||
case 3: // integer
|
||||
case 4: // unsigned integer
|
||||
case 6: // host port
|
||||
type = "number";
|
||||
break;
|
||||
case 5: // string
|
||||
type = "string";
|
||||
break;
|
||||
case 7: // string array
|
||||
type = "array";
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
if (type.empty()) {
|
||||
return true; // Skip this entry but continue processing
|
||||
}
|
||||
|
||||
Local<String> option_key;
|
||||
if (!String::NewFromUtf8(isolate,
|
||||
option_name.data(),
|
||||
v8::NewStringType::kNormal,
|
||||
option_name.size())
|
||||
.ToLocal(&option_key)) {
|
||||
return true; // Skip this entry but continue processing
|
||||
}
|
||||
|
||||
Local<String> type_value;
|
||||
if (!String::NewFromUtf8(
|
||||
isolate, type.data(), v8::NewStringType::kNormal, type.size())
|
||||
.ToLocal(&type_value)) {
|
||||
return true; // Skip this entry but continue processing
|
||||
}
|
||||
|
||||
if (map->Set(context, option_key, type_value).IsEmpty()) {
|
||||
return false; // Error occurred, stop processing
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class DebugOptionsParser : public OptionsParser<DebugOptions> {
|
||||
public:
|
||||
DebugOptionsParser();
|
||||
@ -697,82 +756,119 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||
&EnvironmentOptions::experimental_default_config_file);
|
||||
AddOption("--test",
|
||||
"launch test runner on startup",
|
||||
&EnvironmentOptions::test_runner);
|
||||
&EnvironmentOptions::test_runner,
|
||||
kDisallowedInEnvvar);
|
||||
AddOption("--test-concurrency",
|
||||
"specify test runner concurrency",
|
||||
&EnvironmentOptions::test_runner_concurrency);
|
||||
&EnvironmentOptions::test_runner_concurrency,
|
||||
kDisallowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-force-exit",
|
||||
"force test runner to exit upon completion",
|
||||
&EnvironmentOptions::test_runner_force_exit);
|
||||
&EnvironmentOptions::test_runner_force_exit,
|
||||
kDisallowedInEnvvar,
|
||||
false,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-timeout",
|
||||
"specify test runner timeout",
|
||||
&EnvironmentOptions::test_runner_timeout);
|
||||
&EnvironmentOptions::test_runner_timeout,
|
||||
kDisallowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-update-snapshots",
|
||||
"regenerate test snapshots",
|
||||
&EnvironmentOptions::test_runner_update_snapshots);
|
||||
&EnvironmentOptions::test_runner_update_snapshots,
|
||||
kDisallowedInEnvvar,
|
||||
false,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--experimental-test-coverage",
|
||||
"enable code coverage in the test runner",
|
||||
&EnvironmentOptions::test_runner_coverage);
|
||||
&EnvironmentOptions::test_runner_coverage,
|
||||
kDisallowedInEnvvar,
|
||||
false,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-coverage-branches",
|
||||
"the branch coverage minimum threshold",
|
||||
&EnvironmentOptions::test_coverage_branches,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-coverage-functions",
|
||||
"the function coverage minimum threshold",
|
||||
&EnvironmentOptions::test_coverage_functions,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-coverage-lines",
|
||||
"the line coverage minimum threshold",
|
||||
&EnvironmentOptions::test_coverage_lines,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-isolation",
|
||||
"configures the type of test isolation used in the test runner",
|
||||
&EnvironmentOptions::test_isolation,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
// TODO(cjihrig): Remove this alias in a semver major.
|
||||
AddAlias("--experimental-test-isolation", "--test-isolation");
|
||||
AddOption("--experimental-test-module-mocks",
|
||||
"enable module mocking in the test runner",
|
||||
&EnvironmentOptions::test_runner_module_mocks);
|
||||
AddOption("--experimental-test-snapshots", "", NoOp{});
|
||||
&EnvironmentOptions::test_runner_module_mocks,
|
||||
kDisallowedInEnvvar,
|
||||
false,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--experimental-test-snapshots",
|
||||
"",
|
||||
NoOp{},
|
||||
kDisallowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-name-pattern",
|
||||
"run tests whose name matches this regular expression",
|
||||
&EnvironmentOptions::test_name_pattern,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-reporter",
|
||||
"report test output using the given reporter",
|
||||
&EnvironmentOptions::test_reporter,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-reporter-destination",
|
||||
"report given reporter to the given destination",
|
||||
&EnvironmentOptions::test_reporter_destination,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-only",
|
||||
"run tests with 'only' option set",
|
||||
&EnvironmentOptions::test_only,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
false,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-shard",
|
||||
"run test at specific shard",
|
||||
&EnvironmentOptions::test_shard,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-skip-pattern",
|
||||
"run tests whose name do not match this regular expression",
|
||||
&EnvironmentOptions::test_skip_pattern,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-coverage-include",
|
||||
"include files in coverage report that match this glob pattern",
|
||||
&EnvironmentOptions::coverage_include_pattern,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-coverage-exclude",
|
||||
"exclude files from coverage report that match this glob pattern",
|
||||
&EnvironmentOptions::coverage_exclude_pattern,
|
||||
kAllowedInEnvvar);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-global-setup",
|
||||
"specifies the path to the global setup file",
|
||||
&EnvironmentOptions::test_global_setup_path,
|
||||
kAllowedInEnvvar);
|
||||
AddOption("--test-udp-no-try-send", "", // For testing only.
|
||||
&EnvironmentOptions::test_udp_no_try_send);
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kTestRunnerNamespace);
|
||||
AddOption("--test-udp-no-try-send",
|
||||
"", // For testing only.
|
||||
&EnvironmentOptions::test_udp_no_try_send,
|
||||
kDisallowedInEnvvar);
|
||||
AddOption("--throw-deprecation",
|
||||
"throw an exception on deprecations",
|
||||
&EnvironmentOptions::throw_deprecation,
|
||||
@ -1331,6 +1427,49 @@ MapEnvOptionsFlagInputType() {
|
||||
return type_map;
|
||||
}
|
||||
|
||||
std::vector<std::string> MapAvailableNamespaces() {
|
||||
std::vector<std::string> namespaceNames;
|
||||
auto availableNamespaces = AllNamespaces();
|
||||
for (size_t i = 1; i < availableNamespaces.size(); i++) {
|
||||
OptionNamespaces ns = availableNamespaces[i];
|
||||
std::string ns_string = NamespaceEnumToString(ns);
|
||||
if (!ns_string.empty()) {
|
||||
namespaceNames.push_back(ns_string);
|
||||
}
|
||||
}
|
||||
|
||||
return namespaceNames;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, options_parser::OptionType>
|
||||
MapOptionsByNamespace(std::string namespace_name) {
|
||||
std::unordered_map<std::string, options_parser::OptionType> type_map;
|
||||
const auto& parser = _ppop_instance;
|
||||
for (const auto& item : parser.options_) {
|
||||
if (!item.first.empty() && !item.first.starts_with('[') &&
|
||||
item.second.namespace_id == namespace_name) {
|
||||
type_map[item.first] = item.second.type;
|
||||
}
|
||||
}
|
||||
return type_map;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<std::string, options_parser::OptionType>>
|
||||
MapNamespaceOptionsAssociations() {
|
||||
std::vector<std::string> available_namespaces =
|
||||
options_parser::MapAvailableNamespaces();
|
||||
std::unordered_map<
|
||||
std::string,
|
||||
std::unordered_map<std::string, options_parser::OptionType>>
|
||||
namespace_option_mapping;
|
||||
for (const std::string& available_namespace : available_namespaces) {
|
||||
namespace_option_mapping[available_namespace] =
|
||||
options_parser::MapOptionsByNamespace(available_namespace);
|
||||
}
|
||||
return namespace_option_mapping;
|
||||
}
|
||||
|
||||
struct IterateCLIOptionsScope {
|
||||
explicit IterateCLIOptionsScope(Environment* env) {
|
||||
// Temporarily act as if the current Environment's/IsolateData's options
|
||||
@ -1598,51 +1737,8 @@ void GetEnvOptionsInputType(const FunctionCallbackInfo<Value>& args) {
|
||||
for (const auto& item : _ppop_instance.options_) {
|
||||
if (!item.first.empty() && !item.first.starts_with('[') &&
|
||||
item.second.env_setting == kAllowedInEnvvar) {
|
||||
std::string type;
|
||||
switch (static_cast<int>(item.second.type)) {
|
||||
case 0: // No-op
|
||||
case 1: // V8 flags
|
||||
break; // V8 and NoOp flags are not supported
|
||||
|
||||
case 2:
|
||||
type = "boolean";
|
||||
break;
|
||||
case 3: // integer
|
||||
case 4: // unsigned integer
|
||||
case 6: // host port
|
||||
type = "number";
|
||||
break;
|
||||
case 5: // string
|
||||
type = "string";
|
||||
break;
|
||||
case 7: // string array
|
||||
type = "array";
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
if (type.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Local<String> value;
|
||||
if (!String::NewFromUtf8(
|
||||
isolate, type.data(), v8::NewStringType::kNormal, type.size())
|
||||
.ToLocal(&value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Local<String> field;
|
||||
if (!String::NewFromUtf8(isolate,
|
||||
item.first.data(),
|
||||
v8::NewStringType::kNormal,
|
||||
item.first.size())
|
||||
.ToLocal(&field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flags_map->Set(context, field, value).IsEmpty()) {
|
||||
if (!AddOptionTypeToMap(
|
||||
isolate, context, flags_map, item.first, item.second.type)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -1650,6 +1746,64 @@ void GetEnvOptionsInputType(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(flags_map);
|
||||
}
|
||||
|
||||
// This function returns a two-level nested map containing all the available
|
||||
// options grouped by their namespaces along with their input types. This is
|
||||
// used for config file JSON schema generation
|
||||
void GetNamespaceOptionsInputType(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");
|
||||
}
|
||||
|
||||
Mutex::ScopedLock lock(per_process::cli_options_mutex);
|
||||
|
||||
Local<Map> namespaces_map = Map::New(isolate);
|
||||
|
||||
// Get the mapping of namespaces to their options and types
|
||||
auto namespace_options = options_parser::MapNamespaceOptionsAssociations();
|
||||
|
||||
for (const auto& ns_entry : namespace_options) {
|
||||
const std::string& namespace_name = ns_entry.first;
|
||||
const auto& options_map = ns_entry.second;
|
||||
|
||||
Local<Map> options_type_map = Map::New(isolate);
|
||||
|
||||
for (const auto& opt_entry : options_map) {
|
||||
const std::string& option_name = opt_entry.first;
|
||||
const options_parser::OptionType& option_type = opt_entry.second;
|
||||
|
||||
if (!AddOptionTypeToMap(
|
||||
isolate, context, options_type_map, option_name, option_type)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add namespaces that have options
|
||||
if (options_type_map->Size() > 0) {
|
||||
Local<String> namespace_key;
|
||||
if (!String::NewFromUtf8(isolate,
|
||||
namespace_name.data(),
|
||||
v8::NewStringType::kNormal,
|
||||
namespace_name.size())
|
||||
.ToLocal(&namespace_key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (namespaces_map->Set(context, namespace_key, options_type_map)
|
||||
.IsEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args.GetReturnValue().Set(namespaces_map);
|
||||
}
|
||||
|
||||
void Initialize(Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
@ -1664,6 +1818,10 @@ void Initialize(Local<Object> target,
|
||||
context, target, "getEmbedderOptions", GetEmbedderOptions);
|
||||
SetMethodNoSideEffect(
|
||||
context, target, "getEnvOptionsInputType", GetEnvOptionsInputType);
|
||||
SetMethodNoSideEffect(context,
|
||||
target,
|
||||
"getNamespaceOptionsInputType",
|
||||
GetNamespaceOptionsInputType);
|
||||
Local<Object> env_settings = Object::New(isolate);
|
||||
NODE_DEFINE_CONSTANT(env_settings, kAllowedInEnvvar);
|
||||
NODE_DEFINE_CONSTANT(env_settings, kDisallowedInEnvvar);
|
||||
@ -1690,6 +1848,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(GetCLIOptionsInfo);
|
||||
registry->Register(GetEmbedderOptions);
|
||||
registry->Register(GetEnvOptionsInputType);
|
||||
registry->Register(GetNamespaceOptionsInputType);
|
||||
}
|
||||
} // namespace options_parser
|
||||
|
||||
|
@ -380,7 +380,7 @@ class PerProcessOptions : public Options {
|
||||
namespace options_parser {
|
||||
|
||||
HostPort SplitHostPort(const std::string& arg,
|
||||
std::vector<std::string>* errors);
|
||||
std::vector<std::string>* errors);
|
||||
void GetOptions(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
std::string GetBashCompletion();
|
||||
|
||||
@ -395,6 +395,43 @@ enum OptionType {
|
||||
kStringList,
|
||||
};
|
||||
std::unordered_map<std::string, OptionType> MapEnvOptionsFlagInputType();
|
||||
std::unordered_map<std::string, OptionType> MapOptionsByNamespace(
|
||||
std::string namespace_name);
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<std::string, options_parser::OptionType>>
|
||||
MapNamespaceOptionsAssociations();
|
||||
std::vector<std::string> MapAvailableNamespaces();
|
||||
|
||||
// Define all namespace entries
|
||||
#define OPTION_NAMESPACE_LIST(V) \
|
||||
V(kNoNamespace, "") \
|
||||
V(kTestRunnerNamespace, "testRunner")
|
||||
|
||||
enum class OptionNamespaces {
|
||||
#define V(name, _) name,
|
||||
OPTION_NAMESPACE_LIST(V)
|
||||
#undef V
|
||||
};
|
||||
|
||||
inline const std::string NamespaceEnumToString(OptionNamespaces ns) {
|
||||
switch (ns) {
|
||||
#define V(name, string_value) \
|
||||
case OptionNamespaces::name: \
|
||||
return string_value;
|
||||
OPTION_NAMESPACE_LIST(V)
|
||||
#undef V
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr auto AllNamespaces() {
|
||||
return std::array{
|
||||
#define V(name, _) OptionNamespaces::name,
|
||||
OPTION_NAMESPACE_LIST(V)
|
||||
#undef V
|
||||
};
|
||||
}
|
||||
|
||||
template <typename Options>
|
||||
class OptionsParser {
|
||||
@ -413,39 +450,55 @@ class OptionsParser {
|
||||
// default_is_true is only a hint in printing help text, it does not
|
||||
// affect the default value of the option. Set the default value in the
|
||||
// Options struct instead.
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
bool Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
bool default_is_true = false);
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
uint64_t Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
int64_t Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
std::string Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
std::vector<std::string> Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
HostPort Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
NoOp no_op_tag,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
|
||||
void AddOption(const char* name,
|
||||
const char* help_text,
|
||||
V8Option v8_option_tag,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
bool Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
bool default_is_true = false,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
uint64_t Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
int64_t Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
std::string Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
std::vector<std::string> Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
HostPort Options::*field,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
NoOp no_op_tag,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
void AddOption(
|
||||
const char* name,
|
||||
const char* help_text,
|
||||
V8Option v8_option_tag,
|
||||
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
|
||||
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
|
||||
|
||||
// Adds aliases. An alias can be of the form "--option-a" -> "--option-b",
|
||||
// or have a more complex group expansion, like
|
||||
@ -535,12 +588,15 @@ class OptionsParser {
|
||||
// - A type.
|
||||
// - A way to store/access the property value.
|
||||
// - The information of whether it may occur in an env var or not.
|
||||
// - A default value (if applicable).
|
||||
// - A namespace ID (optional) to allow for namespacing of options.
|
||||
struct OptionInfo {
|
||||
OptionType type;
|
||||
std::shared_ptr<BaseOptionField> field;
|
||||
OptionEnvvarSettings env_setting;
|
||||
std::string help_text;
|
||||
bool default_is_true = false;
|
||||
std::string namespace_id;
|
||||
};
|
||||
|
||||
// An implied option is composed of the information on where to store a
|
||||
@ -581,6 +637,9 @@ class OptionsParser {
|
||||
friend std::string GetBashCompletion();
|
||||
friend std::unordered_map<std::string, OptionType>
|
||||
MapEnvOptionsFlagInputType();
|
||||
friend std::unordered_map<std::string, OptionType> MapOptionsByNamespace(
|
||||
std::string namespace_name);
|
||||
friend std::vector<std::string> MapAvailableNamespaces();
|
||||
friend void GetEnvOptionsInputType(
|
||||
const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
};
|
||||
|
6
test/fixtures/rc/duplicate-namespace-option/node.config.json
vendored
Normal file
6
test/fixtures/rc/duplicate-namespace-option/node.config.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"testRunner": {
|
||||
"test-name-pattern": "first-pattern",
|
||||
"test-name-pattern": "second-pattern"
|
||||
}
|
||||
}
|
3
test/fixtures/rc/empty-valid-namespace.json
vendored
Normal file
3
test/fixtures/rc/empty-valid-namespace.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"testRunner": {}
|
||||
}
|
5
test/fixtures/rc/namespace-with-array.json
vendored
Normal file
5
test/fixtures/rc/namespace-with-array.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"testRunner": {
|
||||
"test-coverage-exclude": ["config-pattern1", "config-pattern2"]
|
||||
}
|
||||
}
|
6
test/fixtures/rc/namespace-with-disallowed-envvar.json
vendored
Normal file
6
test/fixtures/rc/namespace-with-disallowed-envvar.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"testRunner": {
|
||||
"test-concurrency": 1,
|
||||
"experimental-test-coverage": true
|
||||
}
|
||||
}
|
5
test/fixtures/rc/namespaced/node.config.json
vendored
Normal file
5
test/fixtures/rc/namespaced/node.config.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"testRunner": {
|
||||
"test-isolation": "none"
|
||||
}
|
||||
}
|
8
test/fixtures/rc/override-namespace.json
vendored
Normal file
8
test/fixtures/rc/override-namespace.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"testRunner": {
|
||||
"test-isolation": "process"
|
||||
},
|
||||
"nodeOptions": {
|
||||
"test-isolation": "none"
|
||||
}
|
||||
}
|
8
test/fixtures/rc/override-node-option-with-namespace.json
vendored
Normal file
8
test/fixtures/rc/override-node-option-with-namespace.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"nodeOptions": {
|
||||
"test-isolation": "none"
|
||||
},
|
||||
"testRunner": {
|
||||
"test-isolation": "process"
|
||||
}
|
||||
}
|
5
test/fixtures/rc/unknown-flag-namespace.json
vendored
Normal file
5
test/fixtures/rc/unknown-flag-namespace.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"testRunner": {
|
||||
"unknown-flag": true
|
||||
}
|
||||
}
|
5
test/fixtures/rc/unknown-namespace.json
vendored
Normal file
5
test/fixtures/rc/unknown-namespace.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"an-invalid-namespace": {
|
||||
"a-key": "a-value"
|
||||
}
|
||||
}
|
@ -3,8 +3,8 @@
|
||||
const { spawnPromisified, skipIfSQLiteMissing } = require('../common');
|
||||
skipIfSQLiteMissing();
|
||||
const fixtures = require('../common/fixtures');
|
||||
const { match, strictEqual } = require('node:assert');
|
||||
const { test } = require('node:test');
|
||||
const { match, strictEqual, deepStrictEqual } = require('node:assert');
|
||||
const { test, it, describe } = require('node:test');
|
||||
const { chmodSync, constants } = require('node:fs');
|
||||
const common = require('../common');
|
||||
|
||||
@ -55,18 +55,19 @@ test('should parse boolean flag', async () => {
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
|
||||
test('should not override a flag declared twice', async () => {
|
||||
test('should throw an error when a flag is declared twice', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/override-property.json'),
|
||||
fixtures.path('typescript/ts/transformation/test-enum.ts'),
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
strictEqual(result.stdout, 'Hello, TypeScript!\n');
|
||||
strictEqual(result.code, 0);
|
||||
match(result.stderr, /Option --experimental-transform-types is already defined/);
|
||||
strictEqual(result.stdout, '');
|
||||
strictEqual(result.code, 9);
|
||||
});
|
||||
|
||||
|
||||
test('should override env-file', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
@ -97,7 +98,7 @@ test('should not override NODE_OPTIONS', async () => {
|
||||
strictEqual(result.code, 1);
|
||||
});
|
||||
|
||||
test('should not ovverride CLI flags', async () => {
|
||||
test('should not override CLI flags', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--no-experimental-transform-types',
|
||||
@ -375,3 +376,147 @@ test('should throw an error when the file is non readable', { skip: common.isWin
|
||||
chmodSync(fixtures.path('rc/non-readable/node.config.json'),
|
||||
constants.S_IRWXU | constants.S_IRWXG | constants.S_IRWXO);
|
||||
});
|
||||
|
||||
describe('namespace-scoped options', () => {
|
||||
it('should parse a namespace-scoped option correctly', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--expose-internals',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/namespaced/node.config.json'),
|
||||
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
strictEqual(result.stdout, 'none\n');
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
|
||||
it('should throw an error when a namespace-scoped option is not recognised', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/unknown-flag-namespace.json'),
|
||||
'-p', '"Hello, World!"',
|
||||
]);
|
||||
match(result.stderr, /Unknown or not allowed option unknown-flag/);
|
||||
strictEqual(result.stdout, '');
|
||||
strictEqual(result.code, 9);
|
||||
});
|
||||
|
||||
it('should not throw an error when a namespace is not recognised', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/unknown-namespace.json'),
|
||||
'-p', '"Hello, World!"',
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
strictEqual(result.stdout, 'Hello, World!\n');
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
|
||||
it('should handle an empty namespace valid namespace', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/empty-valid-namespace.json'),
|
||||
'-p', '"Hello, World!"',
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
strictEqual(result.stdout, 'Hello, World!\n');
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
|
||||
it('should throw an error if a namespace-scoped option has already been set in node options', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--expose-internals',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/override-node-option-with-namespace.json'),
|
||||
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
|
||||
]);
|
||||
match(result.stderr, /Option --test-isolation is already defined/);
|
||||
strictEqual(result.stdout, '');
|
||||
strictEqual(result.code, 9);
|
||||
});
|
||||
|
||||
it('should throw an error if a node option has already been set in a namespace-scoped option', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--expose-internals',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/override-namespace.json'),
|
||||
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
|
||||
]);
|
||||
match(result.stderr, /Option --test-isolation is already defined/);
|
||||
strictEqual(result.stdout, '');
|
||||
strictEqual(result.code, 9);
|
||||
});
|
||||
|
||||
it('should prioritise CLI namespace-scoped options over config file options', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--expose-internals',
|
||||
'--test-isolation', 'process',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/namespaced/node.config.json'),
|
||||
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
strictEqual(result.stdout, 'process\n');
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
|
||||
it('should append namespace-scoped config file options with CLI options in case of array', async () => {
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--expose-internals',
|
||||
'--test-coverage-exclude', 'cli-pattern1',
|
||||
'--test-coverage-exclude', 'cli-pattern2',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/namespace-with-array.json'),
|
||||
'-p', 'JSON.stringify(require("internal/options").getOptionValue("--test-coverage-exclude"))',
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
const excludePatterns = JSON.parse(result.stdout);
|
||||
const expected = [
|
||||
'config-pattern1',
|
||||
'config-pattern2',
|
||||
'cli-pattern1',
|
||||
'cli-pattern2',
|
||||
];
|
||||
deepStrictEqual(excludePatterns, expected);
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
|
||||
it('should allow setting kDisallowedInEnvvar in the config file if part of a namespace', async () => {
|
||||
// This test assumes that the --test-concurrency flag is configured as kDisallowedInEnvVar
|
||||
// and that it is part of at least one namespace.
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--expose-internals',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
|
||||
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
strictEqual(result.stdout, '1\n');
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
|
||||
it('should override namespace-scoped config file options with CLI options', async () => {
|
||||
// This test assumes that the --test-concurrency flag is configured as kDisallowedInEnvVar
|
||||
// and that it is part of at least one namespace.
|
||||
const result = await spawnPromisified(process.execPath, [
|
||||
'--no-warnings',
|
||||
'--expose-internals',
|
||||
'--test-concurrency', '2',
|
||||
'--experimental-config-file',
|
||||
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
|
||||
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
|
||||
]);
|
||||
strictEqual(result.stderr, '');
|
||||
strictEqual(result.stdout, '2\n');
|
||||
strictEqual(result.code, 0);
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user