ariatosca-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From emblempar...@apache.org
Subject incubator-ariatosca git commit: Testing types and templates [Forced Update!]
Date Mon, 21 Aug 2017 19:28:33 GMT
Repository: incubator-ariatosca
Updated Branches:
  refs/heads/ARIA-1-parser-test-suite fe6f8c038 -> abb35d114 (forced update)


Testing types and templates

* Fix "version" fields in types
* Improve version testing


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

Branch: refs/heads/ARIA-1-parser-test-suite
Commit: abb35d11485d8284a656f2d38908df5102d3400b
Parents: 526befd
Author: Tal Liron <tal.liron@gmail.com>
Authored: Fri Aug 18 15:52:31 2017 -0500
Committer: Tal Liron <tal.liron@gmail.com>
Committed: Mon Aug 21 14:28:28 2017 -0500

----------------------------------------------------------------------
 .../execution_plugin/ctx_proxy/server.py        |   1 +
 aria/parser/consumption/presentation.py         |  15 +-
 aria/parser/presentation/fields.py              |   2 +-
 aria/utils/threading.py                         | 146 ++++++++++++------
 aria/utils/versions.py                          |   2 +-
 .../simple_v1_0/presentation/field_getters.py   |  20 +--
 .../aria_extension_tosca/simple_v1_0/types.py   |  23 ++-
 .../simple_v1_0/conftest.py                     |  23 ++-
 .../aria_extension_tosca/simple_v1_0/data.py    |  40 +++++
 .../simple_v1_0/test_imports.py                 |  18 ++-
 .../simple_v1_0/test_metadata.py                |  58 ++++---
 .../simple_v1_0/test_templates.py               | 129 ++++++++++++++++
 .../simple_v1_0/test_types.py                   | 153 +++++++++++++++++++
 tests/mechanisms/parsing/__init__.py            |  30 +++-
 tests/mechanisms/parsing/aria.py                |   1 +
 tests/mechanisms/web_server.py                  |  11 +-
 .../node-cellar/node-cellar.yaml                |   2 +-
 tests/utils/test_versions.py                    |   8 +-
 tox.ini                                         |  37 +++--
 19 files changed, 589 insertions(+), 130 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/aria/orchestrator/execution_plugin/ctx_proxy/server.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/execution_plugin/ctx_proxy/server.py b/aria/orchestrator/execution_plugin/ctx_proxy/server.py
index 91b95d9..ecdfc2f 100644
--- a/aria/orchestrator/execution_plugin/ctx_proxy/server.py
+++ b/aria/orchestrator/execution_plugin/ctx_proxy/server.py
@@ -144,6 +144,7 @@ class CtxProxy(object):
 
     def __exit__(self, *args, **kwargs):
         self.close()
+        return False
 
 
 class CtxError(RuntimeError):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/aria/parser/consumption/presentation.py
----------------------------------------------------------------------
diff --git a/aria/parser/consumption/presentation.py b/aria/parser/consumption/presentation.py
index 542b3f0..6d34ee1 100644
--- a/aria/parser/consumption/presentation.py
+++ b/aria/parser/consumption/presentation.py
@@ -14,8 +14,8 @@
 # limitations under the License.
 
 
-from ...utils.threading import FixedThreadPoolExecutor
-from ...utils.formatting import json_dumps, yaml_dumps
+from ...utils.threading import (BlockingExecutor, FixedThreadPoolExecutor)
+from ...utils.formatting import (json_dumps, yaml_dumps)
 from ..loading import UriLocation
 from ..reading import AlreadyReadException
 from ..presentation import PresenterNotFoundError
@@ -47,9 +47,14 @@ class Read(Consumer):
         presenter = None
         imported_presentations = None
 
-        executor = FixedThreadPoolExecutor(size=self.context.presentation.threads,
-                                           timeout=self.context.presentation.timeout)
-        executor.print_exceptions = self.context.presentation.print_exceptions
+        if self.context.presentation.threads == 1:
+            executor = BlockingExecutor(print_exceptions=self.context.presentation.print_exceptions)
+        else:
+            executor = FixedThreadPoolExecutor(size=self.context.presentation.threads,
+                                               timeout=self.context.presentation.timeout,
+                                               print_exceptions=self.context.presentation
\
+                                               .print_exceptions)
+
         try:
             presenter = self._present(self.context.presentation.location, None, None, executor)
             executor.drain()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/aria/parser/presentation/fields.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/fields.py b/aria/parser/presentation/fields.py
index 5c08d4a..98394d9 100644
--- a/aria/parser/presentation/fields.py
+++ b/aria/parser/presentation/fields.py
@@ -733,7 +733,7 @@ class Field(object):
                             primitive_dict[k] = self._coerce_primitive(v, context)
                         except ValueError as e:
                             raise InvalidValueError('%s is not a dict of "%s" values:'
-                                                    ' entry "%d" is %s'
+                                                    ' entry "%s" is %s'
                                                     % (self.full_name, self.full_cls_name,
                                                        k, safe_repr(v)),
                                                     locator=self.get_locator(raw),

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/aria/utils/threading.py
----------------------------------------------------------------------
diff --git a/aria/utils/threading.py b/aria/utils/threading.py
index f5ca302..711a898 100644
--- a/aria/utils/threading.py
+++ b/aria/utils/threading.py
@@ -59,10 +59,9 @@ class DaemonThread(Thread):
             pass
 
 
-# https://gist.github.com/tliron/81dd915166b0bfc64be08b4f8e22c835
-class FixedThreadPoolExecutor(object):
+class Executor(object):
     """
-    Executes tasks in a fixed thread pool.
+    Executes tasks.
 
     Makes sure to gather all returned results and thrown exceptions in one place, in order
of task
     submission.
@@ -93,6 +92,103 @@ class FixedThreadPoolExecutor(object):
             print executor.returns
     """
 
+    def __init__(self, print_exceptions=False):
+        self.print_exceptions = print_exceptions
+
+    def submit(self, func, *args, **kwargs):
+        """
+        Submit a task for execution.
+
+        The task will be called ASAP on the next available worker thread in the pool.
+
+        :raises ExecutorException: if cannot be submitted
+        """
+        raise NotImplementedError
+
+    def close(self):
+        """
+        Blocks until all current tasks finish execution and all worker threads are dead.
+
+        You cannot submit tasks anymore after calling this.
+
+        This is called automatically upon exit if you are using the ``with`` keyword.
+        """
+        pass
+
+    def drain(self):
+        """
+        Blocks until all current tasks finish execution, but leaves the worker threads alive.
+        """
+        pass
+
+    @property
+    def returns(self):
+        """
+        The returned values from all tasks, in order of submission.
+        """
+        return ()
+
+    @property
+    def exceptions(self):
+        """
+        The raised exceptions from all tasks, in order of submission.
+        """
+        return ()
+
+    def raise_first(self):
+        """
+        If exceptions were thrown by any task, then the first one will be raised.
+
+        This is rather arbitrary: proper handling would involve iterating all the exceptions.
+        However, if you want to use the "raise" mechanism, you are limited to raising only
one of
+        them.
+        """
+
+        exceptions = self.exceptions
+        if exceptions:
+            raise exceptions[0]
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, the_type, value, traceback):
+        return False
+
+
+class BlockingExecutor(Executor):
+    """
+    Executes tasks in the current thread.
+    """
+
+    def __init__(self, print_exceptions=False):
+        super(BlockingExecutor, self).__init__(print_exceptions=print_exceptions)
+        self._returns = []
+        self._exceptions = []
+
+    def submit(self, func, *args, **kwargs):
+        try:
+            result = func(*args, **kwargs)
+            self._returns.append(result)
+        except Exception as e:
+            self._exceptions.append(e)
+            if self.print_exceptions:
+                print_exception(e)
+
+    @property
+    def returns(self):
+        return self._returns
+
+    @property
+    def exceptions(self):
+        return self._exceptions
+
+
+# https://gist.github.com/tliron/81dd915166b0bfc64be08b4f8e22c835
+class FixedThreadPoolExecutor(Executor):
+    """
+    Executes tasks in a fixed thread pool.
+    """
+
     _CYANIDE = object()  # Special task marker used to kill worker threads.
 
     def __init__(self,
@@ -105,6 +201,8 @@ class FixedThreadPoolExecutor(object):
         :param timeout: timeout in seconds for all blocking operations (``None`` means no
timeout)
         :param print_exceptions: set to ``True`` in order to print exceptions from tasks
         """
+        super(FixedThreadPoolExecutor, self).__init__(print_exceptions=print_exceptions)
+
         if not size:
             try:
                 size = multiprocessing.cpu_count() * 2 + 1
@@ -113,7 +211,6 @@ class FixedThreadPoolExecutor(object):
 
         self.size = size
         self.timeout = timeout
-        self.print_exceptions = print_exceptions
 
         self._tasks = Queue()
         self._returns = {}
@@ -130,28 +227,12 @@ class FixedThreadPoolExecutor(object):
             self._workers.append(worker)
 
     def submit(self, func, *args, **kwargs):
-        """
-        Submit a task for execution.
-
-        The task will be called ASAP on the next available worker thread in the pool.
-
-        :raises ExecutorException: if cannot be submitted
-        """
-
         try:
             self._tasks.put((self._id_creator.next(), func, args, kwargs), timeout=self.timeout)
         except Full:
             raise ExecutorException('cannot submit task: queue is full')
 
     def close(self):
-        """
-        Blocks until all current tasks finish execution and all worker threads are dead.
-
-        You cannot submit tasks anymore after calling this.
-
-        This is called automatically upon exit if you are using the ``with`` keyword.
-        """
-
         self.drain()
         while self.is_alive:
             try:
@@ -161,10 +242,6 @@ class FixedThreadPoolExecutor(object):
         self._workers = None
 
     def drain(self):
-        """
-        Blocks until all current tasks finish execution, but leaves the worker threads alive.
-        """
-
         self._tasks.join()  # oddly, the API does not support a timeout parameter
 
     @property
@@ -180,33 +257,12 @@ class FixedThreadPoolExecutor(object):
 
     @property
     def returns(self):
-        """
-        The returned values from all tasks, in order of submission.
-        """
-
         return [self._returns[k] for k in sorted(self._returns)]
 
     @property
     def exceptions(self):
-        """
-        The raised exceptions from all tasks, in order of submission.
-        """
-
         return [self._exceptions[k] for k in sorted(self._exceptions)]
 
-    def raise_first(self):
-        """
-        If exceptions were thrown by any task, then the first one will be raised.
-
-        This is rather arbitrary: proper handling would involve iterating all the exceptions.
-        However, if you want to use the "raise" mechanism, you are limited to raising only
one of
-        them.
-        """
-
-        exceptions = self.exceptions
-        if exceptions:
-            raise exceptions[0]
-
     def _thread_worker(self):
         while True:
             if not self._execute_next_task():

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/aria/utils/versions.py
----------------------------------------------------------------------
diff --git a/aria/utils/versions.py b/aria/utils/versions.py
index 521004c..507f055 100644
--- a/aria/utils/versions.py
+++ b/aria/utils/versions.py
@@ -24,7 +24,7 @@ _INF = float('inf')
 
 _NULL = (), _INF
 
-_DIGITS_RE = re.compile(r'^\d+$')
+_DIGITS_RE = re.compile(r'^\d+$', flags=re.UNICODE)
 
 _PREFIXES = {
     'dev':   0.0001,

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/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 34dacd6..f53a5cc 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
@@ -16,6 +16,7 @@
 from aria.utils.formatting import safe_repr
 from aria.utils.type import full_type_name
 from aria.parser.exceptions import InvalidValueError
+from aria.parser.presentation import NULL
 
 
 def data_type_class_getter(cls):
@@ -27,13 +28,14 @@ def data_type_class_getter(cls):
 
     def getter(field, presentation, context=None):
         raw = field.default_get(presentation, context)
-        if raw is not None:
-            try:
-                return cls(None, None, raw, None)
-            except ValueError as e:
-                raise InvalidValueError(
-                    '{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))
+        if (raw is None) or (raw is NULL):
+            return raw
+        try:
+            return cls(None, None, raw, None)
+        except ValueError as e:
+            raise InvalidValueError(
+                '{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/abb35d11/extensions/aria_extension_tosca/simple_v1_0/types.py
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/simple_v1_0/types.py b/extensions/aria_extension_tosca/simple_v1_0/types.py
index 787aac2..e118c9f 100644
--- a/extensions/aria_extension_tosca/simple_v1_0/types.py
+++ b/extensions/aria_extension_tosca/simple_v1_0/types.py
@@ -70,7 +70,7 @@ class ArtifactType(ExtensiblePresentation):
         """
 
     @field_getter(data_type_class_getter(Version))
-    @primitive_field()
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Artifact Type definition.
@@ -153,7 +153,8 @@ class DataType(ExtensiblePresentation):
         :type: :obj:`basestring`
         """
 
-    @object_field(Version)
+    @field_getter(data_type_class_getter(Version))
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Data Type definition.
@@ -250,7 +251,8 @@ class CapabilityType(ExtensiblePresentation):
         :type: :obj:`basestring`
         """
 
-    @object_field(Version)
+    @field_getter(data_type_class_getter(Version))
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Capability Type definition.
@@ -351,7 +353,8 @@ class InterfaceType(ExtensiblePresentation):
         :type: :obj:`basestring`
         """
 
-    @object_field(Version)
+    @field_getter(data_type_class_getter(Version))
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Interface Type definition.
@@ -430,7 +433,8 @@ class RelationshipType(ExtensiblePresentation):
         :type: :obj:`basestring`
         """
 
-    @object_field(Version)
+    @field_getter(data_type_class_getter(Version))
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Relationship Type definition.
@@ -545,7 +549,8 @@ class NodeType(ExtensiblePresentation):
         :type: :obj:`basestring`
         """
 
-    @object_field(Version)
+    @field_getter(data_type_class_getter(Version))
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Node Type definition.
@@ -701,7 +706,8 @@ class GroupType(ExtensiblePresentation):
         :type: :obj:`basestring`
         """
 
-    @object_field(Version)
+    @field_getter(data_type_class_getter(Version))
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Group Type definition.
@@ -807,7 +813,8 @@ class PolicyType(ExtensiblePresentation):
         :type: :obj:`basestring`
         """
 
-    @object_field(Version)
+    @field_getter(data_type_class_getter(Version))
+    @primitive_field(str)
     def version(self):
         """
         An optional version for the Policy Type definition.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/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
index 86bbc3f..399e8c8 100644
--- a/tests/extensions/aria_extension_tosca/simple_v1_0/conftest.py
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/conftest.py
@@ -13,16 +13,31 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+"""
+PyTest configuration module.
+"""
+
 import pytest
 
 from ....mechanisms.parsing.aria import AriaParser
 
 
+def pytest_addoption(parser):
+    parser.addoption('--tosca-parser', action='store', default='aria', help='TOSCA parser')
+
+
 def pytest_report_header(config):
-    return 'parser: ARIA'
+    tosca_parser = config.getoption('--tosca-parser')
+    return 'tosca-parser: {0}'.format(tosca_parser)
 
 
 @pytest.fixture(scope='session')
-def parser():
-    with AriaParser() as p:
-        yield p
+def parser(request):
+    tosca_parser = request.config.getoption('--tosca-parser')
+    verbose = request.config.getoption('verbose') > 0
+    if tosca_parser == 'aria':
+        with AriaParser() as p:
+            p.verbose = verbose
+            yield p
+    else:
+        pytest.fail('configured tosca-parser not supported: {0}'.format(tosca_parser))

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/extensions/aria_extension_tosca/simple_v1_0/data.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/simple_v1_0/data.py b/tests/extensions/aria_extension_tosca/simple_v1_0/data.py
new file mode 100644
index 0000000..b24fb29
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/data.py
@@ -0,0 +1,40 @@
+# -*- 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.
+
+
+NOT_A_DICT = ('null', 'a string', '123', '0.123', '[]')
+NOT_A_LIST = ('null', 'a string', '123', '0.123', '{}')
+NOT_A_STRING = ('123', '0.123', '[]', '{}')
+TYPE_NAMES = ('artifact', 'data', 'capability', 'interface', 'relationship', 'node', 'group',
+              'policy')
+TYPE_NAME_PLURAL = {
+    'artifact': 'artifacts',
+    'data': 'datatypes',
+    'capability': 'capabilities',
+    'interface': 'interfaces',
+    'relationship': 'relationships',
+    'node': 'nodes',
+    'group': 'groups',
+    'policy': 'policies'
+}
+TEMPLATE_NAMES = ('node', 'group', 'policy')
+TEMPLATE_NAME_SECTION = {
+    'node': 'node_templates',
+    'group': 'groups',
+    'policy': 'policies'
+}
+GOOD_VERSIONS = ("'6.1'", '2.0.1', '3.1.0.beta', "'1.0.0.alpha-10'")
+BAD_VERSIONS = ('a_string', '1.2.3.4.5', '1.2.beta', '1.0.0.alpha-x')

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/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
index 4d78f40..765cd8b 100644
--- a/tests/extensions/aria_extension_tosca/simple_v1_0/test_imports.py
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/test_imports.py
@@ -16,9 +16,12 @@
 
 import pytest
 
+from . import data
 from ....mechanisms.web_server import WebServer
 
 
+# Fixtures
+
 NODE_TYPE_IMPORT = """
 node_types:
   MyNode:
@@ -35,17 +38,16 @@ node_types:
 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('詠嘆調')),
+    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()
+    with repository:
+        yield repository.root
 
 
 # Syntax
 
-@pytest.mark.parametrize('value', ('null', 'a_string', '123', '0.123', '{}'))
+@pytest.mark.parametrize('value', data.NOT_A_LIST)
 def test_imports_wrong_yaml_type(parser, value):
     parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
@@ -53,7 +55,7 @@ imports: {{ value }}
 """, dict(value=value)).assert_failure()
 
 
-def test_imports_empty_list(parser):
+def test_imports_empty(parser):
     parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
 imports: []
@@ -75,10 +77,10 @@ topology_template:
 
 
 def test_import_single_short_form_unicode(parser, repository):
-    parser.parse_literal(u"""
+    parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
 imports:
-  - {{ repository }}/imports/詠嘆調.yaml
+  - {{ repository }}/imports/節點類型.yaml
 topology_template:
   node_templates:
     my_node:

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/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
index dae5631..3f89bf6 100644
--- a/tests/extensions/aria_extension_tosca/simple_v1_0/test_metadata.py
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/test_metadata.py
@@ -14,12 +14,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import itertools
+
 import pytest
 
+from . import data
+
 
 # Syntax
 
-@pytest.mark.parametrize('value', ('null', 'a_string', '123', '0.123', '[]'))
+@pytest.mark.parametrize('value', data.NOT_A_DICT)
 def test_metadata_wrong_yaml_type(parser, value):
     parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
@@ -27,20 +31,11 @@ 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):
+@pytest.mark.parametrize('field,value', itertools.product(
+    ('template_name', 'template_author', 'template_version'),
+    data.NOT_A_STRING
+))
+def test_metadata_normative_fields_wrong_yaml_type(parser, field, value):
     parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
 metadata:
@@ -48,8 +43,8 @@ metadata:
 """, dict(field=field, value=value)).assert_failure()
 
 
-@pytest.mark.parametrize('value', ('123', '0.123', '[]', '{}'))
-def test_metadata_non_normative_wrong_yaml_type(parser, value):
+@pytest.mark.parametrize('value', data.NOT_A_STRING)
+def test_metadata_non_normative_fields_wrong_yaml_type(parser, value):
     parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
 metadata:
@@ -57,7 +52,7 @@ metadata:
 """, dict(value=value)).assert_failure()
 
 
-def test_metadata_empty_dict(parser):
+def test_metadata_empty(parser):
     parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
 metadata: {}
@@ -66,22 +61,23 @@ metadata: {}
 
 # Normative
 
-@pytest.mark.parametrize('value', ('null', 'a_string', '1.2.3.4.5'))
-def test_metadata_normative_template_bad_version(parser, value):
+@pytest.mark.parametrize('value', data.GOOD_VERSIONS)
+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_failure()
+""", dict(value=value)).assert_success()
 
 
-@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):
+@pytest.mark.parametrize('value', data.BAD_VERSIONS)
+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_success()
+""", dict(value=value)).assert_failure()
+
 
 # Non-normative
 
@@ -91,7 +87,7 @@ tosca_definitions_version: tosca_simple_yaml_1_0
 metadata:
   template_name: name
   template_author: author
-  template_version: 1.0.0.beta
+  template_version: 1.0.0.alpha-10
   non_normative1: non_normative1
   non_normative2: non_normative2
   non_normative3: non_normative3
@@ -104,7 +100,7 @@ tosca_definitions_version: tosca_simple_yaml_1_0
 metadata:
   template_name: null
   template_author: null
-  template_version: 1.0.0.beta
+  template_version: null
   non_normative1: null
   non_normative2: null
   non_normative3: null
@@ -112,13 +108,13 @@ metadata:
 
 
 def test_metadata_with_non_normative_fields_unicode(parser):
-    parser.parse_literal(u"""
+    parser.parse_literal("""
 tosca_definitions_version: tosca_simple_yaml_1_0
 metadata:
   template_name: 詠嘆調
   template_author: 詠嘆調
-  template_version: 1.0.0.詠嘆調
-  non_normative1: 詠嘆調
-  non_normative2: 詠嘆調
-  non_normative3: 詠嘆調
+  template_version: 1.0.0.詠嘆調-10
+  non_normative1: 詠嘆調一
+  non_normative2: 詠嘆調二
+  non_normative3: 詠嘆調三
 """).assert_success()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/extensions/aria_extension_tosca/simple_v1_0/test_templates.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/simple_v1_0/test_templates.py b/tests/extensions/aria_extension_tosca/simple_v1_0/test_templates.py
new file mode 100644
index 0000000..8b0fd0e
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/test_templates.py
@@ -0,0 +1,129 @@
+# -*- 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 itertools
+
+import pytest
+
+from . import data
+
+
+# Syntax
+
+@pytest.mark.parametrize('value', data.NOT_A_DICT)
+def test_topology_template_wrong_yaml_type(parser, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+topology_template: {{ value }}
+""", dict(value=value)).assert_failure()
+
+
+def test_topology_template_emtpy(parser):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+topology_template: {}
+""").assert_success()
+
+
+@pytest.mark.parametrize('name,value', itertools.product(
+    data.TEMPLATE_NAMES,
+    data.NOT_A_DICT
+))
+def test_template_section_wrong_yaml_type(parser, name, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+topology_template:
+  {{ section }}: {{ value }}
+""", dict(section=data.TEMPLATE_NAME_SECTION[name], value=value)).assert_failure()
+
+
+@pytest.mark.parametrize('name,value', itertools.product(
+    data.TEMPLATE_NAMES,
+    data.NOT_A_STRING
+))
+def test_template_type_wrong_yaml_type(parser, name, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+topology_template:
+  {{ section }}:
+    my_template:
+      type: {{ value }}
+""", dict(section=data.TEMPLATE_NAME_SECTION[name], value=value)).assert_failure()
+
+
+# Common fields
+
+@pytest.mark.parametrize('name', data.TEMPLATE_NAMES)
+def test_template_fields(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+topology_template:
+  {{ section }}:
+    my_template:
+      type: tosca.{{ plural }}.Root
+      description: a description
+""", dict(section=data.TEMPLATE_NAME_SECTION[name],
+          plural=data.TYPE_NAME_PLURAL[name])).assert_success()
+
+
+# Of types
+
+@pytest.mark.parametrize('name', data.TEMPLATE_NAMES)
+def test_template_of_type(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+    MyType: {}
+topology_template:
+  {{ section }}:
+    my_template:
+      type: MyType
+""", dict(name=name, section=data.TEMPLATE_NAME_SECTION[name])).assert_success()
+
+
+@pytest.mark.parametrize('name', data.TEMPLATE_NAMES)
+def test_template_of_type_unicode(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+    類型: {}
+topology_template:
+  {{ section }}:
+    模板:
+      type: 類型
+""", dict(name=name, section=data.TEMPLATE_NAME_SECTION[name])).assert_success()
+
+
+@pytest.mark.parametrize('name', data.TEMPLATE_NAMES)
+def test_template_of_unknown_type(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+topology_template:
+  {{ section }}:
+    my_template:
+      type: UnknownType
+""", dict(section=data.TEMPLATE_NAME_SECTION[name])).assert_failure()
+
+
+@pytest.mark.parametrize('name', data.TEMPLATE_NAMES)
+def test_template_of_null_type(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+topology_template:
+  {{ section }}:
+    my_template:
+      type: null
+""", dict(section=data.TEMPLATE_NAME_SECTION[name])).assert_failure()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/extensions/aria_extension_tosca/simple_v1_0/test_types.py
----------------------------------------------------------------------
diff --git a/tests/extensions/aria_extension_tosca/simple_v1_0/test_types.py b/tests/extensions/aria_extension_tosca/simple_v1_0/test_types.py
new file mode 100644
index 0000000..5a4e442
--- /dev/null
+++ b/tests/extensions/aria_extension_tosca/simple_v1_0/test_types.py
@@ -0,0 +1,153 @@
+# -*- 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 itertools
+
+import pytest
+
+from . import data
+
+
+# Syntax
+
+@pytest.mark.parametrize('name,value', itertools.product(
+    data.TYPE_NAMES,
+    data.NOT_A_DICT
+))
+def test_type_wrong_yaml_type(parser, name, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType: {{ value }}
+""", dict(name=name, value=value)).assert_failure()
+
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_empty(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType: {}
+""", dict(name=name)).assert_success()
+
+
+@pytest.mark.parametrize('name,value', itertools.product(
+    data.TYPE_NAMES,
+    data.NOT_A_STRING
+))
+def test_type_derived_from_wrong_yaml_type(parser, name, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType:
+    derived_from: {{ value }}
+""", dict(name=name, value=value)).assert_failure()
+
+
+
+# Derivation
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_derived_from_unknown(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType:
+    derived_from: UnknownType
+""", dict(name=name)).assert_failure()
+
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_derived_from_null(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType:
+    derived_from: null
+""", dict(name=name)).assert_failure()
+
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_derived_from_self(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType:
+    derived_from: MyType
+""", dict(name=name)).assert_failure()
+
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_derived_from_circular(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType1:
+    derived_from: MyType3
+  MyType2:
+    derived_from: MyType1
+  MyType3:
+    derived_from: MyType2
+""", dict(name=name)).assert_failure()
+
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_derived_from_root(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType:
+    derived_from: tosca.{{ plural }}.Root
+""", dict(name=name, plural=data.TYPE_NAME_PLURAL[name])).assert_success()
+
+
+# Common fields
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_fields(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType:
+    derived_from: tosca.{{ plural }}.Root
+    version: 1.0.0
+    description: a description
+""", dict(name=name, plural=data.TYPE_NAME_PLURAL[name])).assert_success()
+
+
+@pytest.mark.parametrize('name', data.TYPE_NAMES)
+def test_type_fields_unicode(parser, name):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  類型:
+    derived_from: tosca.{{ plural }}.Root
+    version: 1.0.0
+    description: 描述
+""", dict(name=name, plural=data.TYPE_NAME_PLURAL[name])).assert_success()
+
+
+@pytest.mark.parametrize('name,value', itertools.product(
+    data.TYPE_NAMES,
+    data.BAD_VERSIONS
+))
+def test_type_bad_version(parser, name, value):
+    parser.parse_literal("""
+tosca_definitions_version: tosca_simple_yaml_1_0
+{{ name }}_types:
+  MyType:
+    version: {{ value }}
+""", dict(name=name, value=value)).assert_failure()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/mechanisms/parsing/__init__.py
----------------------------------------------------------------------
diff --git a/tests/mechanisms/parsing/__init__.py b/tests/mechanisms/parsing/__init__.py
index c1525a8..c710d24 100644
--- a/tests/mechanisms/parsing/__init__.py
+++ b/tests/mechanisms/parsing/__init__.py
@@ -14,42 +14,62 @@
 # limitations under the License.
 
 import pytest
-from jinja2 import Template
+import jinja2
+
+
+LINE_BREAK = '\n' + '-' * 60
 
 
 class Parsed(object):
     def __init__(self):
         self.issues = []
         self.text = ''
+        self.verbose = False
 
     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)))
+        else:
+            if self.verbose:
+                print LINE_BREAK
+                print self.text.strip()
 
     def assert_failure(self):
         __tracebackhide__ = True # pylint: disable=unused-variable
         if len(self.issues) > 0:
-            pass
+            if self.verbose:
+                print LINE_BREAK
+                print u'{0}\n\n{1}'.format(self.text.strip(), u'\n'.join(self.issues))
         else:
             pytest.fail(u'expected parsing errors but got none\n\n{0}'
                         .format(self.text.strip()))
 
 
 class Parser(object):
+    def __init__(self):
+        self.verbose = False
+
     def parse_literal(self, text, context=None):
         text = render(text, context)
-        return self._parse_literal(text)
+        parsed = self._parse_literal(text)
+        parsed.verbose = self.verbose
+        return parsed
+
+    def _parse_literal(self, text):
+        raise NotImplementedError
 
     def __enter__(self):
         return self
 
     def __exit__(self, exc_type, exc_val, exc_tb):
-        pass
+        return False
 
 
 def render(template, context=None):
-    template = Template(template)
+    if not isinstance(template, unicode):
+        template = template.decode('utf-8')
+    template = jinja2.Template(template)
     template = template.render(context or {})
     return template

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/mechanisms/parsing/aria.py
----------------------------------------------------------------------
diff --git a/tests/mechanisms/parsing/aria.py b/tests/mechanisms/parsing/aria.py
index c02d387..3293631 100644
--- a/tests/mechanisms/parsing/aria.py
+++ b/tests/mechanisms/parsing/aria.py
@@ -51,6 +51,7 @@ class AriaParser(Parser):
         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.threads = 1
         context.presentation.print_exceptions = debug
         return context
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/mechanisms/web_server.py
----------------------------------------------------------------------
diff --git a/tests/mechanisms/web_server.py b/tests/mechanisms/web_server.py
index 7db901e..82ad2d1 100644
--- a/tests/mechanisms/web_server.py
+++ b/tests/mechanisms/web_server.py
@@ -43,7 +43,7 @@ class WebServer(threading.Thread):
     def root(self):
         return 'http://localhost:{0}'.format(self.port)
 
-    def add_text(self, url, content, content_type):
+    def add_text(self, url, content, content_type='text/plain'):
         self.content.append((url, TextHandler, dict(content=content, content_type=content_type)))
 
     def add_text_yaml(self, url, content):
@@ -65,6 +65,15 @@ class WebServer(threading.Thread):
     def escape(segment):
         return tornado.escape.url_escape(segment)
 
+    def __enter__(self):
+        self.start()
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.stop()
+        return False
+
+
 class TextHandler(tornado.web.RequestHandler):
     def initialize(self, content, content_type): # pylint: disable=arguments-differ
         self.content = content

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/resources/service-templates/tosca-simple-1.0/node-cellar/node-cellar.yaml
----------------------------------------------------------------------
diff --git a/tests/resources/service-templates/tosca-simple-1.0/node-cellar/node-cellar.yaml
b/tests/resources/service-templates/tosca-simple-1.0/node-cellar/node-cellar.yaml
index 5a46532..ef62676 100644
--- a/tests/resources/service-templates/tosca-simple-1.0/node-cellar/node-cellar.yaml
+++ b/tests/resources/service-templates/tosca-simple-1.0/node-cellar/node-cellar.yaml
@@ -19,7 +19,7 @@ tosca_definitions_version: tosca_simple_profile_for_nfv_1_0
 
 description: >-
   Node Cellar TOSCA blueprint.
-  Here is some Unicode: 中國.
+  Here is some Unicode: 詠嘆調.
 
 metadata:
   template_name: node-cellar

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tests/utils/test_versions.py
----------------------------------------------------------------------
diff --git a/tests/utils/test_versions.py b/tests/utils/test_versions.py
index 222949c..bcbf9ef 100644
--- a/tests/utils/test_versions.py
+++ b/tests/utils/test_versions.py
@@ -1,3 +1,4 @@
+# -*- 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.
@@ -34,8 +35,11 @@ def test_version_string():
     assert VersionString('20.0.1-beta1') < VersionString('20.0.1')
     assert VersionString('20.0.1-beta2') < VersionString('20.0.1-rc2')
     assert VersionString('20.0.1-alpha2') < VersionString('20.0.1-beta1')
-    assert VersionString('20.0.1-dev2') < VersionString('20.0.1-alpha1')
-    assert VersionString('20.0.1-DEV2') < VersionString('20.0.1-ALPHA1')
+    assert VersionString('20.0.1-dev2') < VersionString('20.0.1-ALPHA1')
+    assert VersionString('20.0.1-DEV2') < VersionString('20.0.1-alpha1')
+
+    # With Unicode qualifier
+    assert VersionString(u'20.0.1-詠嘆調1') == VersionString(u'20.0.1-詠嘆調2')
 
     # Coercive comparisons
     assert VersionString('20.0.0') == VersionString(10 * 2)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/abb35d11/tox.ini
----------------------------------------------------------------------
diff --git a/tox.ini b/tox.ini
index ff71e05..7adde30 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,7 @@
 # limitations under the License.
 
 [tox]
-envlist=py27,py26,py27e2e,py26e2e,pywin,py27ssh,pylint_code,pylint_tests,docs
+envlist=py27,py26,py27e2e,py26e2e,py27extensions,py26extensions,py27ssh,py26ssh,pywin,pylint_code,pylint_tests,docs
 processes={env:PYTEST_PROCESSES:auto}
 
 [testenv]
@@ -28,12 +28,14 @@ deps=
   --requirement
     tests/requirements.txt
 basepython=
-  py26: python2.6
   py27: python2.7
-  py26e2e: python2.6
+  py26: python2.6
   py27e2e: python2.7
-  py26ssh: python2.6
+  py26e2e: python2.6
+  py27extensions: python2.7
+  py26extensions: python2.6
   py27ssh: python2.7
+  py26ssh: python2.6
   pywin: {env:PYTHON:}\python.exe
   pylint_code: python2.7
   pylint_tests: python2.7
@@ -44,6 +46,7 @@ commands=
   pytest tests \
     --numprocesses={[tox]processes} \
     --ignore=tests/end2end \
+    --ignore=tests/extensions \
     --ignore=tests/orchestrator/execution_plugin/test_ssh.py \
     --cov-report term-missing \
     --cov aria
@@ -53,6 +56,7 @@ commands=
   pytest tests \
     --numprocesses={[tox]processes} \
     --ignore=tests/end2end \
+    --ignore=tests/extensions \
     --ignore=tests/orchestrator/execution_plugin/test_ssh.py \
     --cov-report term-missing \
     --cov aria
@@ -71,12 +75,17 @@ commands=
     --cov-report term-missing \
     --cov aria
 
-[testenv:pywin]
+[testenv:py27extensions]
 commands=
-  pytest tests \
+  pytest tests/extensions \
+    --numprocesses={[tox]processes} \
+    --cov-report term-missing \
+    --cov aria
+
+[testenv:py26extensions]
+commands=
+  pytest tests/extensions \
     --numprocesses={[tox]processes} \
-    --ignore=tests/end2end \
-    --ignore=tests/orchestrator/execution_plugin/test_ssh.py \
     --cov-report term-missing \
     --cov aria
 
@@ -94,9 +103,19 @@ commands=
   pytest tests/orchestrator/execution_plugin/test_ssh.py \
   --numprocesses={[tox]processes}
 
+[testenv:pywin]
+commands=
+  pytest tests \
+    --numprocesses={[tox]processes} \
+    --ignore=tests/end2end \
+    --ignore=tests/extensions \
+    --ignore=tests/orchestrator/execution_plugin/test_ssh.py \
+    --cov-report term-missing \
+    --cov aria
+
 [testenv:pylint_code]
 commands=
-  pylint aria extensions/aria_extension_tosca/ \
+  pylint aria extensions/aria_extension_tosca \
     --rcfile=aria/.pylintrc \
     --disable=fixme,missing-docstring
 



Mime
View raw message