From 29a5c0f5119fac0a8a1cdeb3de0ed99917b5edde Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 3 May 2023 08:10:31 -0500 Subject: [PATCH 1/7] Sketch dryRun() implementation --- R/dryRun.R | 153 +++++++++++++++++++++++++++++++++++++++++++++ inst/dryRunTrace.R | 35 +++++++++++ 2 files changed, 188 insertions(+) create mode 100644 R/dryRun.R create mode 100644 inst/dryRunTrace.R diff --git a/R/dryRun.R b/R/dryRun.R new file mode 100644 index 00000000..1359ca1b --- /dev/null +++ b/R/dryRun.R @@ -0,0 +1,153 @@ +#' Perform a deployment "dry run" +#' +#' @description +#' `r lifecycle::badge("experimental")` +#' +#' `dryRun()` runs your app locally, attempting to simulate what will happen +#' when you deploy it on another machine. This isn't a 100% reliable way of +#' discovering problems, but it offers a much faster iteration cycle, so where +#' it does reveal a problem, it will typically make identifying and fixing it +#' much faster. +#' +#' This function is still experimental, so please let us know your experiences +#' and where we could do better: +#' +#' +#' ## Where it helps +#' +#' `dryRun()` was motivated by the two most common problems when deploying +#' your app: +#' +#' * The server doesn't install all the packages your app needs to work. +#' `dryRun()` solves this by using `renv::restore()` to create a project +#' specific library that uses only the packages that are explicitly used +#' by your project. +#' +#' * The server doesn't have environment variables you need. `dryRun()` +#' solves this by removing any environment variables that you've +#' set in `~/.Renviron`, except for those that you declare in `envVars`. +#' Additionally, to help debugging it also reports whenever any env var is +#' used. +#' +#' `dryRun` will also log when you use functions that are usually best avoided +#' in deployed code. This includes: +#' +#' * `rsconnect::deployApp()` because you shouldn't deploy an app from another +#' app. This typically indicates that you've included a file with scratch +#' code. +#' +#' * `install.packages()` and `.libPaths()` because you should rely on the +#' server to install and manage your packages. +#' +#' * `browser()`, `browseURL()`, and `rstudioapi::askForPassword()` because +#' they need an interactive session that your deployment server will lack. +#' +#' ## Current limitations +#' +#' * `dryRun()` currently offers no way to diagnose problems with +#' mismatched R/Python/Quarto/pandoc versions. +#' +#' * `dryRun()` doesn't help much with paths. There are two common problems +#' it can't help with: using an absolute path and using the wrong case. +#' Both of these will work locally but fail on the server. [lint()] +#' uses an alternative technique (static analysis) to detect many of these +#' cases. +#' +#' @inheritParams deployApp +#' @param contentCategory Set this to `"site"` if you'd deploy with +#' [deploySite()]; otherwise leave as is. +#' @param verbose If TRUE, prints progress messages to the console +#' @export +dryRun <- function(appDir = getwd(), + envVars = NULL, + appFiles = NULL, + appFileManifest = NULL, + appPrimaryDoc = NULL, + contentCategory = NULL, + quarto = NA) { + appFiles <- listDeploymentFiles( + appDir, + appFiles = appFiles, + appFileManifest = appFileManifest + ) + + appMetadata <- appMetadata( + appDir = appDir, + appFiles = appFiles, + appPrimaryDoc = appPrimaryDoc, + quarto = quarto, + contentCategory = contentCategory, + ) + + # copy files to bundle dir to stage + bundleDir <- bundleAppDir( + appDir = appDir, + appFiles = appFiles, + appPrimaryDoc = appMetadata$appPrimaryDoc + ) + on.exit(unlink(bundleDir, recursive = TRUE), add = TRUE) + + + renv::restore(bundleDir, prompt = FALSE) + + # Add tracing code ------------------------------------------------- + file.copy( + system.file("lint-trace.R", package = "rsconnect"), + "__rsconnect-dryRunTrace.R" + ) + appendLines( + file.path(bundleDir, ".Rprofile"), + c("", 'source("__rsconnect-dryRunTrace.R")') + ) + + # Run --------------------------------------------------------------- + if (appMode %in% c("rmd-shiny", "quarto-shiny", "shiny", "api")) { + cli::cli_alert_info("Terminate the app to complete the dryRun") + } + + envVarNames <- setdiff(userEnvVars(), envVars) + envVarReset <- c(rep_named(envVarNames, ""), callr::rcmd_safe_env()) + + callr::r( + appRunner(appMetadata$appMode), + args = list(primaryDoc = appMetadata$primaryDoc), + env = envVarReset, + wd = bundleDir + ) +} + +appRunner <- function(appMode) { + switch( + "rmd-static" = , + "rmd-shiny" = function(primaryDoc) rmarkdown::render(primaryDoc), + "quarto-static" = , + "quarto-shiny" = function(primaryDoc) quarto::quarto_render(primaryDoc), + "shiny" = function(primaryDoc) shiny::runApp(), + "api" = function(primaryDoc) plumber::pr_run(plumber::pr("plumber.R")), + cli::cli_abort("Content type {appMode} not currently supported") + ) +} + +appendLines <- function(path, lines) { + lines <- c(readLines(path), lines) + writeLines(lines, path) +} + +userEnvVars <- function() { + if (!file.exists("~/.Renviron")) { + return(charater()) + } + + lines <- readLines("~/.Renviron") + lines <- lines[lines != ""] + lines <- lines[!grepl("^#", lines)] + + pieces <- strsplit(lines, "=", fixed = TRUE) + names <- vapply( + pieces, + function(x) if (length(x) >= 2) x[[1]] else "", + character(1) + ) + + sort(unique(names[names != ""])) +} diff --git a/inst/dryRunTrace.R b/inst/dryRunTrace.R new file mode 100644 index 00000000..d901e1e7 --- /dev/null +++ b/inst/dryRunTrace.R @@ -0,0 +1,35 @@ +rsconnect_log <- function(...) { + cat(paste0("[rsconnect] ", ..., "\n", collapse = ""), file = stderr()) +} +traceFun <- function(ns, fun, code) { + if (ns == "base") { + where <- baseenv() + } else { + where <- asNamespace(ns) + } + + trace(fun, substitute(code), where = where, print = FALSE) + invisible() +} + +traceFun("base", "Sys.getenv", rsconnect_log("Getting env var '", x, "'")) +traceFun("base", ".libPaths", { + if (!missing(new)) { + rsconnect_log("Updating .libPaths") + } +}) +traceFun("utils", "install.packages", stop("Shouldn't install on server")) + + +traceFun("rsconnect", "deployApp", stop("Apps can't deploy apps")) +traceFun("base", "browser", rsconnect_log("Can't use browser(); no interactive session")) + +traceFun("utils", "browseURL", { + rsconnect_log("Attempting to browse to <", url, ">") +}) + +# need to do in onLoad hooks + + + +traceFun("rstudioapi", "askForPassword", stop("Cant't get password")) From 7dfb64f2268bd5ab35f76a9e665d5eb8b78e9c65 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 8 May 2023 08:05:02 -0500 Subject: [PATCH 2/7] Re-document --- NAMESPACE | 1 + R/dryRun.R | 5 +-- man/dryRun.Rd | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 man/dryRun.Rd diff --git a/NAMESPACE b/NAMESPACE index 2491bbb2..bb6d8619 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -26,6 +26,7 @@ export(deploySite) export(deployTFModel) export(deployments) export(discoverServers) +export(dryRun) export(forgetDeployment) export(generateAppName) export(lint) diff --git a/R/dryRun.R b/R/dryRun.R index 1359ca1b..fc64f5d5 100644 --- a/R/dryRun.R +++ b/R/dryRun.R @@ -19,12 +19,12 @@ #' your app: #' #' * The server doesn't install all the packages your app needs to work. -#' `dryRun()` solves this by using `renv::restore()` to create a project +#' `dryRun()` reveals this by using `renv::restore()` to create a project #' specific library that uses only the packages that are explicitly used #' by your project. #' #' * The server doesn't have environment variables you need. `dryRun()` -#' solves this by removing any environment variables that you've +#' reveals this by removing any environment variables that you've #' set in `~/.Renviron`, except for those that you declare in `envVars`. #' Additionally, to help debugging it also reports whenever any env var is #' used. @@ -87,7 +87,6 @@ dryRun <- function(appDir = getwd(), ) on.exit(unlink(bundleDir, recursive = TRUE), add = TRUE) - renv::restore(bundleDir, prompt = FALSE) # Add tracing code ------------------------------------------------- diff --git a/man/dryRun.Rd b/man/dryRun.Rd new file mode 100644 index 00000000..747698d1 --- /dev/null +++ b/man/dryRun.Rd @@ -0,0 +1,115 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dryRun.R +\name{dryRun} +\alias{dryRun} +\title{Perform a deployment "dry run"} +\usage{ +dryRun( + appDir = getwd(), + envVars = NULL, + appFiles = NULL, + appFileManifest = NULL, + appPrimaryDoc = NULL, + contentCategory = NULL, + quarto = NA +) +} +\arguments{ +\item{appDir}{A directory containing an application (e.g. a Shiny app +or plumber API). Defaults to the current directory.} + +\item{envVars}{A character vector giving the names of environment variables +whose values should be synchronised with the server (currently supported by +Connect only). The values of the environment variables are sent over an +encrypted connection and are not stored in the bundle, making this a safe +way to send private data to Connect. + +The names (not values) are stored in the deployment record so that future +deployments will automatically update their values. Other environment +variables on the server will not be affected. This means that removing an +environment variable from \code{envVars} will leave it unchanged on the server. +To remove it, either delete it using the Connect UI, or temporarily unset +it (with \code{Sys.unsetenv()} or similar) then re-deploy. + +Environment variables are set prior to deployment so that your code +can use them and the first deployment can still succeed. Note that means +that if the deployment fails, the values will still be updated.} + +\item{appFiles, appFileManifest}{Use \code{appFiles} to specify a +character vector of files to bundle in the app or \code{appManifestFiles} +to provide a path to a file containing a list of such files. If neither +are supplied, will bundle all files in \code{appDir}, apart from standard +exclusions and files listed in a \code{.rscignore} file. See +\code{\link[=listDeploymentFiles]{listDeploymentFiles()}} for more details.} + +\item{appPrimaryDoc}{If the application contains more than one document, this +parameter indicates the primary one, as a path relative to \code{appDir}. Can be +\code{NULL}, in which case the primary document is inferred from the contents +being deployed.} + +\item{contentCategory}{Set this to \code{"site"} if you'd deploy with +\code{\link[=deploySite]{deploySite()}}; otherwise leave as is.} + +\item{quarto}{Should the deployed content be built by quarto? +(\code{TRUE}, \code{FALSE}, or \code{NA}). The default, \code{NA}, will use quarto if +there are \code{.qmd} files in the bundle, or if there is a +\verb{_quarto.yml} and \code{.Rmd} files. + +(This option is ignored and quarto will always be used if the +\code{metadata} contains \code{quarto_version} and \code{quarto_engines} fields.)} + +\item{verbose}{If TRUE, prints progress messages to the console} +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} + +\code{dryRun()} runs your app locally, attempting to simulate what will happen +when you deploy it on another machine. This isn't a 100\% reliable way of +discovering problems, but it offers a much faster iteration cycle, so where +it does reveal a problem, it will typically make identifying and fixing it +much faster. + +This function is still experimental, so please let us know your experiences +and where we could do better: +\url{https://github.com/rstudio/rsconnect/issues/new} +\subsection{Where it helps}{ + +\code{dryRun()} was motivated by the two most common problems when deploying +your app: +\itemize{ +\item The server doesn't install all the packages your app needs to work. +\code{dryRun()} reveals this by using \code{renv::restore()} to create a project +specific library that uses only the packages that are explicitly used +by your project. +\item The server doesn't have environment variables you need. \code{dryRun()} +reveals this by removing any environment variables that you've +set in \verb{~/.Renviron}, except for those that you declare in \code{envVars}. +Additionally, to help debugging it also reports whenever any env var is +used. +} + +\code{dryRun} will also log when you use functions that are usually best avoided +in deployed code. This includes: +\itemize{ +\item \code{rsconnect::deployApp()} because you shouldn't deploy an app from another +app. This typically indicates that you've included a file with scratch +code. +\item \code{install.packages()} and \code{.libPaths()} because you should rely on the +server to install and manage your packages. +\item \code{browser()}, \code{browseURL()}, and \code{rstudioapi::askForPassword()} because +they need an interactive session that your deployment server will lack. +} +} + +\subsection{Current limitations}{ +\itemize{ +\item \code{dryRun()} currently offers no way to diagnose problems with +mismatched R/Python/Quarto/pandoc versions. +\item \code{dryRun()} doesn't help much with paths. There are two common problems +it can't help with: using an absolute path and using the wrong case. +Both of these will work locally but fail on the server. \code{\link[=lint]{lint()}} +uses an alternative technique (static analysis) to detect many of these +cases. +} +} +} From f32cef522556253de9c241f85158d92526df27ff Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 8 May 2023 08:05:10 -0500 Subject: [PATCH 3/7] Test userEnvVars() --- R/dryRun.R | 8 ++++---- tests/testthat/test-dryRun.R | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tests/testthat/test-dryRun.R diff --git a/R/dryRun.R b/R/dryRun.R index fc64f5d5..de4a2270 100644 --- a/R/dryRun.R +++ b/R/dryRun.R @@ -132,12 +132,12 @@ appendLines <- function(path, lines) { writeLines(lines, path) } -userEnvVars <- function() { - if (!file.exists("~/.Renviron")) { - return(charater()) +userEnvVars <- function(path = "~/.Renviron") { + if (!file.exists(path)) { + return(character()) } - lines <- readLines("~/.Renviron") + lines <- readLines(path) lines <- lines[lines != ""] lines <- lines[!grepl("^#", lines)] diff --git a/tests/testthat/test-dryRun.R b/tests/testthat/test-dryRun.R new file mode 100644 index 00000000..58ce4373 --- /dev/null +++ b/tests/testthat/test-dryRun.R @@ -0,0 +1,40 @@ +test_that("multiplication works", { + expect_equal(2 * 2, 4) +}) + +# userEnvVars ------------------------------------------------------------- + +test_that("ignores non-existent file", { + expect_equal(userEnvVars("DOESNTEXIST"), character()) +}) + +test_that("can parse simple .Renviron", { + path <- withr::local_tempfile(lines = c( + "# a comment", + "", + "A=1", + "B=2" + )) + expect_equal(userEnvVars(path), c("A", "B")) +}) + +test_that("removes duplicates", { + path <- withr::local_tempfile(lines = c( + "# a comment", + "", + "A=1", + "A=2" + )) + expect_equal(userEnvVars(path), "A") +}) + +test_that("not troubled by wrong number of equals", { + path <- withr::local_tempfile(lines = c( + "# a comment", + "", + "A", + "B=1", + "C=2=3" + )) + expect_equal(userEnvVars(path), c("B", "C")) +}) From 68777932c7932d122ecfc38340e6a70a100ebbb8 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 8 May 2023 09:15:46 -0500 Subject: [PATCH 4/7] Get the whole thing basically working --- DESCRIPTION | 1 + R/dryRun.R | 53 ++++++++++++++++++++++++++---------- inst/dryRunTrace.R | 24 ++++++++++++---- tests/testthat/test-dryRun.R | 15 +++++++++- 4 files changed, 72 insertions(+), 21 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 4cdfc0e9..821f29a5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -34,6 +34,7 @@ Imports: tools, yaml (>= 2.1.5) Suggests: + callr, knitr, Biobase, BiocManager, diff --git a/R/dryRun.R b/R/dryRun.R index de4a2270..52b99d4d 100644 --- a/R/dryRun.R +++ b/R/dryRun.R @@ -56,7 +56,6 @@ #' @inheritParams deployApp #' @param contentCategory Set this to `"site"` if you'd deploy with #' [deploySite()]; otherwise leave as is. -#' @param verbose If TRUE, prints progress messages to the console #' @export dryRun <- function(appDir = getwd(), envVars = NULL, @@ -65,6 +64,8 @@ dryRun <- function(appDir = getwd(), appPrimaryDoc = NULL, contentCategory = NULL, quarto = NA) { + check_installed("callr") + appFiles <- listDeploymentFiles( appDir, appFiles = appFiles, @@ -80,19 +81,30 @@ dryRun <- function(appDir = getwd(), ) # copy files to bundle dir to stage + cli::cli_alert_info("Bundling app") bundleDir <- bundleAppDir( appDir = appDir, appFiles = appFiles, appPrimaryDoc = appMetadata$appPrimaryDoc ) - on.exit(unlink(bundleDir, recursive = TRUE), add = TRUE) + defer(unlink(bundleDir, recursive = TRUE)) - renv::restore(bundleDir, prompt = FALSE) + cli::cli_alert_info("Creating project specific library") + callr::r( + function() { + options(renv.verbose = FALSE) + renv::init() + renv::restore() + }, + wd = bundleDir + ) # Add tracing code ------------------------------------------------- + cli::cli_alert_info("Adding shims") + file.copy( - system.file("lint-trace.R", package = "rsconnect"), - "__rsconnect-dryRunTrace.R" + system.file("dryRunTrace.R", package = "rsconnect"), + file.path(bundleDir, "__rsconnect-dryRunTrace.R") ) appendLines( file.path(bundleDir, ".Rprofile"), @@ -100,29 +112,40 @@ dryRun <- function(appDir = getwd(), ) # Run --------------------------------------------------------------- - if (appMode %in% c("rmd-shiny", "quarto-shiny", "shiny", "api")) { - cli::cli_alert_info("Terminate the app to complete the dryRun") + cli::cli_alert_info("Starting {appMetadata$appMode}") + if (appMetadata$appMode %in% c("rmd-shiny", "quarto-shiny", "shiny", "api")) { + cli::cli_alert_warning("Terminate the app to complete the dry run") } - envVarNames <- setdiff(userEnvVars(), envVars) + envVarNames <- setdiff(userEnvVars(), c(envVars, "PATH")) envVarReset <- c(rep_named(envVarNames, ""), callr::rcmd_safe_env()) callr::r( appRunner(appMetadata$appMode), - args = list(primaryDoc = appMetadata$primaryDoc), + args = list(primaryDoc = appMetadata$appPrimaryDoc), env = envVarReset, - wd = bundleDir + wd = bundleDir, + show = TRUE ) + invisible() } appRunner <- function(appMode) { - switch( + switch(appMode, "rmd-static" = , - "rmd-shiny" = function(primaryDoc) rmarkdown::render(primaryDoc), + "rmd-shiny" = function(primaryDoc) { + rmarkdown::render(primaryDoc, quiet = TRUE) + }, "quarto-static" = , - "quarto-shiny" = function(primaryDoc) quarto::quarto_render(primaryDoc), - "shiny" = function(primaryDoc) shiny::runApp(), - "api" = function(primaryDoc) plumber::pr_run(plumber::pr("plumber.R")), + "quarto-shiny" = function(primaryDoc) { + quarto::quarto_render(primaryDoc, quiet = TRUE) + }, + "shiny" = function(primaryDoc) { + shiny::runApp() + }, + "api" = function(primaryDoc) { + plumber::pr_run(plumber::pr("plumber.R")) + }, cli::cli_abort("Content type {appMode} not currently supported") ) } diff --git a/inst/dryRunTrace.R b/inst/dryRunTrace.R index d901e1e7..33108e4c 100644 --- a/inst/dryRunTrace.R +++ b/inst/dryRunTrace.R @@ -1,5 +1,5 @@ rsconnect_log <- function(...) { - cat(paste0("[rsconnect] ", ..., "\n", collapse = ""), file = stderr()) + cat(paste0("[dryRun] ", ..., "\n", collapse = ""), file = stderr()) } traceFun <- function(ns, fun, code) { if (ns == "base") { @@ -8,11 +8,25 @@ traceFun <- function(ns, fun, code) { where <- asNamespace(ns) } - trace(fun, substitute(code), where = where, print = FALSE) + suppressMessages( + trace(fun, substitute(code), where = where, print = FALSE) + ) invisible() } -traceFun("base", "Sys.getenv", rsconnect_log("Getting env var '", x, "'")) +env_seen <- new.env(parent = emptyenv()) +env_seen$PATH <- TRUE +env_seen$TESTTHAT <- TRUE +env_seen$RSTUDIO <- TRUE + +traceFun("base", "Sys.getenv", { + if (!grepl("^(_R|R|RSTUDIO|CALLR|CLI|RENV|RMARKDOWN)_", x)) { + if (!exists(x, envir = env_seen)) { + rsconnect_log("Getting env var '", x, "'") + env_seen[[x]] <- TRUE + } + } +}) traceFun("base", ".libPaths", { if (!missing(new)) { rsconnect_log("Updating .libPaths") @@ -21,7 +35,7 @@ traceFun("base", ".libPaths", { traceFun("utils", "install.packages", stop("Shouldn't install on server")) -traceFun("rsconnect", "deployApp", stop("Apps can't deploy apps")) +# traceFun("rsconnect", "deployApp", stop("Apps can't deploy apps")) traceFun("base", "browser", rsconnect_log("Can't use browser(); no interactive session")) traceFun("utils", "browseURL", { @@ -32,4 +46,4 @@ traceFun("utils", "browseURL", { -traceFun("rstudioapi", "askForPassword", stop("Cant't get password")) +# traceFun("rstudioapi", "askForPassword", stop("Cant't get password")) diff --git a/tests/testthat/test-dryRun.R b/tests/testthat/test-dryRun.R index 58ce4373..87382536 100644 --- a/tests/testthat/test-dryRun.R +++ b/tests/testthat/test-dryRun.R @@ -1,5 +1,17 @@ test_that("multiplication works", { - expect_equal(2 * 2, 4) + app <- local_temp_app(list( + "index.Rmd" = c( + "---", + "title: rmd", + "---", + "", + "```{r}", + "Sys.getenv('MYSQL_USER')", + "```" + ) + )) + + dryRun(app) }) # userEnvVars ------------------------------------------------------------- @@ -38,3 +50,4 @@ test_that("not troubled by wrong number of equals", { )) expect_equal(userEnvVars(path), c("B", "C")) }) + From e4bce567d17602525c0637f36a1c40bb7e9c21bb Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 8 May 2023 09:31:12 -0500 Subject: [PATCH 5/7] Improve user messaging --- inst/dryRunTrace.R | 46 ++++++++++++++++++++++++++++++------ tests/testthat/test-dryRun.R | 11 ++++++++- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/inst/dryRunTrace.R b/inst/dryRunTrace.R index 33108e4c..1d2ec788 100644 --- a/inst/dryRunTrace.R +++ b/inst/dryRunTrace.R @@ -2,6 +2,9 @@ rsconnect_log <- function(...) { cat(paste0("[dryRun] ", ..., "\n", collapse = ""), file = stderr()) } traceFun <- function(ns, fun, code) { + traceFun_(ns, fun, substitute(code)) +} +traceFun_ <- function(ns, fun, code) { if (ns == "base") { where <- baseenv() } else { @@ -9,11 +12,13 @@ traceFun <- function(ns, fun, code) { } suppressMessages( - trace(fun, substitute(code), where = where, print = FALSE) + trace(fun, code, where = where, print = FALSE) ) invisible() } +# Messages ---------------------------------------------------------------- + env_seen <- new.env(parent = emptyenv()) env_seen$PATH <- TRUE env_seen$TESTTHAT <- TRUE @@ -32,18 +37,45 @@ traceFun("base", ".libPaths", { rsconnect_log("Updating .libPaths") } }) -traceFun("utils", "install.packages", stop("Shouldn't install on server")) - -# traceFun("rsconnect", "deployApp", stop("Apps can't deploy apps")) -traceFun("base", "browser", rsconnect_log("Can't use browser(); no interactive session")) +traceFun("base", "browser", { + rsconnect_log("Can't use browser(); no interactive session") +}) traceFun("utils", "browseURL", { rsconnect_log("Attempting to browse to <", url, ">") }) -# need to do in onLoad hooks +# Errors ------------------------------------------------------------------ + +errorOnServer <- function(ns, fun, reason) { + code <- substitute( + stop(paste0("[dryRun] `", ns, "::", fun, "()`: ", reason), call. = FALSE) + ) + traceFun_(ns, fun, code) +} +errorOnServer( + "utils", + "install.packages", + "install packages locally, not on the server" +) -# traceFun("rstudioapi", "askForPassword", stop("Cant't get password")) +setHook( + packageEvent("rstudioapi", "onLoad"), + errorOnServer( + "rsconnect", + "deployApp", + "apps shouldn't deploy apps on the server" + ) +) + +setHook( + packageEvent("rstudioapi", "onLoad"), + errorOnServer( + "rstudioapi", + "askForPassword", + "can't interactively ask for password on server" + ) +) diff --git a/tests/testthat/test-dryRun.R b/tests/testthat/test-dryRun.R index 87382536..624e850a 100644 --- a/tests/testthat/test-dryRun.R +++ b/tests/testthat/test-dryRun.R @@ -11,9 +11,18 @@ test_that("multiplication works", { ) )) - dryRun(app) + # dryRun(app) }) +test_that("multiplication works", { + app <- local_temp_app(list( + "app.R" = "rsconnect::deployApp()" + )) + + # dryRun(app) +}) + + # userEnvVars ------------------------------------------------------------- test_that("ignores non-existent file", { From dfa43454d3810cac0a25dfc793970e7d486c6ea4 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 8 May 2023 09:31:17 -0500 Subject: [PATCH 6/7] Add news bullet --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index 38232d19..dbc4b3a2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # rsconnect (development version) +* New experiment `dryRun()` performs a deployment "dry run", simulating running + your app or document as if it's on the server. We can only do a partial + simulation but this allows us to catch a few common problems in a way that + allows you to rapidly iterate. This feature is under active development + so your feedback is appreciated! (#725, #208, #253, #256). + * `deploySite()` now supports quarto websites (#813). * `deployApp()` and `writeManifest()` now respect renv lock files, if present. From 26ec2c7ca8379cef9d139a85a2cdb62ef6db9ead Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 8 May 2023 09:36:50 -0500 Subject: [PATCH 7/7] Re-document; fix lint; add to reference index --- _pkgdown.yml | 1 + man/dryRun.Rd | 2 -- tests/testthat/test-dryRun.R | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/_pkgdown.yml b/_pkgdown.yml index 58b034fd..514decbb 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -29,6 +29,7 @@ reference: contents : - writeManifest - matches("[Dd]eploy") + - dryRun - listDeploymentFiles - title: Servers diff --git a/man/dryRun.Rd b/man/dryRun.Rd index 747698d1..21a169c9 100644 --- a/man/dryRun.Rd +++ b/man/dryRun.Rd @@ -57,8 +57,6 @@ there are \code{.qmd} files in the bundle, or if there is a (This option is ignored and quarto will always be used if the \code{metadata} contains \code{quarto_version} and \code{quarto_engines} fields.)} - -\item{verbose}{If TRUE, prints progress messages to the console} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} diff --git a/tests/testthat/test-dryRun.R b/tests/testthat/test-dryRun.R index 624e850a..9c8d6655 100644 --- a/tests/testthat/test-dryRun.R +++ b/tests/testthat/test-dryRun.R @@ -59,4 +59,3 @@ test_that("not troubled by wrong number of equals", { )) expect_equal(userEnvVars(path), c("B", "C")) }) -