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
.coverage.*
*.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
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
-------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

17
tox.ini
View File

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