diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa1b38c32..21c9ec2787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Added +- [#948](https://github.com/plotly/dash/pull/948) Support setting working directory for R apps run using the `dashr` fixture, primarily useful for tests with assets. `dashr.start_server` supports a `cwd` argument to set an explicit working directory, and has smarter defaults when it's omitted: if `app` is a path to an R script, uses the directory of that path; if `app` is a string, uses the directory the test file itself is in. - [#944](https://github.com/plotly/dash/pull/944) - Relevant `dash.testing` methods can now be called with either an element or a CSS selector: `select_dcc_dropdown`, `multiple_click`, `clear_input`, `zoom_in_graph_by_ratio`, `click_at_coord_fractions`. - Three new `dash.testing` methods: `clear_local_storage`, `clear_session_storage`, and `clear_storage` (to clear both together) diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index c578b31cfd..fd5e6e75f8 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -7,6 +7,7 @@ import threading import subprocess import logging +import inspect import runpy import future.utils as utils @@ -30,7 +31,7 @@ def import_app(app_file, application_name="app"): :Example: - >>> app = import_app('my_app.app') + >>> app = import_app("my_app.app") Will import the application in module `app` of the package `my_app`. @@ -248,11 +249,16 @@ def __init__(self, keep_open=False, stop_timeout=3): self.proc = None # pylint: disable=arguments-differ - def start(self, app, start_timeout=2): - """Start the server with waitress-serve in process flavor.""" + def start(self, app, start_timeout=2, cwd=None): + """Start the server with subprocess and Rscript.""" # app is a R string chunk - if not (os.path.isfile(app) and os.path.exists(app)): + if (os.path.isfile(app) and os.path.exists(app)): + # app is already a file in a dir - use that as cwd + if not cwd: + cwd = os.path.dirname(app) + logger.info("RRunner inferred cwd from app path: %s", cwd) + else: path = ( "/tmp/app_{}.R".format(uuid.uuid4().hex) if not self.is_windows @@ -260,9 +266,9 @@ def start(self, app, start_timeout=2): (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.info("RRunner start => app is R code chunk") + logger.info("make a temporary R file for execution => %s", path) + logger.debug("content of the dashR app") logger.debug("%s", app) with open(path, "w") as fp: @@ -270,6 +276,25 @@ def start(self, app, start_timeout=2): app = path + # try to find the path to the calling script to use as cwd + if not cwd: + for entry in inspect.stack(): + if "/dash/testing/" not in entry[1].replace("\\", "/"): + cwd = os.path.dirname(os.path.realpath(entry[1])) + break + if cwd: + logger.info( + "RRunner inferred cwd from the Python call stack: %s", + cwd + ) + else: + logger.warning( + "RRunner found no cwd in the Python call stack. " + "You may wish to specify an explicit working directory " + "using something like: " + "dashr.run_server(app, cwd=os.path.dirname(__file__))" + ) + logger.info("Run dashR app with Rscript => %s", app) args = shlex.split( "Rscript {}".format(os.path.realpath(app)), @@ -279,7 +304,7 @@ def start(self, app, start_timeout=2): try: self.proc = subprocess.Popen( - args, stdout=subprocess.PIPE, stderr=subprocess.PIPE + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd ) # wait until server is able to answer http request wait.until(lambda: self.accessible(self.url), timeout=start_timeout) diff --git a/dash/testing/composite.py b/dash/testing/composite.py index 3834f6179f..c45d20f8a6 100644 --- a/dash/testing/composite.py +++ b/dash/testing/composite.py @@ -21,10 +21,11 @@ def __init__(self, server, **kwargs): super(DashRComposite, self).__init__(**kwargs) self.server = server - def start_server(self, app): + def start_server(self, app, cwd=None): - # start server with dashR app, the dash arguments are hardcoded - self.server(app) + # start server with dashR app. The app sets its own run_server args + # on the R side, but we support overriding the automatic cwd + self.server(app, cwd=cwd) # set the default server_url, it implicitly call wait_for_page self.server_url = self.server.url