Files
shiny/tests/testthat/test-reactlog.R
Joe Cheng 79ee25620f Limit deep stack growth (#4156)
* Limit deep stack growth

* Improvements to deep stack trace culling

- Keep around the first deep stack trace; it may have useful
  information. (We may want to change this in the future to
  keep the first two stack traces, or even make it an option)
- Print out an indicator that we've elided stack traces, and
  how many

* Add comments

* Add NEWS item

* Add test for unlimited deep stacks

* Code review feedback

* Code review feedback

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>

* Use head() over indexing

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>

* Improve unit test robustness

* Remove vector indices from snapshot

* Make stack trace stripping work across deep stacks

* Pass tests

* Try passing tests again

* Rename keep_head to retain_first_n

* Remove misleading variable assignment

* Add more comments, refine dropTrivialTestFrames

* Don't call stripStackTraces if we're not stripping

* Use deep stack deduplication instead of elision

This hopefully will avoid any potential ..stacktraceon../off..
scoring issues, and will be more useful for users. The downside
is that it's still possible to have uselessly large deep stack
traces, but at least that will only happen now if you have
manually written gigantic async/promise chains by hand or maybe
did some clever metaprogramming. The coro case should be fine.

* Add coro-based unit test

* Use rlang::hash, it's much faster

* typo

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>

* Remove unnecessary logic

* Simplify/robustify reactlog version checking test

* Warn only once on call stack digest cache miss

* Super conservatively wrap appendCallStackWithDupe in try/catch

* Use more specific attribute name

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>

* Remove excessively cautious try/catch

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2024-12-06 10:17:05 -08:00

138 lines
4.1 KiB
R

keyValList <- function(key, value) {
ret <- list()
ret[[key]] <- value
ret
}
withOption <- function(key, value, oldVal = NULL, expr) {
oldVal <- getOption(key, oldVal)
do.call("options", keyValList(key, value))
on.exit({
do.call("options", keyValList(key, oldVal))
})
force(expr)
}
withLogging <- function(expr) {
rLog$reset()
# reset ctx counter
reactiveEnvr <- .getReactiveEnvironment()
reactiveEnvr$.nextId <- 0L
withOption("shiny.reactlog", TRUE, FALSE, {
withOption("shiny.reactlog.console", TRUE, FALSE, {
withOption("shiny.suppressMissingContextError", TRUE, FALSE, {
force(expr)
})
})
})
}
expect_logs <- function(expr, ...) {
expected_messages <- unlist(list(...))
captured_messages <- capture_messages(expr)
captured_messages <- sub("\n$", "", captured_messages)
if (length(captured_messages) != length(expected_messages)) {
cat("\nCaptured: \n"); print(captured_messages)
cat("Expected: \n"); print(expected_messages)
}
expect_equal(
captured_messages,
expected_messages
)
}
test_that("rLog resets when options are FALSE", {
withOption("shiny.reactlog", FALSE, FALSE, {
withOption("shiny.reactlog.console", FALSE, FALSE, {
rLog$reset()
# check for dummy and no reactid information
expect_true(!is.null(rLog$noReactId))
expect_true(!is.null(rLog$dummyReactId))
expect_equal(rLog$msg$getReact(rLog$noReactId, force = TRUE)$reactId, rLog$noReactId)
expect_equal(rLog$msg$getReact(rLog$dummyReactId, force = TRUE)$reactId, rLog$dummyReactId)
expect_equal(length(rLog$msg$reactCache), 2)
})
})
})
test_that("message logger appears", {
withLogging({
expect_logs(
{
val <- reactiveVal(1, label = "val")
},
"- define: r1:'val' - reactiveVal ' num 1'"
)
expect_silent(
{
values <- reactiveValues(a = 2, b = 3)
local({
values_obj <- .subset2(values, 'impl')
values_obj$.label <- "values"
})
}
)
expect_logs(
{
react <- reactive(val() + values$a)
},
"- define: r3:'reactive({\\n val() + values$a\\n})' - observable ' NULL'"
)
expect_logs(
{
react()
},
"- createContext: ctxDummy - isolate",
"- dependsOn: rDummyReactId:'DummyReactId' on r3:'reactive({\\n val() + values$a\\n})' in ctxDummy",
"- createContext: ctx1 - observable",
"- enter: r3:'reactive({\\n val() + values$a\\n})' in ctx1 - observable",
"= - dependsOn: r3:'reactive({\\n val() + values$a\\n})' on r1:'val' in ctx1",
"= - define: r2$a:'values$a' - reactiveValuesKey ' num 2'",
"= - dependsOn: r3:'reactive({\\n val() + values$a\\n})' on r2$a:'values$a' in ctx1",
"- exit: r3:'reactive({\\n val() + values$a\\n})' in ctx1 - observable"
)
expect_logs(
{
val(4)
},
"- valueChange: r1:'val' ' num 4'",
"- invalidateStart: r1:'val'",
"= - invalidateStart: r3:'reactive({\\n val() + values$a\\n})' in ctx1 - observable",
"= = - isolateInvalidateStart: rDummyReactId:'DummyReactId' in ctxDummy",
"= = = - dependsOnRemove: rDummyReactId:'DummyReactId' on r3:'reactive({\\n val() + values$a\\n})' in ctxDummy",
"= = - isolateInvalidateEnd: rDummyReactId:'DummyReactId' in ctxDummy",
"= = - dependsOnRemove: r3:'reactive({\\n val() + values$a\\n})' on r1:'val' in ctx1",
"= = - dependsOnRemove: r3:'reactive({\\n val() + values$a\\n})' on r2$a:'values$a' in ctx1",
"= - invalidateEnd: r3:'reactive({\\n val() + values$a\\n})' in ctx1 - observable",
"- invalidateEnd: r1:'val'"
)
expect_logs(
{values$a <- 5},
"- valueChange: r2$a:'values$a' ' num 5'",
"- invalidateStart: r2$a:'values$a'",
"- invalidateEnd: r2$a:'values$a'"
)
})
})
test_that("reactlog_version is as expected", {
expect_match(
packageDescription("shiny")$Suggests,
# The space between reactlog and the version number can include \n
sprintf("\\breactlog\\s+\\Q(>= %s)\\E", reactlog_min_version)
)
})