diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 636acb0..0000000 --- a/.coveragerc +++ /dev/null @@ -1,29 +0,0 @@ -# .coveragerc to control coverage.py -[run] -branch = True - -source=laos -omit=laos/tests*, - -[report] -# Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # Don't complain about missing debug-only code: - def __repr__ - if self\.debug - - # Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - # Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - -ignore_errors = False - -[html] -directory=cover diff --git a/.gitignore b/.gitignore index f1a9a55..a6d6d20 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ python-troveclient.iml releasenotes/build .coverage.* *.json +.cache diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 9168caa..0000000 --- a/.testr.conf +++ /dev/null @@ -1,6 +0,0 @@ -[DEFAULT] -test_command=PYTHONASYNCIODEBUG=${PYTHONASYNCIODEBUG:-1} \ - ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION - -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/README.md b/README.md index 486c1e7..12586ed 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,53 @@ Once server is launched you can navigate to: http://:/api -Or if you need to create +to see recent API docs + +Testing (general information) +----------------------------- + +In order to run tests you need to install `Tox`: + + $ pip install tox + +Also you'd need live MySQL instance with applied migrations. +Tests are depending on pre-created MySQL database for persistence. +Please set env var + + $ export TEST_DB_URI=mysql://:@:/ + +Testing: PEP8 +------------- + +In order to run `PEP8` style checks run next command: + + $ tox -e pep8 + + +Testing: Functional +------------------- + +In order to run `functional` tests run next command: + + $ tox -e py35-functional + +Pros: + +* lightweight (controllers and DB models testing) +* no OpenStack required +* no IronFunctions required + +Cons: + +* MySQL server required +* OpenStack authentication is not tested +* IronFunctions API stabbed with fake implementation + +Testing: Integration +-------------------- + +TBD + 3rd party bugs to resolve ------------------------- diff --git a/laos/api/controllers/apps.py b/laos/api/controllers/apps.py index f215925..a1edca3 100644 --- a/laos/api/controllers/apps.py +++ b/laos/api/controllers/apps.py @@ -195,7 +195,8 @@ class AppV1Controller(controllers.ServiceControllerBase): """ project_id = request.match_info.get('project_id') app = request.match_info.get('app') - + c = config.Config.config_instance() + fnclient = c.functions_client if not (await app_model.Apps.exists(app, project_id)): return web.json_response(data={ "error": { @@ -203,6 +204,17 @@ class AppV1Controller(controllers.ServiceControllerBase): } }, status=404) + fn_app = await fnclient.apps.show(app, loop=c.event_loop) + fn_app_routes = await fn_app.routes.list(loop=c.event_loop) + + if fn_app_routes: + return web.json_response(data={ + "error": { + "message": ("App {} has routes, " + "delete them in first place") + } + }, status=403) + await app_model.Apps.delete( project_id=project_id, name=app) # TODO(denismakogon): enable DELETE to IronFunctions when once diff --git a/laos/api/controllers/routes.py b/laos/api/controllers/routes.py index f3348b6..e3ffa73 100644 --- a/laos/api/controllers/routes.py +++ b/laos/api/controllers/routes.py @@ -178,7 +178,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): setattr(new_fn_route, "is_public", stored_route.public) view = app_view.AppRouteView( - api_url, project_id, [new_fn_route]).view() + api_url, project_id, [new_fn_route]).view_one() return web.json_response(data={ "route": view, @@ -244,7 +244,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): return web.json_response(data={ "route": app_view.AppRouteView(api_url, project_id, - [route]).view().pop(), + [route]).view_one(), "message": "App route successfully loaded" }, status=200) diff --git a/laos/api/views/app.py b/laos/api/views/app.py index d0e893c..3641aea 100644 --- a/laos/api/views/app.py +++ b/laos/api/views/app.py @@ -38,6 +38,9 @@ class AppRouteView(object): self.api_url = api_url self.project_id = project_id + def view_one(self): + return self.view().pop() + def view(self): view = [] for route in self.routes: diff --git a/laos/common/config.py b/laos/common/config.py index 14dda1e..4e6c700 100644 --- a/laos/common/config.py +++ b/laos/common/config.py @@ -37,18 +37,19 @@ class Connection(object, metaclass=utils.Singleton): def __init__(self, db_uri, loop=None): self.uri = db_uri - self.engine = loop.run_until_complete(self.get_engine(loop=loop)) + self.loop = loop + self.pool = loop.run_until_complete(self.get_pool()) self.loop = loop - def get_engine(self, loop=None): + def get_pool(self): username, password, host, port, db_name = utils.split_db_uri(self.uri) return aiomysql.create_pool(host=host, port=port if port else 3306, user=username, password=password, - db=db_name, loop=loop) + db=db_name, loop=self.loop) @classmethod def from_class(cls): - return cls._instance.engine + return cls._instance.pool class FunctionsClient(client.FunctionsAPIV1, metaclass=utils.Singleton): diff --git a/laos/common/persistence.py b/laos/common/persistence.py index 8db3c92..a26c46b 100644 --- a/laos/common/persistence.py +++ b/laos/common/persistence.py @@ -38,14 +38,15 @@ class BaseDatabaseModel(object): setattr(self, k, v) async def save(self): - logger = config.Config.config_instance().logger + c = config.Config.config_instance() + logger, pool = c.logger, c.connection.pool insert = self.INSERT.format( self.table_name, str(tuple([getattr(self, clmn) for clmn in self.columns])) ) logger.info("Attempting to save object '{}' " "using SQL query: {}".format(self.table_name, insert)) - async with config.Connection.from_class().acquire() as conn: + async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(insert) await conn.commit() @@ -54,12 +55,13 @@ class BaseDatabaseModel(object): @classmethod async def delete(cls, **kwargs): - logger = config.Config.config_instance().logger + c = config.Config.config_instance() + logger, pool = c.logger, c.connection.pool delete = cls.DELETE.format( cls.table_name, cls.__define_where(**kwargs)) logger.info("Attempting to delete object '{}' " "using SQL query: {}".format(cls.table_name, delete)) - async with config.Connection.from_class().acquire() as conn: + async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(delete) await conn.commit() @@ -92,18 +94,19 @@ class BaseDatabaseModel(object): @classmethod async def find_by(cls, **kwargs): - logger = config.Config.config_instance().logger + c = config.Config.config_instance() + logger, pool = c.logger, c.connection.pool where = cls.__define_where(**kwargs) select = cls.SELECT.format( cls.table_name, where) logger.info("Attempting to find object(s) '{}' " "using SQL : {}".format(cls.table_name, select)) - async with config.Connection.from_class().acquire() as conn: + async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(select) results = await cur.fetchall() - return [cls.from_tuple(instance) - for instance in results] if results else [] + return [cls.from_tuple(instance) + for instance in results] if results else [] @classmethod def from_tuple(cls, tpl): diff --git a/laos/service/laos_api.py b/laos/service/laos_api.py index c455e74..effa89b 100644 --- a/laos/service/laos_api.py +++ b/laos/service/laos_api.py @@ -116,13 +116,13 @@ def server(host, port, db_uri, api_version=functions_api_version, ) loop.run_until_complete(fnclient.ping(loop=loop)) - conn = config.Connection(db_uri, loop=loop) + connection_pool = config.Connection(db_uri, loop=loop) config.Config( auth_url=keystone_endpoint, functions_client=fnclient, logger=logger, - connection=conn, + connection=connection_pool, event_loop=loop, ) diff --git a/laos/tests/fakes/__init__.py b/laos/tests/fakes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laos/tests/fakes/functions_api.py b/laos/tests/fakes/functions_api.py new file mode 100644 index 0000000..2e3acb6 --- /dev/null +++ b/laos/tests/fakes/functions_api.py @@ -0,0 +1,122 @@ +# 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 collections +import uuid + +from functionsclient import client +from functionsclient.v1 import apps +from functionsclient.v1 import routes + + +APPS = dict() +APP_ROUTES = collections.defaultdict(list) + + +class FakeRoutes(object): + + def __init__(self, app_name): + self.app_name = app_name + + async def list(self, loop=None): + return APP_ROUTES[self.app_name] + + async def create(self, loop=None, **data): + app_routes = APP_ROUTES[self.app_name] + if data.get("path") in [route.path for route in app_routes]: + raise client.FunctionsAPIException( + "App {} route {} already exist.".format( + self.app_name, data.get("path")), 409) + else: + data.update( + appname=self.app_name, + memory=256 + ) + _route = routes.AppRouteResource(**data) + APP_ROUTES[self.app_name].append(_route) + return _route + + async def show(self, path, loop=None): + app_routes = APP_ROUTES[self.app_name] + for route in app_routes: + if path == route.path: + return route + raise client.FunctionsAPIException( + "App {} route {} not found.".format( + self.app_name, path), 404) + + async def delete(self, path, loop=None): + app_routes = APP_ROUTES[self.app_name] + if path not in [route.path for route in app_routes]: + raise client.FunctionsAPIException( + "App {} route {} not found.".format( + self.app_name, path), 404) + else: + app_routes.pop( + app_routes.index( + await self.show(path, loop=loop) + ) + ) + + async def execute(self, path, loop=None): + app_routes = APP_ROUTES[self.app_name] + if path not in [route.path for route in app_routes]: + raise client.FunctionsAPIException( + "App {} route {} not found.".format( + self.app_name, path), 404) + else: + route = await self.show(path) + return "Hello world!" if route.type == "sync" else { + "call_id": uuid.uuid4().hex + } + + +class FakeApps(object): + + def __init__(self): + pass + + async def list(self, loop=None): + return list(APPS.values()) + + async def create(self, app_name, loop=None): + if app_name not in APPS: + _app = apps.AppResource( + FakeRoutes(app_name), + **{"name": app_name, "config": None}) + APPS.update({app_name: _app}) + return _app + else: + raise client.FunctionsAPIException( + "App {} already exist".format(app_name), 409) + + async def show(self, app_name, loop=None): + if app_name not in APPS: + raise client.FunctionsAPIException( + "App {} already exist.".format(app_name), 404) + else: + return APPS.get(app_name) + + async def delete(self, app_name, loop=None): + if app_name not in APPS: + raise client.FunctionsAPIException( + "App {} already exist.".format(app_name), 404) + else: + del APPS[app_name] + + +class FunctionsAPIV1(object): + + def __init__(self, *args, **kwargs): + self.apps = FakeApps() diff --git a/laos/tests/functional/__init__.py b/laos/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laos/tests/functional/base.py b/laos/tests/functional/base.py new file mode 100644 index 0000000..58e71c2 --- /dev/null +++ b/laos/tests/functional/base.py @@ -0,0 +1,114 @@ +# 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 asyncio +import collections +import datetime +import os +import testtools +import uuid +import uvloop + +from laos.api.controllers import apps +from laos.api.controllers import routes +from laos.api.controllers import runnable +from laos.api.controllers import tasks +from laos.api.middleware import content_type + +from laos.common.base import service +from laos.common import config +from laos.common import logger as log + + +from laos.tests.fakes import functions_api +from laos.tests.functional import client + + +class LaosFunctionalTestsBase(testtools.TestCase): + + def setUp(self): + try: + self.testloop = asyncio.get_event_loop() + except Exception: + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + self.testloop = asyncio.get_event_loop() + + logger = log.UnifiedLogger( + log_to_console=False, + filename=("/tmp/laos-integration-tests-run-{}.log" + .format(datetime.datetime.now())), + level="DEBUG").setup_logger(__package__) + + self.testapp = service.AbstractWebServer( + host="localhost", + port=10001, + private_controllers={ + "v1": [ + apps.AppV1Controller, + routes.AppRouteV1Controller, + tasks.TasksV1Controller, + ], + "private": [ + runnable.RunnableV1Controller, + ] + }, + public_controllers={ + "public": [ + runnable.PublicRunnableV1Controller, + ], + }, + private_middlewares=[ + content_type.content_type_validator, + ], + public_middlewares=[ + content_type.content_type_validator, + ], + event_loop=self.testloop, + logger=logger, + debug=True, + ).root_service + + connection_pool = config.Connection( + os.getenv("TEST_DB_URI"), loop=self.testloop) + + fnclient = functions_api.FunctionsAPIV1() + + self.test_config = config.Config( + logger=logger, + connection=connection_pool, + event_loop=self.testloop, + functions_client=fnclient, + ) + + self.project_id = str(uuid.uuid4()).replace("-", "") + self.test_client = client.ProjectBoundLaosTestClient( + self.testapp, self.project_id) + + self.route_data = { + "type": "sync", + "path": "/hello-sync-private", + "image": "iron/hello", + "is_public": "false" + } + + self.testloop.run_until_complete(self.test_client.start_server()) + super(LaosFunctionalTestsBase, self).setUp() + + def tearDown(self): + functions_api.APPS = {} + functions_api.ROUTES = collections.defaultdict(list) + # ^ temporary solution, + # until https://github.com/iron-io/functions/issues/274 fixed + self.testloop.run_until_complete(self.test_client.close()) + super(LaosFunctionalTestsBase, self).tearDown() diff --git a/laos/tests/functional/client.py b/laos/tests/functional/client.py new file mode 100644 index 0000000..5639dfa --- /dev/null +++ b/laos/tests/functional/client.py @@ -0,0 +1,112 @@ +# 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 json as jsonlib + +from aiohttp import test_utils + + +class AppsV1(object): + + # /v1/{project_id}/apps + apps_path = "/v1/{}/apps" + # /v1/{project_id}/apps/{app} + app_path = apps_path + "/{}" + + def __init__(self, test_client): + self.client = test_client + + async def list(self): + return await self.client.list(self.apps_path) + + async def show(self, app_name): + return await self.client.show(self.app_path, app_name) + + async def create(self, app_name): + data = { + "app": { + "name": app_name + } + } + return await self.client.create(self.apps_path, data) + + async def delete(self, app_name): + return await self.client.remove(self.app_path, app_name) + + +class RoutesV1(object): + + # /v1/{project_id}/apps/{app}/routes + routes_path = "/v1/{}/apps/{}/routes" + # /v1/{project_id}/apps/{app}/routes{} + route_path = routes_path + "{}" + + def __init__(self, test_client): + self.client = test_client + + async def list(self, app_name): + return await self.client.list( + self.routes_path, app_name) + + async def show(self, app_name, path): + return await self.client.show( + self.route_path, app_name, path) + + async def create(self, app_name, **data): + return await self.client.create( + self.routes_path, {"route": data}, app_name) + + async def delete(self, app_name, path): + return await self.client.remove( + self.route_path, app_name, path) + + +class ProjectBoundLaosTestClient(test_utils.TestClient): + + def __init__(self, app_or_server, project_id): + super(ProjectBoundLaosTestClient, self).__init__(app_or_server) + self.project_id = project_id + self.headers = { + "Content-Type": "application/json" + } + self.apps = AppsV1(self) + self.routes = RoutesV1(self) + + async def create(self, route, data, *parts): + resp = await self.post( + route.format(self.project_id, *parts), + data=jsonlib.dumps(data), headers=self.headers) + json = await resp.json() + return json, resp.status + + async def list(self, route, *parts): + resp = await self.get( + route.format(self.project_id, *parts), + headers=self.headers) + json = await resp.json() + return json, resp.status + + async def show(self, route, resource_name, *parts): + resp = await self.get( + route.format(self.project_id, resource_name, *parts), + headers=self.headers) + json = await resp.json() + return json, resp.status + + async def remove(self, route, resource_name, *parts): + resp = await self.delete( + route.format(self.project_id, resource_name, *parts), + headers=self.headers) + json = await resp.json() + return json, resp.status diff --git a/laos/tests/functional/test_apps.py b/laos/tests/functional/test_apps.py new file mode 100644 index 0000000..754366a --- /dev/null +++ b/laos/tests/functional/test_apps.py @@ -0,0 +1,62 @@ +# 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 laos.tests.functional import base + + +class TestApps(base.LaosFunctionalTestsBase): + + def test_list_apps(self): + json, http_status = self.testloop.run_until_complete( + self.test_client.apps.list()) + self.assertIn("message", json) + self.assertIn("apps", json) + self.assertEqual(200, http_status) + + def test_get_unknown(self): + json, http_status = self.testloop.run_until_complete( + self.test_client.apps.show("unknown")) + self.assertEqual(404, http_status) + self.assertIn("error", json) + + def test_create_and_delete(self): + create_json, create_status = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + delete_json, delete_status = self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + + self.assertIn("message", create_json) + self.assertIn("app", create_json) + self.assertEqual(200, create_status) + + self.assertIn("message", delete_json) + self.assertEqual(200, delete_status) + + def test_attempt_to_double_create(self): + app = "testapp" + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app)) + err, status = self.testloop.run_until_complete( + self.test_client.apps.create(app)) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + self.assertEqual(409, status) + self.assertIn("error", err) + self.assertIn("message", err["error"]) + + def test_attempt_delete_unknonw(self): + json, http_status = self.testloop.run_until_complete( + self.test_client.apps.delete("unknown")) + self.assertEqual(404, http_status) + self.assertIn("error", json) diff --git a/laos/tests/functional/test_routes.py b/laos/tests/functional/test_routes.py new file mode 100644 index 0000000..9ecd6b7 --- /dev/null +++ b/laos/tests/functional/test_routes.py @@ -0,0 +1,121 @@ +# 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 json as jsonlib + +from laos.tests.functional import base + + +class TestAppRoutes(base.LaosFunctionalTestsBase): + + def test_list_routes_from_unknown_app(self): + json, status = self.testloop.run_until_complete( + self.test_client.routes.list("uknown_app") + ) + self.assertEqual(status, 404) + self.assertIn("error", json) + self.assertIn("not found", json["error"]["message"]) + + def test_list_routes_from_existing_app(self): + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + json, status = self.testloop.run_until_complete( + self.test_client.routes.list(create_json["app"]["name"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + self.assertEqual(status, 200) + self.assertIn("routes", json) + self.assertIn("message", json) + + def test_show_unknown_route_from_existing_app(self): + path = "/unknown_path" + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + json, status = self.testloop.run_until_complete( + self.test_client.routes.show( + create_json["app"]["name"], path) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + + self.assertEqual(404, status) + self.assertIn("error", json) + self.assertIn("not found", json["error"]["message"]) + self.assertIn(path[1:], json["error"]["message"]) + + def test_delete_unknown_route_from_existing_app(self): + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + json, status = self.testloop.run_until_complete( + self.test_client.routes.delete( + create_json["app"]["name"], "/unknown_path") + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + + self.assertEqual(404, status) + self.assertIn("error", json) + self.assertIn("not found", json["error"]["message"]) + + def test_create_and_delete_route(self): + app, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + route, create_status = self.testloop.run_until_complete( + self.test_client.routes.create( + app["app"]["name"], **self.route_data) + ) + route_deleted, delete_status = self.testloop.run_until_complete( + self.test_client.routes.delete( + app["app"]["name"], self.route_data["path"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(app["app"]["name"])) + after_post = route["route"] + for k in self.route_data: + if k == "path": + self.assertIn(self.route_data["path"], after_post[k]) + continue + if k == "is_public": + is_public = jsonlib.loads(self.route_data[k]) + self.assertEqual(is_public, after_post[k]) + continue + self.assertEqual(self.route_data[k], after_post[k]) + self.assertEqual(200, delete_status) + self.assertEqual(200, create_status) + self.assertIn("message", route_deleted) + + def test_double_create_route(self): + app, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + self.testloop.run_until_complete( + self.test_client.routes.create( + app["app"]["name"], **self.route_data) + ) + + json, double_create_status = self.testloop.run_until_complete( + self.test_client.routes.create( + app["app"]["name"], **self.route_data) + ) + self.testloop.run_until_complete( + self.test_client.routes.delete( + app["app"]["name"], self.route_data["path"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(app["app"]["name"])) + + self.assertEqual(409, double_create_status) + self.assertIn("error", json) + self.assertIn("message", json["error"]) + self.assertIn("already exist", json["error"]["message"]) diff --git a/requirements.txt b/requirements.txt index 4271351..906f5b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,13 +2,13 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -uvloop -aiohttp # Apache-2.0 -aiomysql -alembic>=0.8.4 # MIT -click -keystoneauth1>=2.14.0 # Apache-2.0 -python-keystoneclient==3.6.0 +uvloop==0.6.0 # Apache-2.0 +aiohttp==1.1.5 # Apache-2.0 +aiomysql==0.0.9 # Apache-2.0 +alembic==0.8.8 # MIT +click==6.6 # Apache-2.0 +keystoneauth1==2.15.0 # Apache-2.0 +python-keystoneclient==3.6.0 # Apache-2.0 #swagger-api -aiohttp-swagger +aiohttp-swagger==1.0.2 # Apache-2.0 diff --git a/setup.py b/setup.py index d0964bd..603a486 100644 --- a/setup.py +++ b/setup.py @@ -29,14 +29,14 @@ setuptools.setup( author_email='denis@iron.io', packages=setuptools.find_packages(), install_requires=[ - "uvloop", - "aiohttp", - "aiomysql", - "alembic>=0.8.4", - "click", - "keystoneauth1>=2.14.0", + "uvloop==0.6.0", + "aiohttp==1.1.5", + "aiomysql==0.0.9", + "alembic==0.8.8", + "click==6.6", + "keystoneauth1==2.15.0", "python-keystoneclient==3.6.0", - "aiohttp-swagger", + "aiohttp-swagger==1.0.2", ], license='License :: OSI Approved :: Apache Software License', classifiers=[ @@ -61,10 +61,10 @@ setuptools.setup( tests_require=[ 'flake8==2.5.0', 'hacking<0.11,>=0.10.0', - 'coverage>=4.0', 'sphinx!=1.3b1,<1.4,>=1.2.1', - 'testrepository>=0.0.18', 'testtools>=1.4.0', + "pytest-aiohttp", + "pytest-cov", ], zip_safe=True, entry_points={ diff --git a/test-requirements.txt b/test-requirements.txt index b4b38e1..2fc41b2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ flake8==2.5.0 # fix for https://gitlab.com/pycqa/flake8/issues/94 hacking<0.11,>=0.10.0 -coverage>=4.0 # Apache-2.0 sphinx!=1.3b1,<1.4,>=1.2.1 # BSD -testrepository>=0.0.18 # Apache-2.0/BSD testtools>=1.4.0 # MIT +pytest-aiohttp==0.1.3 +pytest-cov==2.4.0 diff --git a/tox.ini b/tox.ini index 30553ac..a2ba8d3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,20 @@ -# Function Python client +# Project LaOS [tox] -envlist = py35,pep8 +envlist = py35-functional,pep8 minversion = 1.6 skipsdist = True [testenv] passenv = PYTHONASYNCIODEBUG - + TEST_DB_URI setenv = VIRTUAL_ENV={envdir} usedevelop = True install_command = pip install -U {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt + git+ssh://git@github.com/iron-io/functions_python.git#egg=functions-python commands = find . -type f -name "*.pyc" -delete rm -f .testrepository/times.dbm python setup.py testr --testr-args='{posargs}' @@ -26,12 +27,10 @@ commands = flake8 [testenv:venv] commands = {posargs} -[testenv:cover] -commands = - coverage erase - python setup.py testr --coverage --testr-args='{posargs}' - coverage html - coverage report + +[testenv:py35-functional] +commands = pytest --tb=long --capture=sys --cov=laos --capture=fd {toxinidir}/laos/tests/functional + [testenv:docs] commands =