add api unit test

add api unit test to validate the accelerator_post function.

1. load a test app for pecan framework test
2. mock out the accelerator_create function in Conductor RPCAPI
3. post test request to '/accelerators' endpoint
4. do the check between request and response

Change-Id: I5d565d0d7ed56c27555666e52e34f5fd67005dcf
This commit is contained in:
zhuli 2017-08-28 18:03:22 +08:00
parent 4e45b8624e
commit 2a71b8efe6
20 changed files with 482 additions and 77 deletions

View File

@ -57,8 +57,8 @@ class Accelerator(base.APIBase):
setattr(self, field, kwargs.get(field, wtypes.Unset)) setattr(self, field, kwargs.get(field, wtypes.Unset))
@classmethod @classmethod
def convert_with_links(cls, db_accelerator): def convert_with_links(cls, rpc_acc):
accelerator = Accelerator(**db_accelerator.as_dict()) accelerator = Accelerator(**rpc_acc.as_dict())
url = pecan.request.public_url url = pecan.request.public_url
accelerator.links = [ accelerator.links = [
link.Link.make_link('self', url, 'accelerators', link.Link.make_link('self', url, 'accelerators',
@ -82,14 +82,16 @@ class AcceleratorsController(AcceleratorsControllerBase):
@policy.authorize_wsgi("cyborg:accelerator", "create", False) @policy.authorize_wsgi("cyborg:accelerator", "create", False)
@expose.expose(Accelerator, body=types.jsontype, @expose.expose(Accelerator, body=types.jsontype,
status_code=http_client.CREATED) status_code=http_client.CREATED)
def post(self, values): def post(self, accelerator):
"""Create a new accelerator. """Create a new accelerator.
:param accelerator: an accelerator within the request body. :param accelerator: an accelerator within the request body.
""" """
accelerator = pecan.request.conductor_api.accelerator_create( context = pecan.request.context
pecan.request.context, values) rpc_acc = objects.Accelerator(context, **accelerator)
new_acc = pecan.request.conductor_api.accelerator_create(
context, rpc_acc)
# Set the HTTP Location Header # Set the HTTP Location Header
pecan.response.location = link.build_url('accelerators', pecan.response.location = link.build_url('accelerators',
accelerator.uuid) new_acc.uuid)
return Accelerator.convert_with_links(accelerator) return Accelerator.convert_with_links(new_acc)

View File

@ -15,12 +15,15 @@
from oslo_config import cfg from oslo_config import cfg
from cyborg.common import rpc
from cyborg import version from cyborg import version
def parse_args(argv, default_config_files=None): def parse_args(argv, default_config_files=None):
rpc.set_defaults(control_exchange='cyborg')
version_string = version.version_info.release_string() version_string = version.version_info.release_string()
cfg.CONF(argv[1:], cfg.CONF(argv[1:],
project='cyborg', project='cyborg',
version=version_string, version=version_string,
default_config_files=default_config_files) default_config_files=default_config_files)
rpc.init(cfg.CONF)

View File

@ -79,14 +79,12 @@ class RPCService(service.Service):
def prepare_service(argv=None): def prepare_service(argv=None):
log.register_options(CONF) log.register_options(CONF)
log.set_defaults() log.set_defaults(default_log_levels=CONF.default_log_levels)
rpc.set_defaults(control_exchange='cyborg')
argv = argv or [] argv = argv or []
config.parse_args(argv) config.parse_args(argv)
log.setup(CONF, 'cyborg') log.setup(CONF, 'cyborg')
rpc.init(CONF)
objects.register_all() objects.register_all()

View File

@ -16,7 +16,6 @@
import oslo_messaging as messaging import oslo_messaging as messaging
from cyborg.conf import CONF from cyborg.conf import CONF
from cyborg import objects
class ConductorManager(object): class ConductorManager(object):
@ -33,8 +32,12 @@ class ConductorManager(object):
def periodic_tasks(self, context, raise_on_error=False): def periodic_tasks(self, context, raise_on_error=False):
pass pass
def accelerator_create(self, context, values): def accelerator_create(self, context, acc_obj):
"""Create a new accelerator.""" """Create a new accelerator.
accelerator = objects.Accelerator(context, **values)
accelerator.create() :param context: request context.
return accelerator :param acc_obj: a changed (but not saved) accelerator object.
:returns: created accelerator object.
"""
acc_obj.create()
return acc_obj

View File

@ -47,7 +47,12 @@ class ConductorAPI(object):
version_cap=self.RPC_API_VERSION, version_cap=self.RPC_API_VERSION,
serializer=serializer) serializer=serializer)
def accelerator_create(self, context, values): def accelerator_create(self, context, acc_obj):
"""Signal to conductor service to create an accelerator.""" """Signal to conductor service to create an accelerator.
:param context: request context.
:param acc_obj: a created (but not saved) accelerator object.
:returns: created accelerator object.
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host) cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'accelerator_create', values=values) return cctxt.call(context, 'accelerator_create', values=acc_obj)

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # Copyright 2017 Huawei Technologies Co.,LTD.
# All Rights Reserved.
# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -15,9 +13,72 @@
# 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 os
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_context import context
from oslo_db import options
from oslo_log import log
from oslotest import base from oslotest import base
import pecan
from cyborg.common import config as cyborg_config
CONF = cfg.CONF
options.set_defaults(cfg.CONF)
try:
log.register_options(CONF)
except cfg.ArgsAlreadyParsedError:
pass
class TestCase(base.BaseTestCase): class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests.""" """Test case base class for all unit tests."""
def setUp(self):
super(TestCase, self).setUp()
self.context = context.get_admin_context()
self._set_config()
def reset_pecan():
pecan.set_config({}, overwrite=True)
self.addCleanup(reset_pecan)
def _set_config(self):
self.cfg_fixture = self.useFixture(config_fixture.Config(cfg.CONF))
self.config(use_stderr=False,
fatal_exception_format_errors=True)
self.set_defaults(host='fake-mini',
debug=True)
self.set_defaults(connection="sqlite://",
sqlite_synchronous=False,
group='database')
cyborg_config.parse_args([], default_config_files=[])
def config(self, **kw):
"""Override config options for a test."""
self.cfg_fixture.config(**kw)
def set_defaults(self, **kw):
"""Set default values of config options."""
group = kw.pop('group', None)
for o, v in kw.items():
self.cfg_fixture.set_default(o, v, group=group)
def get_path(self, project_file=None):
"""Get the absolute path to a file. Used for testing the API.
:param project_file: File whose path to return. Default: None.
:returns: path to the specified file, or path to project root.
"""
root = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..')
)
if project_file:
return os.path.join(root, project_file)
else:
return root

View File

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_cyborg
----------------------------------
Tests for `cyborg` module.
"""
from cyborg.tests import base
class TestCyborg(base.TestCase):
def test_something(self):
pass

View File

@ -0,0 +1,38 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.
"""
:mod:`cyborg.tests.unit` -- cyborg unit tests
=====================================================
.. automodule:: cyborg.tests.unit
:platform: Unix
"""
import eventlet
from cyborg import objects
eventlet.monkey_patch(os=False)
# Make sure this is done after eventlet monkey patching otherwise
# the threading.local() store used in oslo_messaging will be initialized to
# threadlocal storage rather than greenthread local. This will cause context
# sets and deletes in that storage to clobber each other.
# Make sure we have all of the objects loaded. We do this
# at module import time, because we may be using mock decorators in our
# tests that run at import time.
objects.register_all()

View File

View File

@ -0,0 +1,137 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.
"""Base classes for API tests."""
from oslo_config import cfg
import pecan
import pecan.testing
from cyborg.tests.unit.db import base
cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
class BaseApiTest(base.DbTestCase):
"""Pecan controller functional testing class.
Used for functional tests of Pecan controllers where you need to
test your literal application and its integration with the
framework.
"""
PATH_PREFIX = ''
def setUp(self):
super(BaseApiTest, self).setUp()
cfg.CONF.set_override("auth_version", "v3",
group='keystone_authtoken')
cfg.CONF.set_override("admin_user", "admin",
group='keystone_authtoken')
self.app = self._make_app()
def reset_pecan():
pecan.set_config({}, overwrite=True)
self.addCleanup(reset_pecan)
def _make_app(self):
# Determine where we are so we can set up paths in the config
root_dir = self.get_path()
self.app_config = {
'app': {
'root': 'cyborg.api.controllers.root.RootController',
'modules': ['cyborg.api'],
'static_root': '%s/public' % root_dir,
'template_path': '%s/api/templates' % root_dir,
'acl_public_routes': ['/', '/v1/.*'],
},
}
return pecan.testing.load_test_app(self.app_config)
def _request_json(self, path, params, expect_errors=False, headers=None,
method="post", extra_environ=None, status=None):
"""Sends simulated HTTP request to Pecan test app.
:param path: url path of target service
:param params: content for wsgi.input of request
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param method: Request method type. Appropriate method function call
should be used rather than passing attribute in.
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
"""
response = getattr(self.app, "%s_json" % method)(
str(path),
params=params,
headers=headers,
status=status,
extra_environ=extra_environ,
expect_errors=expect_errors
)
return response
def post_json(self, path, params, expect_errors=False, headers=None,
extra_environ=None, status=None):
"""Sends simulated HTTP POST request to Pecan test app.
:param path: url path of target service
:param params: content for wsgi.input of request
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
"""
full_path = self.PATH_PREFIX + path
return self._request_json(path=full_path, params=params,
expect_errors=expect_errors,
headers=headers, extra_environ=extra_environ,
status=status, method="post")
def gen_headers(self, context, **kw):
"""Generate a header for a simulated HTTP request to Pecan test app.
:param context: context that store the client user information.
:param kw: key word aguments, used to overwrite the context attribute.
note: "is_public_api" is not in headers, it should be in environ
variables to send along with the request. We can pass it by
extra_environ when we call delete, get_json or other method request.
"""
ct = context.to_dict()
ct.update(kw)
headers = {
'X-User-Name': ct.get("user_name") or "user",
'X-User-Id':
ct.get("user") or "1d6d686bc2c949ddb685ffb4682e0047",
'X-Project-Name': ct.get("project_name") or "project",
'X-Project-Id':
ct.get("tenant") or "86f64f561b6d4f479655384572727f70",
'X-User-Domain-Id':
ct.get("domain_id") or "bd5eeb7d0fb046daaf694b36f4df5518",
'X-User-Domain-Name': ct.get("domain_name") or "no_domain",
'X-Auth-Token':
ct.get("auth_token") or "b9764005b8c145bf972634fb16a826e8",
'X-Roles': ct.get("roles") or "cyborg"
}
return headers

View File

@ -0,0 +1,21 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.
from cyborg.tests.unit.api import base
class APITestV1(base.BaseApiTest):
PATH_PREFIX = '/v1'

View File

@ -0,0 +1,58 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.
import mock
from six.moves import http_client
from cyborg.conductor import rpcapi
from cyborg.tests.unit.api.controllers.v1 import base as v1_test
from cyborg.tests.unit.db import utils
def gen_post_body(**kw):
return utils.get_test_accelerator(**kw)
def _rpcapi_accelerator_create(self, context, acc_obj):
"""Fake used to mock out the conductor RPCAPI's accelerator_create method.
Performs creation of the accelerator object and returns the created
accelerator as-per the real method.
"""
acc_obj.create()
return acc_obj
@mock.patch.object(rpcapi.ConductorAPI, 'accelerator_create', autospec=True,
side_effect=_rpcapi_accelerator_create)
class TestPost(v1_test.APITestV1):
ACCELERATOR_UUID = '10efe63d-dfea-4a37-ad94-4116fba50981'
def setUp(self):
super(TestPost, self).setUp()
@mock.patch('oslo_utils.uuidutils.generate_uuid')
def test_accelerator_post(self, mock_uuid, mock_create):
mock_uuid.return_value = self.ACCELERATOR_UUID
body = gen_post_body(name='test_accelerator')
headers = self.gen_headers(self.context)
response = self.post_json('/accelerators', body, headers=headers)
self.assertEqual(http_client.CREATED, response.status_int)
response = response.json
self.assertEqual(self.ACCELERATOR_UUID, response['uuid'])
self.assertEqual(body['name'], response['name'])
mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY)

View File

View File

@ -0,0 +1,71 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.
"""Cyborg DB test base class."""
import fixtures
from oslo_config import cfg
from oslo_db.sqlalchemy import enginefacade
from cyborg.db import api as dbapi
from cyborg.db.sqlalchemy import migration
from cyborg.db.sqlalchemy import models
from cyborg.tests import base
CONF = cfg.CONF
_DB_CACHE = None
class Database(fixtures.Fixture):
def __init__(self, engine, db_migrate, sql_connection):
self.sql_connection = sql_connection
self.engine = engine
self.engine.dispose()
conn = self.engine.connect()
self.setup_sqlite(db_migrate)
self._DB = ''.join(line for line in conn.connection.iterdump())
self.engine.dispose()
def setup_sqlite(self, db_migrate):
if db_migrate.version():
return
models.Base.metadata.create_all(self.engine)
db_migrate.stamp('head')
def setUp(self):
super(Database, self).setUp()
conn = self.engine.connect()
conn.connection.executescript(self._DB)
self.addCleanup(self.engine.dispose)
class DbTestCase(base.TestCase):
def setUp(self):
super(DbTestCase, self).setUp()
self.dbapi = dbapi.get_instance()
global _DB_CACHE
if not _DB_CACHE:
engine = enginefacade.get_legacy_facade().get_engine()
_DB_CACHE = Database(engine, migration,
sql_connection=CONF.database.connection)
self.useFixture(_DB_CACHE)

View File

@ -0,0 +1,29 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.
"""Cyborg test utilities."""
def get_test_accelerator(**kw):
return {
'name': kw.get('name', 'name'),
'description': kw.get('description', 'description'),
'device_type': kw.get('device_type', 'device_type'),
'acc_type': kw.get('acc_type', 'acc_type'),
'acc_capability': kw.get('acc_capability', 'acc_capability'),
'vendor_id': kw.get('vendor_id', 'vendor_id'),
'product_id': kw.get('product_id', 'product_id'),
'remotable': kw.get('remotable', 1),
}

View File

@ -4,9 +4,11 @@
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
coverage>=4.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
mock>=2.0.0 # BSD
python-subunit>=0.0.18 # Apache-2.0/BSD python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD sphinx>=1.6.2 # BSD
ddt>=1.0.1 # MIT ddt>=1.0.1 # MIT
oslosphinx>=4.7.0 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0
@ -14,10 +16,8 @@ testrepository>=0.0.18 # Apache-2.0/BSD
testresources>=0.2.4 # Apache-2.0/BSD testresources>=0.2.4 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT testtools>=1.4.0 # MIT
sphinxcontrib-pecanwsme>=0.8 # Apache-2.0 sphinxcontrib-pecanwsme>=0.8.0 # Apache-2.0
sphinxcontrib-seqdiag # BSD sphinxcontrib-seqdiag # BSD
reno>=1.8.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0
os-api-ref>=1.0.0 # Apache-2.0 os-api-ref>=1.0.0 # Apache-2.0
tempest>=12.1.0 # Apache-2.0 tempest>=16.1.0 # Apache-2.0

19
tox.ini
View File

@ -1,17 +1,20 @@
[tox] [tox]
minversion = 1.8 minversion = 2.0
envlist = py35,py27,pypy,pep8 envlist = py35-constraints,py27-constraints,pypy-constraints,pep8-constraints
skipsdist = True skipsdist = True
[testenv] [testenv]
sitepackages = True
usedevelop = True usedevelop = True
install_command = pip install -U --force-reinstall {opts} {packages} install_command = {[testenv:common-constraints]install_command}
setenv = setenv =
VIRTUAL_ENV={envdir} VIRTUAL_ENV={envdir}
OS_TEST_PATH=cyborg/tests/unit
deps = -r{toxinidir}/test-requirements.txt deps = -r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt commands = rm -f .testrepository/times.dbm
commands = python setup.py testr --slowest --testr-args='{posargs}' python setup.py test --slowest --testr-args='{posargs}'
[testenv:common-constraints]
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
[testenv:genpolicy] [testenv:genpolicy]
sitepackages = False sitepackages = False
@ -22,6 +25,10 @@ commands =
[testenv:pep8] [testenv:pep8]
commands = pep8 {posargs} commands = pep8 {posargs}
[testenv:pep8-constraints]
install_command = {[testenv:common-constraints]install_command}
commands = flake8 {posargs}
[testenv:venv] [testenv:venv]
commands = {posargs} commands = {posargs}