Impelemts functional testing framework

Includes:
  - base functional test case class
  - apps tests
  - route tests
  - coverage included

 README updated
This commit is contained in:
Denis Makogon 2016-11-18 19:00:56 +02:00
parent 967bd9fa3f
commit b78d1a7dc3
21 changed files with 642 additions and 81 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ python-troveclient.iml
releasenotes/build releasenotes/build
.coverage.* .coverage.*
*.json *.json
.cache

View File

@ -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

View File

@ -146,7 +146,53 @@ Once server is launched you can navigate to:
http://<laos-host>:<laos-port>/api http://<laos-host>:<laos-port>/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://<your-user>:<your-user-password>@<mysql-host>:<mysql-port>/<functions-db>
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 3rd party bugs to resolve
------------------------- -------------------------

View File

@ -195,7 +195,8 @@ class AppV1Controller(controllers.ServiceControllerBase):
""" """
project_id = request.match_info.get('project_id') project_id = request.match_info.get('project_id')
app = request.match_info.get('app') 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)): if not (await app_model.Apps.exists(app, project_id)):
return web.json_response(data={ return web.json_response(data={
"error": { "error": {
@ -203,6 +204,17 @@ class AppV1Controller(controllers.ServiceControllerBase):
} }
}, status=404) }, 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( await app_model.Apps.delete(
project_id=project_id, name=app) project_id=project_id, name=app)
# TODO(denismakogon): enable DELETE to IronFunctions when once # TODO(denismakogon): enable DELETE to IronFunctions when once

View File

@ -178,7 +178,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase):
setattr(new_fn_route, "is_public", stored_route.public) setattr(new_fn_route, "is_public", stored_route.public)
view = app_view.AppRouteView( 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={ return web.json_response(data={
"route": view, "route": view,
@ -244,7 +244,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase):
return web.json_response(data={ return web.json_response(data={
"route": app_view.AppRouteView(api_url, "route": app_view.AppRouteView(api_url,
project_id, project_id,
[route]).view().pop(), [route]).view_one(),
"message": "App route successfully loaded" "message": "App route successfully loaded"
}, status=200) }, status=200)

View File

@ -38,6 +38,9 @@ class AppRouteView(object):
self.api_url = api_url self.api_url = api_url
self.project_id = project_id self.project_id = project_id
def view_one(self):
return self.view().pop()
def view(self): def view(self):
view = [] view = []
for route in self.routes: for route in self.routes:

View File

@ -37,18 +37,19 @@ class Connection(object, metaclass=utils.Singleton):
def __init__(self, db_uri, loop=None): def __init__(self, db_uri, loop=None):
self.uri = db_uri 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 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) username, password, host, port, db_name = utils.split_db_uri(self.uri)
return aiomysql.create_pool(host=host, port=port if port else 3306, return aiomysql.create_pool(host=host, port=port if port else 3306,
user=username, password=password, user=username, password=password,
db=db_name, loop=loop) db=db_name, loop=self.loop)
@classmethod @classmethod
def from_class(cls): def from_class(cls):
return cls._instance.engine return cls._instance.pool
class FunctionsClient(client.FunctionsAPIV1, metaclass=utils.Singleton): class FunctionsClient(client.FunctionsAPIV1, metaclass=utils.Singleton):

View File

@ -38,14 +38,15 @@ class BaseDatabaseModel(object):
setattr(self, k, v) setattr(self, k, v)
async def save(self): 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( insert = self.INSERT.format(
self.table_name, self.table_name,
str(tuple([getattr(self, clmn) for clmn in self.columns])) str(tuple([getattr(self, clmn) for clmn in self.columns]))
) )
logger.info("Attempting to save object '{}' " logger.info("Attempting to save object '{}' "
"using SQL query: {}".format(self.table_name, insert)) "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: async with conn.cursor() as cur:
await cur.execute(insert) await cur.execute(insert)
await conn.commit() await conn.commit()
@ -54,12 +55,13 @@ class BaseDatabaseModel(object):
@classmethod @classmethod
async def delete(cls, **kwargs): 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( delete = cls.DELETE.format(
cls.table_name, cls.__define_where(**kwargs)) cls.table_name, cls.__define_where(**kwargs))
logger.info("Attempting to delete object '{}' " logger.info("Attempting to delete object '{}' "
"using SQL query: {}".format(cls.table_name, delete)) "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: async with conn.cursor() as cur:
await cur.execute(delete) await cur.execute(delete)
await conn.commit() await conn.commit()
@ -92,18 +94,19 @@ class BaseDatabaseModel(object):
@classmethod @classmethod
async def find_by(cls, **kwargs): 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) where = cls.__define_where(**kwargs)
select = cls.SELECT.format( select = cls.SELECT.format(
cls.table_name, where) cls.table_name, where)
logger.info("Attempting to find object(s) '{}' " logger.info("Attempting to find object(s) '{}' "
"using SQL : {}".format(cls.table_name, select)) "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: async with conn.cursor() as cur:
await cur.execute(select) await cur.execute(select)
results = await cur.fetchall() results = await cur.fetchall()
return [cls.from_tuple(instance) return [cls.from_tuple(instance)
for instance in results] if results else [] for instance in results] if results else []
@classmethod @classmethod
def from_tuple(cls, tpl): def from_tuple(cls, tpl):

View File

@ -116,13 +116,13 @@ def server(host, port, db_uri,
api_version=functions_api_version, api_version=functions_api_version,
) )
loop.run_until_complete(fnclient.ping(loop=loop)) 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( config.Config(
auth_url=keystone_endpoint, auth_url=keystone_endpoint,
functions_client=fnclient, functions_client=fnclient,
logger=logger, logger=logger,
connection=conn, connection=connection_pool,
event_loop=loop, event_loop=loop,
) )

View File

View File

@ -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()

View File

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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"])

View File

@ -2,13 +2,13 @@
# of appearance. Changing the order has an impact on the overall integration # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
uvloop uvloop==0.6.0 # Apache-2.0
aiohttp # Apache-2.0 aiohttp==1.1.5 # Apache-2.0
aiomysql aiomysql==0.0.9 # Apache-2.0
alembic>=0.8.4 # MIT alembic==0.8.8 # MIT
click click==6.6 # Apache-2.0
keystoneauth1>=2.14.0 # Apache-2.0 keystoneauth1==2.15.0 # Apache-2.0
python-keystoneclient==3.6.0 python-keystoneclient==3.6.0 # Apache-2.0
#swagger-api #swagger-api
aiohttp-swagger aiohttp-swagger==1.0.2 # Apache-2.0

View File

@ -29,14 +29,14 @@ setuptools.setup(
author_email='denis@iron.io', author_email='denis@iron.io',
packages=setuptools.find_packages(), packages=setuptools.find_packages(),
install_requires=[ install_requires=[
"uvloop", "uvloop==0.6.0",
"aiohttp", "aiohttp==1.1.5",
"aiomysql", "aiomysql==0.0.9",
"alembic>=0.8.4", "alembic==0.8.8",
"click", "click==6.6",
"keystoneauth1>=2.14.0", "keystoneauth1==2.15.0",
"python-keystoneclient==3.6.0", "python-keystoneclient==3.6.0",
"aiohttp-swagger", "aiohttp-swagger==1.0.2",
], ],
license='License :: OSI Approved :: Apache Software License', license='License :: OSI Approved :: Apache Software License',
classifiers=[ classifiers=[
@ -61,10 +61,10 @@ setuptools.setup(
tests_require=[ tests_require=[
'flake8==2.5.0', 'flake8==2.5.0',
'hacking<0.11,>=0.10.0', 'hacking<0.11,>=0.10.0',
'coverage>=4.0',
'sphinx!=1.3b1,<1.4,>=1.2.1', 'sphinx!=1.3b1,<1.4,>=1.2.1',
'testrepository>=0.0.18',
'testtools>=1.4.0', 'testtools>=1.4.0',
"pytest-aiohttp",
"pytest-cov",
], ],
zip_safe=True, zip_safe=True,
entry_points={ entry_points={

View File

@ -4,7 +4,7 @@
flake8==2.5.0 # fix for https://gitlab.com/pycqa/flake8/issues/94 flake8==2.5.0 # fix for https://gitlab.com/pycqa/flake8/issues/94
hacking<0.11,>=0.10.0 hacking<0.11,>=0.10.0
coverage>=4.0 # Apache-2.0
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testtools>=1.4.0 # MIT testtools>=1.4.0 # MIT
pytest-aiohttp==0.1.3
pytest-cov==2.4.0

17
tox.ini
View File

@ -1,19 +1,20 @@
# Function Python client # Project LaOS
[tox] [tox]
envlist = py35,pep8 envlist = py35-functional,pep8
minversion = 1.6 minversion = 1.6
skipsdist = True skipsdist = True
[testenv] [testenv]
passenv = passenv =
PYTHONASYNCIODEBUG PYTHONASYNCIODEBUG
TEST_DB_URI
setenv = VIRTUAL_ENV={envdir} setenv = VIRTUAL_ENV={envdir}
usedevelop = True usedevelop = True
install_command = pip install -U {opts} {packages} install_command = pip install -U {opts} {packages}
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-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 commands = find . -type f -name "*.pyc" -delete
rm -f .testrepository/times.dbm rm -f .testrepository/times.dbm
python setup.py testr --testr-args='{posargs}' python setup.py testr --testr-args='{posargs}'
@ -26,12 +27,10 @@ commands = flake8
[testenv:venv] [testenv:venv]
commands = {posargs} commands = {posargs}
[testenv:cover]
commands = [testenv:py35-functional]
coverage erase commands = pytest --tb=long --capture=sys --cov=laos --capture=fd {toxinidir}/laos/tests/functional
python setup.py testr --coverage --testr-args='{posargs}'
coverage html
coverage report
[testenv:docs] [testenv:docs]
commands = commands =