Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pytest fixtures #744

Merged
merged 54 commits into from
Jun 10, 2019
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5440e41
:hocho: test.sh is already in circleci config
May 23, 2019
9f8d4c8
Merge branch 'master' into pytest-fixtures
byronz May 23, 2019
f33adad
Merge branch 'master' into pytest-fixtures
May 23, 2019
48c4826
:tada: basic set up for dash.testing
May 24, 2019
d67e3e5
:white_check_mark: add dash smoke test with fixtures
May 24, 2019
0832a18
:construction: fixing issues in python2
May 26, 2019
13f8b97
:construction: debug with circleci cli
May 26, 2019
fb72a18
:alembic: dbg
May 29, 2019
73b4dbc
:alembic: add missing test file
May 29, 2019
791f03d
:fire: :construction: add pythonpath
May 29, 2019
fc0d0ff
:art: fix lint
May 29, 2019
12b8c39
:art: lint
May 29, 2019
3bd185b
:bug: fix one logic error, skip unit to check intg
May 29, 2019
5361cc5
:bug: mock also html
May 29, 2019
6efa443
:construction: skip test_assets until fixture for selenium is ready
May 29, 2019
ae8fc18
:lipstick: polish the app_runner
May 29, 2019
8282fc5
:alembic: improvement
May 29, 2019
30dc943
:alembic: more delete
May 29, 2019
31a9f93
:fire: skip the process test for python2 now
May 29, 2019
bb755f5
Merge branch 'master' into pytest-fixtures
byronz May 29, 2019
98249cf
:tada: browser class for wd fixtures
May 31, 2019
7b97105
Merge remote-tracking branch 'origin/pytest-fixtures' into pytest-fix…
May 31, 2019
e17a0c0
:art: fix lint
May 31, 2019
7ffd6ef
:art: fix lint
May 31, 2019
bd2cbb8
Merge branch 'pytest-fixtures' of github.com:plotly/dash into pytest-…
May 31, 2019
4d5e7f0
:ok_hand: based on feedbacks
Jun 3, 2019
b89219c
:art: fix lint
Jun 3, 2019
545a02c
:bug: fix logic error in wait
Jun 3, 2019
e6c545f
:sparkles: improve apis
Jun 3, 2019
0d6b1e2
:truck: make all fixtures with dash_ prefix, so it has less chance to…
Jun 3, 2019
4005368
:white_check_mark: refactoring on devtools with new fixtures
Jun 3, 2019
0c29ee8
:art: fix lint
Jun 3, 2019
e5f2e93
:wrench: remove --vv from pytest.ini
Jun 3, 2019
49a74ba
:twisted_rightwards_arrows: fix merge conflict
Jun 3, 2019
049f717
:pencil2: adding back the logs
Jun 3, 2019
25ace9a
:alembic:
Jun 3, 2019
12fa1b1
:alembic: dbg
Jun 4, 2019
688385e
:construction: improve for dbg experience
Jun 5, 2019
8bb8312
:alien: add and improve APIs
Jun 6, 2019
4849ace
:art: :recycle: adapt / translate tests
Jun 6, 2019
6690ccf
:recycle: API polish
Jun 6, 2019
43596f5
minor update tcid
Jun 6, 2019
bccbf29
:recycle: polish API
Jun 7, 2019
6782fe7
:hocho: move out
Jun 7, 2019
5e65b84
:white_check_mark: callbacks and other minor changes
Jun 7, 2019
4303e61
:boom: use a specific dcc commit for failed 4 cases, this needs to ge…
Jun 7, 2019
42a32dc
:wrench: html change also breaks one test
Jun 7, 2019
68e0dda
:wrench: all all integration test back
Jun 7, 2019
0b1adf2
:bug: oh forget to migrate this one
Jun 7, 2019
eee8429
:bug: fix multiple_clicks
Jun 7, 2019
b68b290
Merge branch 'master' into pytest-fixtures
byronz Jun 7, 2019
3bf9a9e
:ok_hand: remove other clicks and add seconds
Jun 10, 2019
69371f4
:boom: shorten the api name from start_app_server to start_server
Jun 10, 2019
162bed7
Merge branch 'master' into pytest-fixtures
Jun 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
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 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,
unexpected-keyword-arg
byronz marked this conversation as resolved.
Show resolved Hide resolved

# 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
4 changes: 3 additions & 1 deletion .pylintrc37
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ disable=invalid-name,
useless-object-inheritance,
possibly-unused-variable,
too-many-lines,
too-many-statements
too-many-statements,
bad-continuation,
unexpected-keyword-arg

# 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
24 changes: 24 additions & 0 deletions dash/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,27 @@ class SameInputOutputException(CallbackException):

class MissingCallbackContextException(CallbackException):
pass


class DashTestingError(Exception):
"""Base error for pytest-dash."""
byronz marked this conversation as resolved.
Show resolved Hide resolved


class InvalidDriverError(DashTestingError):
"""An invalid selenium driver was specified."""


class NoAppFoundError(DashTestingError):
"""No `app` was found in the file."""


class DashAppLoadingError(DashTestingError):
"""The dash app failed to load"""


class ServerCloseError(DashTestingError):
"""The server cannot be closed"""


class TestingTimeoutError(DashTestingError):
""""all timeout error about dash testing"""
File renamed without changes.
204 changes: 204 additions & 0 deletions dash/testing/application_runners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
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.exceptions 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 {} timeout".format(
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved
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
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 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
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