Merge "Switch run_tests.sh to use subunit-trace"
diff --git a/HACKING.rst b/HACKING.rst
index 9f7487d..0962f80 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -330,24 +330,25 @@
# The created server should be in the detailed list of all servers
...
-Tempest includes a ``check_uuid.py`` tool that will test for the existence
-and uniqueness of idempotent_id metadata for every test. By default the
-tool runs against the Tempest package by calling::
+Tempest-lib includes a ``check-uuid`` tool that will test for the existence
+and uniqueness of idempotent_id metadata for every test. If you have
+tempest-lib installed you run the tool against Tempest by calling from the
+tempest repo::
- python check_uuid.py
+ check-uuid
It can be invoked against any test suite by passing a package name::
- python check_uuid.py --package <package_name>
+ check-uuid --package <package_name>
Tests without an ``idempotent_id`` can be automatically fixed by running
the command with the ``--fix`` flag, which will modify the source package
by inserting randomly generated uuids for every test that does not have
one::
- python check_uuid.py --fix
+ check-uuid --fix
-The ``check_uuid.py`` tool is used as part of the tempest gate job
+The ``check-uuid`` tool is used as part of the tempest gate job
to ensure that all tests have an ``idempotent_id`` decorator.
Branchless Tempest Considerations
diff --git a/tempest/api_schema/response/compute/v2_1/agents.py b/tempest/api_schema/response/compute/v2_1/agents.py
deleted file mode 100644
index da38198..0000000
--- a/tempest/api_schema/response/compute/v2_1/agents.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright 2014 NEC Corporation. All rights reserved.
-#
-# 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.
-
-common_agent_info = {
- 'type': 'object',
- 'properties': {
- 'agent_id': {'type': ['integer', 'string']},
- 'hypervisor': {'type': 'string'},
- 'os': {'type': 'string'},
- 'architecture': {'type': 'string'},
- 'version': {'type': 'string'},
- 'url': {'type': 'string', 'format': 'uri'},
- 'md5hash': {'type': 'string'}
- },
- 'additionalProperties': False,
- 'required': ['agent_id', 'hypervisor', 'os', 'architecture',
- 'version', 'url', 'md5hash']
-}
-
-list_agents = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'agents': {
- 'type': 'array',
- 'items': common_agent_info
- }
- },
- 'additionalProperties': False,
- 'required': ['agents']
- }
-}
-
-create_agent = {
- 'status_code': [200],
- 'response_body': {
- 'type': 'object',
- 'properties': {
- 'agent': common_agent_info
- },
- 'additionalProperties': False,
- 'required': ['agent']
- }
-}
-
-delete_agent = {
- 'status_code': [200]
-}
diff --git a/tempest/clients.py b/tempest/clients.py
index 8f63ab1..d16b47b 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -35,7 +35,8 @@
from tempest_lib.services.compute.hosts_client import HostsClient
from tempest_lib.services.compute.hypervisor_client import \
HypervisorClient
-from tempest_lib.services.compute.images_client import ImagesClient
+from tempest_lib.services.compute.images_client import ImagesClient \
+ as ComputeImagesClient
from tempest_lib.services.compute.instance_usage_audit_log_client import \
InstanceUsagesAuditLogClient
from tempest_lib.services.compute.limits_client import LimitsClient
@@ -102,8 +103,8 @@
RegionClient as RegionV3Client
from tempest.services.identity.v3.json.service_client import \
ServiceClient as ServiceV3Client
-from tempest.services.image.v1.json.image_client import ImageClient
-from tempest.services.image.v2.json.image_client import ImageClientV2
+from tempest.services.image.v1.json.images_client import ImagesClient
+from tempest.services.image.v2.json.images_client import ImagesClientV2
from tempest.services.messaging.json.messaging_client import \
MessagingClient
from tempest.services.network.json.floating_ips_client import FloatingIPsClient
@@ -260,7 +261,7 @@
endpoint_type=CONF.alarming.endpoint_type,
**self.default_params_with_timeout_values)
if CONF.service_available.glance:
- self.image_client = ImageClient(
+ self.image_client = ImagesClient(
self.auth_provider,
CONF.image.catalog_type,
CONF.image.region or CONF.identity.region,
@@ -268,7 +269,7 @@
build_interval=CONF.image.build_interval,
build_timeout=CONF.image.build_timeout,
**self.default_params)
- self.image_client_v2 = ImageClientV2(
+ self.image_client_v2 = ImagesClientV2(
self.auth_provider,
CONF.image.catalog_type,
CONF.image.region or CONF.identity.region,
@@ -329,7 +330,7 @@
self.server_groups_client = ServerGroupsClient(
self.auth_provider, **params)
self.limits_client = LimitsClient(self.auth_provider, **params)
- self.images_client = ImagesClient(self.auth_provider, **params)
+ self.images_client = ComputeImagesClient(self.auth_provider, **params)
self.keypairs_client = KeyPairsClient(self.auth_provider, **params)
self.quotas_client = QuotasClient(self.auth_provider, **params)
self.quota_classes_client = QuotaClassesClient(self.auth_provider,
diff --git a/tempest/cmd/javelin.py b/tempest/cmd/javelin.py
index b126ef6..c5e4a7e 100755
--- a/tempest/cmd/javelin.py
+++ b/tempest/cmd/javelin.py
@@ -128,7 +128,7 @@
from tempest.services.compute.json import security_group_rules_client
from tempest.services.compute.json import servers_client
from tempest.services.identity.v2.json import identity_client
-from tempest.services.image.v2.json import image_client
+from tempest.services.image.v2.json import images_client
from tempest.services.network.json import network_client
from tempest.services.network.json import subnets_client
from tempest.services.object_storage import container_client
@@ -213,7 +213,7 @@
**object_storage_params)
self.containers = container_client.ContainerClient(
_auth, **object_storage_params)
- self.images = image_client.ImageClientV2(
+ self.images = images_client.ImagesClientV2(
_auth,
CONF.image.catalog_type,
CONF.image.region or CONF.identity.region,
diff --git a/tempest/common/glance_http.py b/tempest/common/glance_http.py
index e5431a0..800e977 100644
--- a/tempest/common/glance_http.py
+++ b/tempest/common/glance_http.py
@@ -24,12 +24,10 @@
import OpenSSL
from oslo_log import log as logging
-from oslo_serialization import jsonutils as json
import six
from six import moves
from six.moves import http_client as httplib
from six.moves.urllib import parse as urlparse
-from tempest_lib import exceptions as lib_exc
from tempest import exceptions as exc
@@ -51,19 +49,20 @@
self.endpoint_port = endpoint_parts.port
self.endpoint_path = endpoint_parts.path
- self.connection_class = self.get_connection_class(self.endpoint_scheme)
- self.connection_kwargs = self.get_connection_kwargs(
+ self.connection_class = self._get_connection_class(
+ self.endpoint_scheme)
+ self.connection_kwargs = self._get_connection_kwargs(
self.endpoint_scheme, **kwargs)
@staticmethod
- def get_connection_class(scheme):
+ def _get_connection_class(scheme):
if scheme == 'https':
return VerifiedHTTPSConnection
else:
return httplib.HTTPConnection
@staticmethod
- def get_connection_kwargs(scheme, **kwargs):
+ def _get_connection_kwargs(scheme, **kwargs):
_kwargs = {'timeout': float(kwargs.get('timeout', 600))}
if scheme == 'https':
@@ -75,7 +74,7 @@
return _kwargs
- def get_connection(self):
+ def _get_connection(self):
_class = self.connection_class
try:
return _class(self.endpoint_hostname, self.endpoint_port,
@@ -95,7 +94,7 @@
self._log_request(method, url, kwargs['headers'])
- conn = self.get_connection()
+ conn = self._get_connection()
try:
url_parts = urlparse.urlparse(url)
@@ -159,30 +158,6 @@
self.LOG.debug("Large body (%d) md5 summary: %s", length,
hashlib.md5(str_body).hexdigest())
- def json_request(self, method, url, **kwargs):
- kwargs.setdefault('headers', {})
- kwargs['headers'].setdefault('Content-Type', 'application/json')
- if kwargs['headers']['Content-Type'] != 'application/json':
- msg = "Only application/json content-type is supported."
- raise lib_exc.InvalidContentType(msg)
-
- if 'body' in kwargs:
- kwargs['body'] = json.dumps(kwargs['body'])
-
- resp, body_iter = self._http_request(url, method, **kwargs)
-
- if 'application/json' in resp.getheader('content-type', ''):
- body = ''.join([chunk for chunk in body_iter])
- try:
- body = json.loads(body)
- except ValueError:
- LOG.error('Could not decode response body as JSON')
- else:
- msg = "Only json/application content-type is supported."
- raise lib_exc.InvalidContentType(msg)
-
- return resp, body
-
def raw_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
diff --git a/tempest/services/image/v1/json/image_client.py b/tempest/services/image/v1/json/images_client.py
similarity index 99%
rename from tempest/services/image/v1/json/image_client.py
rename to tempest/services/image/v1/json/images_client.py
index d97da36..4884106 100644
--- a/tempest/services/image/v1/json/image_client.py
+++ b/tempest/services/image/v1/json/images_client.py
@@ -32,13 +32,13 @@
LOG = logging.getLogger(__name__)
-class ImageClient(service_client.ServiceClient):
+class ImagesClient(service_client.ServiceClient):
def __init__(self, auth_provider, catalog_type, region, endpoint_type=None,
build_interval=None, build_timeout=None,
disable_ssl_certificate_validation=None,
ca_certs=None, trace_requests=None):
- super(ImageClient, self).__init__(
+ super(ImagesClient, self).__init__(
auth_provider,
catalog_type,
region,
diff --git a/tempest/services/image/v2/json/image_client.py b/tempest/services/image/v2/json/images_client.py
similarity index 98%
rename from tempest/services/image/v2/json/image_client.py
rename to tempest/services/image/v2/json/images_client.py
index f7d70ea..33bfcb8 100644
--- a/tempest/services/image/v2/json/image_client.py
+++ b/tempest/services/image/v2/json/images_client.py
@@ -22,13 +22,13 @@
from tempest.common import service_client
-class ImageClientV2(service_client.ServiceClient):
+class ImagesClientV2(service_client.ServiceClient):
def __init__(self, auth_provider, catalog_type, region, endpoint_type=None,
build_interval=None, build_timeout=None,
disable_ssl_certificate_validation=None, ca_certs=None,
trace_requests=None):
- super(ImageClientV2, self).__init__(
+ super(ImagesClientV2, self).__init__(
auth_provider,
catalog_type,
region,
diff --git a/tempest/test.py b/tempest/test.py
index 435d10a..30eb93d 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -27,6 +27,7 @@
from oslo_serialization import jsonutils as json
from oslo_utils import importutils
import six
+from tempest_lib import decorators
import testscenarios
import testtools
@@ -43,6 +44,8 @@
CONF = config.CONF
+idempotent_id = decorators.idempotent_id
+
def attr(**kwargs):
"""A decorator which applies the testtools attr decorator
@@ -62,23 +65,6 @@
return decorator
-def idempotent_id(id):
- """Stub for metadata decorator"""
- if not isinstance(id, six.string_types):
- raise TypeError('Test idempotent_id must be string not %s'
- '' % type(id).__name__)
- uuid.UUID(id)
-
- def decorator(f):
- f = testtools.testcase.attr('id-%s' % id)(f)
- if f.__doc__:
- f.__doc__ = 'Test idempotent id: %s\n%s' % (id, f.__doc__)
- else:
- f.__doc__ = 'Test idempotent id: %s' % id
- return f
- return decorator
-
-
def get_service_list():
service_list = {
'compute': CONF.service_available.nova,
diff --git a/tempest/tests/common/test_service_clients.py b/tempest/tests/common/test_service_clients.py
index 272eba4..8de014f 100644
--- a/tempest/tests/common/test_service_clients.py
+++ b/tempest/tests/common/test_service_clients.py
@@ -36,8 +36,8 @@
from tempest.services.identity.v3.json import policy_client
from tempest.services.identity.v3.json import region_client
from tempest.services.identity.v3.json import service_client
-from tempest.services.image.v1.json import image_client
-from tempest.services.image.v2.json import image_client as image_v2_client
+from tempest.services.image.v1.json import images_client
+from tempest.services.image.v2.json import images_client as images_v2_client
from tempest.services.messaging.json import messaging_client
from tempest.services.network.json import network_client
from tempest.services.object_storage import account_client
@@ -130,8 +130,8 @@
policy_client.PolicyClient,
region_client.RegionClient,
service_client.ServiceClient,
- image_client.ImageClient,
- image_v2_client.ImageClientV2
+ images_client.ImagesClient,
+ images_v2_client.ImagesClientV2
]
for client in test_clients:
diff --git a/tempest/tests/test_glance_http.py b/tempest/tests/test_glance_http.py
index 105caec..ed886da 100644
--- a/tempest/tests/test_glance_http.py
+++ b/tempest/tests/test_glance_http.py
@@ -13,14 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-import socket
-
import mock
-from oslo_serialization import jsonutils as json
from oslotest import mockpatch
import six
from six.moves import http_client as httplib
-from tempest_lib import exceptions as lib_exc
from tempest.common import glance_http
from tempest import exceptions
@@ -56,60 +52,6 @@
'getresponse', return_value=resp))
return resp
- def test_json_request_without_content_type_header_in_response(self):
- self._set_response_fixture({}, 200, 'fake_response_body')
- self.assertRaises(lib_exc.InvalidContentType,
- self.client.json_request, 'GET', '/images')
-
- def test_json_request_with_xml_content_type_header_in_request(self):
- self.assertRaises(lib_exc.InvalidContentType,
- self.client.json_request, 'GET', '/images',
- headers={'Content-Type': 'application/xml'})
-
- def test_json_request_with_xml_content_type_header_in_response(self):
- self._set_response_fixture({'content-type': 'application/xml'},
- 200, 'fake_response_body')
- self.assertRaises(lib_exc.InvalidContentType,
- self.client.json_request, 'GET', '/images')
-
- def test_json_request_with_json_content_type_header_only_in_resp(self):
- self._set_response_fixture({'content-type': 'application/json'},
- 200, 'fake_response_body')
- resp, body = self.client.json_request('GET', '/images')
- self.assertEqual(200, resp.status)
- self.assertEqual('fake_response_body', body)
-
- def test_json_request_with_json_content_type_header_in_req_and_resp(self):
- self._set_response_fixture({'content-type': 'application/json'},
- 200, 'fake_response_body')
- resp, body = self.client.json_request('GET', '/images', headers={
- 'Content-Type': 'application/json'})
- self.assertEqual(200, resp.status)
- self.assertEqual('fake_response_body', body)
-
- def test_json_request_fails_to_json_loads(self):
- self._set_response_fixture({'content-type': 'application/json'},
- 200, 'fake_response_body')
- self.useFixture(mockpatch.PatchObject(json, 'loads',
- side_effect=ValueError()))
- resp, body = self.client.json_request('GET', '/images')
- self.assertEqual(200, resp.status)
- self.assertEqual(body, 'fake_response_body')
-
- def test_json_request_socket_timeout(self):
- self.useFixture(mockpatch.PatchObject(httplib.HTTPConnection,
- 'request',
- side_effect=socket.timeout()))
- self.assertRaises(exceptions.TimeoutException,
- self.client.json_request, 'GET', '/images')
-
- def test_json_request_endpoint_not_found(self):
- self.useFixture(mockpatch.PatchObject(httplib.HTTPConnection,
- 'request',
- side_effect=socket.gaierror()))
- self.assertRaises(exceptions.EndpointNotFound,
- self.client.json_request, 'GET', '/images')
-
def test_raw_request(self):
self._set_response_fixture({}, 200, 'fake_response_body')
resp, body = self.client.raw_request('GET', '/images')
@@ -141,22 +83,22 @@
self.assertEqual(call_count - 1, req_body.tell())
def test_get_connection_class_for_https(self):
- conn_class = self.client.get_connection_class('https')
+ conn_class = self.client._get_connection_class('https')
self.assertEqual(glance_http.VerifiedHTTPSConnection, conn_class)
def test_get_connection_class_for_http(self):
- conn_class = (self.client.get_connection_class('http'))
+ conn_class = (self.client._get_connection_class('http'))
self.assertEqual(httplib.HTTPConnection, conn_class)
def test_get_connection_http(self):
- self.assertTrue(isinstance(self.client.get_connection(),
+ self.assertTrue(isinstance(self.client._get_connection(),
httplib.HTTPConnection))
def test_get_connection_https(self):
endpoint = 'https://fake_url.com'
self.fake_auth.base_url = mock.MagicMock(return_value=endpoint)
self.client = glance_http.HTTPClient(self.fake_auth, {})
- self.assertTrue(isinstance(self.client.get_connection(),
+ self.assertTrue(isinstance(self.client._get_connection(),
glance_http.VerifiedHTTPSConnection))
def test_get_connection_url_not_fount(self):
@@ -164,22 +106,22 @@
side_effect=httplib.InvalidURL()
))
self.assertRaises(exceptions.EndpointNotFound,
- self.client.get_connection)
+ self.client._get_connection)
def test_get_connection_kwargs_default_for_http(self):
- kwargs = self.client.get_connection_kwargs('http')
+ kwargs = self.client._get_connection_kwargs('http')
self.assertEqual(600, kwargs['timeout'])
self.assertEqual(1, len(kwargs.keys()))
def test_get_connection_kwargs_set_timeout_for_http(self):
- kwargs = self.client.get_connection_kwargs('http', timeout=10,
- ca_certs='foo')
+ kwargs = self.client._get_connection_kwargs('http', timeout=10,
+ ca_certs='foo')
self.assertEqual(10, kwargs['timeout'])
# nothing more than timeout is evaluated for http connections
self.assertEqual(1, len(kwargs.keys()))
def test_get_connection_kwargs_default_for_https(self):
- kwargs = self.client.get_connection_kwargs('https')
+ kwargs = self.client._get_connection_kwargs('https')
self.assertEqual(600, kwargs['timeout'])
self.assertEqual(None, kwargs['ca_certs'])
self.assertEqual(None, kwargs['cert_file'])
@@ -189,12 +131,12 @@
self.assertEqual(6, len(kwargs.keys()))
def test_get_connection_kwargs_set_params_for_https(self):
- kwargs = self.client.get_connection_kwargs('https', timeout=10,
- ca_certs='foo',
- cert_file='/foo/bar.cert',
- key_file='/foo/key.pem',
- insecure=True,
- ssl_compression=False)
+ kwargs = self.client._get_connection_kwargs('https', timeout=10,
+ ca_certs='foo',
+ cert_file='/foo/bar.cert',
+ key_file='/foo/key.pem',
+ insecure=True,
+ ssl_compression=False)
self.assertEqual(10, kwargs['timeout'])
self.assertEqual('foo', kwargs['ca_certs'])
self.assertEqual('/foo/bar.cert', kwargs['cert_file'])
diff --git a/tools/check_uuid.py b/tools/check_uuid.py
deleted file mode 100755
index a71ad39..0000000
--- a/tools/check_uuid.py
+++ /dev/null
@@ -1,358 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright 2014 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.
-
-import argparse
-import ast
-import importlib
-import inspect
-import os
-import sys
-import unittest
-import urllib
-import uuid
-
-DECORATOR_MODULE = 'test'
-DECORATOR_NAME = 'idempotent_id'
-DECORATOR_IMPORT = 'tempest.%s' % DECORATOR_MODULE
-IMPORT_LINE = 'from tempest import %s' % DECORATOR_MODULE
-DECORATOR_TEMPLATE = "@%s.%s('%%s')" % (DECORATOR_MODULE,
- DECORATOR_NAME)
-UNIT_TESTS_EXCLUDE = 'tempest.tests'
-
-
-class SourcePatcher(object):
-
- """"Lazy patcher for python source files"""
-
- def __init__(self):
- self.source_files = None
- self.patches = None
- self.clear()
-
- def clear(self):
- """Clear inner state"""
- self.source_files = {}
- self.patches = {}
-
- @staticmethod
- def _quote(s):
- return urllib.quote(s)
-
- @staticmethod
- def _unquote(s):
- return urllib.unquote(s)
-
- def add_patch(self, filename, patch, line_no):
- """Add lazy patch"""
- if filename not in self.source_files:
- with open(filename) as f:
- self.source_files[filename] = self._quote(f.read())
- patch_id = str(uuid.uuid4())
- if not patch.endswith('\n'):
- patch += '\n'
- self.patches[patch_id] = self._quote(patch)
- lines = self.source_files[filename].split(self._quote('\n'))
- lines[line_no - 1] = ''.join(('{%s:s}' % patch_id, lines[line_no - 1]))
- self.source_files[filename] = self._quote('\n').join(lines)
-
- def _save_changes(self, filename, source):
- print('%s fixed' % filename)
- with open(filename, 'w') as f:
- f.write(source)
-
- def apply_patches(self):
- """Apply all patches"""
- for filename in self.source_files:
- patched_source = self._unquote(
- self.source_files[filename].format(**self.patches)
- )
- self._save_changes(filename, patched_source)
- self.clear()
-
-
-class TestChecker(object):
-
- def __init__(self, package):
- self.package = package
- self.base_path = os.path.abspath(os.path.dirname(package.__file__))
-
- def _path_to_package(self, path):
- relative_path = path[len(self.base_path) + 1:]
- if relative_path:
- return '.'.join((self.package.__name__,) +
- tuple(relative_path.split('/')))
- else:
- return self.package.__name__
-
- def _modules_search(self):
- """Recursive search for python modules in base package"""
- modules = []
- for root, dirs, files in os.walk(self.base_path):
- if not os.path.exists(os.path.join(root, '__init__.py')):
- continue
- root_package = self._path_to_package(root)
- for item in files:
- if item.endswith('.py'):
- module_name = '.'.join((root_package,
- os.path.splitext(item)[0]))
- if not module_name.startswith(UNIT_TESTS_EXCLUDE):
- modules.append(module_name)
- return modules
-
- @staticmethod
- def _get_idempotent_id(test_node):
- # Return key-value dict with all metadata from @test.idempotent_id
- # decorators for test method
- idempotent_id = None
- for decorator in test_node.decorator_list:
- if (hasattr(decorator, 'func') and
- hasattr(decorator.func, 'attr') and
- decorator.func.attr == DECORATOR_NAME and
- hasattr(decorator.func, 'value') and
- decorator.func.value.id == DECORATOR_MODULE):
- for arg in decorator.args:
- idempotent_id = ast.literal_eval(arg)
- return idempotent_id
-
- @staticmethod
- def _is_decorator(line):
- return line.strip().startswith('@')
-
- @staticmethod
- def _is_def(line):
- return line.strip().startswith('def ')
-
- def _add_uuid_to_test(self, patcher, test_node, source_path):
- with open(source_path) as src:
- src_lines = src.read().split('\n')
- lineno = test_node.lineno
- insert_position = lineno
- while True:
- if (self._is_def(src_lines[lineno - 1]) or
- (self._is_decorator(src_lines[lineno - 1]) and
- (DECORATOR_TEMPLATE.split('(')[0] <=
- src_lines[lineno - 1].strip().split('(')[0]))):
- insert_position = lineno
- break
- lineno += 1
- patcher.add_patch(
- source_path,
- ' ' * test_node.col_offset + DECORATOR_TEMPLATE % uuid.uuid4(),
- insert_position
- )
-
- @staticmethod
- def _is_test_case(module, node):
- if (node.__class__ is ast.ClassDef and
- hasattr(module, node.name) and
- inspect.isclass(getattr(module, node.name))):
- return issubclass(getattr(module, node.name), unittest.TestCase)
-
- @staticmethod
- def _is_test_method(node):
- return (node.__class__ is ast.FunctionDef
- and node.name.startswith('test_'))
-
- @staticmethod
- def _next_node(body, node):
- if body.index(node) < len(body):
- return body[body.index(node) + 1]
-
- @staticmethod
- def _import_name(node):
- if type(node) == ast.Import:
- return node.names[0].name
- elif type(node) == ast.ImportFrom:
- return '%s.%s' % (node.module, node.names[0].name)
-
- def _add_import_for_test_uuid(self, patcher, src_parsed, source_path):
- with open(source_path) as f:
- src_lines = f.read().split('\n')
- line_no = 0
- tempest_imports = [node for node in src_parsed.body
- if self._import_name(node) and
- 'tempest.' in self._import_name(node)]
- if not tempest_imports:
- import_snippet = '\n'.join(('', IMPORT_LINE, ''))
- else:
- for node in tempest_imports:
- if self._import_name(node) < DECORATOR_IMPORT:
- continue
- else:
- line_no = node.lineno
- import_snippet = IMPORT_LINE
- break
- else:
- line_no = tempest_imports[-1].lineno
- while True:
- if (not src_lines[line_no - 1] or
- getattr(self._next_node(src_parsed.body,
- tempest_imports[-1]),
- 'lineno') == line_no or
- line_no == len(src_lines)):
- break
- line_no += 1
- import_snippet = '\n'.join((IMPORT_LINE, ''))
- patcher.add_patch(source_path, import_snippet, line_no)
-
- def get_tests(self):
- """Get test methods with sources from base package with metadata"""
- tests = {}
- for module_name in self._modules_search():
- tests[module_name] = {}
- module = importlib.import_module(module_name)
- source_path = '.'.join(
- (os.path.splitext(module.__file__)[0], 'py')
- )
- with open(source_path, 'r') as f:
- source = f.read()
- tests[module_name]['source_path'] = source_path
- tests[module_name]['tests'] = {}
- source_parsed = ast.parse(source)
- tests[module_name]['ast'] = source_parsed
- tests[module_name]['import_valid'] = (
- hasattr(module, DECORATOR_MODULE) and
- inspect.ismodule(getattr(module, DECORATOR_MODULE))
- )
- test_cases = (node for node in source_parsed.body
- if self._is_test_case(module, node))
- for node in test_cases:
- for subnode in filter(self._is_test_method, node.body):
- test_name = '%s.%s' % (node.name, subnode.name)
- tests[module_name]['tests'][test_name] = subnode
- return tests
-
- @staticmethod
- def _filter_tests(function, tests):
- """Filter tests with condition 'function(test_node) == True'"""
- result = {}
- for module_name in tests:
- for test_name in tests[module_name]['tests']:
- if function(module_name, test_name, tests):
- if module_name not in result:
- result[module_name] = {
- 'ast': tests[module_name]['ast'],
- 'source_path': tests[module_name]['source_path'],
- 'import_valid': tests[module_name]['import_valid'],
- 'tests': {}
- }
- result[module_name]['tests'][test_name] = \
- tests[module_name]['tests'][test_name]
- return result
-
- def find_untagged(self, tests):
- """Filter all tests without uuid in metadata"""
- def check_uuid_in_meta(module_name, test_name, tests):
- idempotent_id = self._get_idempotent_id(
- tests[module_name]['tests'][test_name])
- return not idempotent_id
- return self._filter_tests(check_uuid_in_meta, tests)
-
- def report_collisions(self, tests):
- """Reports collisions if there are any.
-
- Returns true if collisions exist.
- """
- uuids = {}
-
- def report(module_name, test_name, tests):
- test_uuid = self._get_idempotent_id(
- tests[module_name]['tests'][test_name])
- if not test_uuid:
- return
- if test_uuid in uuids:
- error_str = "%s:%s\n uuid %s collision: %s<->%s\n%s:%s" % (
- tests[module_name]['source_path'],
- tests[module_name]['tests'][test_name].lineno,
- test_uuid,
- test_name,
- uuids[test_uuid]['test_name'],
- uuids[test_uuid]['source_path'],
- uuids[test_uuid]['test_node'].lineno,
- )
- print(error_str)
- print("cannot automatically resolve the collision, please "
- "manually remove the duplicate value on the new test.")
- return True
- else:
- uuids[test_uuid] = {
- 'module': module_name,
- 'test_name': test_name,
- 'test_node': tests[module_name]['tests'][test_name],
- 'source_path': tests[module_name]['source_path']
- }
- return bool(self._filter_tests(report, tests))
-
- def report_untagged(self, tests):
- """Reports untagged tests if there are any.
-
- Returns true if untagged tests exist.
- """
- def report(module_name, test_name, tests):
- error_str = "%s:%s\nmissing @test.idempotent_id('...')\n%s\n" % (
- tests[module_name]['source_path'],
- tests[module_name]['tests'][test_name].lineno,
- test_name
- )
- print(error_str)
- return True
- return bool(self._filter_tests(report, tests))
-
- def fix_tests(self, tests):
- """Add uuids to all tests specified in tests and fix it"""
- patcher = SourcePatcher()
- for module_name in tests:
- add_import_once = True
- for test_name in tests[module_name]['tests']:
- if not tests[module_name]['import_valid'] and add_import_once:
- self._add_import_for_test_uuid(
- patcher,
- tests[module_name]['ast'],
- tests[module_name]['source_path']
- )
- add_import_once = False
- self._add_uuid_to_test(
- patcher, tests[module_name]['tests'][test_name],
- tests[module_name]['source_path'])
- patcher.apply_patches()
-
-
-def run():
- parser = argparse.ArgumentParser()
- parser.add_argument('--package', action='store', dest='package',
- default='tempest', type=str,
- help='Package with tests')
- parser.add_argument('--fix', action='store_true', dest='fix_tests',
- help='Attempt to fix tests without UUIDs')
- args = parser.parse_args()
- sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
- pkg = importlib.import_module(args.package)
- checker = TestChecker(pkg)
- errors = False
- tests = checker.get_tests()
- untagged = checker.find_untagged(tests)
- errors = checker.report_collisions(tests) or errors
- if args.fix_tests and untagged:
- checker.fix_tests(untagged)
- else:
- errors = checker.report_untagged(untagged) or errors
- if errors:
- sys.exit("@test.idempotent_id existence and uniqueness checks failed\n"
- "Run 'tox -v -euuidgen' to automatically fix tests with\n"
- "missing @test.idempotent_id decorators.")
-
-if __name__ == '__main__':
- run()
diff --git a/tox.ini b/tox.ini
index ecd4f24..41eece1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -114,11 +114,11 @@
[testenv:pep8]
commands =
flake8 {posargs}
- python tools/check_uuid.py
+ check-uuid
[testenv:uuidgen]
commands =
- python tools/check_uuid.py --fix
+ check-uuid --fix
[hacking]
local-check-factory = tempest.hacking.checks.factory