diff --git a/DESCRIPTION b/DESCRIPTION index 4785036..77e93fd 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,6 +22,7 @@ BugReports: https://github.com/rstudio/reactlog/issues Depends: R (>= 3.0.2) Imports: + cli, jsonlite (>= 0.9.16), rlang, rmarkdown @@ -36,6 +37,7 @@ Suggests: withr VignetteBuilder: knitr +Config/testthat/edition: 3 Config/roxygen2/version: 8.1.1 Encoding: UTF-8 Language: en-US diff --git a/NEWS.md b/NEWS.md index 05f4605..b170546 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # reactlog (development) +- Error and warning messages now use cli and name the function you called. Missing or too-old shiny now gives a clear message from `rlang::check_installed()`, including when `reactlog_show()` or `reactlog_widget()` is called with no `log`. cli is now an imported package (#164) + - Added `reactlog_widget()`, which shows the reactlog as an htmlwidget so it can be embedded in R Markdown or Quarto documents. Like `reactlog_show()`, `log` defaults to `shiny::reactlog()`. To show it in a Shiny app, use `reactlog_widget_module_ui()` and `reactlog_widget_module_server()`, or `reactlog_widget_output()` and `render_reactlog_widget()`. Requires the htmlwidgets package (#160) - `reactlog_show()` now uses `shiny::reactlog()` as its default `log`, so `reactlog_show()` works with no arguments after running an app (#91) diff --git a/R/renderReactLog.R b/R/renderReactLog.R index f19c043..53c2076 100644 --- a/R/renderReactLog.R +++ b/R/renderReactLog.R @@ -11,10 +11,20 @@ inst_reactlog_assets <- function() { #' @param session_token token to be used to subset which session is displayed. #' Defaults to all sessions. reactlog_render <- function(log, session_token = NULL, time = TRUE) { - html <- reactlog_page_html(log, session_token = session_token, time = time) + reactlog_render_file(log, session_token = session_token, time = time) +} + +# `call` names the user-facing function in error messages +reactlog_render_file <- function( + log, + session_token = NULL, + time = TRUE, + call = rlang::caller_env() +) { + html <- reactlog_page_html(log, session_token = session_token, time = time, call = call) # check if shiny is even available - if (requireNamespace("shiny", quietly = TRUE)) { + if (rlang::is_installed("shiny")) { # add resource path to pkg files for shiny # (avoids warning of tmp folder moving around) shiny::addResourcePath( @@ -42,8 +52,13 @@ fixed_sub <- function(...) { } # The reactlog page as one string. Assets are still linked from `reactlogAsset/`. -reactlog_page_html <- function(log, session_token = NULL, time = TRUE) { - log <- reactlog_upgrade(log) +reactlog_page_html <- function( + log, + session_token = NULL, + time = TRUE, + call = rlang::caller_env() +) { + log <- reactlog_upgrade(log, call = call) template_file <- inst_reactlog_file("reactlog.html") # does not need utf8 support diff --git a/R/setReactLog.R b/R/setReactLog.R index de6c27c..37ca2b9 100644 --- a/R/setReactLog.R +++ b/R/setReactLog.R @@ -15,12 +15,14 @@ reactlog_enable <- function() { #' @export #' @rdname setReactLog reactlog_disable <- function(warn = TRUE) { - if (!isTRUE(warn) && !identical(warn, FALSE)) { - stop("'warn' must be either 'TRUE' or 'FALSE'") + if (!rlang::is_bool(warn)) { + cli::cli_abort( + "{.arg warn} must be {.code TRUE} or {.code FALSE}, not {.obj_type_friendly {warn}}." + ) } options(shiny.reactlog = FALSE) if (isTRUE(warn)) { - warning("Please restart R to free up 'reactlog' memory", call. = FALSE) + cli::cli_warn("Please restart R to free up {.pkg reactlog} memory.") } } diff --git a/R/shinyModule.R b/R/shinyModule.R index 440eac7..78c89fc 100644 --- a/R/shinyModule.R +++ b/R/shinyModule.R @@ -144,10 +144,14 @@ test_shiny_version <- function() { FALSE }) } -assert_shiny_version <- function() { - if (!test_shiny_version()) { - stop("`shiny` v", shiny_version_required(), " or greater must be installed") - } +assert_shiny_version <- function(call = rlang::caller_env()) { + # Fail instead of asking to install; this runs at the start of a live session + rlang::local_options(rlib_restart_package_not_found = FALSE) + rlang::check_installed( + "shiny", + version = as.character(shiny_version_required()), + call = call + ) } diff --git a/R/showReactLog.R b/R/showReactLog.R index 80650cd..9c68c2d 100644 --- a/R/showReactLog.R +++ b/R/showReactLog.R @@ -61,7 +61,8 @@ #' } reactlog_show <- function(log = shiny::reactlog(), time = TRUE, ...) { - file <- reactlog_render(log, time = as.logical(time), ...) + if (missing(log)) rlang::check_installed("shiny") + file <- reactlog_render_file(log, time = as.logical(time), ...) utils::browseURL(file) } diff --git a/R/version.R b/R/version.R index c04eca1..49df0a5 100644 --- a/R/version.R +++ b/R/version.R @@ -1,17 +1,19 @@ -reactlog_upgrade <- function(log) { +reactlog_upgrade <- function(log, call = rlang::caller_env()) { version <- attr(log, "version") if (is.null(version)) { - stop("'log' is missing a 'version' attribute. This is required.") + cli::cli_abort(c( + "{.arg log} is missing a {.field version} attribute.", + "i" = "Use {.fn shiny::reactlog} to get a log." + ), call = call) } version <- as.character(version) if (identical(version, "1")) { return(log) } - stop( - "Versions available: 1\n", - "Latest reactlog version: 1\n", - "Version provided: ", version - ) + cli::cli_abort(c( + "x" = "{.arg log} has version {.val {version}}.", + "i" = "{.pkg reactlog} supports version {.val 1}." + ), call = call) } diff --git a/R/widget.R b/R/widget.R index d722cde..54a6848 100644 --- a/R/widget.R +++ b/R/widget.R @@ -33,6 +33,7 @@ reactlog_widget <- function( elementId = NULL # nolint: object_name_linter. ) { rlang::check_installed("htmlwidgets") + if (missing(log)) rlang::check_installed("shiny") html <- reactlog_page_html(log, time = as.logical(time), ...) diff --git a/tests/testthat/_snaps/errors.md b/tests/testthat/_snaps/errors.md new file mode 100644 index 0000000..c4c7cb4 --- /dev/null +++ b/tests/testthat/_snaps/errors.md @@ -0,0 +1,43 @@ +# reactlog_disable() checks `warn` and warns about memory + + Code + reactlog_disable(warn = "yes") + Condition + Error in `reactlog_disable()`: + ! `warn` must be `TRUE` or `FALSE`, not a string. + +--- + + Code + reactlog_disable() + Condition + Warning: + Please restart R to free up reactlog memory. + +# bad log versions are explained by the function the user called + + Code + reactlog_render(list()) + Condition + Error in `reactlog_render()`: + ! `log` is missing a version attribute. + i Use `shiny::reactlog()` to get a log. + +--- + + Code + reactlog_render(structure(list(), version = "2")) + Condition + Error in `reactlog_render()`: + x `log` has version "2". + i reactlog supports version "1". + +--- + + Code + reactlog_show(list()) + Condition + Error in `reactlog_show()`: + ! `log` is missing a version attribute. + i Use `shiny::reactlog()` to get a log. + diff --git a/tests/testthat/test-errors.R b/tests/testthat/test-errors.R new file mode 100644 index 0000000..3ca93b9 --- /dev/null +++ b/tests/testthat/test-errors.R @@ -0,0 +1,34 @@ +test_that("reactlog_disable() checks `warn` and warns about memory", { + withr::local_options(shiny.reactlog = TRUE) + expect_snapshot(error = TRUE, reactlog_disable(warn = "yes")) + expect_snapshot(reactlog_disable()) + expect_silent(reactlog_disable(warn = FALSE)) +}) + +test_that("bad log versions are explained by the function the user called", { + expect_snapshot(error = TRUE, reactlog_render(list())) + expect_snapshot(error = TRUE, reactlog_render(structure(list(), version = "2"))) + expect_snapshot(error = TRUE, reactlog_show(list())) +}) + +test_that("assert_shiny_version() uses rlang::check_installed() without asking to install", { + local_mocked_bindings(shiny_version_required = function(...) package_version("999.0.0")) + withr::local_options(rlang_interactive = TRUE) + expect_error(assert_shiny_version(), class = "rlib_error_package_not_found") +}) + +test_that("reactlog_show() and reactlog_widget() check for shiny when `log` is missing", { + local_mocked_bindings( + check_installed = function(pkg, ...) stop("checked ", pkg), + .package = "rlang" + ) + # if the check is skipped, don't open a browser + withr::local_options(browser = function(...) NULL) + expect_error(reactlog_show(), "checked shiny") + skip_if_not_installed("htmlwidgets") + local_mocked_bindings( + check_installed = function(pkg, ...) if (pkg == "shiny") stop("checked ", pkg), + .package = "rlang" + ) + expect_error(reactlog_widget(), "checked shiny") +})