Simplify retargetable test framework
The retargetable testing prototype previously relied on each test case defining the 'scenarios' attribute used to parametize testing with testscenarios. Anticipating the requirement to retrofit the imported tempest api test cases, this change moves scenario definition to a base class since scenarios are common across all api tests. This change also sets the retargetable test to skip when invoked against rest. Tempest uses class-level setup for auth and this needs to be broken out into fixtures before the retargetable testing will work again. Change-Id: I70eb21db9b983d45e9bcc7ea90e36f202d3e3e45
This commit is contained in:
parent
15a507afb1
commit
5723970e5f
@ -16,8 +16,6 @@ import unittest
|
|||||||
# Allow the retargetable and tempest api tests to be executed as part
|
# Allow the retargetable and tempest api tests to be executed as part
|
||||||
# of the same job by ensuring that tests from both tests are
|
# of the same job by ensuring that tests from both tests are
|
||||||
# discovered.
|
# discovered.
|
||||||
#
|
|
||||||
# TODO(marun) Remove once the tempest tests have been moved to api/
|
|
||||||
|
|
||||||
|
|
||||||
def _discover(loader, path, pattern):
|
def _discover(loader, path, pattern):
|
||||||
@ -30,8 +28,12 @@ def load_tests(_, tests, pattern):
|
|||||||
|
|
||||||
loader = unittest.loader.TestLoader()
|
loader = unittest.loader.TestLoader()
|
||||||
suite.addTests(_discover(loader, "./neutron/tests/api", pattern))
|
suite.addTests(_discover(loader, "./neutron/tests/api", pattern))
|
||||||
|
# TODO(marun) Remove once the tempest tests have been moved to api/
|
||||||
suite.addTests(_discover(loader,
|
suite.addTests(_discover(loader,
|
||||||
"./neutron/tests/tempest/api/network",
|
"./neutron/tests/tempest/api/network",
|
||||||
pattern))
|
pattern))
|
||||||
|
suite.addTests(_discover(loader,
|
||||||
|
"./neutron/tests/retargetable",
|
||||||
|
pattern))
|
||||||
|
|
||||||
return suite
|
return suite
|
||||||
|
@ -1,129 +0,0 @@
|
|||||||
# Copyright 2014, Red Hat Inc.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
This module defines functional tests for the Neutron V2 API in the
|
|
||||||
BaseTestApi class. The intention is that the class will be overridden
|
|
||||||
and configured for use with testscenarios as follows:
|
|
||||||
|
|
||||||
- A subclass should override the 'scenarios' class member with a
|
|
||||||
list of tuple pairs, e.g.
|
|
||||||
|
|
||||||
scenarios = [('scenario_id', dict(client=Client())]
|
|
||||||
|
|
||||||
The first element of each scenario tuple is a user-defined textual
|
|
||||||
id, and the second element is a dictionary whose client parameter
|
|
||||||
should be a subclass of BaseNeutronClient.
|
|
||||||
|
|
||||||
- The module containing the test class should defines a 'load_tests'
|
|
||||||
variable as follows:
|
|
||||||
|
|
||||||
load_tests = testscenarios.load_tests_apply_scenarios
|
|
||||||
|
|
||||||
Examples of use include:
|
|
||||||
|
|
||||||
neutron.tests.functional.api.test_v2_plugin - targets the plugin api
|
|
||||||
for each configured plugin
|
|
||||||
|
|
||||||
neutron.tests.api.test_v2_rest_client - targets neutron server
|
|
||||||
via the tempest rest client
|
|
||||||
|
|
||||||
The tests in neutron.tests.api depend on Neutron and Tempest being
|
|
||||||
deployed (e.g. with Devstack) and are intended to be run in advisory
|
|
||||||
check jobs.
|
|
||||||
|
|
||||||
Reference: https://pypi.python.org/pypi/testscenarios/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import abc
|
|
||||||
|
|
||||||
import six
|
|
||||||
import testtools
|
|
||||||
|
|
||||||
from neutron.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class BaseNeutronClient(object):
|
|
||||||
"""
|
|
||||||
Base class for a client that can interact the neutron api in some
|
|
||||||
manner.
|
|
||||||
|
|
||||||
Reference: :file:`neutron/neutron_plugin_base_v2.py`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self, test_case):
|
|
||||||
"""Configure the api for use with a test case
|
|
||||||
|
|
||||||
:param test_case: The test case that will exercise the api
|
|
||||||
"""
|
|
||||||
self.test_case = test_case
|
|
||||||
|
|
||||||
@abc.abstractproperty
|
|
||||||
def NotFound(self):
|
|
||||||
"""The exception that indicates a resource could not be found.
|
|
||||||
|
|
||||||
Tests can use this property to assert for a missing resource
|
|
||||||
in a client-agnostic way.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def create_network(self, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def update_network(self, id_, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_network(self, id_, fields=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_networks(self, filters=None, fields=None,
|
|
||||||
sorts=None, limit=None, marker=None, page_reverse=False):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def delete_network(self, id_):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTestApi(base.BaseTestCase):
|
|
||||||
|
|
||||||
scenarios = ()
|
|
||||||
|
|
||||||
def setUp(self, setup_parent=True):
|
|
||||||
# Calling the parent setUp is optional - the subclass may be
|
|
||||||
# calling it already via a different ancestor.
|
|
||||||
if setup_parent:
|
|
||||||
super(BaseTestApi, self).setUp()
|
|
||||||
self.client.setUp(self)
|
|
||||||
|
|
||||||
def test_network_lifecycle(self):
|
|
||||||
net = self.client.create_network(name=base.get_rand_name())
|
|
||||||
listed_networks = dict((x.id, x.name)
|
|
||||||
for x in self.client.get_networks())
|
|
||||||
self.assertIn(net.id, listed_networks)
|
|
||||||
self.assertEqual(listed_networks[net.id], net.name,
|
|
||||||
'Listed network name is not as expected.')
|
|
||||||
updated_name = 'new %s' % net.name
|
|
||||||
updated_net = self.client.update_network(net.id, name=updated_name)
|
|
||||||
self.assertEqual(updated_name, updated_net.name,
|
|
||||||
'Updated network name is not as expected.')
|
|
||||||
self.client.delete_network(net.id)
|
|
||||||
with testtools.ExpectedException(self.client.NotFound,
|
|
||||||
msg='Network was not deleted'):
|
|
||||||
self.client.get_network(net.id)
|
|
@ -1,82 +0,0 @@
|
|||||||
# Copyright 2014, Red Hat Inc.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
This module implements BaseNeutronClient for the Tempest rest client
|
|
||||||
and configures the api tests with scenarios targeting the Neutron API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tempest_lib import exceptions
|
|
||||||
import testscenarios
|
|
||||||
|
|
||||||
from neutron.tests.api import base_v2
|
|
||||||
from neutron.tests import base
|
|
||||||
from neutron.tests.tempest.api.network import base as t_base
|
|
||||||
|
|
||||||
# Required to generate tests from scenarios. Not compatible with nose.
|
|
||||||
load_tests = testscenarios.load_tests_apply_scenarios
|
|
||||||
|
|
||||||
|
|
||||||
class TempestRestClient(base_v2.BaseNeutronClient):
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self):
|
|
||||||
if not hasattr(self, '_client'):
|
|
||||||
manager = t_base.BaseNetworkTest.get_client_manager()
|
|
||||||
self._client = manager.network_client
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
@property
|
|
||||||
def NotFound(self):
|
|
||||||
return exceptions.NotFound
|
|
||||||
|
|
||||||
def _cleanup_network(self, id_):
|
|
||||||
try:
|
|
||||||
self.delete_network(id_)
|
|
||||||
except self.NotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_network(self, **kwargs):
|
|
||||||
network = self._create_network(**kwargs)
|
|
||||||
self.test_case.addCleanup(self._cleanup_network, network.id)
|
|
||||||
return network
|
|
||||||
|
|
||||||
def _create_network(self, **kwargs):
|
|
||||||
# Internal method - use create_network() instead
|
|
||||||
body = self.client.create_network(**kwargs)
|
|
||||||
return base.AttributeDict(body['network'])
|
|
||||||
|
|
||||||
def update_network(self, id_, **kwargs):
|
|
||||||
body = self.client.update_network(id_, **kwargs)
|
|
||||||
return base.AttributeDict(body['network'])
|
|
||||||
|
|
||||||
def get_network(self, id_, **kwargs):
|
|
||||||
body = self.client.show_network(id_, **kwargs)
|
|
||||||
return base.AttributeDict(body['network'])
|
|
||||||
|
|
||||||
def get_networks(self, **kwargs):
|
|
||||||
body = self.client.list_networks(**kwargs)
|
|
||||||
return [base.AttributeDict(x) for x in body['networks']]
|
|
||||||
|
|
||||||
def delete_network(self, id_):
|
|
||||||
self.client.delete_network(id_)
|
|
||||||
|
|
||||||
|
|
||||||
class TestApiWithRestClient(base_v2.BaseTestApi):
|
|
||||||
scenarios = [('tempest', {'client': TempestRestClient()})]
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
raise self.skipException(
|
|
||||||
'Tempest fixture requirements prevent this test from running')
|
|
@ -13,17 +13,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Previously, running 'tox -e dsvm-functional' simply ran a normal test discovery
|
In order to save gate resources, test paths that have similar
|
||||||
of the ./neutron/tests/functional tree. In order to save gate resources, we
|
environmental requirements to the functional path are marked for
|
||||||
decided to forgo adding a new gate job specifically for the full-stack
|
discovery.
|
||||||
framework, and instead discover tests that are present in
|
|
||||||
./neutron/tests/fullstack.
|
|
||||||
|
|
||||||
In short, running 'tox -e dsvm-functional' now runs both functional tests and
|
|
||||||
full-stack tests, and this code allows for the test discovery needed.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
@ -37,8 +31,8 @@ def load_tests(_, tests, pattern):
|
|||||||
|
|
||||||
loader = unittest.loader.TestLoader()
|
loader = unittest.loader.TestLoader()
|
||||||
suite.addTests(_discover(loader, "./neutron/tests/functional", pattern))
|
suite.addTests(_discover(loader, "./neutron/tests/functional", pattern))
|
||||||
|
|
||||||
if os.getenv('OS_RUN_FULLSTACK') == '1':
|
|
||||||
suite.addTests(_discover(loader, "./neutron/tests/fullstack", pattern))
|
suite.addTests(_discover(loader, "./neutron/tests/fullstack", pattern))
|
||||||
|
suite.addTests(_discover(loader, "./neutron/tests/retargetable",
|
||||||
|
pattern))
|
||||||
|
|
||||||
return suite
|
return suite
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
# Copyright 2014, Red Hat Inc.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
This module implements BaseNeutronClient for the programmatic plugin
|
|
||||||
api and configures the api tests with scenarios targeting individual
|
|
||||||
plugins.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import testscenarios
|
|
||||||
|
|
||||||
from neutron.common import exceptions as q_exc
|
|
||||||
from neutron import context
|
|
||||||
from neutron import manager
|
|
||||||
from neutron.tests.api import base_v2
|
|
||||||
from neutron.tests import base
|
|
||||||
from neutron.tests.unit.ml2 import test_ml2_plugin
|
|
||||||
from neutron.tests.unit import testlib_api
|
|
||||||
|
|
||||||
|
|
||||||
# Each plugin must add a class to plugin_configurations that can configure the
|
|
||||||
# plugin for use with PluginClient. For a given plugin, the setup
|
|
||||||
# used for NeutronDbPluginV2TestCase can usually be reused. See the
|
|
||||||
# configuration classes listed below for examples of this reuse.
|
|
||||||
|
|
||||||
#TODO(marun) Discover plugin conf via a metaclass
|
|
||||||
plugin_configurations = [
|
|
||||||
test_ml2_plugin.Ml2PluginConf,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Required to generate tests from scenarios. Not compatible with nose.
|
|
||||||
load_tests = testscenarios.load_tests_apply_scenarios
|
|
||||||
|
|
||||||
|
|
||||||
class PluginClient(base_v2.BaseNeutronClient):
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ctx(self):
|
|
||||||
if not hasattr(self, '_ctx'):
|
|
||||||
self._ctx = context.Context('', 'test-tenant')
|
|
||||||
return self._ctx
|
|
||||||
|
|
||||||
@property
|
|
||||||
def plugin(self):
|
|
||||||
return manager.NeutronManager.get_plugin()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def NotFound(self):
|
|
||||||
return q_exc.NetworkNotFound
|
|
||||||
|
|
||||||
def create_network(self, **kwargs):
|
|
||||||
# Supply defaults that are expected to be set by the api
|
|
||||||
# framwork
|
|
||||||
kwargs.setdefault('admin_state_up', True)
|
|
||||||
kwargs.setdefault('vlan_transparent', False)
|
|
||||||
kwargs.setdefault('shared', False)
|
|
||||||
data = dict(network=kwargs)
|
|
||||||
result = self.plugin.create_network(self.ctx, data)
|
|
||||||
return base.AttributeDict(result)
|
|
||||||
|
|
||||||
def update_network(self, id_, **kwargs):
|
|
||||||
data = dict(network=kwargs)
|
|
||||||
result = self.plugin.update_network(self.ctx, id_, data)
|
|
||||||
return base.AttributeDict(result)
|
|
||||||
|
|
||||||
def get_network(self, *args, **kwargs):
|
|
||||||
result = self.plugin.get_network(self.ctx, *args, **kwargs)
|
|
||||||
return base.AttributeDict(result)
|
|
||||||
|
|
||||||
def get_networks(self, *args, **kwargs):
|
|
||||||
result = self.plugin.get_networks(self.ctx, *args, **kwargs)
|
|
||||||
return [base.AttributeDict(x) for x in result]
|
|
||||||
|
|
||||||
def delete_network(self, id_):
|
|
||||||
self.plugin.delete_network(self.ctx, id_)
|
|
||||||
|
|
||||||
|
|
||||||
def get_scenarios():
|
|
||||||
scenarios = []
|
|
||||||
client = PluginClient()
|
|
||||||
for conf in plugin_configurations:
|
|
||||||
name = conf.plugin_name
|
|
||||||
class_name = name[name.rfind('.') + 1:]
|
|
||||||
scenarios.append((class_name, {'client': client, 'plugin_conf': conf}))
|
|
||||||
return scenarios
|
|
||||||
|
|
||||||
|
|
||||||
class TestPluginApi(base_v2.BaseTestApi,
|
|
||||||
testlib_api.SqlTestCase):
|
|
||||||
|
|
||||||
scenarios = get_scenarios()
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# BaseTestApi is not based on BaseTestCase to avoid import
|
|
||||||
# errors when importing Tempest. When targeting the plugin
|
|
||||||
# api, it is necessary to avoid calling BaseTestApi's parent
|
|
||||||
# setUp, since that setup will be called by SqlTestCase.setUp.
|
|
||||||
super(TestPluginApi, self).setUp(setup_parent=False)
|
|
||||||
testlib_api.SqlTestCase.setUp(self)
|
|
||||||
self.useFixture(base.PluginFixture(self.plugin_conf.plugin_name))
|
|
||||||
self.plugin_conf.setUp(self)
|
|
0
neutron/tests/retargetable/__init__.py
Normal file
0
neutron/tests/retargetable/__init__.py
Normal file
80
neutron/tests/retargetable/base.py
Normal file
80
neutron/tests/retargetable/base.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module defines a base test case that uses testscenarios to
|
||||||
|
parametize the test methods of subclasses by varying the client
|
||||||
|
fixture used to target the Neutron API.
|
||||||
|
|
||||||
|
PluginClientFixture targets the Neutron API directly via the plugin
|
||||||
|
api, and will be executed by default. testscenarios will ensure that
|
||||||
|
each test is run against all plugins defined in plugin_configurations.
|
||||||
|
|
||||||
|
RestClientFixture targets a deployed Neutron daemon, and will be used
|
||||||
|
instead of PluginClientFixture only if OS_TEST_API_WITH_REST is set to 1.
|
||||||
|
|
||||||
|
Reference: https://pypi.python.org/pypi/testscenarios/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import testscenarios
|
||||||
|
|
||||||
|
from neutron.tests import base as tests_base
|
||||||
|
from neutron.tests.retargetable import client_fixtures
|
||||||
|
from neutron.tests.unit.ml2 import test_ml2_plugin
|
||||||
|
|
||||||
|
|
||||||
|
# Each plugin must add a class to plugin_configurations that can configure the
|
||||||
|
# plugin for use with PluginClient. For a given plugin, the setup
|
||||||
|
# used for NeutronDbPluginV2TestCase can usually be reused. See the
|
||||||
|
# configuration classes listed below for examples of this reuse.
|
||||||
|
|
||||||
|
# TODO(marun) Discover plugin conf via a metaclass
|
||||||
|
plugin_configurations = [
|
||||||
|
test_ml2_plugin.Ml2ConfFixture(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def rest_enabled():
|
||||||
|
return tests_base.bool_from_env('OS_TEST_API_WITH_REST')
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_scenarios():
|
||||||
|
scenarios = []
|
||||||
|
for conf in plugin_configurations:
|
||||||
|
name = conf.plugin_name
|
||||||
|
class_name = name.rsplit('.', 1)[-1]
|
||||||
|
client = client_fixtures.PluginClientFixture(conf)
|
||||||
|
scenarios.append((class_name, {'client': client}))
|
||||||
|
return scenarios
|
||||||
|
|
||||||
|
|
||||||
|
def get_scenarios():
|
||||||
|
if rest_enabled():
|
||||||
|
# FIXME(marun) Remove local import once tempest config is safe
|
||||||
|
# to import alonside neutron config
|
||||||
|
from neutron.tests.retargetable import rest_fixture
|
||||||
|
return [('tempest', {'client': rest_fixture.RestClientFixture()})]
|
||||||
|
else:
|
||||||
|
return get_plugin_scenarios()
|
||||||
|
|
||||||
|
|
||||||
|
class RetargetableApiTest(testscenarios.WithScenarios,
|
||||||
|
tests_base.BaseTestCase):
|
||||||
|
|
||||||
|
scenarios = get_scenarios()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(RetargetableApiTest, self).setUp()
|
||||||
|
if rest_enabled():
|
||||||
|
raise self.skipException(
|
||||||
|
'Tempest fixture requirements prevent this test from running')
|
||||||
|
self.useFixture(self.client)
|
117
neutron/tests/retargetable/client_fixtures.py
Normal file
117
neutron/tests/retargetable/client_fixtures.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module defines client fixtures that can be used to target the
|
||||||
|
Neutron API via different methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
import fixtures
|
||||||
|
import six
|
||||||
|
|
||||||
|
from neutron.common import exceptions as q_exc
|
||||||
|
from neutron import context
|
||||||
|
from neutron import manager
|
||||||
|
from neutron.tests import base
|
||||||
|
from neutron.tests.unit import testlib_api
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class AbstractClientFixture(fixtures.Fixture):
|
||||||
|
"""
|
||||||
|
Base class for a client that can interact the neutron api in some
|
||||||
|
manner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def NotFound(self):
|
||||||
|
"""The exception that indicates a resource could not be found.
|
||||||
|
|
||||||
|
Tests can use this property to assert for a missing resource
|
||||||
|
in a client-agnostic way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create_network(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def update_network(self, id_, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_network(self, id_, fields=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_networks(self, filters=None, fields=None,
|
||||||
|
sorts=None, limit=None, marker=None, page_reverse=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def delete_network(self, id_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PluginClientFixture(AbstractClientFixture):
|
||||||
|
"""Targets the Neutron API via the plugin API"""
|
||||||
|
|
||||||
|
def __init__(self, plugin_conf):
|
||||||
|
self.plugin_conf = plugin_conf
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(PluginClientFixture, self).setUp()
|
||||||
|
self.useFixture(testlib_api.SqlFixture())
|
||||||
|
self.useFixture(self.plugin_conf)
|
||||||
|
self.useFixture(base.PluginFixture(self.plugin_conf.plugin_name))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ctx(self):
|
||||||
|
if not hasattr(self, '_ctx'):
|
||||||
|
self._ctx = context.Context('', 'test-tenant')
|
||||||
|
return self._ctx
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin(self):
|
||||||
|
return manager.NeutronManager.get_plugin()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def NotFound(self):
|
||||||
|
return q_exc.NetworkNotFound
|
||||||
|
|
||||||
|
def create_network(self, **kwargs):
|
||||||
|
# Supply defaults that are expected to be set by the api
|
||||||
|
# framwork
|
||||||
|
kwargs.setdefault('admin_state_up', True)
|
||||||
|
kwargs.setdefault('vlan_transparent', False)
|
||||||
|
kwargs.setdefault('shared', False)
|
||||||
|
data = dict(network=kwargs)
|
||||||
|
result = self.plugin.create_network(self.ctx, data)
|
||||||
|
return base.AttributeDict(result)
|
||||||
|
|
||||||
|
def update_network(self, id_, **kwargs):
|
||||||
|
data = dict(network=kwargs)
|
||||||
|
result = self.plugin.update_network(self.ctx, id_, data)
|
||||||
|
return base.AttributeDict(result)
|
||||||
|
|
||||||
|
def get_network(self, *args, **kwargs):
|
||||||
|
result = self.plugin.get_network(self.ctx, *args, **kwargs)
|
||||||
|
return base.AttributeDict(result)
|
||||||
|
|
||||||
|
def get_networks(self, *args, **kwargs):
|
||||||
|
result = self.plugin.get_networks(self.ctx, *args, **kwargs)
|
||||||
|
return [base.AttributeDict(x) for x in result]
|
||||||
|
|
||||||
|
def delete_network(self, id_):
|
||||||
|
self.plugin.delete_network(self.ctx, id_)
|
70
neutron/tests/retargetable/rest_fixture.py
Normal file
70
neutron/tests/retargetable/rest_fixture.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module defines a client fixture that can be used to target a
|
||||||
|
deployed neutron daemon. The potential for conflict between Tempest
|
||||||
|
configuration and Neutron configuration requires that
|
||||||
|
neutron.tests.tempest imports be isolated in this module for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tempest_lib import exceptions as tlib_exceptions
|
||||||
|
|
||||||
|
from neutron.tests import base
|
||||||
|
from neutron.tests.retargetable import client_fixtures
|
||||||
|
from neutron.tests.tempest import test as t_test
|
||||||
|
|
||||||
|
|
||||||
|
class RestClientFixture(client_fixtures.AbstractClientFixture):
|
||||||
|
"""Targets the Neutron API via REST."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
if not hasattr(self, '_client'):
|
||||||
|
manager = t_test.BaseTestCase.get_client_manager()
|
||||||
|
self._client = manager.network_client
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def NotFound(self):
|
||||||
|
return tlib_exceptions.NotFound
|
||||||
|
|
||||||
|
def _cleanup_network(self, id_):
|
||||||
|
try:
|
||||||
|
self.delete_network(id_)
|
||||||
|
except self.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_network(self, **kwargs):
|
||||||
|
network = self._create_network(**kwargs)
|
||||||
|
self.addCleanup(self._cleanup_network, network.id)
|
||||||
|
return network
|
||||||
|
|
||||||
|
def _create_network(self, **kwargs):
|
||||||
|
# Internal method - use create_network() instead
|
||||||
|
body = self.client.create_network(**kwargs)
|
||||||
|
return base.AttributeDict(body['network'])
|
||||||
|
|
||||||
|
def update_network(self, id_, **kwargs):
|
||||||
|
body = self.client.update_network(id_, **kwargs)
|
||||||
|
return base.AttributeDict(body['network'])
|
||||||
|
|
||||||
|
def get_network(self, id_, **kwargs):
|
||||||
|
body = self.client.show_network(id_, **kwargs)
|
||||||
|
return base.AttributeDict(body['network'])
|
||||||
|
|
||||||
|
def get_networks(self, **kwargs):
|
||||||
|
body = self.client.list_networks(**kwargs)
|
||||||
|
return [base.AttributeDict(x) for x in body['networks']]
|
||||||
|
|
||||||
|
def delete_network(self, id_):
|
||||||
|
self.client.delete_network(id_)
|
39
neutron/tests/retargetable/test_example.py
Normal file
39
neutron/tests/retargetable/test_example.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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 testtools
|
||||||
|
|
||||||
|
from neutron.tests import base as tests_base
|
||||||
|
from neutron.tests.retargetable import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestExample(base.RetargetableApiTest):
|
||||||
|
"""This class is an example of how to write a retargetable api test.
|
||||||
|
|
||||||
|
See the parent class for details about how the 'client' attribute
|
||||||
|
is configured via testscenarios.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_network_lifecycle(self):
|
||||||
|
net = self.client.create_network(name=tests_base.get_rand_name())
|
||||||
|
listed_networks = {x.id: x.name for x in self.client.get_networks()}
|
||||||
|
self.assertIn(net.id, listed_networks)
|
||||||
|
self.assertEqual(listed_networks[net.id], net.name,
|
||||||
|
'Listed network name is not as expected.')
|
||||||
|
updated_name = 'new %s' % net.name
|
||||||
|
updated_net = self.client.update_network(net.id, name=updated_name)
|
||||||
|
self.assertEqual(updated_name, updated_net.name,
|
||||||
|
'Updated network name is not as expected.')
|
||||||
|
self.client.delete_network(net.id)
|
||||||
|
with testtools.ExpectedException(self.client.NotFound,
|
||||||
|
msg='Network was not deleted'):
|
||||||
|
self.client.get_network(net.id)
|
@ -4,3 +4,7 @@ WARNING
|
|||||||
The files under this path are maintained automatically by the script
|
The files under this path are maintained automatically by the script
|
||||||
tools/copy_api_tests_from_tempest.sh. It's contents should not be
|
tools/copy_api_tests_from_tempest.sh. It's contents should not be
|
||||||
manually modified until further notice.
|
manually modified until further notice.
|
||||||
|
|
||||||
|
Note that neutron.tests.tempest.config uses the global cfg.CONF
|
||||||
|
instance for now and importing it outside of the api tests has the
|
||||||
|
potential to break Neutron's use of cfg.CONF.
|
||||||
|
@ -21,6 +21,7 @@ import testtools
|
|||||||
import uuid
|
import uuid
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
|
import fixtures
|
||||||
from oslo_db import exception as db_exc
|
from oslo_db import exception as db_exc
|
||||||
|
|
||||||
from neutron.callbacks import registry
|
from neutron.callbacks import registry
|
||||||
@ -66,20 +67,24 @@ DEVICE_OWNER_COMPUTE = 'compute:None'
|
|||||||
HOST = 'fake_host'
|
HOST = 'fake_host'
|
||||||
|
|
||||||
|
|
||||||
class Ml2PluginConf(object):
|
# TODO(marun) - Move to somewhere common for reuse
|
||||||
"""Plugin configuration shared across the unit and functional tests.
|
class PluginConfFixture(fixtures.Fixture):
|
||||||
|
"""Plugin configuration shared across the unit and functional tests."""
|
||||||
|
|
||||||
TODO(marun) Evolve a configuration interface usable across all plugins.
|
def __init__(self, plugin_name, parent_setup=None):
|
||||||
"""
|
self.plugin_name = plugin_name
|
||||||
|
self.parent_setup = parent_setup
|
||||||
|
|
||||||
plugin_name = PLUGIN_NAME
|
def setUp(self):
|
||||||
|
super(PluginConfFixture, self).setUp()
|
||||||
|
if self.parent_setup:
|
||||||
|
self.parent_setup()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def setUp(test_case, parent_setup=None):
|
class Ml2ConfFixture(PluginConfFixture):
|
||||||
"""Perform additional configuration around the parent's setUp."""
|
|
||||||
if parent_setup:
|
def __init__(self, parent_setup=None):
|
||||||
parent_setup()
|
super(Ml2ConfFixture, self).__init__(PLUGIN_NAME, parent_setup)
|
||||||
test_case.port_create_status = 'DOWN'
|
|
||||||
|
|
||||||
|
|
||||||
class Ml2PluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase):
|
class Ml2PluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase):
|
||||||
@ -95,10 +100,11 @@ class Ml2PluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase):
|
|||||||
# by the common configuration setUp.
|
# by the common configuration setUp.
|
||||||
parent_setup = functools.partial(
|
parent_setup = functools.partial(
|
||||||
super(Ml2PluginV2TestCase, self).setUp,
|
super(Ml2PluginV2TestCase, self).setUp,
|
||||||
plugin=Ml2PluginConf.plugin_name,
|
plugin=PLUGIN_NAME,
|
||||||
service_plugins=service_plugins,
|
service_plugins=service_plugins,
|
||||||
)
|
)
|
||||||
Ml2PluginConf.setUp(self, parent_setup)
|
self.useFixture(Ml2ConfFixture(parent_setup))
|
||||||
|
self.port_create_status = 'DOWN'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Enable the test mechanism driver to ensure that
|
# Enable the test mechanism driver to ensure that
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import fixtures
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from neutron.db import api as db_api
|
from neutron.db import api as db_api
|
||||||
@ -52,18 +53,18 @@ def create_request(path, body, content_type, method='GET',
|
|||||||
return req
|
return req
|
||||||
|
|
||||||
|
|
||||||
class SqlTestCase(base.BaseTestCase):
|
class SqlFixture(fixtures.Fixture):
|
||||||
|
|
||||||
# flag to indicate that the models have been loaded
|
# flag to indicate that the models have been loaded
|
||||||
_TABLES_ESTABLISHED = False
|
_TABLES_ESTABLISHED = False
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(SqlTestCase, self).setUp()
|
super(SqlFixture, self).setUp()
|
||||||
# Register all data models
|
# Register all data models
|
||||||
engine = db_api.get_engine()
|
engine = db_api.get_engine()
|
||||||
if not SqlTestCase._TABLES_ESTABLISHED:
|
if not SqlFixture._TABLES_ESTABLISHED:
|
||||||
model_base.BASEV2.metadata.create_all(engine)
|
model_base.BASEV2.metadata.create_all(engine)
|
||||||
SqlTestCase._TABLES_ESTABLISHED = True
|
SqlFixture._TABLES_ESTABLISHED = True
|
||||||
|
|
||||||
def clear_tables():
|
def clear_tables():
|
||||||
with engine.begin() as conn:
|
with engine.begin() as conn:
|
||||||
@ -74,6 +75,13 @@ class SqlTestCase(base.BaseTestCase):
|
|||||||
self.addCleanup(clear_tables)
|
self.addCleanup(clear_tables)
|
||||||
|
|
||||||
|
|
||||||
|
class SqlTestCase(base.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SqlTestCase, self).setUp()
|
||||||
|
self.useFixture(SqlFixture())
|
||||||
|
|
||||||
|
|
||||||
class WebTestCase(SqlTestCase):
|
class WebTestCase(SqlTestCase):
|
||||||
fmt = 'json'
|
fmt = 'json'
|
||||||
|
|
||||||
|
2
tox.ini
2
tox.ini
@ -28,6 +28,7 @@ setenv = VIRTUAL_ENV={envdir}
|
|||||||
[testenv:api]
|
[testenv:api]
|
||||||
setenv = OS_TEST_PATH=./neutron/tests/api
|
setenv = OS_TEST_PATH=./neutron/tests/api
|
||||||
TEMPEST_CONFIG_DIR={env:TEMPEST_CONFIG_DIR:/opt/stack/tempest/etc}
|
TEMPEST_CONFIG_DIR={env:TEMPEST_CONFIG_DIR:/opt/stack/tempest/etc}
|
||||||
|
OS_TEST_API_WITH_REST=1
|
||||||
|
|
||||||
[testenv:functional]
|
[testenv:functional]
|
||||||
setenv = OS_TEST_PATH=./neutron/tests/functional
|
setenv = OS_TEST_PATH=./neutron/tests/functional
|
||||||
@ -43,7 +44,6 @@ setenv = OS_TEST_PATH=./neutron/tests/functional
|
|||||||
OS_ROOTWRAP_DAEMON_CMD=sudo {envbindir}/neutron-rootwrap-daemon {envdir}/etc/neutron/rootwrap.conf
|
OS_ROOTWRAP_DAEMON_CMD=sudo {envbindir}/neutron-rootwrap-daemon {envdir}/etc/neutron/rootwrap.conf
|
||||||
OS_FAIL_ON_MISSING_DEPS=1
|
OS_FAIL_ON_MISSING_DEPS=1
|
||||||
OS_TEST_TIMEOUT=90
|
OS_TEST_TIMEOUT=90
|
||||||
OS_RUN_FULLSTACK=1
|
|
||||||
sitepackages=True
|
sitepackages=True
|
||||||
deps =
|
deps =
|
||||||
{[testenv:functional]deps}
|
{[testenv:functional]deps}
|
||||||
|
Loading…
Reference in New Issue
Block a user