Skip to content

Commit

Permalink
Merge pull request #744 from plotly/pytest-fixtures
Browse files Browse the repository at this point in the history
Pytest fixtures
  • Loading branch information
byronz committed Jun 10, 2019
2 parents c0c581b + 162bed7 commit 84133a3
Show file tree
Hide file tree
Showing 52 changed files with 2,059 additions and 1,644 deletions.
17 changes: 9 additions & 8 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ jobs:
flake8 dash setup.py
flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests
pylint dash setup.py --rcfile=$PYLINTRC
pylint tests/unit -d all -e C0410,C0411,C0412,C0413,W0109
pylint tests/unit tests/integration/devtools tests/integration/renderer tests/integration/dash_assets -d all -e C0410,C0411,C0412,C0413,W0109
cd dash-renderer && npm install --ignore-scripts && npm run lint:test && npm run format:test
- run:
name: ⛑ Run unit tests
command: |
. venv/bin/activate
mkdir test-reports
pytest --junitxml=test-reports/junit.xml tests/unit
PYTHONPATH=~/dash/tests/assets pytest --junitxml=test-reports/junit.xml tests/unit
- store_test_results:
path: test-reports
- store_artifacts:
Expand All @@ -74,13 +74,13 @@ jobs:
- run:
name: 🚧 install dependencies from latest master commit
command: |
git clone --depth 1 https://github.com/plotly/dash-core-components.git
git clone --depth 1 https://github.com/plotly/dash-html-components.git
git clone https://github.com/plotly/dash-core-components.git
git clone https://github.com/plotly/dash-html-components.git
git clone --depth 1 https://github.com/plotly/dash-table.git
git clone --depth 1 https://github.com/plotly/dash-renderer-test-components
. venv/bin/activate
cd dash-core-components && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
cd dash-html-components && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
cd dash-core-components && git checkout 2932409 && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
cd dash-html-components && git checkout 446b114 && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
cd dash-table && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
cd dash-renderer-test-components && npm install --ignore-scripts && npm run build:all && pip install -e . && cd ..
Expand All @@ -89,11 +89,12 @@ jobs:
command: |
. venv/bin/activate
pytest --junitxml=test-reports/junit_intg.xml tests/integration/
- store_artifacts:
path: test-reports
- store_test_results:
path: test-reports
- store_artifacts:
path: test-reports
path: /tmp/dash_artifacts


"python-3.6":
Expand Down
3 changes: 2 additions & 1 deletion .circleci/requirements/dev-requirements-py37.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ requests
beautifulsoup4
pytest
pytest-sugar
pytest-mock
pytest-mock
waitress
1 change: 1 addition & 0 deletions .circleci/requirements/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ pytest-mock
lxml
requests
beautifulsoup4
waitress
4 changes: 3 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ disable=fixme,
invalid-name,
too-many-lines,
old-style-class,
superfluous-parens
superfluous-parens,
bad-continuation,


# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
3 changes: 2 additions & 1 deletion .pylintrc37
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ disable=invalid-name,
useless-object-inheritance,
possibly-unused-variable,
too-many-lines,
too-many-statements
too-many-statements,
bad-continuation

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
2 changes: 1 addition & 1 deletion dash-renderer/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.23.0'
__version__ = '0.24.0'
File renamed without changes.
216 changes: 216 additions & 0 deletions dash/testing/application_runners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
from __future__ import print_function

import sys
import uuid
import shlex
import threading
import subprocess
import logging

import runpy
import six
import flask
import requests

from dash.testing.errors import (
NoAppFoundError,
TestingTimeoutError,
ServerCloseError,
)
import dash.testing.wait as wait


logger = logging.getLogger(__name__)


def import_app(app_file, application_name="app"):
"""
Import a dash application from a module.
The import path is in dot notation to the module.
The variable named app will be returned.
:Example:
>>> app = import_app('my_app.app')
Will import the application in module `app` of the package `my_app`.
:param app_file: Path to the app (dot-separated).
:type app_file: str
:param application_name: The name of the dash application instance.
:raise: dash_tests.errors.NoAppFoundError
:return: App from module.
:rtype: dash.Dash
"""
try:
app_module = runpy.run_module(app_file)
app = app_module[application_name]
except KeyError:
logger.exception("the app name cannot be found")
raise NoAppFoundError(
"No dash `app` instance was found in {}".format(app_file)
)
return app


class BaseDashRunner(object):
"""Base context manager class for running applications."""

def __init__(self, keep_open, stop_timeout):
self.port = 8050
self.started = None
self.keep_open = keep_open
self.stop_timeout = stop_timeout

def start(self, *args, **kwargs):
raise NotImplementedError # pragma: no cover

def stop(self):
raise NotImplementedError # pragma: no cover

def __call__(self, *args, **kwargs):
return self.start(*args, **kwargs)

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, traceback):
if self.started and not self.keep_open:
try:
logger.info("killing the app runner")
self.stop()
except TestingTimeoutError:
raise ServerCloseError(
"Cannot stop server within {}s timeout".format(
self.stop_timeout
)
)

@property
def url(self):
"""the default server url"""
return "http://localhost:{}".format(self.port)


class ThreadedRunner(BaseDashRunner):
"""Runs a dash application in a thread
this is the default flavor to use in dash integration tests
"""

def __init__(self, keep_open=False, stop_timeout=3):
super(ThreadedRunner, self).__init__(
keep_open=keep_open, stop_timeout=stop_timeout
)
self.stop_route = "/_stop-{}".format(uuid.uuid4().hex)
self.thread = None

@staticmethod
def _stop_server():
# https://werkzeug.palletsprojects.com/en/0.15.x/serving/#shutting-down-the-server
stopper = flask.request.environ.get("werkzeug.server.shutdown")
if stopper is None:
raise RuntimeError("Not running with the Werkzeug Server")
stopper()
return "Flask server is shutting down"

# pylint: disable=arguments-differ,C0330
def start(self, app, **kwargs):
"""Start the app server in threading flavor"""
app.server.add_url_rule(
self.stop_route, self.stop_route, self._stop_server
)

def _handle_error():
self._stop_server()

app.server.errorhandler(500)(_handle_error)

def run():
app.scripts.config.serve_locally = True
app.css.config.serve_locally = True
if "port" not in kwargs:
kwargs["port"] = self.port
else:
self.port = kwargs["port"]
app.run_server(threaded=True, **kwargs)

self.thread = threading.Thread(target=run)
self.thread.daemon = True
try:
self.thread.start()
except RuntimeError: # multiple call on same thread
logger.exception("threaded server failed to start")
self.started = False

self.started = self.thread.is_alive()

def accessible():
try:
requests.get(self.url)
except requests.exceptions.RequestException:
return False
return True

# wait until server is able to answer http request
wait.until(accessible, timeout=1)

def stop(self):
requests.get("{}{}".format(self.url, self.stop_route))
wait.until_not(self.thread.is_alive, self.stop_timeout)


class ProcessRunner(BaseDashRunner):
"""Runs a dash application in a waitress-serve subprocess
this flavor is closer to production environment but slower
"""

def __init__(self, keep_open=False, stop_timeout=3):
super(ProcessRunner, self).__init__(
keep_open=keep_open, stop_timeout=stop_timeout
)
self.proc = None

# pylint: disable=arguments-differ
def start(self, app_module, application_name="app", port=8050):
"""Start the server with waitress-serve in process flavor """
entrypoint = "{}:{}.server".format(app_module, application_name)
self.port = port

args = shlex.split(
"waitress-serve --listen=0.0.0.0:{} {}".format(port, entrypoint),
posix=sys.platform != "win32",
)
logger.debug("start dash process with %s", args)

try:
self.proc = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except (OSError, ValueError):
logger.exception("process server has encountered an error")
self.started = False
return

self.started = True

def stop(self):
if self.proc:
try:
self.proc.terminate()
if six.PY3:
# pylint:disable=no-member
_except = subprocess.TimeoutExpired
# pylint: disable=unexpected-keyword-arg
self.proc.communicate(timeout=self.stop_timeout)
else:
_except = OSError
self.proc.communicate()
except _except:
logger.exception(
"subprocess terminate not success, trying to kill "
"the subprocess in a safe manner"
)
self.proc.kill()
self.proc.communicate()
Loading

0 comments on commit 84133a3

Please sign in to comment.