You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ariatosca.apache.org by em...@apache.org on 2017/08/30 22:13:21 UTC

[05/10] incubator-ariatosca git commit: Initial work on mechanism, imports, and metadata

Initial work on mechanism, imports, and metadata

* Metadata 'template_version' is now version data type
* Unicode support for version
* Better Unicode support for validation issues


Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/36e0aa56
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/36e0aa56
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/36e0aa56

Branch: refs/heads/ARIA-1-parser-test-suite
Commit: 36e0aa56615cbf0f836c2fdcfce6c21323b511aa
Parents: 969149b
Author: Tal Liron <ta...@gmail.com>
Authored: Thu Aug 17 17:50:27 2017 -0500
Committer: Tal Liron <ta...@gmail.com>
Committed: Wed Aug 30 10:40:50 2017 -0500

----------------------------------------------------------------------
 aria/modeling/service_common.py                 |   6 +-
 aria/parser/validation/issue.py                 |   4 +-
 .../simple_v1_0/data_types.py                   |  21 ++-
 .../aria_extension_tosca/simple_v1_0/misc.py    |   9 +-
 .../simple_v1_0/presentation/field_getters.py   |   6 +-
 tests/extensions/__init__.py                    |  14 ++
 .../extensions/aria_extension_tosca/__init__.py |  14 ++
 .../simple_v1_0/__init__.py                     |  14 ++
 .../simple_v1_0/conftest.py                     |  28 ++++
 .../simple_v1_0/test_imports.py                 | 152 +++++++++++++++++++
 .../simple_v1_0/test_metadata.py                | 124 +++++++++++++++
 tests/mechanisms/__init__.py                    |  14 ++
 tests/mechanisms/parsing/__init__.py            |  55 +++++++
 tests/mechanisms/parsing/aria.py                |  63 ++++++++
 tests/mechanisms/web_server.py                  |  75 +++++++++
 tests/requirements.txt                          |   1 +
 16 files changed, 578 insertions(+), 22 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/aria/modeling/service_common.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_common.py b/aria/modeling/service_common.py
index 478e530..0cc88f4 100644
--- a/aria/modeling/service_common.py
+++ b/aria/modeling/service_common.py
@@ -22,7 +22,8 @@ ARIA modeling service common module
 from sqlalchemy import (
     Column,
     Text,
-    Boolean
+    Boolean,
+    PickleType
 )
 from sqlalchemy.ext.declarative import declared_attr
 
@@ -587,12 +588,11 @@ class MetadataBase(TemplateModelMixin):
     :ivar name: name
     :vartype name: basestring
     :ivar value: value
-    :vartype value: basestring
     """
 
     __tablename__ = 'metadata'
 
-    value = Column(Text)
+    value = Column(PickleType)
 
     @property
     def as_raw(self):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/aria/parser/validation/issue.py
----------------------------------------------------------------------
diff --git a/aria/parser/validation/issue.py b/aria/parser/validation/issue.py
index 42fc580..cc68737 100644
--- a/aria/parser/validation/issue.py
+++ b/aria/parser/validation/issue.py
@@ -66,9 +66,9 @@ class Issue(object):
     def __init__(self, message=None, exception=None, location=None, line=None,
                  column=None, locator=None, snippet=None, level=0):
         if message is not None:
-            self.message = str(message)
+            self.message = unicode(message)
         elif exception is not None:
-            self.message = str(exception)
+            self.message = unicode(exception)
         else:
             self.message = 'unknown issue'
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/extensions/aria_extension_tosca/simple_v1_0/data_types.py
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/simple_v1_0/data_types.py b/extensions/aria_extension_tosca/simple_v1_0/data_types.py
index 513b517..23417b0 100644
--- a/extensions/aria_extension_tosca/simple_v1_0/data_types.py
+++ b/extensions/aria_extension_tosca/simple_v1_0/data_types.py
@@ -41,7 +41,7 @@ class Timezone(tzinfo):
         return self._offset
 
     def tzname(self, dt): # pylint: disable=unused-argument
-        return str(self._offset)
+        return unicode(self._offset)
 
     def dst(self, dt): # pylint: disable=unused-argument
         return Timezone._ZERO
@@ -75,7 +75,7 @@ class Timestamp(object):
     CANONICAL = '%Y-%m-%dT%H:%M:%S'
 
     def __init__(self, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument
-        value = str(value)
+        value = unicode(value)
         match = re.match(Timestamp.REGULAR_SHORT, value)
         if match is not None:
             # Parse short form
@@ -165,7 +165,7 @@ class Version(object):
 
     REGULAR = \
         r'^(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<fix>\d+)' + \
-        r'((\.(?P<qualifier>\d+))(\-(?P<build>\d+))?)?)?$'
+        r'((\.(?P<qualifier>\w+))(\-(?P<build>\d+))?)?)?$'
 
     @staticmethod
     def key(version):
@@ -175,8 +175,8 @@ class Version(object):
         return (version.major, version.minor, version.fix, version.qualifier, version.build)
 
     def __init__(self, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument
-        str_value = str(value)
-        match = re.match(Version.REGULAR, str_value)
+        str_value = unicode(value)
+        match = re.match(Version.REGULAR, str_value, flags=re.UNICODE)
         if match is None:
             raise ValueError(
                 'version must be formatted as <major_version>.<minor_version>'
@@ -193,8 +193,6 @@ class Version(object):
         if self.fix is not None:
             self.fix = int(self.fix)
         self.qualifier = match.group('qualifier')
-        if self.qualifier is not None:
-            self.qualifier = int(self.qualifier)
         self.build = match.group('build')
         if self.build is not None:
             self.build = int(self.build)
@@ -215,6 +213,7 @@ class Version(object):
         return (self.major, self.minor, self.fix, self.qualifier, self.build) == \
             (version.major, version.minor, version.fix, version.qualifier, version.build)
 
+    @implements_specification('3.2.2.1', 'tosca-simple-1.0')
     def __lt__(self, version):
         if self.major < version.major:
             return True
@@ -225,9 +224,7 @@ class Version(object):
                 if self.fix < version.fix:
                     return True
                 elif self.fix == version.fix:
-                    if self.qualifier < version.qualifier:
-                        return True
-                    elif self.qualifier == version.qualifier:
+                    if self.qualifier == version.qualifier:
                         if self.build < version.build:
                             return True
         return False
@@ -375,8 +372,8 @@ class Scalar(object):
         return scalar.value
 
     def __init__(self, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument
-        str_value = str(value)
-        match = re.match(self.REGULAR, str_value) # pylint: disable=no-member
+        str_value = unicode(value)
+        match = re.match(self.REGULAR, str_value, flags=re.UNICODE) # pylint: disable=no-member
         if match is None:
             raise ValueError('scalar must be formatted as <scalar> <unit>: %s' % safe_repr(value))
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/extensions/aria_extension_tosca/simple_v1_0/misc.py
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/simple_v1_0/misc.py b/extensions/aria_extension_tosca/simple_v1_0/misc.py
index 221163c..418fe31 100644
--- a/extensions/aria_extension_tosca/simple_v1_0/misc.py
+++ b/extensions/aria_extension_tosca/simple_v1_0/misc.py
@@ -20,14 +20,16 @@ from aria.parser import implements_specification
 from aria.parser.presentation import (AsIsPresentation, has_fields, allow_unknown_fields,
                                       short_form_field, primitive_field, primitive_list_field,
                                       primitive_dict_unknown_fields, object_field,
-                                      object_list_field, object_dict_field, field_validator,
-                                      type_validator)
+                                      object_list_field, object_dict_field, field_getter,
+                                      field_validator, type_validator)
 
+from .data_types import Version
 from .modeling.data_types import (get_data_type, get_data_type_value, get_property_constraints,
                                   apply_constraint_to_value)
 from .modeling.substitution_mappings import (validate_substitution_mappings_requirement,
                                              validate_substitution_mappings_capability)
 from .presentation.extensible import ExtensiblePresentation
+from .presentation.field_getters import data_type_class_getter
 from .presentation.field_validators import (constraint_clause_field_validator,
                                             constraint_clause_in_range_validator,
                                             constraint_clause_valid_values_validator,
@@ -79,6 +81,7 @@ class MetaData(ExtensiblePresentation):
         as a single-line string value.
         """
 
+    @field_getter(data_type_class_getter(Version))
     @primitive_field(str)
     @implements_specification('3.9.3.5', 'tosca-simple-1.0')
     def template_version(self):
@@ -87,7 +90,7 @@ class MetaData(ExtensiblePresentation):
         service template as a single-line string value.
         """
 
-    @primitive_dict_unknown_fields()
+    @primitive_dict_unknown_fields(str)
     def custom(self):
         """
         :type: dict

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/extensions/aria_extension_tosca/simple_v1_0/presentation/field_getters.py
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/simple_v1_0/presentation/field_getters.py b/extensions/aria_extension_tosca/simple_v1_0/presentation/field_getters.py
index f14164a..34dacd6 100644
--- a/extensions/aria_extension_tosca/simple_v1_0/presentation/field_getters.py
+++ b/extensions/aria_extension_tosca/simple_v1_0/presentation/field_getters.py
@@ -14,6 +14,7 @@
 # limitations under the License.
 
 from aria.utils.formatting import safe_repr
+from aria.utils.type import full_type_name
 from aria.parser.exceptions import InvalidValueError
 
 
@@ -31,7 +32,8 @@ def data_type_class_getter(cls):
                 return cls(None, None, raw, None)
             except ValueError as e:
                 raise InvalidValueError(
-                    '%s is not a valid "%s" in "%s": %s'
-                    % (field.full_name, field.full_cls_name, presentation._name, safe_repr(raw)),
+                    '{0} is not a valid "{1}" in "{2}": {3}'
+                    .format(field.full_name, full_type_name(cls), presentation._name,
+                            safe_repr(raw)),
                     cause=e, locator=field.get_locator(raw))
     return getter

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/extensions/__init__.py
----------------------------------------------------------------------
diff --git a/tests/extensions/__init__.py b/tests/extensions/__init__.py
new file mode 100644
index 0000000..ae1e83e
--- /dev/null
+++ b/tests/extensions/__init__.py
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/extensions/aria_extension_tosca/__init__.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/__init__.py b/tests/extensions/aria_extension_tosca/__init__.py
new file mode 100644
index 0000000..ae1e83e
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/__init__.py
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/extensions/aria_extension_tosca/simple_v1_0/__init__.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/simple_v1_0/__init__.py b/tests/extensions/aria_extension_tosca/simple_v1_0/__init__.py
new file mode 100644
index 0000000..ae1e83e
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/__init__.py
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/extensions/aria_extension_tosca/simple_v1_0/conftest.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/simple_v1_0/conftest.py b/tests/extensions/aria_extension_tosca/simple_v1_0/conftest.py
new file mode 100644
index 0000000..86bbc3f
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/conftest.py
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from ....mechanisms.parsing.aria import AriaParser
+
+
+def pytest_report_header(config):
+    return 'parser: ARIA'
+
+
+@pytest.fixture(scope='session')
+def parser():
+    with AriaParser() as p:
+        yield p

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/extensions/aria_extension_tosca/simple_v1_0/test_imports.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/simple_v1_0/test_imports.py b/tests/extensions/aria_extension_tosca/simple_v1_0/test_imports.py
new file mode 100644
index 0000000..4d78f40
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/test_imports.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from ....mechanisms.web_server import WebServer
+
+
+NODE_TYPE_IMPORT = """
+node_types:
+  MyNode:
+    derived_from: tosca.nodes.Root
+"""
+
+BAD_IMPORT = """
+node_types:
+  MyNode:
+    derived_from: not.a.node.type
+"""
+
+@pytest.fixture(scope='session')
+def repository():
+    repository = WebServer()
+    repository.add_text_yaml('/imports/node-type.yaml', NODE_TYPE_IMPORT)
+    repository.add_text_yaml('/imports/{0}.yaml'.format(WebServer.escape('詠嘆調')),
+                             NODE_TYPE_IMPORT)
+    repository.add_text_yaml('/imports/bad.yaml', BAD_IMPORT)
+    repository.start()
+    yield repository.root
+    repository.stop()
+
+
+# Syntax
+
+@pytest.mark.parametrize('value', ('null', 'a_string', '123', '0.123', '{}'))
+def test_imports_wrong_yaml_type(parser, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports: {{ value }}
+""", dict(value=value)).assert_failure()
+
+
+def test_imports_empty_list(parser):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports: []
+""").assert_success()
+
+
+# Variants
+
+def test_import_single_short_form(parser, repository):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - {{ repository }}/imports/node-type.yaml
+topology_template:
+  node_templates:
+    my_node:
+      type: MyNode
+""", dict(repository=repository)).assert_success()
+
+
+def test_import_single_short_form_unicode(parser, repository):
+    parser.parse_literal(u"""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - {{ repository }}/imports/詠嘆調.yaml
+topology_template:
+  node_templates:
+    my_node:
+      type: MyNode
+""", dict(repository=repository)).assert_success()
+
+
+def test_import_single_long_form(parser, repository):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - file: {{ repository }}/imports/node-type.yaml
+topology_template:
+  node_templates:
+    my_node:
+      type: MyNode
+""", dict(repository=repository)).assert_success()
+
+
+@pytest.mark.skip(reason='not yet supported')
+def test_import_single_repository(parser, repository):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+repositories:
+  myrepository:
+    url: {{ repository }}/imports/
+imports:
+  - file: node-type.yaml
+    repository: myrepository
+topology_template:
+  node_templates:
+    my_node:
+      type: MyNode
+""", dict(repository=repository)).assert_success()
+
+
+@pytest.mark.skip(reason='not yet supported')
+def test_import_single_namespace(parser, repository):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - file: {{ repository }}/imports/node-type.yaml
+    namespace_uri:
+    namespace_prefix: my_namespace
+topology_template:
+  node_templates:
+    my_node:
+      type: my_namespace.MyNode
+""", dict(repository=repository)).assert_success()
+
+
+# Failures
+
+def test_import_not_found(parser):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - does_not_exist
+""").assert_failure()
+
+
+def test_import_bad(parser, repository):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - {{ repository }}/imports/bad.yaml
+topology_template:
+  node_templates:
+    my_node:
+      type: MyNode
+""", dict(repository=repository)).assert_failure()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/extensions/aria_extension_tosca/simple_v1_0/test_metadata.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/simple_v1_0/test_metadata.py b/tests/extensions/aria_extension_tosca/simple_v1_0/test_metadata.py
new file mode 100644
index 0000000..dae5631
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/test_metadata.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+# Syntax
+
+@pytest.mark.parametrize('value', ('null', 'a_string', '123', '0.123', '[]'))
+def test_metadata_wrong_yaml_type(parser, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata: {{ value }}
+""", dict(value=value)).assert_failure()
+
+
+@pytest.mark.parametrize('field,value', (
+    ('template_name', '123'),
+    ('template_name', '0.123'),
+    ('template_name', '[]'),
+    ('template_name', '{}'),
+    ('template_author', '123'),
+    ('template_author', '0.123'),
+    ('template_author', '[]'),
+    ('template_author', '{}'),
+    ('template_version', '123'),
+    ('template_version', '0.123'),
+    ('template_version', '[]'),
+    ('template_version', '{}')))
+def test_metadata_normative_wrong_yaml_type(parser, field, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata:
+    {{ field }}: {{ value }}
+""", dict(field=field, value=value)).assert_failure()
+
+
+@pytest.mark.parametrize('value', ('123', '0.123', '[]', '{}'))
+def test_metadata_non_normative_wrong_yaml_type(parser, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata:
+    non_normative: {{ value }}
+""", dict(value=value)).assert_failure()
+
+
+def test_metadata_empty_dict(parser):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata: {}
+""").assert_success()
+
+
+# Normative
+
+@pytest.mark.parametrize('value', ('null', 'a_string', '1.2.3.4.5'))
+def test_metadata_normative_template_bad_version(parser, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata:
+    template_version: {{ value }}
+""", dict(value=value)).assert_failure()
+
+
+@pytest.mark.parametrize('value', ("'6.1'", '2.0.1', '3.1.0.beta', "'1.0.0.alpha-10'"))
+def test_metadata_normative_template_version(parser, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata:
+    template_version: {{ value }}
+""", dict(value=value)).assert_success()
+
+# Non-normative
+
+def test_metadata_with_non_normative_fields(parser):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata:
+  template_name: name
+  template_author: author
+  template_version: 1.0.0.beta
+  non_normative1: non_normative1
+  non_normative2: non_normative2
+  non_normative3: non_normative3
+""").assert_success()
+
+
+def test_metadata_with_non_normative_fields_nulls(parser):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata:
+  template_name: null
+  template_author: null
+  template_version: 1.0.0.beta
+  non_normative1: null
+  non_normative2: null
+  non_normative3: null
+""").assert_success()
+
+
+def test_metadata_with_non_normative_fields_unicode(parser):
+    parser.parse_literal(u"""
+tosca_definitions_version: tosca_simple_yaml_1_0
+metadata:
+  template_name: 詠嘆調
+  template_author: 詠嘆調
+  template_version: 1.0.0.詠嘆調
+  non_normative1: 詠嘆調
+  non_normative2: 詠嘆調
+  non_normative3: 詠嘆調
+""").assert_success()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/mechanisms/__init__.py
----------------------------------------------------------------------
diff --git a/tests/mechanisms/__init__.py b/tests/mechanisms/__init__.py
new file mode 100644
index 0000000..ae1e83e
--- /dev/null
+++ b/tests/mechanisms/__init__.py
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/mechanisms/parsing/__init__.py
----------------------------------------------------------------------
diff --git a/tests/mechanisms/parsing/__init__.py b/tests/mechanisms/parsing/__init__.py
new file mode 100644
index 0000000..c1525a8
--- /dev/null
+++ b/tests/mechanisms/parsing/__init__.py
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from jinja2 import Template
+
+
+class Parsed(object):
+    def __init__(self):
+        self.issues = []
+        self.text = ''
+
+    def assert_success(self):
+        __tracebackhide__ = True # pylint: disable=unused-variable
+        if len(self.issues) > 0:
+            pytest.fail(u'did not expect parsing errors\n\n{0}\n\n{1}'
+                        .format(self.text.strip(), u'\n'.join(self.issues)))
+
+    def assert_failure(self):
+        __tracebackhide__ = True # pylint: disable=unused-variable
+        if len(self.issues) > 0:
+            pass
+        else:
+            pytest.fail(u'expected parsing errors but got none\n\n{0}'
+                        .format(self.text.strip()))
+
+
+class Parser(object):
+    def parse_literal(self, text, context=None):
+        text = render(text, context)
+        return self._parse_literal(text)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        pass
+
+
+def render(template, context=None):
+    template = Template(template)
+    template = template.render(context or {})
+    return template

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/mechanisms/parsing/aria.py
----------------------------------------------------------------------
diff --git a/tests/mechanisms/parsing/aria.py b/tests/mechanisms/parsing/aria.py
new file mode 100644
index 0000000..c02d387
--- /dev/null
+++ b/tests/mechanisms/parsing/aria.py
@@ -0,0 +1,63 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import  # so we can import root 'aria'
+
+from aria.parser.loading import LiteralLocation
+from aria.parser.consumption import (
+    ConsumptionContext,
+    ConsumerChain,
+    Read,
+    Validate,
+    ServiceTemplate
+)
+from aria.utils.imports import import_fullname
+
+from . import Parser, Parsed
+
+
+class AriaParser(Parser):
+    def _parse_literal(self, text):
+        context = AriaParser.create_context()
+        context.presentation.location = LiteralLocation(text)
+        consumer = AriaParser.create_consumer(context)
+        consumer.consume()
+        parsed = Parsed()
+        parsed.text = text
+        for issue in context.validation.issues:
+            parsed.issues.append(unicode(issue))
+        return parsed
+
+    @staticmethod
+    def create_context(loader_source='aria.parser.loading.DefaultLoaderSource',
+                       reader_source='aria.parser.reading.DefaultReaderSource',
+                       presenter_source='aria.parser.presentation.DefaultPresenterSource',
+                       presenter=None,
+                       debug=False):
+        context = ConsumptionContext()
+        context.loading.loader_source = import_fullname(loader_source)()
+        context.reading.reader_source = import_fullname(reader_source)()
+        context.presentation.presenter_source = import_fullname(presenter_source)()
+        context.presentation.presenter_class = import_fullname(presenter)
+        context.presentation.print_exceptions = debug
+        return context
+
+    @staticmethod
+    def create_consumer(context):
+        return ConsumerChain(context, (
+            Read,
+            Validate,
+            ServiceTemplate
+        ))

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/mechanisms/web_server.py
----------------------------------------------------------------------
diff --git a/tests/mechanisms/web_server.py b/tests/mechanisms/web_server.py
new file mode 100644
index 0000000..7db901e
--- /dev/null
+++ b/tests/mechanisms/web_server.py
@@ -0,0 +1,75 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import threading
+
+import tornado.web
+import tornado.ioloop
+import tornado.netutil
+
+
+logging.getLogger('tornado.access').disabled = True
+
+
+class WebServer(threading.Thread):
+    def __init__(self):
+        super(WebServer, self).__init__()
+        self.daemon = True
+
+        self.content = []
+
+        # Arbitrary free socket
+        self.sockets = tornado.netutil.bind_sockets(0, '')
+        for s in self.sockets:
+            name = s.getsockname()
+            if name[0] == '0.0.0.0': # IPv4 (IPv6 would be '::')
+                self.port = name[1]
+                break
+
+    @property
+    def root(self):
+        return 'http://localhost:{0}'.format(self.port)
+
+    def add_text(self, url, content, content_type):
+        self.content.append((url, TextHandler, dict(content=content, content_type=content_type)))
+
+    def add_text_yaml(self, url, content):
+        self.add_text(url, content, 'application/x-yaml')
+
+    def stop(self):
+        self.ioloop.add_callback(self.ioloop.stop)
+
+    def run(self): # Thread override
+        application = tornado.web.Application(self.content)
+        server = tornado.httpserver.HTTPServer(application)
+        server.add_sockets(self.sockets)
+        self.ioloop = tornado.ioloop.IOLoop.current()
+        print 'Tornado starting'
+        self.ioloop.start()
+        print 'Tornado stopped'
+
+    @staticmethod
+    def escape(segment):
+        return tornado.escape.url_escape(segment)
+
+class TextHandler(tornado.web.RequestHandler):
+    def initialize(self, content, content_type): # pylint: disable=arguments-differ
+        self.content = content
+        self.content_type = content_type
+
+    def get(self):
+        self.write(self.content)
+        self.set_header('Content-Type', self.content_type)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/36e0aa56/tests/requirements.txt
----------------------------------------------------------------------
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 56a7bf5..bdd5e2c 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -13,6 +13,7 @@
 testtools==2.3.0
 fasteners==0.14.1
 sh==1.12.14
+tornado==4.3 # last release to support Python 2.6
 psutil==5.2.2
 mock==2.0.0
 pylint==1.6.5