Skip to content

Commit 902488a

Browse files
authored
Merge pull request #827 from plotly/dashr-test-support
Dashr test support
2 parents 000d775 + 339141a commit 902488a

File tree

5 files changed

+117
-18
lines changed

5 files changed

+117
-18
lines changed

Diff for: dash/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
### Added
4+
5+
- [#827](https://github.com/plotly/dash/pull/827) Adds support for dashR testing using pytest framework
6+
17
## [1.0.2] - 2019-07-15
28
### Fixed
39
- [#821](https://github.com/plotly/dash/pull/821) Fix a bug with callback error reporting, [#791](https://github.com/plotly/dash/issues/791).

Diff for: dash/testing/application_runners.py

+70-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import print_function
22

33
import sys
4+
import os
45
import uuid
56
import shlex
67
import threading
@@ -68,6 +69,14 @@ def start(self, *args, **kwargs):
6869
def stop(self):
6970
raise NotImplementedError # pragma: no cover
7071

72+
@staticmethod
73+
def accessible(url):
74+
try:
75+
requests.get(url)
76+
except requests.exceptions.RequestException:
77+
return False
78+
return True
79+
7180
def __call__(self, *args, **kwargs):
7281
return self.start(*args, **kwargs)
7382

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

103+
@property
104+
def is_windows(self):
105+
return sys.platform == "win32"
106+
94107

95108
class ThreadedRunner(BaseDashRunner):
96109
"""Runs a dash application in a thread
@@ -145,15 +158,8 @@ def run():
145158

146159
self.started = self.thread.is_alive()
147160

148-
def accessible():
149-
try:
150-
requests.get(self.url)
151-
except requests.exceptions.RequestException:
152-
return False
153-
return True
154-
155161
# wait until server is able to answer http request
156-
wait.until(accessible, timeout=1)
162+
wait.until(lambda: self.accessible(self.url), timeout=1)
157163

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

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

187193
try:
188194
self.proc = subprocess.Popen(
189195
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
190196
)
197+
# wait until server is able to answer http request
198+
wait.until(lambda: self.accessible(self.url), timeout=3)
199+
191200
except (OSError, ValueError):
192201
logger.exception("process server has encountered an error")
193202
self.started = False
@@ -214,3 +223,55 @@ def stop(self):
214223
)
215224
self.proc.kill()
216225
self.proc.communicate()
226+
227+
228+
class RRunner(ProcessRunner):
229+
def __init__(self, keep_open=False, stop_timeout=3):
230+
super(RRunner, self).__init__(
231+
keep_open=keep_open, stop_timeout=stop_timeout
232+
)
233+
self.proc = None
234+
235+
# pylint: disable=arguments-differ
236+
def start(self, app):
237+
"""Start the server with waitress-serve in process flavor """
238+
239+
# app is a R string chunk
240+
if not (os.path.isfile(app) and os.path.exists(app)):
241+
path = (
242+
"/tmp/app_{}.R".format(uuid.uuid4().hex)
243+
if not self.is_windows
244+
else os.path.join(
245+
(os.getenv("TEMP"), "app_{}.R".format(uuid.uuid4().hex))
246+
)
247+
)
248+
logger.info("RRuner start => app is R code chunk")
249+
logger.info("make a temporay R file for execution=> %s", path)
250+
logger.debug("the content of dashR app")
251+
logger.debug("%s", app)
252+
253+
with open(path, "w") as fp:
254+
fp.write(app)
255+
256+
app = path
257+
258+
logger.info("Run dashR app with Rscript => %s", app)
259+
args = shlex.split(
260+
"Rscript {}".format(os.path.realpath(app)),
261+
posix=not self.is_windows,
262+
)
263+
logger.debug("start dash process with %s", args)
264+
265+
try:
266+
self.proc = subprocess.Popen(
267+
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
268+
)
269+
# wait until server is able to answer http request
270+
wait.until(lambda: self.accessible(self.url), timeout=2)
271+
272+
except (OSError, ValueError):
273+
logger.exception("process server has encountered an error")
274+
self.started = False
275+
return
276+
277+
self.started = True

Diff for: dash/testing/composite.py

+14
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,17 @@ def start_server(self, app, **kwargs):
1414

1515
# set the default server_url, it implicitly call wait_for_page
1616
self.server_url = self.server.url
17+
18+
19+
class DashRComposite(Browser):
20+
def __init__(self, server, **kwargs):
21+
super(DashRComposite, self).__init__(**kwargs)
22+
self.server = server
23+
24+
def start_server(self, app):
25+
26+
# start server with dashR app, the dash arguments are hardcoded
27+
self.server(app)
28+
29+
# set the default server_url, it implicitly call wait_for_page
30+
self.server_url = self.server.url

Diff for: dash/testing/plugin.py

+27-7
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
try:
55
import pytest
66

7-
from dash.testing.application_runners import ThreadedRunner, ProcessRunner
7+
from dash.testing.application_runners import (
8+
ThreadedRunner,
9+
ProcessRunner,
10+
RRunner,
11+
)
812
from dash.testing.browser import Browser
9-
from dash.testing.composite import DashComposite
13+
from dash.testing.composite import DashComposite, DashRComposite
1014
except ImportError:
1115
warnings.warn("run `pip install dash[testing]` if you need dash.testing")
1216

@@ -26,9 +30,7 @@ def pytest_addoption(parser):
2630
)
2731

2832
dash.addoption(
29-
"--headless",
30-
action="store_true",
31-
help="Run tests in headless mode",
33+
"--headless", action="store_true", help="Run tests in headless mode"
3234
)
3335

3436

@@ -79,13 +81,19 @@ def dash_process_server():
7981
yield starter
8082

8183

84+
@pytest.fixture
85+
def dashr_server():
86+
with RRunner() as starter:
87+
yield starter
88+
89+
8290
@pytest.fixture
8391
def dash_br(request, tmpdir):
8492
with Browser(
8593
browser=request.config.getoption("webdriver"),
8694
headless=request.config.getoption("headless"),
8795
options=request.config.hook.pytest_setup_options(),
88-
download_path=tmpdir.mkdir('download').strpath
96+
download_path=tmpdir.mkdir("download").strpath,
8997
) as browser:
9098
yield browser
9199

@@ -97,6 +105,18 @@ def dash_duo(request, dash_thread_server, tmpdir):
97105
browser=request.config.getoption("webdriver"),
98106
headless=request.config.getoption("headless"),
99107
options=request.config.hook.pytest_setup_options(),
100-
download_path=tmpdir.mkdir('download').strpath
108+
download_path=tmpdir.mkdir("download").strpath,
109+
) as dc:
110+
yield dc
111+
112+
113+
@pytest.fixture
114+
def dashr(request, dashr_server, tmpdir):
115+
with DashRComposite(
116+
dashr_server,
117+
browser=request.config.getoption("webdriver"),
118+
headless=request.config.getoption("headless"),
119+
options=request.config.hook.pytest_setup_options(),
120+
download_path=tmpdir.mkdir("download").strpath,
101121
) as dc:
102122
yield dc

Diff for: tests/unit/test_app_runners.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import time
21
import sys
32
import requests
43
import pytest
@@ -27,7 +26,6 @@ def test_threaded_server_smoke(dash_thread_server):
2726
)
2827
def test_process_server_smoke(dash_process_server):
2928
dash_process_server("simple_app")
30-
time.sleep(2.5)
3129
r = requests.get(dash_process_server.url)
3230
assert r.status_code == 200, "the server is reachable"
3331
assert 'id="react-entry-point"' in r.text, "the entrypoint is present"

0 commit comments

Comments
 (0)