diff options
-rw-r--r-- | buildscripts/idl/idl/ast.py | 50 | ||||
-rw-r--r-- | buildscripts/idl/idl/binder.py | 140 | ||||
-rw-r--r-- | buildscripts/idl/idl/errors.py | 56 | ||||
-rw-r--r-- | buildscripts/idl/idl/generator.py | 214 | ||||
-rw-r--r-- | buildscripts/idl/idl/parser.py | 60 | ||||
-rw-r--r-- | buildscripts/idl/idl/syntax.py | 50 | ||||
-rw-r--r-- | buildscripts/idl/tests/test_binder.py | 182 | ||||
-rw-r--r-- | src/mongo/idl/SConscript | 12 | ||||
-rw-r--r-- | src/mongo/idl/config_option_test.cpp | 371 | ||||
-rw-r--r-- | src/mongo/idl/config_option_test.idl | 154 | ||||
-rw-r--r-- | src/mongo/util/options_parser/constraints.h | 81 | ||||
-rw-r--r-- | src/mongo/util/options_parser/option_description.h | 44 |
12 files changed, 1390 insertions, 24 deletions
diff --git a/buildscripts/idl/idl/ast.py b/buildscripts/idl/idl/ast.py index dfa8b4471e6..cf6fcd0ba31 100644 --- a/buildscripts/idl/idl/ast.py +++ b/buildscripts/idl/idl/ast.py @@ -67,6 +67,7 @@ class IDLAST(object): self.structs = [] # type: List[Struct] self.server_parameters = [] # type: List[ServerParameter] + self.configs = [] # type: List[ConfigOption] class Global(common.SourceLocation): @@ -81,6 +82,8 @@ class Global(common.SourceLocation): """Construct a Global.""" self.cpp_namespace = None # type: unicode self.cpp_includes = [] # type: List[unicode] + self.configs = None # type: ConfigGlobal + super(Global, self).__init__(file_name, line, column) @@ -260,3 +263,50 @@ class ServerParameter(common.SourceLocation): self.from_string = None # type: unicode super(ServerParameter, self).__init__(file_name, line, column) + + +class ConfigGlobal(common.SourceLocation): + """IDL ConfigOption Globals.""" + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a ConfigGlobal.""" + + # Other config globals are consumed in bind phase. + self.initializer_name = None # type: unicode + + super(ConfigGlobal, self).__init__(file_name, line, column) + + +class ConfigOption(common.SourceLocation): + """IDL ConfigOption setting.""" + + # pylint: disable=too-many-instance-attributes + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a ConfigOption.""" + self.name = None # type: unicode + self.short_name = None # type: unicode + self.deprecated_name = [] # type: List[unicode] + self.deprecated_short_name = [] # type: List[unicode] + + self.description = None # type: unicode + self.section = None # type: unicode + self.arg_vartype = None # type: unicode + self.cpp_vartype = None # type: unicode + self.cpp_varname = None # type: unicode + + self.conflicts = [] # type: List[unicode] + self.requires = [] # type: List[unicode] + self.hidden = False # type: bool + self.default = None # type: unicode + self.implicit = None # type: unicode + self.source = None # type: unicode + + self.duplicates_append = False # type: bool + self.positional_start = None # type: int + self.positional_end = None # type: int + self.validator = None # type: Validator + + super(ConfigOption, self).__init__(file_name, line, column) diff --git a/buildscripts/idl/idl/binder.py b/buildscripts/idl/idl/binder.py index 6fed2fd8ada..a3c065cf6c7 100644 --- a/buildscripts/idl/idl/binder.py +++ b/buildscripts/idl/idl/binder.py @@ -745,6 +745,12 @@ def _bind_globals(parsed_spec): parsed_spec.globals.column) ast_global.cpp_namespace = parsed_spec.globals.cpp_namespace ast_global.cpp_includes = parsed_spec.globals.cpp_includes + + configs = parsed_spec.globals.configs + if configs: + ast_global.configs = ast.ConfigGlobal(configs.file_name, configs.line, configs.column) + ast_global.configs.initializer_name = configs.initializer_name + else: ast_global = ast.Global("<implicit>", 0, 0) @@ -885,6 +891,137 @@ def _bind_server_parameter(ctxt, param): return ast_param +def _is_invalid_config_short_name(name): + # type: (unicode) -> bool + """Check if a given name is valid as a short name.""" + return ('.' in name) or (',' in name) + + +def _parse_config_option_sources(source_list): + # type: (List[unicode]) -> unicode + """Parse source list into enum value used by runtime.""" + sources = 0 + if not source_list: + return None + + for source in source_list: + if source == "cli": + sources |= 1 + elif source == "ini": + sources |= 2 + elif source == "yaml": + sources |= 4 + else: + return None + + source_map = [ + "SourceCommandLine", + "SourceINIConfig", + "SourceAllLegacy", # cli + ini + "SourceYAMLConfig", + "SourceYAMLCLI", # cli + yaml + "SourceAllConfig", # ini + yaml + "SourceAll", + ] + return source_map[sources - 1] + + +def _bind_config_option(ctxt, globals_spec, option): + # type: (errors.ParserContext, syntax.Global, syntax.ConfigOption) -> ast.ConfigOption + """Bind a config setting.""" + + # pylint: disable=too-many-branches,too-many-statements,too-many-return-statements + node = ast.ConfigOption(option.file_name, option.line, option.column) + + if _is_invalid_config_short_name(option.short_name or ''): + ctxt.add_invalid_short_name(option, option.short_name) + return None + + for name in option.deprecated_short_name: + if _is_invalid_config_short_name(name): + ctxt.add_invalid_short_name(option, name) + return None + + if option.single_name is not None: + if (len(option.single_name) != 1) or not option.single_name.isalpha(): + ctxt.add_invalid_single_name(option, option.single_name) + return None + + node.name = option.name + node.short_name = option.short_name + node.deprecated_name = option.deprecated_name + node.deprecated_short_name = option.deprecated_short_name + + if (node.short_name is None) and not _is_invalid_config_short_name(node.name): + # If the "dotted name" is usable as a "short name", mirror it by default. + node.short_name = node.name + + if option.single_name: + # Compose short_name/single_name into boost::program_options format. + if not node.short_name: + ctxt.add_missing_short_name_with_single_name(option, option.single_name) + return None + + node.short_name = node.short_name + ',' + option.single_name + + node.description = option.description + node.arg_vartype = option.arg_vartype + node.cpp_vartype = option.cpp_vartype + node.cpp_varname = option.cpp_varname + + node.requires = option.requires + node.conflicts = option.conflicts + node.hidden = option.hidden + node.default = option.default + node.implicit = option.implicit + + # Commonly repeated attributes section and source may be set in globals. + if globals_spec and globals_spec.configs: + node.section = option.section or globals_spec.configs.section + source_list = option.source or globals_spec.configs.source or [] + else: + node.section = option.section + source_list = option.source or [] + + node.source = _parse_config_option_sources(source_list) + if node.source is None: + ctxt.add_bad_source_specifier(option, ', '.join(source_list)) + return None + + if option.duplicate_behavior: + if option.duplicate_behavior == "append": + node.duplicates_append = True + elif option.duplicate_behavior != "overwrite": + ctxt.add_bad_duplicate_behavior(option, option.duplicate_behavior) + return None + + if option.positional: + if not node.short_name: + ctxt.add_missing_shortname_for_positional_arg(option) + return None + + # Parse single digit, closed range, or open range of digits. + spread = option.positional.split('-') + if len(spread) == 1: + # Make a single number behave like a range of that number, (e.g. "2" -> "2-2"). + spread.append(spread[0]) + if (len(spread) != 2) or ((spread[0] == "") and (spread[1] == "")): + ctxt.add_bad_numeric_range(option, 'positional', option.positional) + try: + node.positional_start = int(spread[0] or "-1") + node.positional_end = int(spread[1] or "-1") + except ValueError: + ctxt.add_bad_numeric_range(option, 'positional', option.positional) + return None + + if option.validator is not None: + node.validator = _bind_validator(ctxt, option.validator) + if node.validator is None: + return None + + return node + + def bind(parsed_spec): # type: (syntax.IDLSpec) -> ast.IDLBoundSpec """Read an idl.syntax, create an idl.ast tree, and validate the final IDL Specification.""" @@ -913,6 +1050,9 @@ def bind(parsed_spec): for server_parameter in parsed_spec.server_parameters: bound_spec.server_parameters.append(_bind_server_parameter(ctxt, server_parameter)) + for option in parsed_spec.configs: + bound_spec.configs.append(_bind_config_option(ctxt, parsed_spec.globals, option)) + if ctxt.errors.has_errors(): return ast.IDLBoundSpec(None, ctxt.errors) diff --git a/buildscripts/idl/idl/errors.py b/buildscripts/idl/idl/errors.py index 07050d2048a..8090dfcbe74 100644 --- a/buildscripts/idl/idl/errors.py +++ b/buildscripts/idl/idl/errors.py @@ -103,6 +103,13 @@ ERROR_ID_SERVER_PARAM_MISSING_METHOD = "ID0054" ERROR_ID_SERVER_PARAM_ATTR_NO_STORAGE = "ID0055" ERROR_ID_SERVER_PARAM_ATTR_WITH_STORAGE = "ID0056" ERROR_ID_BAD_SETAT_SPECIFIER = "ID0057" +ERROR_ID_BAD_SOURCE_SPECIFIER = "ID0058" +ERROR_ID_BAD_DUPLICATE_BEHAVIOR_SPECIFIER = "ID0059" +ERROR_ID_BAD_NUMERIC_RANGE = "ID0060" +ERROR_ID_MISSING_SHORTNAME_FOR_POSITIONAL = "ID0061" +ERROR_ID_INVALID_SHORT_NAME = "ID0062" +ERROR_ID_INVALID_SINGLE_NAME = "ID0063" +ERROR_ID_MISSING_SHORT_NAME_WITH_SINGLE_NAME = "ID0064" class IDLError(Exception): @@ -723,6 +730,55 @@ class ParserContext(object): ("'%s' conflicts with server parameter definition with storage") % (attrname)) + def add_bad_source_specifier(self, location, value): + # type: (common.SourceLocation, unicode) -> None + """Add an error about invalid source specifier.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_BAD_SOURCE_SPECIFIER, + ("'%s' is not a valid source specifier") % (value)) + + def add_bad_duplicate_behavior(self, location, value): + # type: (common.SourceLocation, unicode) -> None + """Add an error about invalid duplicate behavior specifier.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_BAD_DUPLICATE_BEHAVIOR_SPECIFIER, + ("'%s' is not a valid duplicate behavior specifier") % (value)) + + def add_bad_numeric_range(self, location, attrname, value): + # type: (common.SourceLocation, unicode, unicode) -> None + """Add an error about invalid range specifier.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_BAD_NUMERIC_RANGE, + ("'%s' is not a valid numeric range for '%s'") % (value, attrname)) + + def add_missing_shortname_for_positional_arg(self, location): + # type: (common.SourceLocation) -> None + """Add an error about required short_name for positional args.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_MISSING_SHORTNAME_FOR_POSITIONAL, + "Missing 'short_name' for positional arg") + + def add_invalid_short_name(self, location, name): + # type: (common.SourceLocation, unicode) -> None + """Add an error about invalid short names.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_INVALID_SHORT_NAME, + ("Invalid 'short_name' value '%s'") % (name)) + + def add_invalid_single_name(self, location, name): + # type: (common.SourceLocation, unicode) -> None + """Add an error about invalid single names.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_INVALID_SINGLE_NAME, + ("Invalid 'single_name' value '%s'") % (name)) + + def add_missing_short_name_with_single_name(self, location, name): + # type: (common.SourceLocation, unicode) -> None + """Add an error about missing required short name when using single name.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_MISSING_SHORT_NAME_WITH_SINGLE_NAME, + ("Missing 'short_name' required with 'single_name' value '%s'") % (name)) + def _assert_unique_error_messages(): # type: () -> None diff --git a/buildscripts/idl/idl/generator.py b/buildscripts/idl/idl/generator.py index 816946a897c..2a829ee3362 100644 --- a/buildscripts/idl/idl/generator.py +++ b/buildscripts/idl/idl/generator.py @@ -36,7 +36,8 @@ import os import string import sys import textwrap -from typing import cast, List, Mapping, Union +import uuid +from typing import cast, Dict, List, Mapping, Union from . import ast from . import bson @@ -314,7 +315,6 @@ def _encaps(val): # type: (unicode) -> unicode if val is None: return '""' - assert isinstance(val, unicode) for i in ["\\", '"', "'"]: if i in val: @@ -322,6 +322,15 @@ def _encaps(val): return '"' + val + '"' +# Turn a list of pything strings into a C++ initializer list. +def _encaps_list(vals): + # type: (List[unicode]) -> unicode + if vals is None: + return '{}' + + return '{' + ', '.join([_encaps(v) for v in vals]) + '}' + + class _CppFileWriterBase(object): """ C++ File writer. @@ -687,19 +696,23 @@ class _CppHeaderFileWriter(_CppFileWriterBase): self.write_empty_line() - def gen_extern_server_parameters(self, scps): - # type: (List[ast.ServerParameter]) -> None - """Generate externs for storage declaring server parameters.""" - for scp in scps: - if (scp.cpp_vartype is None) or (scp.cpp_varname is None): - continue - idents = scp.cpp_varname.split('::') - decl = idents.pop() - for ns in idents: - self._writer.write_line('namespace %s {' % (ns)) - self._writer.write_line('extern %s %s;' % (scp.cpp_vartype, decl)) - for ns in reversed(idents): - self._writer.write_line('} // namespace ' + ns) + def gen_extern_declaration(self, vartype, varname): + # type: (unicode, unicode) -> None + """Generate externs for storage declaration.""" + if (vartype is None) or (varname is None): + return + + idents = varname.split('::') + decl = idents.pop() + for ns in idents: + self._writer.write_line('namespace %s {' % (ns)) + + self._writer.write_line('extern %s %s;' % (vartype, decl)) + + for ns in reversed(idents): + self._writer.write_line('} // namespace ' + ns) + + if idents: self.write_empty_line() def generate(self, spec): @@ -721,6 +734,9 @@ class _CppHeaderFileWriter(_CppFileWriterBase): 'vector', ] + if spec.server_parameters: + header_list.append('boost/thread/synchronized_value.hpp') + header_list.sort() for include in header_list: @@ -735,11 +751,12 @@ class _CppHeaderFileWriter(_CppFileWriterBase): 'mongo/bson/bsonobj.h', 'mongo/bson/bsonobjbuilder.h', 'mongo/idl/idl_parser.h', - 'mongo/idl/server_parameter.h', - 'mongo/idl/server_parameter_with_storage.h', 'mongo/rpc/op_msg.h', ] + spec.globals.cpp_includes + if spec.configs: + header_list.append('mongo/util/options_parser/option_description.h') + header_list.sort() for include in header_list: @@ -820,7 +837,10 @@ class _CppHeaderFileWriter(_CppFileWriterBase): self.write_empty_line() - self.gen_extern_server_parameters(spec.server_parameters) + for scp in spec.server_parameters: + self.gen_extern_declaration(scp.cpp_vartype, scp.cpp_varname) + for opt in spec.configs: + self.gen_extern_declaration(opt.cpp_vartype, opt.cpp_varname) class _CppSourceFileWriter(_CppFileWriterBase): @@ -1674,7 +1694,7 @@ class _CppSourceFileWriter(_CppFileWriterBase): # pylint: disable=too-many-branches for param in params: - # Optiona storage declarations. + # Optional storage declarations. if (param.cpp_vartype is not None) and (param.cpp_varname is not None): self._writer.write_line('%s %s;' % (param.cpp_vartype, param.cpp_varname)) @@ -1731,6 +1751,147 @@ class _CppSourceFileWriter(_CppFileWriterBase): self.write_empty_line() + def gen_config_option(self, opt, section): + # type: (ast.ConfigOption, unicode) -> None + """Generate Config Option instance.""" + + # Derive cpp_vartype from arg_vartype if needed. + vartype = ("moe::OptionTypeMap<moe::%s>::type" % + (opt.arg_vartype)) if opt.cpp_vartype is None else opt.cpp_vartype + + with self._block(section, ';'): + self._writer.write_line( + common.template_args( + '.addOptionChaining(${name}, ${short}, moe::${argtype}, ${desc}, ${deprname}, ${deprshortname})', + name=_encaps(opt.name), short=_encaps(opt.short_name), + argtype=opt.arg_vartype, desc=_encaps(opt.description), deprname=_encaps_list( + opt.deprecated_name), deprshortname=_encaps_list( + opt.deprecated_short_name))) + self._writer.write_line('.setSources(moe::%s)' % (opt.source)) + if opt.hidden: + self._writer.write_line('.hidden()') + for requires in opt.requires: + self._writer.write_line('.requires(%s)' % (_encaps(requires))) + for conflicts in opt.conflicts: + self._writer.write_line('.incompatibleWith(%s)' % (_encaps(conflicts))) + if opt.default is not None: + dflt = _encaps(opt.default) if opt.arg_vartype == "String" else opt.default + self._writer.write_line('.setDefault(moe::Value(%s))' % (dflt)) + if opt.implicit is not None: + impl = _encaps(opt.implicit) if opt.arg_vartype == "String" else opt.implicit + self._writer.write_line('.setImplicit(moe::Value(%s))' % (impl)) + if opt.duplicates_append: + self._writer.write_line('.composing()') + if (opt.positional_start is not None) and (opt.positional_end is not None): + self._writer.write_line('.positional(%d, %d)' % (opt.positional_start, + opt.positional_end)) + + if opt.validator: + if opt.validator.callback: + self._writer.write_line( + common.template_args( + '.addConstraint(new moe::CallbackKeyConstraint<${argtype}>(${key}, ${callback}))', + argtype=vartype, key=_encaps( + opt.name), callback=opt.validator.callback)) + + if (opt.validator.gt is not None) or (opt.validator.lt is not None) or ( + opt.validator.gte is not None) or (opt.validator.lte is not None): + self._writer.write_line( + common.template_args( + '.addConstraint(new moe::BoundaryKeyConstraint<${argtype}>(${key}, ${gt}, ${lt}, ${gte}, ${lte}))', + argtype=vartype, key=_encaps(opt.name), gt='boost::none' + if opt.validator.gt is None else unicode(opt.validator.gt), + lt='boost::none' if opt.validator.lt is None else unicode( + opt.validator.lt), gte='boost::none' + if opt.validator.gte is None else unicode( + opt.validator.gte), lte='boost::none' + if opt.validator.lte is None else unicode(opt.validator.lte))) + + self.write_empty_line() + + def gen_config_options(self, spec): + # type: (ast.IDLAST) -> None + """Generate Config Option instances.""" + + # pylint: disable=too-many-branches + + has_storage_targets = False + for opt in spec.configs: + if opt.cpp_varname is not None: + has_storage_targets = True + if opt.cpp_vartype is not None: + self._writer.write_line('%s %s;' % (opt.cpp_vartype, opt.cpp_varname)) + self.write_empty_line() + + root_opts = [] # type: List[ast.ConfigOption] + sections = {} # type: Dict[unicode, List[ast.ConfigOption]] + for opt in spec.configs: + if opt.section: + try: + sections[opt.section].append(opt) + except KeyError: + sections[opt.section] = [opt] + else: + root_opts.append(opt) + + with self.gen_namespace_block(''): + # Group together options by section + if spec.globals.configs and spec.globals.configs.initializer_name: + blockname = spec.globals.configs.initializer_name + else: + blockname = 'idl_' + uuid.uuid4().hex + + with self._block('MONGO_MODULE_STARTUP_OPTIONS_REGISTER(%s)(InitializerContext*) {' % + (blockname), '}'): + self._writer.write_line('namespace moe = ::mongo::optionenvironment;') + self.write_empty_line() + + for opt in root_opts: + self.gen_config_option(opt, 'moe::startupOptions') + + for section_name, section_opts in sections.iteritems(): + with self._block('{', '}'): + self._writer.write_line('moe::OptionSection section(%s);' % + (_encaps(section_name))) + self.write_empty_line() + + for opt in section_opts: + self.gen_config_option(opt, 'section') + + self._writer.write_line( + 'auto status = moe::startupOptions.addSection(section);') + with self._block('if (!status.isOK()) {', '}'): + self._writer.write_line('return status;') + self.write_empty_line() + + self._writer.write_line('return Status::OK();') + self.write_empty_line() + + if has_storage_targets: + # Setup initializer for storing configured options in their variables. + with self._block('MONGO_STARTUP_OPTIONS_STORE(%s)(InitializerContext*) {' % + (blockname), '}'): + self._writer.write_line('namespace moe = ::mongo::optionenvironment;') + self._writer.write_line('const auto& params = moe::startupOptionsParsed;') + self.write_empty_line() + + for opt in spec.configs: + if opt.cpp_varname is None: + continue + + vartype = ("moe::OptionTypeMap<moe::%s>::type" % ( + opt.arg_vartype)) if opt.cpp_vartype is None else opt.cpp_vartype + with self._block('if (params.count(%s)) {' % (_encaps(opt.name)), '}'): + self._writer.write_line('%s = params[%s].as<%s>();' % + (opt.cpp_varname, _encaps(opt.name), vartype)) + self.write_empty_line() + + self._writer.write_line('return Status::OK();') + + self.write_empty_line() + + self.write_empty_line() + def generate(self, spec, header_file_name): # type: (ast.IDLAST, unicode) -> None """Generate the C++ header to a stream.""" @@ -1761,6 +1922,16 @@ class _CppSourceFileWriter(_CppFileWriterBase): 'mongo/db/command_generic_argument.h', 'mongo/db/commands.h', ] + + if spec.server_parameters: + header_list.append('mongo/idl/server_parameter.h') + header_list.append('mongo/idl/server_parameter_with_storage.h') + + if spec.configs: + header_list.append('mongo/util/options_parser/option_section.h') + header_list.append('mongo/util/options_parser/startup_option_init.h') + header_list.append('mongo/util/options_parser/startup_options.h') + header_list.sort() for include in header_list: @@ -1811,7 +1982,10 @@ class _CppSourceFileWriter(_CppFileWriterBase): self.gen_to_bson_serializer_method(struct) self.write_empty_line() - self.gen_server_parameter(spec.server_parameters or []) + if spec.server_parameters: + self.gen_server_parameter(spec.server_parameters) + if spec.configs: + self.gen_config_options(spec) def generate_header_str(spec): diff --git a/buildscripts/idl/idl/parser.py b/buildscripts/idl/idl/parser.py index c1454874641..49c2c2191fd 100644 --- a/buildscripts/idl/idl/parser.py +++ b/buildscripts/idl/idl/parser.py @@ -154,6 +154,21 @@ def _parse_mapping( func(ctxt, spec, first_name, second_node) +def _parse_config_global(ctxt, node): + # type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.ConfigGlobal + """Parse global settings for config options.""" + config = syntax.ConfigGlobal(ctxt.file_name, node.start_mark.line, node.start_mark.column) + + _generic_parser( + ctxt, node, "configs", config, { + "section": _RuleDesc("scalar"), + "source": _RuleDesc("scalar_or_sequence"), + "initializer_name": _RuleDesc("scalar"), + }) + + return config + + def _parse_global(ctxt, spec, node): # type: (errors.ParserContext, syntax.IDLSpec, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None """Parse a global section in the IDL file.""" @@ -162,10 +177,11 @@ def _parse_global(ctxt, spec, node): idlglobal = syntax.Global(ctxt.file_name, node.start_mark.line, node.start_mark.column) - _generic_parser(ctxt, node, "global", idlglobal, { - "cpp_namespace": _RuleDesc("scalar"), - "cpp_includes": _RuleDesc("scalar_or_sequence"), - }) + _generic_parser( + ctxt, node, "global", idlglobal, { + "cpp_namespace": _RuleDesc("scalar"), "cpp_includes": _RuleDesc("scalar_or_sequence"), + "configs": _RuleDesc("mapping", mapping_parser_func=_parse_config_global) + }) spec.globals = idlglobal @@ -515,6 +531,40 @@ def _parse_server_parameter(ctxt, spec, name, node): spec.server_parameters.append(param) +def _parse_config_option(ctxt, spec, name, node): + # type: (errors.ParserContext, syntax.IDLSpec, unicode, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None + """Parse a configs section in the IDL file.""" + if not ctxt.is_mapping_node(node, "configs"): + return + + option = syntax.ConfigOption(ctxt.file_name, node.start_mark.line, node.start_mark.column) + option.name = name + + _generic_parser( + ctxt, node, "configs", option, { + "short_name": _RuleDesc('scalar'), + "single_name": _RuleDesc('scalar'), + "deprecated_name": _RuleDesc('scalar_or_sequence'), + "deprecated_short_name": _RuleDesc('scalar_or_sequence'), + "description": _RuleDesc('scalar', _RuleDesc.REQUIRED), + "section": _RuleDesc('scalar'), + "arg_vartype": _RuleDesc('scalar', _RuleDesc.REQUIRED), + "cpp_vartype": _RuleDesc('scalar'), + "cpp_varname": _RuleDesc('scalar'), + "conflicts": _RuleDesc('scalar_or_sequence'), + "requires": _RuleDesc('scalar_or_sequence'), + "hidden": _RuleDesc('bool_scalar'), + "default": _RuleDesc('scalar'), + "implicit": _RuleDesc('scalar'), + "source": _RuleDesc('scalar_or_sequence'), + "duplicate_behavior": _RuleDesc('scalar'), + "positional": _RuleDesc('scalar'), + "validator": _RuleDesc('mapping', mapping_parser_func=_parse_validator), + }) + + spec.configs.append(option) + + def _prefix_with_namespace(cpp_namespace, cpp_name): # type: (unicode, unicode) -> unicode """Preface a C++ type name with a namespace if not already qualified or a primitive type.""" @@ -595,6 +645,8 @@ def _parse(stream, error_file_name): _parse_mapping(ctxt, spec, second_node, 'commands', _parse_command) elif first_name == "server_parameters": _parse_mapping(ctxt, spec, second_node, "server_parameters", _parse_server_parameter) + elif first_name == "configs": + _parse_mapping(ctxt, spec, second_node, "configs", _parse_config_option) else: ctxt.add_unknown_root_node_error(first_node) diff --git a/buildscripts/idl/idl/syntax.py b/buildscripts/idl/idl/syntax.py index e82c9456c82..5c49c0dccc7 100644 --- a/buildscripts/idl/idl/syntax.py +++ b/buildscripts/idl/idl/syntax.py @@ -68,6 +68,7 @@ class IDLSpec(object): self.globals = None # type: Optional[Global] self.imports = None # type: Optional[Import] self.server_parameters = [] # type: List[ServerParameter] + self.configs = [] # type: List[ConfigOption] def parse_array_type(name): @@ -231,6 +232,8 @@ class Global(common.SourceLocation): """Construct a Global.""" self.cpp_namespace = None # type: unicode self.cpp_includes = [] # type: List[unicode] + self.configs = None # type: ConfigGlobal + super(Global, self).__init__(file_name, line, column) @@ -483,3 +486,50 @@ class ServerParameter(common.SourceLocation): self.from_string = None # type: unicode super(ServerParameter, self).__init__(file_name, line, column) + + +class ConfigGlobal(common.SourceLocation): + """Global values to apply to all ConfigOptions.""" + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a ConfigGlobal.""" + self.section = None # type: unicode + self.source = [] # type: List[unicode] + self.initializer_name = None # type: unicode + + super(ConfigGlobal, self).__init__(file_name, line, column) + + +class ConfigOption(common.SourceLocation): + """Runtime configuration setting definition.""" + + # pylint: disable=too-many-instance-attributes + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a ConfigOption.""" + self.name = None # type: unicode + self.deprecated_name = [] # type: List[unicode] + self.short_name = None # type: unicode + self.single_name = None # type: unicode + self.deprecated_short_name = [] # type: List[unicode] + + self.description = None # type: unicode + self.section = None # type: unicode + self.arg_vartype = None # type: unicode + self.cpp_vartype = None # type: unicode + self.cpp_varname = None # type: unicode + + self.conflicts = [] # type: List[unicode] + self.requires = [] # type: List[unicode] + self.hidden = False # type: bool + self.default = None # type: unicode + self.implicit = None # type: unicode + self.source = [] # type: List[unicode] + + self.duplicate_behavior = None # type: unicode + self.positional = None # type unicode + self.validator = None # type: Validator + + super(ConfigOption, self).__init__(file_name, line, column) diff --git a/buildscripts/idl/tests/test_binder.py b/buildscripts/idl/tests/test_binder.py index ef6d3559473..edcaf82d0c0 100644 --- a/buildscripts/idl/tests/test_binder.py +++ b/buildscripts/idl/tests/test_binder.py @@ -1736,6 +1736,188 @@ class TestBinder(testcase.IDLTestcase): cpp_varname: baz """), idl.errors.ERROR_ID_BAD_SETAT_SPECIFIER) + def test_config_option_positive(self): + # type: () -> None + """Posative config option test cases.""" + + # Every field. + self.assert_bind( + textwrap.dedent(""" + configs: + foo: + short_name: bar + deprecated_name: baz + deprecated_short_name: qux + description: comment + section: here + arg_vartype: String + cpp_varname: gStringVal + conflicts: bling + requires: blong + hidden: false + default: one + implicit: two + duplicate_behavior: append + source: yaml + positional: 1-2 + validator: + gt: 0 + lt: 100 + gte: 1 + lte: 99 + callback: doSomething + """)) + + # Required fields only. + self.assert_bind( + textwrap.dedent(""" + configs: + foo: + description: comment + arg_vartype: Switch + source: yaml + """)) + + # List and enum variants. + self.assert_bind( + textwrap.dedent(""" + configs: + foo: + deprecated_name: [ baz, baz ] + deprecated_short_name: [ bling, blong ] + description: comment + arg_vartype: StringVector + source: [ cli, ini, yaml ] + conflicts: [ a, b, c ] + requires: [ d, e, f ] + hidden: true + duplicate_behavior: overwrite + """)) + + # Positional variants. + for positional in ['1', '1-', '-2', '1-2']: + self.assert_bind( + textwrap.dedent(""" + configs: + foo: + short_name: foo + description: comment + arg_vartype: Bool + source: cli + positional: %s + """ % (positional))) + # With implicit short name. + self.assert_bind( + textwrap.dedent(""" + configs: + foo: + description: comment + arg_vartype: Bool + source: cli + positional: %s + """ % (positional))) + + def test_config_option_negative(self): + # type: () -> None + """Negative config option test cases.""" + + # Invalid source. + self.assert_bind_fail( + textwrap.dedent(""" + configs: + foo: + description: comment + arg_vartype: Long + source: json + """), idl.errors.ERROR_ID_BAD_SOURCE_SPECIFIER) + + self.assert_bind_fail( + textwrap.dedent(""" + configs: + foo: + description: comment + arg_vartype: StringMap + source: [ cli, yaml ] + duplicate_behavior: guess + """), idl.errors.ERROR_ID_BAD_DUPLICATE_BEHAVIOR_SPECIFIER) + + for positional in ["x", "1-2-3", "-2-", "1--3"]: + self.assert_bind_fail( + textwrap.dedent(""" + configs: + foo: + description: comment + arg_vartype: String + source: cli + positional: %s + """ % (positional)), idl.errors.ERROR_ID_BAD_NUMERIC_RANGE) + + self.assert_bind_fail( + textwrap.dedent(""" + configs: + foo: + description: comment + short_name: "bar.baz" + arg_vartype: Bool + source: cli + """), idl.errors.ERROR_ID_INVALID_SHORT_NAME) + + self.assert_bind_fail( + textwrap.dedent(""" + configs: + foo: + description: comment + short_name: bar + deprecated_short_name: "baz.qux" + arg_vartype: Long + source: cli + """), idl.errors.ERROR_ID_INVALID_SHORT_NAME) + + # dottedName is not valid as a shortName. + self.assert_bind_fail( + textwrap.dedent(""" + configs: + "foo.bar": + description: comment + arg_vartype: String + source: cli + positional: 1 + """), idl.errors.ERROR_ID_MISSING_SHORTNAME_FOR_POSITIONAL) + + # Invalid shortname using boost::po format directly. + self.assert_bind_fail( + textwrap.dedent(""" + configs: + foo: + short_name: "foo,f" + arg_vartype: Switch + description: comment + source: cli + """), idl.errors.ERROR_ID_INVALID_SHORT_NAME) + + # Invalid single names, must be single alpha char. + for name in ["foo", "1", ".", ""]: + self.assert_bind_fail( + textwrap.dedent(""" + configs: + foo: + single_name: "%s" + arg_vartype: Switch + description: comment + source: cli + """ % (name)), idl.errors.ERROR_ID_INVALID_SINGLE_NAME) + + # Single names require a valid short name. + self.assert_bind_fail( + textwrap.dedent(""" + configs: + "foo.bar": + single_name: f + arg_vartype: Switch + description: comment + source: cli + """), idl.errors.ERROR_ID_MISSING_SHORT_NAME_WITH_SINGLE_NAME) + if __name__ == '__main__': diff --git a/src/mongo/idl/SConscript b/src/mongo/idl/SConscript index 14d58001fba..78c54ed60d0 100644 --- a/src/mongo/idl/SConscript +++ b/src/mongo/idl/SConscript @@ -53,3 +53,15 @@ env.CppUnitTest( 'idl_parser', ], ) + +env.CppUnitTest( + target='idl_config_option_test', + source=[ + 'config_option_test.cpp', + env.Idlc('config_option_test.idl')[0], + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/util/options_parser/options_parser', + ], +) diff --git a/src/mongo/idl/config_option_test.cpp b/src/mongo/idl/config_option_test.cpp new file mode 100644 index 00000000000..31824b893bf --- /dev/null +++ b/src/mongo/idl/config_option_test.cpp @@ -0,0 +1,371 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kDefault + +#include "mongo/platform/basic.h" + +#include "mongo/idl/config_option_test_gen.h" +#include "mongo/unittest/unittest.h" +#include "mongo/util/log.h" +#include "mongo/util/options_parser/options_parser.h" +#include "mongo/util/options_parser/startup_option_init.h" +#include "mongo/util/options_parser/startup_options.h" + +namespace mongo { +namespace test { + +namespace moe = ::mongo::optionenvironment; + +namespace { + +Status parseArgv(const std::vector<std::string>& argv, moe::Environment* parsed) { + auto status = moe::OptionsParser().run(moe::startupOptions, argv, {}, parsed); + if (!status.isOK()) { + return status; + } + return parsed->validate(); +} + +Status parseConfig(const std::string& config, moe::Environment* parsed) { + auto status = moe::OptionsParser().runConfigFile(moe::startupOptions, config, {}, parsed); + if (!status.isOK()) { + return status; + } + return parsed->validate(); +} + +Status parseMixed(const std::vector<std::string>& argv, + const std::string& config, + moe::Environment* env) try { + moe::OptionsParser mixedParser; + + moe::Environment conf; + uassertStatusOK(mixedParser.runConfigFile(moe::startupOptions, config, {}, &conf)); + uassertStatusOK(env->setAll(conf)); + + moe::Environment cli; + uassertStatusOK(mixedParser.run(moe::startupOptions, argv, {}, &cli)); + uassertStatusOK(env->setAll(cli)); + + return env->validate(); +} catch (const DBException& ex) { + return ex.toStatus(); +} + +MONGO_STARTUP_OPTIONS_PARSE(ConfigOption)(InitializerContext*) { + // Fake argv for default arg parsing. + const std::vector<std::string> argv = { + "mongo", + "--testConfigOpt2", + "true", + "--testConfigOpt8", + "8", + "--testConfigOpt12", + "command-line option", + }; + return parseArgv(argv, &moe::startupOptionsParsed); +} + +template <typename T> +void ASSERT_OPTION_SET(const moe::Environment& env, const moe::Key& name, const T& exp) { + ASSERT_TRUE(env.count(name)); + ASSERT_EQ(env[name].as<T>(), exp); +} + +// ASSERT_EQ can't handle vectors, so take slightly more pains. +template <typename T> +void ASSERT_VECTOR_OPTION_SET(const moe::Environment& env, + const moe::Key& name, + const std::vector<T>& exp) { + ASSERT_TRUE(env.count(name)); + auto value = env[name].as<std::vector<T>>(); + ASSERT_EQ(exp.size(), value.size()); + for (size_t i = 0; i < exp.size(); ++i) { + ASSERT_EQ(exp[i], value[i]); + } +} + +template <typename T> +void ASSERT_OPTION_NOT_SET(const moe::Environment& env, const moe::Key& name) { + ASSERT_FALSE(env.count(name)); + ASSERT_THROWS(env[name].as<T>(), AssertionException); +} + +TEST(ConfigOption, Opt1) { + ASSERT_OPTION_NOT_SET<bool>(moe::startupOptionsParsed, "test.config.opt1"); + + moe::Environment parsed; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt1"}, &parsed)); + ASSERT_OPTION_SET<bool>(parsed, "test.config.opt1", true); + + moe::Environment parsedYAML; + ASSERT_OK(parseConfig("test: { config: { opt1: true } }", &parsedYAML)); + ASSERT_OPTION_SET<bool>(parsedYAML, "test.config.opt1", true); + + moe::Environment parsedINI; + ASSERT_OK(parseConfig("testConfigOpt1=true", &parsedINI)); + ASSERT_OPTION_SET<bool>(parsedINI, "test.config.opt1", true); +} + +TEST(ConfigOption, Opt2) { + ASSERT_OPTION_SET<bool>(moe::startupOptionsParsed, "test.config.opt2", true); + + moe::Environment parsedAbsent; + ASSERT_OK(parseArgv({"mongod"}, &parsedAbsent)); + ASSERT_OPTION_NOT_SET<bool>(parsedAbsent, "test.config.opt2"); + + moe::Environment parsedTrue; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt2", "true"}, &parsedTrue)); + ASSERT_OPTION_SET<bool>(parsedTrue, "test.config.opt2", true); + + moe::Environment parsedFalse; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt2", "false"}, &parsedFalse)); + ASSERT_OPTION_SET<bool>(parsedFalse, "test.config.opt2", false); + + moe::Environment parsedFail; + ASSERT_NOT_OK(parseArgv({"mongod", "--testConfigOpt2"}, &parsedFail)); + ASSERT_NOT_OK(parseArgv({"mongod", "--testConfigOpt2", "banana"}, &parsedFail)); + ASSERT_NOT_OK(parseConfig("test: { config: { opt2: true } }", &parsedFail)); + ASSERT_NOT_OK(parseConfig("testConfigOpt2=true", &parsedFail)); +} + +TEST(ConfigOption, Opt3) { + ASSERT_OPTION_NOT_SET<bool>(moe::startupOptionsParsed, "test.config.opt3"); + + moe::Environment parsedAbsent; + ASSERT_OK(parseArgv({"mongod"}, &parsedAbsent)); + ASSERT_OPTION_NOT_SET<bool>(parsedAbsent, "test.config.opt3"); + + moe::Environment parsedTrue; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt3", "true"}, &parsedTrue)); + ASSERT_OPTION_SET<bool>(parsedTrue, "test.config.opt3", true); + + moe::Environment parsedFalse; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt3", "false"}, &parsedFalse)); + ASSERT_OPTION_SET<bool>(parsedFalse, "test.config.opt3", false); + + moe::Environment parsedImplicit; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt3"}, &parsedImplicit)); + ASSERT_OPTION_SET<bool>(parsedImplicit, "test.config.opt3", true); +} + +TEST(ConfigOption, Opt4) { + ASSERT_OPTION_SET<std::string>(moe::startupOptionsParsed, "test.config.opt4", "Default Value"); + + moe::Environment parsedDefault; + ASSERT_OK(parseArgv({"mongod"}, &parsedDefault)); + ASSERT_OPTION_SET<std::string>(parsedDefault, "test.config.opt4", "Default Value"); + + moe::Environment parsedHello; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt4", "Hello"}, &parsedHello)); + ASSERT_OPTION_SET<std::string>(parsedHello, "test.config.opt4", "Hello"); + + moe::Environment parsedFail; + ASSERT_NOT_OK(parseArgv({"mongod", "--testConfigOpt4"}, &parsedFail)); +} + +TEST(ConfigOption, Opt5) { + ASSERT_OPTION_NOT_SET<int>(moe::startupOptionsParsed, "test.config.opt5"); + + moe::Environment parsedFail; + ASSERT_NOT_OK(parseArgv({"mongod", "--testConfigOpt5"}, &parsedFail)); + ASSERT_NOT_OK(parseArgv({"mongod", "--testConfigOpt5", "123"}, &parsedFail)); + ASSERT_NOT_OK(parseConfig("test: { config: { opt5: 123 } }", &parsedFail)); + + moe::Environment parsedINI; + ASSERT_OK(parseConfig("testConfigOpt5=123", &parsedINI)); + ASSERT_OPTION_SET<int>(parsedINI, "test.config.opt5", 123); +} + +TEST(ConfigOption, Opt6) { + ASSERT_OPTION_NOT_SET<std::string>(moe::startupOptionsParsed, "testConfigOpt6"); + + moe::Environment parsed; + ASSERT_OK(parseArgv({"mongod", "some value"}, &parsed)); + ASSERT_OPTION_SET<std::string>(parsed, "testConfigOpt6", "some value"); + + moe::Environment parsedINI; + ASSERT_OK(parseConfig("testConfigOpt6=other thing", &parsedINI)); + ASSERT_OPTION_SET<std::string>(parsedINI, "testConfigOpt6", "other thing"); +} + +TEST(ConfigOption, Opt7) { + ASSERT_OPTION_NOT_SET<std::vector<std::string>>(moe::startupOptionsParsed, "testConfigOpt7"); + + // Single arg goes to opt6 per positioning. + moe::Environment parsedSingleArg; + ASSERT_OK(parseArgv({"mongod", "value1"}, &parsedSingleArg)); + ASSERT_OPTION_SET<std::string>(parsedSingleArg, "testConfigOpt6", "value1"); + ASSERT_OPTION_NOT_SET<std::vector<std::string>>(parsedSingleArg, "testConfigOpt7"); + + moe::Environment parsedMultiArg; + ASSERT_OK(parseArgv({"mongod", "value1", "value2", "value3"}, &parsedMultiArg)); + ASSERT_OPTION_SET<std::string>(parsedMultiArg, "testConfigOpt6", "value1"); + + // ASSERT macros can't deal with vector<string>, so break out the test manually. + ASSERT_VECTOR_OPTION_SET<std::string>(parsedMultiArg, "testConfigOpt7", {"value2", "value3"}); +} + +TEST(ConfigOption, Opt8) { + ASSERT_OPTION_SET<long>(moe::startupOptionsParsed, "test.config.opt8", 8); + + moe::Environment parsed; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt8", "42"}, &parsed)); + ASSERT_OPTION_SET<long>(parsed, "test.config.opt8", 42); + + moe::Environment parsedDeprShort; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt8a", "43"}, &parsedDeprShort)); + ASSERT_OPTION_SET<long>(parsedDeprShort, "test.config.opt8", 43); + + moe::Environment parsedDeprDotted; + ASSERT_OK(parseConfig("test: { config: { opt8b: 44 } }", &parsedDeprDotted)); + ASSERT_OPTION_SET<long>(parsedDeprDotted, "test.config.opt8", 44); +} + +TEST(ConfigOption, Opt9) { + ASSERT_OPTION_NOT_SET<unsigned>(moe::startupOptionsParsed, "test.config.opt9"); + ASSERT_OPTION_NOT_SET<long>(moe::startupOptionsParsed, "test.config.opt9a"); + ASSERT_OPTION_NOT_SET<unsigned long long>(moe::startupOptionsParsed, "test.config.opt9b"); + + moe::Environment parsedCLI; + ASSERT_OK( + parseArgv({"mongod", "--testConfigOpt9", "42", "--testConfigOpt9a", "43"}, &parsedCLI)); + ASSERT_OPTION_SET<unsigned>(parsedCLI, "test.config.opt9", 42); + ASSERT_OPTION_SET<long>(parsedCLI, "test.config.opt9a", 43); + ASSERT_OPTION_NOT_SET<unsigned long long>(parsedCLI, "test.config.opt9b"); + + moe::Environment parsedINI; + ASSERT_OK(parseConfig("testConfigOpt9=42\ntestConfigOpt9a=43", &parsedINI)); + ASSERT_OPTION_SET<unsigned>(parsedINI, "test.config.opt9", 42); + ASSERT_OPTION_SET<long>(parsedINI, "test.config.opt9a", 43); + ASSERT_OPTION_NOT_SET<unsigned long long>(parsedINI, "test.config.opt9b"); + + moe::Environment parsedYAML; + ASSERT_OK(parseConfig("test: { config: { opt9: 42, opt9a: 43 } }", &parsedYAML)); + ASSERT_OPTION_SET<unsigned>(parsedYAML, "test.config.opt9", 42); + ASSERT_OPTION_SET<long>(parsedYAML, "test.config.opt9a", 43); + ASSERT_OPTION_NOT_SET<unsigned long long>(parsedYAML, "test.config.opt9b"); + + moe::Environment parsedMixed; + ASSERT_OK(parseMixed( + {"mongod", "--testConfigOpt9", "42"}, "test: { config: { opt9a: 43 } }", &parsedMixed)); + ASSERT_OPTION_SET<unsigned>(parsedMixed, "test.config.opt9", 42); + ASSERT_OPTION_SET<long>(parsedMixed, "test.config.opt9a", 43); + ASSERT_OPTION_NOT_SET<unsigned long long>(parsedMixed, "test.config.opt9b"); + + moe::Environment parsedFail; + ASSERT_NOT_OK(parseArgv({"mongod", "--testConfigOpt9", "42"}, &parsedFail)); + ASSERT_NOT_OK( + parseArgv({"mongod", "--testConfigOpt9", "42", "--testConfigOpt9b", "44"}, &parsedFail)); + ASSERT_NOT_OK(parseArgv( + {"mongod", "--testConfigOpt9", "42", "--testConfigOpt9a", "43", "--testConfigOpt9b", "44"}, + &parsedFail)); + ASSERT_NOT_OK(parseConfig("testConfigOpt9=42", &parsedFail)); + ASSERT_NOT_OK(parseConfig("testConfigOpt9=42\ntestConfigOpt9b=44", &parsedFail)); + ASSERT_NOT_OK( + parseConfig("testConfigOpt9=42\ntestConfigOpt9a=43\ntestConfigOpt9b=44", &parsedFail)); + ASSERT_NOT_OK(parseConfig("test: { config: { opt9: 42 } }", &parsedFail)); + ASSERT_NOT_OK(parseConfig("test: { config: { opt9: 42, opt9b: 44 } }", &parsedFail)); + ASSERT_NOT_OK(parseConfig("test: { config: { opt9: 42, opt9a: 43, opt9b: 44 } }", &parsedFail)); +} + +TEST(ConfigOption, Opt10) { + ASSERT_OPTION_NOT_SET<int>(moe::startupOptionsParsed, "test.config.opt10a"); + ASSERT_OPTION_NOT_SET<int>(moe::startupOptionsParsed, "test.config.opt10b"); + + const auto tryParse = [](int a, int b) { + moe::Environment parsed; + ASSERT_OK(parseArgv({"mongod", + "--testConfigOpt10a", + std::to_string(a), + "--testConfigOpt10b", + std::to_string(b)}, + &parsed)); + ASSERT_OPTION_SET<int>(parsed, "test.config.opt10a", a); + ASSERT_OPTION_SET<int>(parsed, "test.config.opt10b", b); + }; + const auto failParse = [](int a, int b) { + moe::Environment parsedFail; + ASSERT_NOT_OK(parseArgv({"mongod", + "--testConfigOpt10a", + std::to_string(a), + "--testConfigOpt10b", + std::to_string(b)}, + &parsedFail)); + }; + tryParse(1, 1); + tryParse(99, 99); + tryParse(1, 0); + tryParse(99, 100); + failParse(0, 0); + failParse(100, 100); +} + +TEST(ConfigOption, Opt11) { + ASSERT_OPTION_NOT_SET<int>(moe::startupOptionsParsed, "test.config.opt11"); + + const auto tryParse = [](int val) { + moe::Environment parsed; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt11", std::to_string(val)}, &parsed)); + ASSERT_OPTION_SET<int>(parsed, "test.config.opt11", val); + }; + const auto failParse = [](int val) { + moe::Environment parsed; + ASSERT_NOT_OK(parseArgv({"mongod", "--testConfigOpt11", std::to_string(val)}, &parsed)); + }; + tryParse(1); + tryParse(123456789); + failParse(0); + failParse(2); + failParse(123456780); +} + +TEST(ConfigOption, Opt12) { + ASSERT_OPTION_SET<std::string>( + moe::startupOptionsParsed, "test.config.opt12", "command-line option"); + ASSERT_EQ(gTestConfigOpt12, "command-line option"); +} + +TEST(ConfigOption, Opt13) { + ASSERT_OPTION_NOT_SET<std::string>(moe::startupOptionsParsed, "test.config.opt13"); + + moe::Environment parsedSingle; + ASSERT_OK(parseArgv({"mongod", "-o", "single"}, &parsedSingle)); + ASSERT_OPTION_SET<std::string>(parsedSingle, "test.config.opt13", "single"); + + moe::Environment parsedShort; + ASSERT_OK(parseArgv({"mongod", "--testConfigOpt13", "short"}, &parsedShort)); + ASSERT_OPTION_SET<std::string>(parsedShort, "test.config.opt13", "short"); +} + +} // namespace + +} // namespace test +} // namespace mongo diff --git a/src/mongo/idl/config_option_test.idl b/src/mongo/idl/config_option_test.idl new file mode 100644 index 00000000000..6c85b939421 --- /dev/null +++ b/src/mongo/idl/config_option_test.idl @@ -0,0 +1,154 @@ +# Copyright (C) 2018-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +global: + cpp_namespace: "mongo::test" + cpp_includes: + - "mongo/idl/server_parameter_with_storage_test.h" + configs: + initializer_name: TestConfigs + +imports: + - "mongo/idl/basic_types.idl" + +configs: + "test.config.opt1": + short_name: testConfigOpt1 + description: "Basic switch" + arg_vartype: Switch + source: [ cli, yaml, ini ] + + "test.config.opt2": + short_name: testConfigOpt2 + description: "Boolean option without implicit value" + arg_vartype: Bool + source: cli + + "test.config.opt3": + short_name: testConfigOpt3 + description: "Boolean option with implicit value" + arg_vartype: Bool + source: cli + implicit: true + + "test.config.opt4": + short_name: testConfigOpt4 + description: "String option with a default value" + arg_vartype: String + source: cli + default: "Default Value" + + "test.config.opt5": + short_name: testConfigOpt5 + description: "Int option only settable from INI" + arg_vartype: Int + source: ini + + # Positional options must be configured with a "short name" only. + "testConfigOpt6": + description: "Positional string argument" + arg_vartype: String + source: [ cli, ini ] + positional: 1 + hidden: true + + "testConfigOpt7": + description: "Muilti-value positional string arguments" + arg_vartype: StringVector + source: cli + positional: 2- + hidden: true + + "test.config.opt8": + short_name: testConfigOpt8 + deprecated_name: [ "test.config.opt8a", "test.config.opt8b" ] + deprecated_short_name: [ testConfigOpt8a, testConfigOpt8b ] + description: "Option with deprecated names" + source: [ cli, yaml ] + arg_vartype: Long + + "test.config.opt9": + short_name: testConfigOpt9 + description: "Option with dependencies" + arg_vartype: Unsigned + source: [ cli, ini, yaml ] + requires: "test.config.opt9a" + conflicts: "test.config.opt9b" + + "test.config.opt9a": + short_name: testConfigOpt9a + description: "Required with opt9" + arg_vartype: Long + source: [ cli, ini, yaml ] + + "test.config.opt9b": + short_name: testConfigOpt9b + description: "Conflicts with opt9" + arg_vartype: UnsignedLongLong + source: [ cli, ini, yaml ] + + "test.config.opt10a": + short_name: testConfigOpt10a + description: "Integer from 0 to 100 exclusive" + arg_vartype: Int + source: cli + validator: + gt: 0 + lt: 100 + + "test.config.opt10b": + short_name: testConfigOpt10b + description: "Integer from 0 to 100 inclusive" + arg_vartype: Int + source: cli + validator: + gte: 0 + lte: 100 + + "test.config.opt11": + short_name: testConfigOpt11 + description: "Odd integer (callback test)" + arg_vartype: Int + source: cli + validator: + callback: "validateOdd" + + "test.config.opt12": + short_name: testConfigOpt12 + description: "Test declared storage" + arg_vartype: String + source: cli + cpp_vartype: std::string + cpp_varname: gTestConfigOpt12 + + "test.config.opt13": + description: "Test with single name" + short_name: testConfigOpt13 + single_name: o + arg_vartype: String + source: cli diff --git a/src/mongo/util/options_parser/constraints.h b/src/mongo/util/options_parser/constraints.h index 9c4f425e920..5537adfe937 100644 --- a/src/mongo/util/options_parser/constraints.h +++ b/src/mongo/util/options_parser/constraints.h @@ -30,6 +30,8 @@ #pragma once +#include <boost/optional.hpp> + #include "mongo/base/status.h" #include "mongo/bson/util/builder.h" #include "mongo/util/options_parser/environment.h" @@ -176,5 +178,84 @@ private: } }; +/** + * Proxy constraint for callbacks used by IDL based config options with a key. + * Callback may take either the entire environment, or just the value being validated. + */ +template <typename T> +class CallbackKeyConstraint : public KeyConstraint { +public: + using Callback = std::function<Status(const Environment&, const Key&)>; + using ValueCallback = std::function<Status(const T&)>; + + CallbackKeyConstraint(const Key& k, ValueCallback callback) + : KeyConstraint(k), _valueCallback(std::move(callback)) {} + CallbackKeyConstraint(const Key& k, Callback callback) + : KeyConstraint(k), _callback(std::move(callback)) {} + +private: + Status check(const Environment& env) override { + if (_callback) { + return _callback(env, _key); + } + + if (!_valueCallback) { + return Status::OK(); + } + + Value val; + auto status = env.get(_key, &val); + if (!status.isOK()) { + // Key not set, skipping callback constraint check. + return Status::OK(); + } + + T typedVal; + if (!val.get(&typedVal).isOK()) { + return {ErrorCodes::InternalError, + str::stream() << "Error: value for key: " << _key << " was found as type: " + << val.typeToString() + << " but is required to be type: " + << typeid(typedVal).name()}; + } + + return _valueCallback(typedVal); + } + + Callback _callback; + ValueCallback _valueCallback; +}; + +/** + * General boundary constraint for numeric type values. + */ +template <typename T> +class BoundaryKeyConstraint : public CallbackKeyConstraint<T> { +public: + BoundaryKeyConstraint(const Key& k, + const boost::optional<T>& gt, + const boost::optional<T>& lt, + const boost::optional<T>& gte, + const boost::optional<T>& lte) + : CallbackKeyConstraint<T>(k, [=](const T& val) -> Status { + if (gt && !(val > *gt)) { + return {ErrorCodes::BadValue, + str::stream() << k << " must be greater than " << *gt}; + } + if (lt && !(val < *lt)) { + return {ErrorCodes::BadValue, str::stream() << k << " must be less than " << *lt}; + } + if (gte && !(val >= *gte)) { + return {ErrorCodes::BadValue, + str::stream() << k << " must be greater than or equal to " << *gte}; + } + if (lte && !(val <= *lte)) { + return {ErrorCodes::BadValue, + str::stream() << k << " must be less than or equal to " << *lte}; + } + return Status::OK(); + }) {} +}; + } // namespace optionenvironment } // namespace mongo diff --git a/src/mongo/util/options_parser/option_description.h b/src/mongo/util/options_parser/option_description.h index 3869ad630fd..45979bc27ee 100644 --- a/src/mongo/util/options_parser/option_description.h +++ b/src/mongo/util/options_parser/option_description.h @@ -244,5 +244,49 @@ public: Canonicalize_t _canonicalize; }; +template <OptionType T> +struct OptionTypeMap; + +template <> +struct OptionTypeMap<StringVector> { + using type = std::vector<std::string>; +}; +template <> +struct OptionTypeMap<StringMap> { + using type = std::vector<std::string>; +}; +template <> +struct OptionTypeMap<Bool> { + using type = bool; +}; +template <> +struct OptionTypeMap<Double> { + using type = double; +}; +template <> +struct OptionTypeMap<Int> { + using type = int; +}; +template <> +struct OptionTypeMap<Long> { + using type = long; +}; +template <> +struct OptionTypeMap<String> { + using type = std::string; +}; +template <> +struct OptionTypeMap<UnsignedLongLong> { + using type = unsigned long long; +}; +template <> +struct OptionTypeMap<Unsigned> { + using type = unsigned; +}; +template <> +struct OptionTypeMap<Switch> { + using type = bool; +}; + } // namespace optionenvironment } // namespace mongo |