diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 1648957888096d..031bf59b9242d6 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -584,6 +584,138 @@ } }, "type": "object" + }, + "test_runner": { + "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-udp-no-try-send": { + "type": "boolean" + }, + "test-update-snapshots": { + "type": "boolean" + } + } } }, "type": "object" diff --git a/lib/internal/options.js b/lib/internal/options.js index 12548ac49a2aff..55f027c83242e1 100644 --- a/lib/internal/options.js +++ b/lib/internal/options.js @@ -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(key, 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(key, type); + } + + // 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(optionName, 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 proerties by key alphabetically. + // 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; } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 4c2949f4158ab1..e8a1f971012c72 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -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']; diff --git a/src/node.cc b/src/node.cc index f921303b639dbc..9fba1def661377 100644 --- a/src/node.cc +++ b/src/node.cc @@ -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 extra_argv = + per_process::config_reader.AssignNodeNonEnvOptions(); + // [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; } diff --git a/src/node_config_file.cc b/src/node_config_file.cc index e697002ae1a2e0..01bce055390c83 100644 --- a/src/node_config_file.cc +++ b/src/node_config_file.cc @@ -37,121 +37,203 @@ std::optional 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; - - for (auto field : *node_options_object) { - if (field.unescaped_key().get(key) || field.value().get(ondemand_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; - } +ParseResult ConfigReader::ProcessOptionValue( + const std::string_view& key, + const std::string& option_name, + simdjson::ondemand::value& ondemand_value, + options_parser::OptionType option_type, + std::vector* output, + std::unordered_set* unique_options = nullptr) { + switch (option_type) { + case options_parser::OptionType::kBoolean: { + bool result; + if (ondemand_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 - node_options_.push_back(it->first); - } + if (result) { + // If the value is true, we need to set the flag + node_options_.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 (ondemand_value.type().get(field_type)) { + 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 result; + simdjson::ondemand::array raw_imports; + if (ondemand_value.get_array().get(raw_imports)) { + FPrintF(stderr, "Invalid value for %s\n", option_name.c_str()); return ParseResult::InvalidContent; } - switch (field_type) { - case simdjson::ondemand::json_type::array: { - std::vector 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()); + 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 options_parser::OptionType::kString: { + 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; - } - 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()); + FPrintF(stderr, "Invalid value for %s\n", option_name.c_str()); return ParseResult::InvalidContent; } - node_options_.push_back(it->first + "=" + std::to_string(result)); + output->push_back(option_name + "=" + 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(); + FPrintF(stderr, "Invalid value for %s\n", option_name.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", option_name.c_str()); + return ParseResult::InvalidContent; + } + output->push_back(option_name + "=" + result); + break; + } + case options_parser::OptionType::kInteger: { + int64_t result; + if (ondemand_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 (ondemand_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(); + } + if (unique_options != nullptr) { + unique_options->insert(option_name); + } + return ParseResult::Valid; +} + +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; + +for (auto field : *node_options_object) { + if (field.unescaped_key().get(key) || field.value().get(ondemand_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()) { + // If the option has already been set in the namespace options + // we return an invalid content error + if (unique_namespace_options_.contains(it->first)) { + FPrintF(stderr, + "Option %s is already set in namespace options\n", + it->first.c_str()); + return ParseResult::InvalidContent; + } + ParseResult result = ProcessOptionValue(key, + it->first, + ondemand_value, + it->second, + &node_options_, + &unique_node_options_); + if (result != ParseResult::Valid) { + return result; + } + } else { + FPrintF(stderr, "Unknown or not allowed option %s\n", key.data()); + return ParseResult::InvalidContent; + } +} +return ParseResult::Valid; +} + +ParseResult ConfigReader::ParseNamespaceOptions( + simdjson::ondemand::object* options_object, + const std::string& namespace_name) { + // MapOptions could send also options non settable via nodeOptions + auto options_map = options_parser::MapOptionsByNamespace(namespace_name); + + if (!env_options_initialized_) { + env_options_map_ = options_parser::MapEnvOptionsFlagInputType(); + env_options_initialized_ = true; + } + + simdjson::ondemand::value ondemand_value; + std::string_view key; + + for (auto field : *options_object) { + if (field.unescaped_key().get(key) || field.value().get(ondemand_value)) { + return ParseResult::InvalidContent; + } + + // The key needs to match the option for this namespace + std::string prefix = "--"; + auto it = options_map.find(prefix.append(key)); + if (it != options_map.end()) { + // If the option has already been set in the nodeOptions + // we return an invalid content error + if (unique_node_options_.contains(it->first)) { + FPrintF(stderr, + "Option %s is already set in nodeOptions\n", + it->first.c_str()); + return ParseResult::InvalidContent; + } + // Process the option for env options + ParseResult result = ProcessOptionValue(key, + it->first, + ondemand_value, + it->second, + &namespace_non_env_options_, + &unique_namespace_options_); + 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", + key.data(), + namespace_name.c_str()); return ParseResult::InvalidContent; } } @@ -165,7 +247,10 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { if (r != 0) { const char* err = uv_strerror(r); FPrintF( - stderr, "Cannot read configuration from %s: %s\n", config_path, err); + stderr, + "Cannot read configuration from %s: %s\n", + config_path, + err); return ParseResult::FileError; } @@ -177,49 +262,90 @@ 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", - config_path.data()); + FPrintF( + stderr, + "Root value unexpected not an object for %s\n\n", + config_path.data()); } else { FPrintF(stderr, "Can't parse %s\n", config_path.data()); } 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 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 valid_namespaces(available_namespaces.begin(), + available_namespaces.end()); + + // 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.find(namespace_name) == valid_namespaces.end()) { + // 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); + + // Special case for backward compatibility: handle nodeOptions with existing + // method + if (namespace_name == "nodeOptions") { + ParseResult result = ParseNodeOptions(&namespace_object); + if (result != ParseResult::Valid) { + return result; + } + } else { + // Process options for this namespace + ParseResult result = + ParseNamespaceOptions(&namespace_object, 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 acc = ""; + const size_t total_options = node_options_.size(); + acc.reserve(total_options * 2); + for (size_t i = 0; i < node_options_.size(); ++i) { + acc += " " + node_options_[i]; } + return acc; +} + +std::vector ConfigReader::AssignNodeNonEnvOptions() { + return namespace_non_env_options_; } size_t ConfigReader::GetFlagsSize() { diff --git a/src/node_config_file.h b/src/node_config_file.h index 5419590a9e05fb..41c86d2fd46a89 100644 --- a/src/node_config_file.h +++ b/src/node_config_file.h @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include #include "node_internals.h" #include "simdjson.h" #include "util-inl.h" @@ -30,13 +33,36 @@ class ConfigReader { const std::vector& args); std::string AssignNodeOptions(); + std::vector AssignNodeNonEnvOptions(); size_t GetFlagsSize(); private: + // Parse the nodeOptions object from the configuration file ParseResult ParseNodeOptions(simdjson::ondemand::object* node_options_object); + // Parse options for a specific namespace + ParseResult ParseNamespaceOptions(simdjson::ondemand::object* options_object, + const std::string& namespace_name); + + // Process a single option value based on its type + ParseResult ProcessOptionValue( + const std::string_view& key, + const std::string& option_name, + simdjson::ondemand::value& ondemand_value, + options_parser::OptionType option_type, + std::vector* output, + std::unordered_set* unique_options); + + std::unordered_set unique_node_options_; std::vector node_options_; + std::unordered_set unique_namespace_options_; + std::vector namespace_options_; + std::vector namespace_non_env_options_; + + // Cache for fast lookup of environment options + std::unordered_map env_options_map_; + bool env_options_initialized_ = false; }; } // namespace node diff --git a/src/node_options-inl.h b/src/node_options-inl.h index 55078af457fc7c..236fc3c8f69c48 100644 --- a/src/node_options-inl.h +++ b/src/node_options-inl.h @@ -31,98 +31,132 @@ namespace options_parser { template void OptionsParser::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, + const char* namespace_id) { + // Create the OptionInfo object correctly with the namespace_id included + // This avoids trying to modify the map entry after insertion + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; options_.emplace(name, OptionInfo{kBoolean, std::make_shared>(field), env_setting, help_text, - default_is_true}); + default_is_true, + namespace_id_str}); } template void OptionsParser::AddOption(const char* name, const char* help_text, - uint64_t Options::* field, - OptionEnvvarSettings env_setting) { + uint64_t Options::*field, + OptionEnvvarSettings env_setting, + const char* namespace_id) { + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; options_.emplace( name, OptionInfo{kUInteger, std::make_shared>(field), env_setting, - help_text}); + help_text, + false, + namespace_id_str}); } template void OptionsParser::AddOption(const char* name, const char* help_text, - int64_t Options::* field, - OptionEnvvarSettings env_setting) { + int64_t Options::*field, + OptionEnvvarSettings env_setting, + const char* namespace_id) { + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; options_.emplace( name, OptionInfo{kInteger, std::make_shared>(field), env_setting, - help_text}); + help_text, + false, + namespace_id_str}); } template void OptionsParser::AddOption(const char* name, const char* help_text, - std::string Options::* field, - OptionEnvvarSettings env_setting) { + std::string Options::*field, + OptionEnvvarSettings env_setting, + const char* namespace_id) { + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; options_.emplace( name, OptionInfo{kString, std::make_shared>(field), env_setting, - help_text}); + help_text, + false, + namespace_id_str}); } template -void OptionsParser::AddOption( - const char* name, - const char* help_text, - std::vector Options::* field, - OptionEnvvarSettings env_setting) { - options_.emplace(name, OptionInfo { - kStringList, - std::make_shared>>(field), - env_setting, - help_text - }); +void OptionsParser::AddOption(const char* name, + const char* help_text, + std::vector Options::*field, + OptionEnvvarSettings env_setting, + const char* namespace_id) { + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; + options_.emplace( + name, + OptionInfo{ + kStringList, + std::make_shared>>(field), + env_setting, + help_text, + false, + namespace_id_str}); } template void OptionsParser::AddOption(const char* name, const char* help_text, - HostPort Options::* field, - OptionEnvvarSettings env_setting) { + HostPort Options::*field, + OptionEnvvarSettings env_setting, + const char* namespace_id) { + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; options_.emplace( name, OptionInfo{kHostPort, std::make_shared>(field), env_setting, - help_text}); + help_text, + false, + namespace_id_str}); } template void OptionsParser::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, + const char* namespace_id) { + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; + options_.emplace( + name, + OptionInfo{ + kNoOp, nullptr, env_setting, help_text, false, namespace_id_str}); } template void OptionsParser::AddOption(const char* name, const char* help_text, V8Option v8_option_tag, - OptionEnvvarSettings env_setting) { - options_.emplace(name, - OptionInfo{kV8Option, nullptr, env_setting, help_text}); + OptionEnvvarSettings env_setting, + const char* namespace_id) { + std::string namespace_id_str = namespace_id != nullptr ? namespace_id : ""; + options_.emplace( + name, + OptionInfo{ + kV8Option, nullptr, env_setting, help_text, false, namespace_id_str}); } template @@ -198,7 +232,8 @@ auto OptionsParser::Convert( Convert(original.field, get_child), original.env_setting, original.help_text, - original.default_is_true}; + original.default_is_true, + original.namespace_id}; } template diff --git a/src/node_options.cc b/src/node_options.cc index cd2b8e4995e9c2..688bfccd9c785d 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -17,6 +17,7 @@ #include #include #include +#include using v8::Boolean; using v8::Context; @@ -241,6 +242,64 @@ void EnvironmentOptions::CheckOptions(std::vector* 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, + Local map, + const std::string& option_name, + const OptionType& option_type) { + std::string type; + switch (static_cast(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 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 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 { public: DebugOptionsParser(); @@ -694,82 +753,121 @@ 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, + "test_runner"); AddOption("--test-force-exit", "force test runner to exit upon completion", - &EnvironmentOptions::test_runner_force_exit); + &EnvironmentOptions::test_runner_force_exit, + kDisallowedInEnvvar, + false, + "test_runner"); AddOption("--test-timeout", "specify test runner timeout", - &EnvironmentOptions::test_runner_timeout); + &EnvironmentOptions::test_runner_timeout, + kDisallowedInEnvvar, + "test_runner"); AddOption("--test-update-snapshots", "regenerate test snapshots", - &EnvironmentOptions::test_runner_update_snapshots); + &EnvironmentOptions::test_runner_update_snapshots, + kDisallowedInEnvvar, + false, + "test_runner"); AddOption("--experimental-test-coverage", "enable code coverage in the test runner", - &EnvironmentOptions::test_runner_coverage); + &EnvironmentOptions::test_runner_coverage, + kDisallowedInEnvvar, + false, + "test_runner"); AddOption("--test-coverage-branches", "the branch coverage minimum threshold", &EnvironmentOptions::test_coverage_branches, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-coverage-functions", "the function coverage minimum threshold", &EnvironmentOptions::test_coverage_functions, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-coverage-lines", "the line coverage minimum threshold", &EnvironmentOptions::test_coverage_lines, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-isolation", "configures the type of test isolation used in the test runner", &EnvironmentOptions::test_isolation, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); // 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, + "test_runner"); + AddOption("--experimental-test-snapshots", + "", + NoOp{}, + kDisallowedInEnvvar, + "test_runner"); AddOption("--test-name-pattern", "run tests whose name matches this regular expression", &EnvironmentOptions::test_name_pattern, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-reporter", "report test output using the given reporter", &EnvironmentOptions::test_reporter, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-reporter-destination", "report given reporter to the given destination", &EnvironmentOptions::test_reporter_destination, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-only", "run tests with 'only' option set", &EnvironmentOptions::test_only, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + "test_runner"); AddOption("--test-shard", "run test at specific shard", &EnvironmentOptions::test_shard, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-skip-pattern", "run tests whose name do not match this regular expression", &EnvironmentOptions::test_skip_pattern, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-coverage-include", "include files in coverage report that match this glob pattern", &EnvironmentOptions::coverage_include_pattern, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); AddOption("--test-coverage-exclude", "exclude files from coverage report that match this glob pattern", &EnvironmentOptions::coverage_exclude_pattern, - kAllowedInEnvvar); + kAllowedInEnvvar, + "test_runner"); 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, + "test_runner"); + AddOption("--test-udp-no-try-send", + "", // For testing only. + &EnvironmentOptions::test_udp_no_try_send, + kDisallowedInEnvvar, + false, + "test_runner"); AddOption("--throw-deprecation", "throw an exception on deprecations", &EnvironmentOptions::throw_deprecation, @@ -1326,6 +1424,48 @@ MapEnvOptionsFlagInputType() { return type_map; } +std::vector MapAvailableNamespaces() { + std::vector namespaces; + const auto& parser = _ppop_instance; + for (const auto& item : parser.options_) { + if (!item.first.empty() && !item.first.starts_with('[') && + item.second.namespace_id != "" && + item.second.env_setting == kAllowedInEnvvar) { + namespaces.push_back(item.second.namespace_id); + } + } + return namespaces; +} + +std::unordered_map +MapOptionsByNamespace(std::string namespace_name) { + std::unordered_map 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> +MapNamespaceOptionsAssociations() { + std::vector available_namespaces = + options_parser::MapAvailableNamespaces(); + std::unordered_map< + std::string, + std::unordered_map> + 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 @@ -1593,56 +1733,71 @@ void GetEnvOptionsInputType(const FunctionCallbackInfo& 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(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 (!AddOptionTypeToMap( + isolate, context, flags_map, item.first, item.second.type)) { + return; } + } + } + args.GetReturnValue().Set(flags_map); +} - if (type.empty()) { - continue; - } +// 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& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); - Local value; - if (!String::NewFromUtf8( - isolate, type.data(), v8::NewStringType::kNormal, type.size()) - .ToLocal(&value)) { - continue; + 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 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 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; } + } - Local field; + // Only add namespaces that have options + if (options_type_map->Size() > 0) { + Local namespace_key; if (!String::NewFromUtf8(isolate, - item.first.data(), + namespace_name.data(), v8::NewStringType::kNormal, - item.first.size()) - .ToLocal(&field)) { + namespace_name.size()) + .ToLocal(&namespace_key)) { continue; } - if (flags_map->Set(context, field, value).IsEmpty()) { + if (namespaces_map->Set(context, namespace_key, options_type_map) + .IsEmpty()) { return; } } } - args.GetReturnValue().Set(flags_map); + + args.GetReturnValue().Set(namespaces_map); } void Initialize(Local target, @@ -1659,6 +1814,10 @@ void Initialize(Local target, context, target, "getEmbedderOptions", GetEmbedderOptions); SetMethodNoSideEffect( context, target, "getEnvOptionsInputType", GetEnvOptionsInputType); + SetMethodNoSideEffect(context, + target, + "getNamespaceOptionsInputType", + GetNamespaceOptionsInputType); Local env_settings = Object::New(isolate); NODE_DEFINE_CONSTANT(env_settings, kAllowedInEnvvar); NODE_DEFINE_CONSTANT(env_settings, kDisallowedInEnvvar); @@ -1685,6 +1844,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(GetCLIOptionsInfo); registry->Register(GetEmbedderOptions); registry->Register(GetEnvOptionsInputType); + registry->Register(GetNamespaceOptionsInputType); } } // namespace options_parser diff --git a/src/node_options.h b/src/node_options.h index d0c73722c9a68a..b317d72d8f060c 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -395,6 +395,12 @@ enum OptionType { kStringList, }; std::unordered_map MapEnvOptionsFlagInputType(); +std::unordered_map MapOptionsByNamespace( + std::string namespace_name); +std::unordered_map> +MapNamespaceOptionsAssociations(); +std::vector MapAvailableNamespaces(); template class OptionsParser { @@ -417,35 +423,43 @@ class OptionsParser { const char* help_text, bool Options::*field, OptionEnvvarSettings env_setting = kDisallowedInEnvvar, - bool default_is_true = false); + bool default_is_true = false, + const char* namespace_id = nullptr); void AddOption(const char* name, const char* help_text, uint64_t Options::*field, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar); + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + const char* namespace_id = nullptr); void AddOption(const char* name, const char* help_text, int64_t Options::*field, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar); + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + const char* namespace_id = nullptr); void AddOption(const char* name, const char* help_text, std::string Options::*field, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar); + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + const char* namespace_id = nullptr); void AddOption(const char* name, const char* help_text, std::vector Options::*field, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar); + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + const char* namespace_id = nullptr); void AddOption(const char* name, const char* help_text, HostPort Options::*field, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar); + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + const char* namespace_id = nullptr); void AddOption(const char* name, const char* help_text, NoOp no_op_tag, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar); + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + const char* namespace_id = nullptr); void AddOption(const char* name, const char* help_text, V8Option v8_option_tag, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar); + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + const char* namespace_id = nullptr); // Adds aliases. An alias can be of the form "--option-a" -> "--option-b", // or have a more complex group expansion, like @@ -535,12 +549,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 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 +598,9 @@ class OptionsParser { friend std::string GetBashCompletion(); friend std::unordered_map MapEnvOptionsFlagInputType(); + friend std::unordered_map MapOptionsByNamespace( + std::string namespace_name); + friend std::vector MapAvailableNamespaces(); friend void GetEnvOptionsInputType( const v8::FunctionCallbackInfo& args); }; diff --git a/test/fixtures/rc/empty-valid-namespace.json b/test/fixtures/rc/empty-valid-namespace.json new file mode 100644 index 00000000000000..4592f141aad2cc --- /dev/null +++ b/test/fixtures/rc/empty-valid-namespace.json @@ -0,0 +1,3 @@ +{ + "test_runner": {} +} diff --git a/test/fixtures/rc/namespace-with-array.json b/test/fixtures/rc/namespace-with-array.json new file mode 100644 index 00000000000000..4a2f367afb0c7c --- /dev/null +++ b/test/fixtures/rc/namespace-with-array.json @@ -0,0 +1,5 @@ +{ + "test_runner": { + "test-coverage-exclude": ["config-pattern1", "config-pattern2"] + } +} diff --git a/test/fixtures/rc/namespace-with-disallowed-envvar.json b/test/fixtures/rc/namespace-with-disallowed-envvar.json new file mode 100644 index 00000000000000..6b26aa954a9fac --- /dev/null +++ b/test/fixtures/rc/namespace-with-disallowed-envvar.json @@ -0,0 +1,5 @@ +{ + "test_runner": { + "test-concurrency": 1 + } +} diff --git a/test/fixtures/rc/namespaced/node.config.json b/test/fixtures/rc/namespaced/node.config.json new file mode 100644 index 00000000000000..1575fb249a10ba --- /dev/null +++ b/test/fixtures/rc/namespaced/node.config.json @@ -0,0 +1,5 @@ +{ + "test_runner": { + "test-isolation": "none" + } +} \ No newline at end of file diff --git a/test/fixtures/rc/override-namespace.json b/test/fixtures/rc/override-namespace.json new file mode 100644 index 00000000000000..ce36ee6d094e73 --- /dev/null +++ b/test/fixtures/rc/override-namespace.json @@ -0,0 +1,8 @@ +{ + "test_runner": { + "test-isolation": "process" + }, + "nodeOptions": { + "test-isolation": "none" + } +} \ No newline at end of file diff --git a/test/fixtures/rc/override-node-option-with-namespace.json b/test/fixtures/rc/override-node-option-with-namespace.json new file mode 100644 index 00000000000000..40b08b8fea7833 --- /dev/null +++ b/test/fixtures/rc/override-node-option-with-namespace.json @@ -0,0 +1,8 @@ +{ + "nodeOptions": { + "test-isolation": "none" + }, + "test_runner": { + "test-isolation": "process" + } +} \ No newline at end of file diff --git a/test/fixtures/rc/unknown-flag-namespace.json b/test/fixtures/rc/unknown-flag-namespace.json new file mode 100644 index 00000000000000..685c6502bc3a96 --- /dev/null +++ b/test/fixtures/rc/unknown-flag-namespace.json @@ -0,0 +1,5 @@ +{ + "test_runner": { + "unknown-flag": true + } +} diff --git a/test/fixtures/rc/unknown-namespace.json b/test/fixtures/rc/unknown-namespace.json new file mode 100644 index 00000000000000..14730d83efff73 --- /dev/null +++ b/test/fixtures/rc/unknown-namespace.json @@ -0,0 +1,5 @@ +{ + "an-invalid-namespace": { + "a-key": "a-value" + } +} diff --git a/test/parallel/test-config-file.js b/test/parallel/test-config-file.js index ac7f9edf3b4082..32c21214890f74 100644 --- a/test/parallel/test-config-file.js +++ b/test/parallel/test-config-file.js @@ -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'); @@ -375,3 +375,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 set in nodeOptions/); + 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 set in namespace options/); + 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); + }); +});