diff --git a/cyborg/api/controllers/v1/accelerators.py b/cyborg/api/controllers/v1/accelerators.py index ca254765..0b6a27de 100644 --- a/cyborg/api/controllers/v1/accelerators.py +++ b/cyborg/api/controllers/v1/accelerators.py @@ -57,8 +57,8 @@ class Accelerator(base.APIBase): setattr(self, field, kwargs.get(field, wtypes.Unset)) @classmethod - def convert_with_links(cls, db_accelerator): - accelerator = Accelerator(**db_accelerator.as_dict()) + def convert_with_links(cls, rpc_acc): + accelerator = Accelerator(**rpc_acc.as_dict()) url = pecan.request.public_url accelerator.links = [ link.Link.make_link('self', url, 'accelerators', @@ -82,14 +82,16 @@ class AcceleratorsController(AcceleratorsControllerBase): @policy.authorize_wsgi("cyborg:accelerator", "create", False) @expose.expose(Accelerator, body=types.jsontype, status_code=http_client.CREATED) - def post(self, values): + def post(self, accelerator): """Create a new accelerator. :param accelerator: an accelerator within the request body. """ - accelerator = pecan.request.conductor_api.accelerator_create( - pecan.request.context, values) + context = pecan.request.context + rpc_acc = objects.Accelerator(context, **accelerator) + new_acc = pecan.request.conductor_api.accelerator_create( + context, rpc_acc) # Set the HTTP Location Header pecan.response.location = link.build_url('accelerators', - accelerator.uuid) - return Accelerator.convert_with_links(accelerator) + new_acc.uuid) + return Accelerator.convert_with_links(new_acc) diff --git a/cyborg/common/config.py b/cyborg/common/config.py index 29ba17ad..954e36d9 100644 --- a/cyborg/common/config.py +++ b/cyborg/common/config.py @@ -15,12 +15,15 @@ from oslo_config import cfg +from cyborg.common import rpc from cyborg import version def parse_args(argv, default_config_files=None): + rpc.set_defaults(control_exchange='cyborg') version_string = version.version_info.release_string() cfg.CONF(argv[1:], project='cyborg', version=version_string, default_config_files=default_config_files) + rpc.init(cfg.CONF) diff --git a/cyborg/common/service.py b/cyborg/common/service.py index 1c501188..2eaeac97 100644 --- a/cyborg/common/service.py +++ b/cyborg/common/service.py @@ -79,14 +79,12 @@ class RPCService(service.Service): def prepare_service(argv=None): log.register_options(CONF) - log.set_defaults() - rpc.set_defaults(control_exchange='cyborg') + log.set_defaults(default_log_levels=CONF.default_log_levels) argv = argv or [] config.parse_args(argv) log.setup(CONF, 'cyborg') - rpc.init(CONF) objects.register_all() diff --git a/cyborg/conductor/manager.py b/cyborg/conductor/manager.py index fd47eba8..137275a9 100644 --- a/cyborg/conductor/manager.py +++ b/cyborg/conductor/manager.py @@ -16,7 +16,6 @@ import oslo_messaging as messaging from cyborg.conf import CONF -from cyborg import objects class ConductorManager(object): @@ -33,8 +32,12 @@ class ConductorManager(object): def periodic_tasks(self, context, raise_on_error=False): pass - def accelerator_create(self, context, values): - """Create a new accelerator.""" - accelerator = objects.Accelerator(context, **values) - accelerator.create() - return accelerator + def accelerator_create(self, context, acc_obj): + """Create a new accelerator. + + :param context: request context. + :param acc_obj: a changed (but not saved) accelerator object. + :returns: created accelerator object. + """ + acc_obj.create() + return acc_obj diff --git a/cyborg/conductor/rpcapi.py b/cyborg/conductor/rpcapi.py index ae9f6cd0..94e26a97 100644 --- a/cyborg/conductor/rpcapi.py +++ b/cyborg/conductor/rpcapi.py @@ -47,7 +47,12 @@ class ConductorAPI(object): version_cap=self.RPC_API_VERSION, serializer=serializer) - def accelerator_create(self, context, values): - """Signal to conductor service to create an accelerator.""" + def accelerator_create(self, context, acc_obj): + """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) - return cctxt.call(context, 'accelerator_create', values=values) + return cctxt.call(context, 'accelerator_create', values=acc_obj) diff --git a/cyborg/tests/base.py b/cyborg/tests/base.py index 1c30cdb5..8b912313 100644 --- a/cyborg/tests/base.py +++ b/cyborg/tests/base.py @@ -1,23 +1,84 @@ -# -*- coding: utf-8 -*- +# 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. -# 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 -# 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 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 +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): - """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 diff --git a/cyborg/tests/functional/__init__.py b/cyborg/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cyborg/tests/test_cyborg.py b/cyborg/tests/test_cyborg.py deleted file mode 100644 index 60504a96..00000000 --- a/cyborg/tests/test_cyborg.py +++ /dev/null @@ -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 diff --git a/cyborg/tests/unit/__init__.py b/cyborg/tests/unit/__init__.py new file mode 100644 index 00000000..eeaaced6 --- /dev/null +++ b/cyborg/tests/unit/__init__.py @@ -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() diff --git a/cyborg/tests/unit/api/__init__.py b/cyborg/tests/unit/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cyborg/tests/unit/api/base.py b/cyborg/tests/unit/api/base.py new file mode 100644 index 00000000..72a79171 --- /dev/null +++ b/cyborg/tests/unit/api/base.py @@ -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 diff --git a/cyborg/tests/unit/api/controllers/__init__.py b/cyborg/tests/unit/api/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cyborg/tests/unit/api/controllers/v1/__init__.py b/cyborg/tests/unit/api/controllers/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cyborg/tests/unit/api/controllers/v1/base.py b/cyborg/tests/unit/api/controllers/v1/base.py new file mode 100644 index 00000000..c16eaee3 --- /dev/null +++ b/cyborg/tests/unit/api/controllers/v1/base.py @@ -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' diff --git a/cyborg/tests/unit/api/controllers/v1/test_accelerators.py b/cyborg/tests/unit/api/controllers/v1/test_accelerators.py new file mode 100644 index 00000000..97ea2812 --- /dev/null +++ b/cyborg/tests/unit/api/controllers/v1/test_accelerators.py @@ -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) diff --git a/cyborg/tests/unit/db/__init__.py b/cyborg/tests/unit/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cyborg/tests/unit/db/base.py b/cyborg/tests/unit/db/base.py new file mode 100644 index 00000000..1d40ce0e --- /dev/null +++ b/cyborg/tests/unit/db/base.py @@ -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) diff --git a/cyborg/tests/unit/db/utils.py b/cyborg/tests/unit/db/utils.py new file mode 100644 index 00000000..c74388db --- /dev/null +++ b/cyborg/tests/unit/db/utils.py @@ -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), + } diff --git a/test-requirements.txt b/test-requirements.txt index abce126c..d6bfcecc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,9 +4,11 @@ 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 -sphinx!=1.3b1,<1.4,>=1.2.1 # BSD +sphinx>=1.6.2 # BSD ddt>=1.0.1 # MIT oslosphinx>=4.7.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 testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -sphinxcontrib-pecanwsme>=0.8 # Apache-2.0 +sphinxcontrib-pecanwsme>=0.8.0 # Apache-2.0 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 -tempest>=12.1.0 # Apache-2.0 - - +tempest>=16.1.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 10ba1142..10574d7d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,20 @@ [tox] -minversion = 1.8 -envlist = py35,py27,pypy,pep8 +minversion = 2.0 +envlist = py35-constraints,py27-constraints,pypy-constraints,pep8-constraints skipsdist = True [testenv] -sitepackages = True usedevelop = True -install_command = pip install -U --force-reinstall {opts} {packages} +install_command = {[testenv:common-constraints]install_command} setenv = - VIRTUAL_ENV={envdir} + VIRTUAL_ENV={envdir} + OS_TEST_PATH=cyborg/tests/unit deps = -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt -commands = python setup.py testr --slowest --testr-args='{posargs}' +commands = rm -f .testrepository/times.dbm + 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] sitepackages = False @@ -22,6 +25,10 @@ commands = [testenv:pep8] commands = pep8 {posargs} +[testenv:pep8-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = flake8 {posargs} + [testenv:venv] commands = {posargs}