diff options
author | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2017-03-29 11:32:59 -0400 |
---|---|---|
committer | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2017-03-29 11:35:06 -0400 |
commit | 008a46edd04a5dca21f5aa61965b173bce109bbe (patch) | |
tree | dd33e3f7d81e8cbdf856dbf518675aaaab6982b8 /buildscripts/idl | |
parent | 97f86c66421ca3e16fbc260e833fd400d83b71c1 (diff) | |
download | mongo-008a46edd04a5dca21f5aa61965b173bce109bbe.tar.gz |
SERVER-28305 IDL Binder
Diffstat (limited to 'buildscripts/idl')
-rw-r--r-- | buildscripts/idl/idl/ast.py | 116 | ||||
-rw-r--r-- | buildscripts/idl/idl/binder.py | 313 | ||||
-rw-r--r-- | buildscripts/idl/idl/bson.py | 155 | ||||
-rw-r--r-- | buildscripts/idl/tests/test_binder.py | 542 | ||||
-rw-r--r-- | buildscripts/idl/tests/testcase.py | 40 |
5 files changed, 1166 insertions, 0 deletions
diff --git a/buildscripts/idl/idl/ast.py b/buildscripts/idl/idl/ast.py new file mode 100644 index 00000000000..ff55418a7a6 --- /dev/null +++ b/buildscripts/idl/idl/ast.py @@ -0,0 +1,116 @@ +# Copyright (C) 2017 MongoDB Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3, +# as published by the Free Software Foundation. +# +# 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 +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +""" +IDL AST classes. + +Represents the derived IDL specification after type resolution in the binding pass has occurred. + +This is a lossy translation from the IDL Syntax tree as the IDL AST only contains information about +the structs that need code generated for them, and just enough information to do that. +""" + +from __future__ import absolute_import, print_function, unicode_literals + +#from typing import List, Union, Any, Optional, Tuple + +from . import common +from . import errors + + +class IDLBoundSpec(object): + """A bound IDL document or a set of errors if parsing failed.""" + + def __init__(self, spec, error_collection): + # type: (IDLAST, errors.ParserErrorCollection) -> None + """Must specify either an IDL document or errors, not both.""" + assert (spec is None and error_collection is not None) or (spec is not None and + error_collection is None) + self.spec = spec + self.errors = error_collection + + +class IDLAST(object): + """The in-memory representation of an IDL file.""" + + def __init__(self): + # type: () -> None + """Construct an IDLAST.""" + self.globals = None # type: Global + self.structs = [] # type: List[Struct] + + +class Global(common.SourceLocation): + """ + IDL global object container. + + cpp_namespace and cpp_includes are only populated if the IDL document contains these YAML nodes. + """ + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a Global.""" + self.cpp_namespace = None # type: unicode + self.cpp_includes = [] # type: List[unicode] + super(Global, self).__init__(file_name, line, column) + + +class Struct(common.SourceLocation): + """ + IDL struct information. + + All fields are either required or have a non-None default. + """ + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a struct.""" + self.name = None # type: unicode + self.description = None # type: unicode + self.strict = True # type: bool + self.fields = [] # type: List[Field] + super(Struct, self).__init__(file_name, line, column) + + +class Field(common.SourceLocation): + """ + An instance of a field in a struct. + + Name is always populated. + A struct will either have a struct_type or a cpp_type, but not both. + Not all fields are set, it depends on the input document. + """ + + # pylint: disable=too-many-instance-attributes + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a Field.""" + self.name = None # type: unicode + self.description = None # type: unicode + self.optional = False # type: bool + self.ignore = False # type: bool + + # Properties specific to fields which are types. + self.cpp_type = None # type: unicode + self.bson_serialization_type = None # type: List[unicode] + self.serializer = None # type: unicode + self.deserializer = None # type: unicode + self.bindata_subtype = None # type: unicode + self.default = None # type: unicode + + # Properties specific to fields with are structs. + self.struct_type = None # type: unicode + + super(Field, self).__init__(file_name, line, column) diff --git a/buildscripts/idl/idl/binder.py b/buildscripts/idl/idl/binder.py new file mode 100644 index 00000000000..067eea93364 --- /dev/null +++ b/buildscripts/idl/idl/binder.py @@ -0,0 +1,313 @@ +# Copyright (C) 2017 MongoDB Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3, +# as published by the Free Software Foundation. +# +# 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 +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +"""Transform idl.syntax trees from the parser into well-defined idl.ast trees.""" + +from __future__ import absolute_import, print_function, unicode_literals + +import re +# from typing import Union + +from . import ast +from . import bson +from . import errors +from . import syntax + + +def _validate_single_bson_type(ctxt, idl_type, syntax_type): + # type: (errors.ParserContext, Union[syntax.Type, ast.Field], unicode) -> bool + """Validate bson serialization type is correct for a type.""" + bson_type = idl_type.bson_serialization_type[0] + + # Any is only valid if it is the only bson type specified + if bson_type == "any": + return True + + if not bson.is_valid_bson_type(bson_type): + ctxt.add_bad_bson_type_error(idl_type, syntax_type, idl_type.name, bson_type) + return False + + # Validate bindata_subytpe + if bson_type == "bindata": + subtype = idl_type.bindata_subtype + + if subtype is None: + subtype = "<unknown>" + + if not bson.is_valid_bindata_subtype(subtype): + ctxt.add_bad_bson_bindata_subtype_value_error(idl_type, syntax_type, idl_type.name, + subtype) + elif idl_type.bindata_subtype is not None: + ctxt.add_bad_bson_bindata_subtype_error(idl_type, syntax_type, idl_type.name, bson_type) + + return True + + +def _validate_bson_types_list(ctxt, idl_type, syntax_type): + # type: (errors.ParserContext, Union[syntax.Type, ast.Field], unicode) -> bool + """Validate bson serialization type(s) is correct for a type.""" + + bson_types = idl_type.bson_serialization_type + if len(bson_types) == 1: + return _validate_single_bson_type(ctxt, idl_type, syntax_type) + + for bson_type in bson_types: + if bson_type == "any": + ctxt.add_bad_any_type_use_error(idl_type, syntax_type, idl_type.name) + return False + + if not bson.is_valid_bson_type(bson_type): + ctxt.add_bad_bson_type_error(idl_type, syntax_type, idl_type.name, bson_type) + return False + + # V1 restiction: cannot mix bindata into list of types + if bson_type == "bindata": + ctxt.add_bad_bson_type_error(idl_type, syntax_type, idl_type.name, bson_type) + return False + + # Cannot mix non-scalar types into the list of types + if not bson.is_scalar_bson_type(bson_type): + ctxt.add_bad_bson_scalar_type_error(idl_type, syntax_type, idl_type.name, bson_type) + return False + + return True + + +def _validate_type(ctxt, idl_type): + # type: (errors.ParserContext, syntax.Type) -> None + """Validate each type is correct.""" + + # Validate naming restrictions + if idl_type.name.startswith("array"): + ctxt.add_array_not_valid_error(idl_type, "type", idl_type.name) + + _validate_type_properties(ctxt, idl_type, 'type') + + +def _validate_cpp_type(ctxt, idl_type, syntax_type): + # type: (errors.ParserContext, Union[syntax.Type, ast.Field], unicode) -> None + """Validate the cpp_type is correct.""" + + # Validate cpp_type + # Do not allow StringData, use std::string instead. + if "StringData" in idl_type.cpp_type: + ctxt.add_no_string_data_error(idl_type, syntax_type, idl_type.name) + + # We do not support C++ char and float types for style reasons + if idl_type.cpp_type in ['char', 'wchar_t', 'char16_t', 'char32_t', 'float']: + ctxt.add_bad_cpp_numeric_type_use_error(idl_type, syntax_type, idl_type.name, + idl_type.cpp_type) + + # We do not support C++ builtin integer for style reasons + for numeric_word in ['signed', "unsigned", "int", "long", "short"]: + if re.search(r'\b%s\b' % (numeric_word), idl_type.cpp_type): + ctxt.add_bad_cpp_numeric_type_use_error(idl_type, syntax_type, idl_type.name, + idl_type.cpp_type) + # Return early so we only throw one error for types like "signed short int" + return + + # Check for std fixed integer types which are allowed + if idl_type.cpp_type in ["std::int32_t", "std::int64_t", "std::uint32_t", "std::uint64_t"]: + return + + # Check for std fixed integer types which are not allowed. These are not allowed even if they + # have the "std::" prefix. + for std_numeric_type in [ + "int8_t", "int16_t", "int32_t", "int64_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t" + ]: + if std_numeric_type in idl_type.cpp_type: + ctxt.add_bad_cpp_numeric_type_use_error(idl_type, syntax_type, idl_type.name, + idl_type.cpp_type) + return + + +def _validate_type_properties(ctxt, idl_type, syntax_type): + # type: (errors.ParserContext, Union[syntax.Type, ast.Field], unicode) -> None + """Validate each type or field is correct.""" + + # Validate bson type restrictions + if not _validate_bson_types_list(ctxt, idl_type, syntax_type): + return + + if len(idl_type.bson_serialization_type) == 1: + bson_type = idl_type.bson_serialization_type[0] + if bson_type == "any": + # For any, a deserialer is required but the user can try to get away with the default + # serialization for their C++ type. + if idl_type.deserializer is None: + ctxt.add_missing_ast_required_field_error(idl_type, syntax_type, idl_type.name, + "deserializer") + elif bson_type == "object": + if idl_type.deserializer is None: + ctxt.add_missing_ast_required_field_error(idl_type, syntax_type, idl_type.name, + "deserializer") + + if idl_type.serializer is None: + ctxt.add_missing_ast_required_field_error(idl_type, syntax_type, idl_type.name, + "serializer") + elif not bson_type == "string": + if idl_type.deserializer is not None and "BSONElement" not in idl_type.deserializer: + ctxt.add_not_custom_scalar_serialization_not_supported_error( + idl_type, syntax_type, idl_type.name, bson_type) + + if idl_type.serializer is not None: + ctxt.add_not_custom_scalar_serialization_not_supported_error( + idl_type, syntax_type, idl_type.name, bson_type) + else: + # Now, this is a list of scalar types + if idl_type.deserializer is None: + ctxt.add_missing_ast_required_field_error(idl_type, syntax_type, idl_type.name, + "deserializer") + + _validate_cpp_type(ctxt, idl_type, syntax_type) + + +def _validate_types(ctxt, parsed_spec): + # type: (errors.ParserContext, syntax.IDLSpec) -> None + """Validate all types are correct.""" + + for idl_type in parsed_spec.symbols.types: + _validate_type(ctxt, idl_type) + + +def _bind_struct(ctxt, parsed_spec, struct): + # type: (errors.ParserContext, syntax.IDLSpec, syntax.Struct) -> ast.Struct + """ + Bind a struct. + + - Validating a struct and fields. + - Create the idl.ast version from the idl.syntax tree. + """ + + ast_struct = ast.Struct(struct.file_name, struct.line, struct.column) + ast_struct.name = struct.name + ast_struct.description = struct.description + ast_struct.strict = struct.strict + + # Validate naming restrictions + if ast_struct.name.startswith("array"): + ctxt.add_array_not_valid_error(ast_struct, "struct", ast_struct.name) + + for field in struct.fields: + ast_field = _bind_field(ctxt, parsed_spec, field) + if ast_field: + ast_struct.fields.append(ast_field) + + return ast_struct + + +def _validate_ignored_field(ctxt, field): + # type: (errors.ParserContext, syntax.Field) -> None + """Validate that for ignored fields, no other properties are set.""" + if field.optional: + ctxt.add_ignored_field_must_be_empty_error(field, field.name, "optional") + if field.default is not None: + ctxt.add_ignored_field_must_be_empty_error(field, field.name, "default") + + +def _validate_field_of_type_struct(ctxt, field): + # type: (errors.ParserContext, syntax.Field) -> None + """Validate that for fields with a type of struct, no other properties are set.""" + if field.default is not None: + ctxt.add_ignored_field_must_be_empty_error(field, field.name, "default") + + +def _bind_field(ctxt, parsed_spec, field): + # type: (errors.ParserContext, syntax.IDLSpec, syntax.Field) -> ast.Field + """ + Bind a field from the idl.syntax tree. + + - Create the idl.ast version from the idl.syntax tree. + - Validate the resulting type is correct. + """ + ast_field = ast.Field(field.file_name, field.line, field.column) + ast_field.name = field.name + ast_field.description = field.description + ast_field.optional = field.optional + + # Validate naming restrictions + if ast_field.name.startswith("array"): + ctxt.add_array_not_valid_error(ast_field, "field", ast_field.name) + + if field.ignore: + ast_field.ignore = field.ignore + _validate_ignored_field(ctxt, field) + return ast_field + + # TODO: support array + (struct, idltype) = parsed_spec.symbols.resolve_field_type(ctxt, field) + if not struct and not idltype: + return None + + # Copy over only the needed information if this a struct or a type + if struct: + ast_field.struct_type = struct.name + ast_field.bson_serialization_type = ["object"] + _validate_field_of_type_struct(ctxt, field) + else: + # Produce the union of type information for the type and this field. + + # Copy over the type fields first + ast_field.cpp_type = idltype.cpp_type + ast_field.bson_serialization_type = idltype.bson_serialization_type + ast_field.bindata_subtype = idltype.bindata_subtype + ast_field.serializer = idltype.serializer + ast_field.deserializer = idltype.deserializer + ast_field.default = idltype.default + + if field.default: + ast_field.default = field.default + + # Validate merged type + _validate_type_properties(ctxt, ast_field, "field") + + return ast_field + + +def _bind_globals(parsed_spec): + # type: (syntax.IDLSpec) -> ast.Global + """Bind the globals object from the idl.syntax tree into the idl.ast tree by doing a deep copy.""" + if parsed_spec.globals: + ast_global = ast.Global(parsed_spec.globals.file_name, parsed_spec.globals.line, + parsed_spec.globals.column) + ast_global.cpp_namespace = parsed_spec.globals.cpp_namespace + ast_global.cpp_includes = parsed_spec.globals.cpp_includes + else: + ast_global = ast.Global("<implicit>", 0, 0) + + # If no namespace has been set, default it do "mongo" + ast_global.cpp_namespace = "mongo" + + return ast_global + + +def bind(parsed_spec): + # type: (syntax.IDLSpec) -> ast.IDLBoundSpec + """Read an idl.syntax, create an idl.ast tree, and validate the final IDL Specification.""" + + ctxt = errors.ParserContext("unknown", errors.ParserErrorCollection()) + + bound_spec = ast.IDLAST() + + bound_spec.globals = _bind_globals(parsed_spec) + + _validate_types(ctxt, parsed_spec) + + for struct in parsed_spec.symbols.structs: + bound_spec.structs.append(_bind_struct(ctxt, parsed_spec, struct)) + + if ctxt.errors.has_errors(): + return ast.IDLBoundSpec(None, ctxt.errors) + else: + return ast.IDLBoundSpec(bound_spec, None) diff --git a/buildscripts/idl/idl/bson.py b/buildscripts/idl/idl/bson.py new file mode 100644 index 00000000000..570a2cecab2 --- /dev/null +++ b/buildscripts/idl/idl/bson.py @@ -0,0 +1,155 @@ +# Copyright (C) 2017 MongoDB Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3, +# as published by the Free Software Foundation. +# +# 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 +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +""" +BSON Type Information. + +Utilities for validating bson types, etc. +""" + +from __future__ import absolute_import, print_function, unicode_literals + +# from typing import Dict, List + +# Dictionary of BSON type Information +# scalar: True if the type is not an array or object +# bson_type_enum: The BSONType enum value for the given type +_BSON_TYPE_INFORMATION = { + "double": { + 'scalar': True, + 'bson_type_enum': 'NumberDouble' + }, + "string": { + 'scalar': True, + 'bson_type_enum': 'String' + }, + "object": { + 'scalar': False, + 'bson_type_enum': 'Object' + }, + # TODO: add support: "array" : { 'scalar' : False, 'bson_type_enum' : 'Array'}, + "bindata": { + 'scalar': True, + 'bson_type_enum': 'BinData' + }, + "undefined": { + 'scalar': True, + 'bson_type_enum': 'Undefined' + }, + "objectid": { + 'scalar': True, + 'bson_type_enum': 'jstOID' + }, + "bool": { + 'scalar': True, + 'bson_type_enum': 'Bool' + }, + "date": { + 'scalar': True, + 'bson_type_enum': 'Date' + }, + "null": { + 'scalar': True, + 'bson_type_enum': 'jstNULL' + }, + "regex": { + 'scalar': True, + 'bson_type_enum': 'RegEx' + }, + "int": { + 'scalar': True, + 'bson_type_enum': 'NumberInt' + }, + "timestamp": { + 'scalar': True, + 'bson_type_enum': 'bsonTimestamp' + }, + "long": { + 'scalar': True, + 'bson_type_enum': 'NumberLong' + }, + "decimal": { + 'scalar': True, + 'bson_type_enum': 'NumberDecimal' + }, +} + +# Dictionary of BinData subtype type Information +# scalar: True if the type is not an array or object +# bindata_enum: The BinDataType enum value for the given type +_BINDATA_SUBTYPE = { + "generic": { + 'scalar': True, + 'bindata_enum': 'BinDataGeneral' + }, + "function": { + 'scalar': True, + 'bindata_enum': 'Function' + }, + "binary": { + 'scalar': False, + 'bindata_enum': 'ByteArrayDeprecated' + }, + "uuid_old": { + 'scalar': False, + 'bindata_enum': 'bdtUUID' + }, + "uuid": { + 'scalar': True, + 'bindata_enum': 'newUUID' + }, + "md5": { + 'scalar': True, + 'bindata_enum': 'MD5Type' + }, +} + + +def is_valid_bson_type(name): + # type: (unicode) -> bool + """Return True if this is a valid bson type.""" + return name in _BSON_TYPE_INFORMATION + + +def is_scalar_bson_type(name): + # type: (unicode) -> bool + """Return True if this bson type is a scalar.""" + assert is_valid_bson_type(name) + return _BSON_TYPE_INFORMATION[name]['scalar'] # type: ignore + + +def cpp_bson_type_name(name): + # type: (unicode) -> unicode + """Return the C++ type name for a bson type.""" + assert is_valid_bson_type(name) + return _BSON_TYPE_INFORMATION[name]['bson_type_enum'] # type: ignore + + +def list_valid_types(): + # type: () -> List[unicode] + """Return a list of supported bson types.""" + return [a for a in _BSON_TYPE_INFORMATION.iterkeys()] + + +def is_valid_bindata_subtype(name): + # type: (unicode) -> bool + """Return True if this bindata subtype is valid.""" + return name in _BINDATA_SUBTYPE + + +def cpp_bindata_subtype_type_name(name): + # type: (unicode) -> unicode + """Return the C++ type name for a bindata subtype.""" + assert is_valid_bindata_subtype(name) + return _BINDATA_SUBTYPE[name]['bindata_enum'] # type: ignore diff --git a/buildscripts/idl/tests/test_binder.py b/buildscripts/idl/tests/test_binder.py new file mode 100644 index 00000000000..6048f136a20 --- /dev/null +++ b/buildscripts/idl/tests/test_binder.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python2 +# Copyright (C) 2017 MongoDB Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3, +# as published by the Free Software Foundation. +# +# 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 +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +"""Test cases for IDL binder.""" + +from __future__ import absolute_import, print_function, unicode_literals + +import textwrap +import unittest + +# import package so that it works regardless of whether we run as a module or file +if __package__ is None: + import sys + from os import path + sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) + from context import idl + import testcase +else: + from .context import idl + from . import testcase + +# All YAML tests assume 4 space indent +INDENT_SPACE_COUNT = 4 + + +def fill_spaces(count): + # type: (int) -> unicode + """Fill a string full of spaces.""" + fill = '' + for _ in range(count * INDENT_SPACE_COUNT): + fill += ' ' + + return fill + + +def indent_text(count, unindented_text): + # type: (int, unicode) -> unicode + """Indent each line of a multi-line string.""" + lines = unindented_text.splitlines() + fill = fill_spaces(count) + return '\n'.join(fill + line for line in lines) + + +class TestBinder(testcase.IDLTestcase): + """Test cases for the IDL binder.""" + + def test_empty(self): + # type: () -> None + """Test an empty document works.""" + self.assert_bind("") + + def test_global_positive(self): + # type: () -> None + """Postive global tests.""" + spec = self.assert_bind( + textwrap.dedent(""" + global: + cpp_namespace: 'something' + cpp_includes: + - 'bar' + - 'foo'""")) + self.assertEquals(spec.globals.cpp_namespace, "something") + self.assertListEqual(spec.globals.cpp_includes, ['bar', 'foo']) + + def test_type_positive(self): + # type: () -> None + """Positive type tests.""" + self.assert_bind( + textwrap.dedent(""" + types: + foo: + description: foo + cpp_type: foo + bson_serialization_type: string + serializer: foo + deserializer: foo + default: foo + """)) + + # Test supported types + for bson_type in [ + "bool", "date", "null", "decimal", "double", "int", "long", "objectid", "regex", + "string", "timestamp", "undefined" + ]: + self.assert_bind( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: %s + default: foo + """ % (bson_type))) + + # Test supported numeric types + for cpp_type in [ + "std::int32_t", + "std::uint32_t", + "std::int32_t", + "std::uint64_t", + ]: + self.assert_bind( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: %s + bson_serialization_type: int + """ % (cpp_type))) + + # Test object + self.assert_bind( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: object + serializer: foo + deserializer: foo + default: foo + """)) + + # Test object + self.assert_bind( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: any + serializer: foo + deserializer: foo + default: foo + """)) + + # Test supported bindata_subtype + for bindata_subtype in ["generic", "function", "binary", "uuid_old", "uuid", "md5"]: + self.assert_bind( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: bindata + bindata_subtype: %s + default: foo + """ % (bindata_subtype))) + + def test_type_negative(self): + # type: () -> None + """Negative type tests for properties that types and fields share.""" + + # Test bad bson type name + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: foo + """), idl.errors.ERROR_ID_BAD_BSON_TYPE) + + # Test bad cpp_type name + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: StringData + bson_serialization_type: string + """), idl.errors.ERROR_ID_NO_STRINGDATA) + + # Test unsupported serialization + for cpp_type in [ + "char", + "signed char", + "unsigned char", + "signed short int", + "short int", + "short", + "signed short", + "unsigned short", + "unsigned short int", + "signed int", + "signed", + "unsigned int", + "unsigned", + "signed long int", + "signed long", + "int", + "long int", + "long", + "unsigned long int", + "unsigned long", + "signed long long int", + "signed long long", + "long long int", + "long long", + "unsigned long int", + "unsigned long", + "wchar_t", + "char16_t", + "char32_t", + "int8_t", + "int16_t", + "int32_t", + "int64_t", + "uint8_t", + "uint16_t", + "uint32_t", + "uint64_t", + ]: + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: %s + bson_serialization_type: int + """ % (cpp_type)), idl.errors.ERROR_ID_BAD_NUMERIC_CPP_TYPE) + + # Test the std prefix 8 and 16-byte integers fail + for std_cpp_type in ["std::int8_t", "std::int16_t", "std::uint8_t", "std::uint16_t"]: + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: %s + bson_serialization_type: int + """ % (cpp_type)), idl.errors.ERROR_ID_BAD_NUMERIC_CPP_TYPE) + + # Test bindata_subtype missing + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: bindata + """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_VALUE) + + # Test bindata_subtype wrong + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: bindata + bindata_subtype: foo + """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_VALUE) + + # Test bindata_subtype on wrong type + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: string + bindata_subtype: generic + """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_TYPE) + + # Test bindata in list of types + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: + - bindata + - string + """), idl.errors.ERROR_ID_BAD_BSON_TYPE) + + # Test bindata in list of types + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: StringData + bson_serialization_type: + - bindata + - string + """), idl.errors.ERROR_ID_BAD_BSON_TYPE) + + # Test any in list of types + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: + - any + - int + """), idl.errors.ERROR_ID_BAD_ANY_TYPE_USE) + + # Test unsupported serialization + for bson_type in [ + "bool", "date", "null", "decimal", "double", "int", "long", "objectid", "regex", + "timestamp", "undefined" + ]: + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: std::string + bson_serialization_type: %s + serializer: foo + """ % (bson_type)), + idl.errors.ERROR_ID_CUSTOM_SCALAR_SERIALIZATION_NOT_SUPPORTED) + + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: std::string + bson_serialization_type: %s + deserializer: foo + """ % (bson_type)), + idl.errors.ERROR_ID_CUSTOM_SCALAR_SERIALIZATION_NOT_SUPPORTED) + + # Test object serialization needs deserializer & serializer + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: object + serializer: foo + """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD) + + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: object + deserializer: foo + """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD) + + # Test any serialization needs deserializer + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: foo + bson_serialization_type: any + """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD) + + # Test list of bson types needs deserializer + self.assert_bind_fail( + textwrap.dedent(""" + types: + foofoo: + description: foo + cpp_type: std::int32_t + bson_serialization_type: + - int + - string + """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD) + + # Test array as name + self.assert_bind_fail( + textwrap.dedent(""" + types: + array<foo>: + description: foo + cpp_type: foo + bson_serialization_type: string + """), idl.errors.ERROR_ID_ARRAY_NOT_VALID_TYPE) + + def test_struct_positive(self): + # type: () -> None + """Positive struct tests.""" + + # Setup some common types + test_preamble = textwrap.dedent(""" + types: + string: + description: foo + cpp_type: foo + bson_serialization_type: string + serializer: foo + deserializer: foo + default: foo + """) + + self.assert_bind(test_preamble + textwrap.dedent(""" + structs: + foo: + description: foo + strict: true + fields: + foo: string + """)) + + def test_struct_negative(self): + # type: () -> None + """Negative struct tests.""" + + # Setup some common types + test_preamble = textwrap.dedent(""" + types: + string: + description: foo + cpp_type: foo + bson_serialization_type: string + serializer: foo + deserializer: foo + default: foo + """) + + # Test array as name + self.assert_bind_fail(test_preamble + textwrap.dedent(""" + structs: + array<foo>: + description: foo + strict: true + fields: + foo: string + """), idl.errors.ERROR_ID_ARRAY_NOT_VALID_TYPE) + + def test_field_positive(self): + # type: () -> None + """Positive test cases for field.""" + + # Setup some common types + test_preamble = textwrap.dedent(""" + types: + string: + description: foo + cpp_type: foo + bson_serialization_type: string + serializer: foo + deserializer: foo + default: foo + """) + + # Short type + self.assert_bind(test_preamble + textwrap.dedent(""" + structs: + bar: + description: foo + strict: false + fields: + foo: string + """)) + + # Long type + self.assert_bind(test_preamble + textwrap.dedent(""" + structs: + bar: + description: foo + strict: false + fields: + foo: + type: string + """)) + + # Long type with default + self.assert_bind(test_preamble + textwrap.dedent(""" + structs: + bar: + description: foo + strict: false + fields: + foo: + type: string + default: bar + """)) + + def test_field_negative(self): + # type: () -> None + """Negative field tests.""" + + # Setup some common types + test_preamble = textwrap.dedent(""" + types: + string: + description: foo + cpp_type: foo + bson_serialization_type: string + serializer: foo + deserializer: foo + default: foo + """) + + # Test array as field name + self.assert_bind_fail(test_preamble + textwrap.dedent(""" + structs: + foo: + description: foo + strict: true + fields: + array<foo>: string + """), idl.errors.ERROR_ID_ARRAY_NOT_VALID_TYPE) + + def test_ignored_field_negative(self): + # type: () -> None + """Test that if a field is marked as ignored, no other properties are set.""" + for test_value in [ + "optional: true", + ]: + self.assert_bind_fail( + textwrap.dedent(""" + structs: + foo: + description: foo + strict: false + fields: + foo: + type: string + ignore: true + %s + """ % (test_value)), idl.errors.ERROR_ID_FIELD_MUST_BE_EMPTY_FOR_IGNORED) + + +if __name__ == '__main__': + + unittest.main() diff --git a/buildscripts/idl/tests/testcase.py b/buildscripts/idl/tests/testcase.py index 18e4ac32960..d3def880db0 100644 --- a/buildscripts/idl/tests/testcase.py +++ b/buildscripts/idl/tests/testcase.py @@ -85,3 +85,43 @@ class IDLTestcase(unittest.TestCase): "For document:\n%s\nExpected error message '%s' but received only errors:\n %s" % (doc_str, error_id, errors_to_str(parsed_doc.errors))) + def assert_bind(self, doc_str): + # type: (unicode) -> idl.ast.IDLBoundSpec + """Assert a document parsed and bound correctly by the IDL compiler and returned no errors.""" + parsed_doc = self._parse(doc_str) + self._assert_parse(doc_str, parsed_doc) + + bound_doc = idl.binder.bind(parsed_doc.spec) + + self.assertIsNone(bound_doc.errors, + "Expected no binder errors\nFor document:\n%s\nReceived errors:\n\n%s" % + (doc_str, errors_to_str(bound_doc.errors))) + self.assertIsNotNone(bound_doc.spec, "Expected a bound doc") + + return bound_doc.spec + + def assert_bind_fail(self, doc_str, error_id): + # type: (unicode, unicode) -> None + """ + Assert a document parsed correctly by the YAML parser and IDL parser, but not bound by the IDL binder. + + Asserts only one error is found in the document to make future IDL changes easier. + """ + parsed_doc = self._parse(doc_str) + self._assert_parse(doc_str, parsed_doc) + + bound_doc = idl.binder.bind(parsed_doc.spec) + + self.assertIsNone(bound_doc.spec, "Expected no bound doc\nFor document:\n%s\n" % (doc_str)) + self.assertIsNotNone(bound_doc.errors, "Expected binder errors") + + # Assert that negative test cases are only testing one fault in a test. + self.assertTrue( + bound_doc.errors.count() == 1, + "For document:\n%s\nExpected only error message '%s' but received multiple errors:\n\n%s" + % (doc_str, error_id, errors_to_str(bound_doc.errors))) + + self.assertTrue( + bound_doc.errors.contains(error_id), + "For document:\n%s\nExpected error message '%s' but received only errors:\n %s" % + (doc_str, error_id, errors_to_str(bound_doc.errors))) |