summaryrefslogtreecommitdiff
path: root/buildscripts/idl
diff options
context:
space:
mode:
authorMark Benvenuto <mark.benvenuto@mongodb.com>2017-03-29 11:32:59 -0400
committerMark Benvenuto <mark.benvenuto@mongodb.com>2017-03-29 11:35:06 -0400
commit008a46edd04a5dca21f5aa61965b173bce109bbe (patch)
treedd33e3f7d81e8cbdf856dbf518675aaaab6982b8 /buildscripts/idl
parent97f86c66421ca3e16fbc260e833fd400d83b71c1 (diff)
downloadmongo-008a46edd04a5dca21f5aa61965b173bce109bbe.tar.gz
SERVER-28305 IDL Binder
Diffstat (limited to 'buildscripts/idl')
-rw-r--r--buildscripts/idl/idl/ast.py116
-rw-r--r--buildscripts/idl/idl/binder.py313
-rw-r--r--buildscripts/idl/idl/bson.py155
-rw-r--r--buildscripts/idl/tests/test_binder.py542
-rw-r--r--buildscripts/idl/tests/testcase.py40
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)))