diff --git a/doc/source/tools/shaker-cleanup.txt b/doc/source/tools/shaker-cleanup.txt
index cc12aa5..d77d20e 100644
--- a/doc/source/tools/shaker-cleanup.txt
+++ b/doc/source/tools/shaker-cleanup.txt
@@ -10,6 +10,7 @@ usage: shaker-cleanup [-h] [--cleanup-on-error] [--config-dir DIR]
                       [--nowatch-log-file] [--os-auth-url <auth-url>]
                       [--os-cacert <auth-cacert>] [--os-insecure]
                       [--os-password <auth-password>]
+                      [--os-project-name <auth-project-name>]
                       [--os-region-name <auth-region-name>]
                       [--os-tenant-name <auth-tenant-name>]
                       [--os-username <auth-username>]
@@ -84,6 +85,10 @@ optional arguments:
                         defaults to env[OS_INSECURE].
   --os-password <auth-password>
                         Authentication password, defaults to env[OS_PASSWORD].
+  --os-project-name <auth-project-name>
+                        Another way to specify tenant name. This option is
+                        mutually exclusive with --os-tenant-name. Defaults to
+                        env[OS_PROJECT_NAME].
   --os-region-name <auth-region-name>
                         Authentication region name, defaults to
                         env[OS_REGION_NAME].
diff --git a/doc/source/tools/shaker-image-builder.txt b/doc/source/tools/shaker-image-builder.txt
index 08a7cb2..4ef8e04 100644
--- a/doc/source/tools/shaker-image-builder.txt
+++ b/doc/source/tools/shaker-image-builder.txt
@@ -12,6 +12,7 @@ usage: shaker-image-builder [-h] [--cleanup-on-error] [--config-dir DIR]
                             [--nowatch-log-file] [--os-auth-url <auth-url>]
                             [--os-cacert <auth-cacert>] [--os-insecure]
                             [--os-password <auth-password>]
+                            [--os-project-name <auth-project-name>]
                             [--os-region-name <auth-region-name>]
                             [--os-tenant-name <auth-tenant-name>]
                             [--os-username <auth-username>]
@@ -86,6 +87,10 @@ optional arguments:
                         defaults to env[OS_INSECURE].
   --os-password <auth-password>
                         Authentication password, defaults to env[OS_PASSWORD].
+  --os-project-name <auth-project-name>
+                        Another way to specify tenant name. This option is
+                        mutually exclusive with --os-tenant-name. Defaults to
+                        env[OS_PROJECT_NAME].
   --os-region-name <auth-region-name>
                         Authentication region name, defaults to
                         env[OS_REGION_NAME].
diff --git a/doc/source/tools/shaker.txt b/doc/source/tools/shaker.txt
index 330dde1..3a13bad 100644
--- a/doc/source/tools/shaker.txt
+++ b/doc/source/tools/shaker.txt
@@ -10,6 +10,7 @@ usage: shaker [-h] [--agent-join-timeout AGENT_JOIN_TIMEOUT]
               [--noverbose] [--nowatch-log-file] [--os-auth-url <auth-url>]
               [--os-cacert <auth-cacert>] [--os-insecure]
               [--os-password <auth-password>]
+              [--os-project-name <auth-project-name>]
               [--os-region-name <auth-region-name>]
               [--os-tenant-name <auth-tenant-name>]
               [--os-username <auth-username>] [--output OUTPUT]
@@ -99,6 +100,10 @@ optional arguments:
                         defaults to env[OS_INSECURE].
   --os-password <auth-password>
                         Authentication password, defaults to env[OS_PASSWORD].
+  --os-project-name <auth-project-name>
+                        Another way to specify tenant name. This option is
+                        mutually exclusive with --os-tenant-name. Defaults to
+                        env[OS_PROJECT_NAME].
   --os-region-name <auth-region-name>
                         Authentication region name, defaults to
                         env[OS_REGION_NAME].
diff --git a/etc/shaker.conf b/etc/shaker.conf
index 44f3d49..606d0d6 100644
--- a/etc/shaker.conf
+++ b/etc/shaker.conf
@@ -112,6 +112,10 @@
 # Authentication tenant name, defaults to env[OS_TENANT_NAME]. (string value)
 #os_tenant_name =
 
+# Another way to specify tenant name. This option is mutually exclusive with
+# --os-tenant-name. Defaults to env[OS_PROJECT_NAME]. (string value)
+#os_project_name =
+
 # Authentication username, defaults to env[OS_USERNAME]. (string value)
 #os_username =
 
diff --git a/requirements.txt b/requirements.txt
index 2489b3e..c326504 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,25 +2,26 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 
-pbr>=1.6
+pbr>=1.6 # Apache-2.0
 
-iso8601>=0.1.9
-Jinja2>=2.6 # BSD License (3 clause)
-oslo.concurrency>=2.3.0 # Apache-2.0
-oslo.config>=2.6.0 # Apache-2.0
-oslo.i18n>=1.5.0 # Apache-2.0
-oslo.log>=1.12.0 # Apache-2.0
+iso8601>=0.1.9 # MIT
+Jinja2>=2.8 # BSD License (3 clause)
+keystoneauth1>=2.1.0 # Apache-2.0
+os-client-config>=1.13.1 # Apache-2.0
+oslo.concurrency>=3.5.0 # Apache-2.0
+oslo.config>=3.9.0 # Apache-2.0
+oslo.i18n>=2.1.0 # Apache-2.0
+oslo.log>=1.14.0 # Apache-2.0
 oslo.serialization>=1.10.0 # Apache-2.0
-oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0
-psutil<2.0.0,>=1.1.1
+oslo.utils>=3.5.0 # Apache-2.0
+psutil<2.0.0,>=1.1.1 # BSD
 pygal
 pykwalify
-python-glanceclient>=0.18.0
-python-keystoneclient!=1.8.0,>=1.6.0
-python-neutronclient>=2.6.0
-python-novaclient!=2.33.0,>=2.29.0
-python-heatclient>=0.6.0
-python-subunit>=0.0.18
-PyYAML>=3.1.0
+python-glanceclient>=2.0.0 # Apache-2.0
+python-neutronclient>=4.2.0 # Apache-2.0
+python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
+python-heatclient>=0.6.0 # Apache-2.0
+python-subunit>=0.0.18 # Apache-2.0/BSD
+PyYAML>=3.1.0 # MIT
 pyzmq>=15.2.0
-six>=1.9.0
+six>=1.9.0 # MIT
diff --git a/shaker/engine/config.py b/shaker/engine/config.py
index b66e9c7..da075d4 100644
--- a/shaker/engine/config.py
+++ b/shaker/engine/config.py
@@ -78,6 +78,12 @@ OPENSTACK_OPTS = [
                sample_default='',
                help='Authentication tenant name, defaults to '
                     'env[OS_TENANT_NAME].'),
+    cfg.StrOpt('os-project-name', metavar='<auth-project-name>',
+               default=utils.env('OS_PROJECT_NAME'),
+               sample_default='',
+               help='Another way to specify tenant name. This option is '
+                    'mutually exclusive with --os-tenant-name. '
+                    'Defaults to env[OS_PROJECT_NAME].'),
     cfg.StrOpt('os-username', metavar='<auth-username>',
                default=utils.env('OS_USERNAME'),
                sample_default='',
diff --git a/shaker/engine/deploy.py b/shaker/engine/deploy.py
index 03c2106..a237afd 100644
--- a/shaker/engine/deploy.py
+++ b/shaker/engine/deploy.py
@@ -220,15 +220,11 @@ class Deployment(object):
         self.has_stack = False
         self.privileged_mode = True
 
-    def connect_to_openstack(self, os_username, os_password, os_tenant_name,
-                             os_auth_url, os_region_name, external_net,
-                             flavor_name, image_name, os_cacert, os_insecure):
+    def connect_to_openstack(self, openstack_params, flavor_name, image_name,
+                             external_net):
         LOG.debug('Connecting to OpenStack')
 
-        self.openstack_client = openstack.OpenStackClient(
-            username=os_username, password=os_password,
-            tenant_name=os_tenant_name, auth_url=os_auth_url,
-            region_name=os_region_name, cacert=os_cacert, insecure=os_insecure)
+        self.openstack_client = openstack.OpenStackClient(openstack_params)
 
         self.flavor_name = flavor_name
         self.image_name = image_name
diff --git a/shaker/engine/image_builder.py b/shaker/engine/image_builder.py
index 5386ae7..4109210 100644
--- a/shaker/engine/image_builder.py
+++ b/shaker/engine/image_builder.py
@@ -34,23 +34,14 @@ def init():
     utils.init_config_and_logging(
         config.OPENSTACK_OPTS + config.IMAGE_BUILDER_OPTS)
 
-    openstack_client = None
+    openstack_params = utils.pack_openstack_params(cfg.CONF)
     try:
-        openstack_client = openstack.OpenStackClient(
-            username=cfg.CONF.os_username, password=cfg.CONF.os_password,
-            tenant_name=cfg.CONF.os_tenant_name, auth_url=cfg.CONF.os_auth_url,
-            region_name=cfg.CONF.os_region_name, cacert=cfg.CONF.os_cacert,
-            insecure=cfg.CONF.os_insecure
-        )
+        return openstack.OpenStackClient(openstack_params)
     except Exception as e:
-        LOG.error('Error establishing connection to OpenStack: %s. '
-                  'Please verify OpenStack credentials (--os-username, '
-                  '--os-password, --os-tenant-name, --os-auth-url, '
-                  '--os-cacert, --os-insecure)', e)
+        LOG.error('Failed to connect to OpenStack: %s. '
+                  'Please verify parameters: %s', e, openstack_params)
         exit(1)
 
-    return openstack_client
-
 
 def build_image():
     openstack_client = init()
diff --git a/shaker/engine/server.py b/shaker/engine/server.py
index 4df4bae..809d853 100644
--- a/shaker/engine/server.py
+++ b/shaker/engine/server.py
@@ -136,7 +136,7 @@ def execute(output, quorum, execution, agents, matrix=None):
 def _under_openstack():
     required = ['os_username', 'os_password', 'os_tenant_name', 'os_auth_url']
     for param in required:
-        if param not in cfg.CONF or not cfg.CONF[param]:
+        if param not in cfg.CONF:
             return False
     return True
 
@@ -149,12 +149,14 @@ def play_scenario(scenario):
         deployment = deploy.Deployment()
 
         if _under_openstack():
-            deployment.connect_to_openstack(
-                cfg.CONF.os_username, cfg.CONF.os_password,
-                cfg.CONF.os_tenant_name, cfg.CONF.os_auth_url,
-                cfg.CONF.os_region_name, cfg.CONF.external_net,
-                cfg.CONF.flavor_name, cfg.CONF.image_name,
-                cfg.CONF.os_cacert, cfg.CONF.os_insecure)
+            openstack_params = utils.pack_openstack_params(cfg.CONF)
+            try:
+                deployment.connect_to_openstack(
+                    openstack_params, cfg.CONF.flavor_name,
+                    cfg.CONF.image_name, cfg.CONF.external_net)
+            except Exception as e:
+                LOG.warning('Failed to connect to OpenStack: %s. Please '
+                            'verify parameters: %s', e, openstack_params)
 
         base_dir = os.path.dirname(scenario['file_name'])
         scenario_deployment = scenario.get('deployment', {})
@@ -192,8 +194,7 @@ def play_scenario(scenario):
             record = dict(id=utils.make_record_id(), status='interrupted')
         else:
             error_msg = 'Error while executing scenario: %s' % e
-            LOG.error(error_msg)
-            LOG.exception(e)
+            LOG.error(error_msg, exc_info=True)
             record = dict(id=utils.make_record_id(), status='error',
                           stderr=error_msg)
         output['records'][record['id']] = record
diff --git a/shaker/engine/utils.py b/shaker/engine/utils.py
index 304110b..89c20e0 100644
--- a/shaker/engine/utils.py
+++ b/shaker/engine/utils.py
@@ -57,8 +57,12 @@ def init_config_and_logging(opts):
     conf.register_cli_opts(opts)
     conf.register_opts(opts)
     logging.register_options(conf)
-    logging.set_defaults(
-        default_log_levels=conf.default_log_levels + ['pykwalify=INFO'])
+
+    # requests to OpenStack services should be visible at DEBUG level
+    default_log_levels = [l for l in conf.default_log_levels
+                          if not l.startswith('keystoneauth')]
+    default_log_levels += ['pykwalify=INFO']
+    logging.set_defaults(default_log_levels=default_log_levels)
 
     try:
         conf(project='shaker')
@@ -250,3 +254,17 @@ def copy_value_by_path(src, src_param, dst, dst_param):
         set_value_by_path(dst, dst_param, v)
         return True
     return False
+
+
+def pack_openstack_params(conf):
+    params = dict(auth=dict(username=cfg.CONF.os_username,
+                            password=cfg.CONF.os_password,
+                            auth_url=cfg.CONF.os_auth_url),
+                  os_region_name=cfg.CONF.os_region_name,
+                  os_cacert=cfg.CONF.os_cacert,
+                  os_insecure=cfg.CONF.os_insecure)
+    if cfg.CONF.os_tenant_name:
+        params['auth']['tenant_name'] = cfg.CONF.os_tenant_name
+    if cfg.CONF.os_project_name:
+        params['auth']['project_name'] = cfg.CONF.os_project_name
+    return params
diff --git a/shaker/openstack/clients/glance.py b/shaker/openstack/clients/glance.py
index 9029d71..65b2db3 100644
--- a/shaker/openstack/clients/glance.py
+++ b/shaker/openstack/clients/glance.py
@@ -13,21 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from glanceclient import client as glance_client_pkg
-
-
-GLANCE_VERSION = '1'
-
-
-def create_client(keystone_client, os_region_name, cacert, insecure):
-    image_api_url = keystone_client.service_catalog.url_for(
-        service_type='image', region_name=os_region_name)
-    return glance_client_pkg.Client(GLANCE_VERSION,
-                                    endpoint=image_api_url,
-                                    token=keystone_client.auth_token,
-                                    cacert=cacert,
-                                    insecure=insecure)
-
 
 def get_image(glance_client, image_name):
     for image in glance_client.images.list():
diff --git a/shaker/openstack/clients/heat.py b/shaker/openstack/clients/heat.py
index 85673b5..fab76ba 100644
--- a/shaker/openstack/clients/heat.py
+++ b/shaker/openstack/clients/heat.py
@@ -15,26 +15,12 @@
 
 import time
 
-from heatclient import client as heat_client_pkg
 from oslo_log import log as logging
 
 
 LOG = logging.getLogger(__name__)
 
 
-HEAT_VERSION = '1'
-
-
-def create_client(keystone_client, os_region_name, cacert, insecure):
-    orchestration_api_url = keystone_client.service_catalog.url_for(
-        service_type='orchestration', region_name=os_region_name)
-    return heat_client_pkg.Client(HEAT_VERSION,
-                                  endpoint=orchestration_api_url,
-                                  token=keystone_client.auth_token,
-                                  ca_file=cacert,
-                                  insecure=insecure)
-
-
 def create_stack(heat_client, stack_name, template, parameters):
     stack_params = {
         'stack_name': stack_name,
diff --git a/shaker/openstack/clients/keystone.py b/shaker/openstack/clients/keystone.py
deleted file mode 100644
index 0163eff..0000000
--- a/shaker/openstack/clients/keystone.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright (c) 2015 Mirantis Inc.
-#
-# Licensed 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 keystoneclient.auth.identity import v2 as auth_v2
-from keystoneclient import discover as keystone_discover
-from keystoneclient import session
-from keystoneclient.v2_0 import client as keystone_v2
-from keystoneclient.v3 import client as keystone_v3
-
-
-def create_keystone_client(**kwargs):
-    discover = keystone_discover.Discover(**kwargs)
-    for version_data in discover.version_data():
-        version = version_data["version"]
-        if version[0] <= 2:
-            return keystone_v2.Client(**kwargs)
-        elif version[0] == 3:
-            return keystone_v3.Client(**kwargs)
-    raise Exception(
-        'Failed to discover keystone version for url %(auth_url)s.', **kwargs)
-
-
-def create_keystone_session(cacert, insecure, **kwargs):
-    auth = auth_v2.Password(**kwargs)
-    return session.Session(auth=auth, cert=cacert, verify=not insecure)
diff --git a/shaker/openstack/clients/neutron.py b/shaker/openstack/clients/neutron.py
index bf63f70..4f20d78 100644
--- a/shaker/openstack/clients/neutron.py
+++ b/shaker/openstack/clients/neutron.py
@@ -13,22 +13,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from neutronclient.neutron import client as neutron_client_pkg
 from oslo_log import log as logging
 
 
 LOG = logging.getLogger(__name__)
 
 
-NEUTRON_VERSION = '2.0'
-
-
-def create_client(keystone_session, os_region_name):
-    return neutron_client_pkg.Client(NEUTRON_VERSION,
-                                     session=keystone_session,
-                                     region_name=os_region_name)
-
-
 def choose_external_net(neutron_client):
     ext_nets = neutron_client.list_networks(
         **{'router:external': True})['networks']
diff --git a/shaker/openstack/clients/nova.py b/shaker/openstack/clients/nova.py
index 38836be..c1ab930 100644
--- a/shaker/openstack/clients/nova.py
+++ b/shaker/openstack/clients/nova.py
@@ -23,18 +23,11 @@ from oslo_log import log as logging
 
 LOG = logging.getLogger(__name__)
 
-NOVA_VERSION = '2'
-
 
 class ForbiddenException(nova_client_pkg.exceptions.Forbidden):
     pass
 
 
-def create_client(keystone_session, os_region_name):
-    return nova_client_pkg.Client(NOVA_VERSION, session=keystone_session,
-                                  region_name=os_region_name)
-
-
 def get_available_compute_nodes(nova_client):
     try:
         return [dict(host=svc.host, zone=svc.zone)
diff --git a/shaker/openstack/clients/openstack.py b/shaker/openstack/clients/openstack.py
index ff091c4..208114c 100644
--- a/shaker/openstack/clients/openstack.py
+++ b/shaker/openstack/clients/openstack.py
@@ -13,90 +13,27 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import functools
-import time
-
-from shaker.openstack.clients import glance
-from shaker.openstack.clients import heat
-from shaker.openstack.clients import keystone
-from shaker.openstack.clients import neutron
-from shaker.openstack.clients import nova
+import os_client_config
+from oslo_log import log as logging
 
 
-# As of now only Nova and Neutron clients support Keystone sessions.
-# Thus the only way to create clients is from keystone client instance
-# and auth token. The token gets expired in an hour and there is no
-# way to automatically refresh it. So the current implementation is to
-# recreate keystone client from scratch
-
-MODERN_CLIENT_MAKERS = {
-    'neutron': neutron.create_client,
-    'nova': nova.create_client,
-}
-OLD_CLIENT_MAKERS = {
-    'glance': glance.create_client,
-    'heat': heat.create_client,
-}
-
-KEYSTONE_AUTH_EXPIRATION = 60
-
-
-class OpenStackClientProxy(object):
-    def __init__(self, keystone_creator, client_creator):
-        self.keystone_creator = keystone_creator
-        self.client_creator = client_creator
-        self.last_update_time = 0
-
-    def __getattribute__(self, name):
-        if name in ['keystone_creator', 'client_creator',
-                    'client', 'last_update_time']:
-            return super(OpenStackClientProxy, self).__getattribute__(name)
-        else:
-            now = int(time.time())
-            if now > self.last_update_time + KEYSTONE_AUTH_EXPIRATION:
-                self.last_update_time = now
-                self.client = self.client_creator(
-                    keystone_client=self.keystone_creator())
-            return self.client.__getattribute__(name)
+LOG = logging.getLogger(__name__)
 
 
 class OpenStackClient(object):
-    def __init__(self, username, password, tenant_name, auth_url, region_name,
-                 cacert, insecure):
-        self.region_name = region_name or 'RegionOne'
-        self.cacert = cacert or ''
-        self.insecure = insecure or False
-        self._osc_cache = {}
-        self.keystone_creator = functools.partial(
-            keystone.create_keystone_client,
-            username=username, password=password,
-            tenant_name=tenant_name, auth_url=auth_url, cacert=cacert,
-            insecure=insecure)
-        self.session_creator = functools.partial(
-            keystone.create_keystone_session, cacert,
-            username=username, password=password,
-            tenant_name=tenant_name, auth_url=auth_url,
-            insecure=insecure)
-        # ping OpenStack
-        self.keystone_creator()
+    def __init__(self, openstack_params):
+        LOG.debug('Establishing connection to OpenStack')
 
-    def __getattribute__(self, name):
-        if name != '_osc_cache' and name in self._osc_cache:
-            return self._osc_cache[name]
+        config = os_client_config.OpenStackConfig()
+        cloud_config = config.get_one_cloud(**openstack_params)
 
-        client = None
-        if name in MODERN_CLIENT_MAKERS:
-            session = self.session_creator()
-            client = MODERN_CLIENT_MAKERS[name](session, self.region_name)
-        elif name in OLD_CLIENT_MAKERS:
-            client_creator = functools.partial(
-                OLD_CLIENT_MAKERS[name], os_region_name=self.region_name,
-                cacert=self.cacert, insecure=self.insecure)
-            client = OpenStackClientProxy(self.keystone_creator,
-                                          client_creator)
+        self.keystone_session = cloud_config.get_session()
+        self.nova = cloud_config.get_legacy_client('compute')
+        self.neutron = cloud_config.get_legacy_client('network')
+        self.glance = cloud_config.get_legacy_client('image')
+        self.heat = cloud_config.get_legacy_client('orchestration')
 
-        if client:
-            self._osc_cache[name] = client
-            return client
-        else:
-            return super(OpenStackClient, self).__getattribute__(name)
+        # Ping OpenStack
+        self.keystone_session.get_token()
+
+        LOG.info('Connection to OpenStack is initialized')
diff --git a/shaker/tests/test_server.py b/shaker/tests/test_server.py
index b8e4099..7939580 100644
--- a/shaker/tests/test_server.py
+++ b/shaker/tests/test_server.py
@@ -184,10 +184,13 @@ class TestServerPlayScenario(testtools.TestCase):
         deploy_obj.deploy.assert_called_once_with(
             self.deployment, base_dir='folder',
             server_endpoint='127.0.0.1:5999')
+        openstack_params = dict(
+            auth=dict(username='user', password='password',
+                      tenant_name='tenant', auth_url='auth-url'),
+            os_region_name='RegionOne',
+            os_cacert=None, os_insecure=False)
         deploy_obj.connect_to_openstack.assert_called_once_with(
-            'user', 'password', 'tenant', 'auth-url', 'RegionOne', None,
-            'shaker-flavor', 'shaker-image', None, False
-        )
+            openstack_params, 'shaker-flavor', 'shaker-image', None)
         deploy_obj.cleanup.assert_called_once_with()
 
     @mock.patch('shaker.engine.deploy.Deployment')
diff --git a/test-requirements.txt b/test-requirements.txt
index aaa7f58..a339e79 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -3,12 +3,12 @@
 # process, which may cause wedges in the gate later.
 
 # Hacking already pins down pep8, pyflakes and flake8
-coverage>=3.6
+coverage>=3.6 # Apache-2.0
 hacking<0.11,>=0.10
-mock>=1.2
+mock>=1.2 # BSD
 oslotest>=1.10.0 # Apache-2.0
-sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
-sphinxcontrib-httpdomain
+sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
+sphinxcontrib-httpdomain # BSD
 sphinx_rtd_theme
-testrepository>=0.0.18
-testtools>=1.4.0
+testrepository>=0.0.18 # Apache-2.0/BSD
+testtools>=1.4.0 # MIT