Skip to content

Commit

Permalink
Merge pull request #827 from plotly/dashr-test-support
Browse files Browse the repository at this point in the history
Dashr test support
  • Loading branch information
byronz authored Jul 22, 2019
2 parents 000d775 + 339141a commit 902488a
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 18 deletions.
6 changes: 6 additions & 0 deletions dash/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Added

- [#827](https://github.com/plotly/dash/pull/827) Adds support for dashR testing using pytest framework

## [1.0.2] - 2019-07-15
### Fixed
- [#821](https://github.com/plotly/dash/pull/821) Fix a bug with callback error reporting, [#791](https://github.com/plotly/dash/issues/791).
Expand Down
79 changes: 70 additions & 9 deletions dash/testing/application_runners.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import print_function

import sys
import os
import uuid
import shlex
import threading
Expand Down Expand Up @@ -68,6 +69,14 @@ def start(self, *args, **kwargs):
def stop(self):
raise NotImplementedError # pragma: no cover

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

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

Expand All @@ -91,6 +100,10 @@ def url(self):
"""the default server url"""
return "http://localhost:{}".format(self.port)

@property
def is_windows(self):
return sys.platform == "win32"


class ThreadedRunner(BaseDashRunner):
"""Runs a dash application in a thread
Expand Down Expand Up @@ -145,15 +158,8 @@ def run():

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)
wait.until(lambda: self.accessible(self.url), timeout=1)

def stop(self):
requests.get("{}{}".format(self.url, self.stop_route))
Expand All @@ -180,14 +186,17 @@ def start(self, app_module, application_name="app", port=8050):

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

try:
self.proc = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# wait until server is able to answer http request
wait.until(lambda: self.accessible(self.url), timeout=3)

except (OSError, ValueError):
logger.exception("process server has encountered an error")
self.started = False
Expand All @@ -214,3 +223,55 @@ def stop(self):
)
self.proc.kill()
self.proc.communicate()


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

# pylint: disable=arguments-differ
def start(self, app):
"""Start the server with waitress-serve in process flavor """

# app is a R string chunk
if not (os.path.isfile(app) and os.path.exists(app)):
path = (
"/tmp/app_{}.R".format(uuid.uuid4().hex)
if not self.is_windows
else os.path.join(
(os.getenv("TEMP"), "app_{}.R".format(uuid.uuid4().hex))
)
)
logger.info("RRuner start => app is R code chunk")
logger.info("make a temporay R file for execution=> %s", path)
logger.debug("the content of dashR app")
logger.debug("%s", app)

with open(path, "w") as fp:
fp.write(app)

app = path

logger.info("Run dashR app with Rscript => %s", app)
args = shlex.split(
"Rscript {}".format(os.path.realpath(app)),
posix=not self.is_windows,
)
logger.debug("start dash process with %s", args)

try:
self.proc = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# wait until server is able to answer http request
wait.until(lambda: self.accessible(self.url), timeout=2)

except (OSError, ValueError):
logger.exception("process server has encountered an error")
self.started = False
return

self.started = True
14 changes: 14 additions & 0 deletions dash/testing/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ def start_server(self, app, **kwargs):

# set the default server_url, it implicitly call wait_for_page
self.server_url = self.server.url


class DashRComposite(Browser):
def __init__(self, server, **kwargs):
super(DashRComposite, self).__init__(**kwargs)
self.server = server

def start_server(self, app):

# start server with dashR app, the dash arguments are hardcoded
self.server(app)

# set the default server_url, it implicitly call wait_for_page
self.server_url = self.server.url
34 changes: 27 additions & 7 deletions dash/testing/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
try:
import pytest

from dash.testing.application_runners import ThreadedRunner, ProcessRunner
from dash.testing.application_runners import (
ThreadedRunner,
ProcessRunner,
RRunner,
)
from dash.testing.browser import Browser
from dash.testing.composite import DashComposite
from dash.testing.composite import DashComposite, DashRComposite
except ImportError:
warnings.warn("run `pip install dash[testing]` if you need dash.testing")

Expand All @@ -26,9 +30,7 @@ def pytest_addoption(parser):
)

dash.addoption(
"--headless",
action="store_true",
help="Run tests in headless mode",
"--headless", action="store_true", help="Run tests in headless mode"
)


Expand Down Expand Up @@ -79,13 +81,19 @@ def dash_process_server():
yield starter


@pytest.fixture
def dashr_server():
with RRunner() as starter:
yield starter


@pytest.fixture
def dash_br(request, tmpdir):
with Browser(
browser=request.config.getoption("webdriver"),
headless=request.config.getoption("headless"),
options=request.config.hook.pytest_setup_options(),
download_path=tmpdir.mkdir('download').strpath
download_path=tmpdir.mkdir("download").strpath,
) as browser:
yield browser

Expand All @@ -97,6 +105,18 @@ def dash_duo(request, dash_thread_server, tmpdir):
browser=request.config.getoption("webdriver"),
headless=request.config.getoption("headless"),
options=request.config.hook.pytest_setup_options(),
download_path=tmpdir.mkdir('download').strpath
download_path=tmpdir.mkdir("download").strpath,
) as dc:
yield dc


@pytest.fixture
def dashr(request, dashr_server, tmpdir):
with DashRComposite(
dashr_server,
browser=request.config.getoption("webdriver"),
headless=request.config.getoption("headless"),
options=request.config.hook.pytest_setup_options(),
download_path=tmpdir.mkdir("download").strpath,
) as dc:
yield dc
2 changes: 0 additions & 2 deletions tests/unit/test_app_runners.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import time
import sys
import requests
import pytest
Expand Down Expand Up @@ -27,7 +26,6 @@ def test_threaded_server_smoke(dash_thread_server):
)
def test_process_server_smoke(dash_process_server):
dash_process_server("simple_app")
time.sleep(2.5)
r = requests.get(dash_process_server.url)
assert r.status_code == 200, "the server is reachable"
assert 'id="react-entry-point"' in r.text, "the entrypoint is present"

0 comments on commit 902488a

Please sign in to comment.