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

Dashr test support #827

Merged
merged 10 commits into from
Jul 22, 2019
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))
)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this will work, but it doesn't remove the file afterward, does it? Would there have been a way to do this with tmpdir? Not sure how to go about using one fixture from within the definition of another, but it feels like all the edge cases and cleanup have already been sorted out there.

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"