Files
shiny/tests/testthat/test-reactivity.r
Joe Cheng 1f823d2a1b Fix #931: Observer memory leak (#1256)
* Fix #931: Observer memory leak

Observers were being prevented from being garbage collected by
their own onReactiveDomainEnded() event handlers. This commit
fixes that by making sure that those event handlers are only
registered when autoDestroy=TRUE, and that they are unregistered
both on destruction and when autoDestroy is changed.

* Remove extraneous self$ prefixes

* Add comment explaining autoDestroyHandle
2016-07-26 11:56:47 -07:00

979 lines
24 KiB
R

context("reactivity")
# Test for correct behavior of ReactiveValues
test_that("ReactiveValues", {
# Creation and indexing into ReactiveValues -------------------------------
values <- reactiveValues()
# $ indexing
values$a <- 3
expect_equal(isolate(values$a), 3)
# [[ indexing
values[['a']] <- 4
expect_equal(isolate(values[['a']]), 4)
# Create with initialized values
values <- reactiveValues(a=1, b=2)
expect_equal(isolate(values$a), 1)
expect_equal(isolate(values[['b']]), 2)
# NULL values -------------------------------------------------------------
# Initializing with NULL value
values <- reactiveValues(a=NULL, b=2)
# a should exist and be NULL
expect_equal(isolate(names(values)), c("a", "b"))
expect_true(is.null(isolate(values$a)))
# Assigning NULL should keep object (not delete it), and set value to NULL
values$b <- NULL
expect_equal(isolate(names(values)), c("a", "b"))
expect_true(is.null(isolate(values$b)))
# Errors -----------------------------------------------------------------
# Error: indexing with non-string
expect_error(isolate(values[[1]]))
expect_error(isolate(values[[NULL]]))
expect_error(isolate(values[[list('a')]]))
# Error: [ indexing shouldn't work
expect_error(isolate(values['a']))
expect_error(isolate(values['a'] <- 1))
# Error: unnamed arguments
expect_error(reactiveValues(1))
expect_error(reactiveValues(1, b=2))
# Error: assignment to readonly values
values <- .createReactiveValues(ReactiveValues$new(), readonly = TRUE)
expect_error(values$a <- 1)
})
# Test for overreactivity. funcB has an indirect dependency on valueA (via
# funcA) and also a direct dependency on valueA. When valueA changes, funcB
# should only execute once.
test_that("Functions are not over-reactive", {
values <- reactiveValues(A=10)
funcA <- reactive({
values$A
})
funcB <- reactive({
funcA()
values$A
})
obsC <- observe({
funcB()
})
flushReact()
expect_equal(execCount(funcB), 1)
expect_equal(execCount(obsC), 1)
values$A <- 11
flushReact()
expect_equal(execCount(funcB), 2)
expect_equal(execCount(obsC), 2)
})
## "foo => bar" is defined as "foo is a dependency of bar"
##
## vA => fB
## (fB, vA) => obsE
## (fB, vA) => obsF
##
## obsE and obsF should each execute once when vA changes.
test_that("overreactivity2", {
# ----------------------------------------------
# Test 1
# B depends on A, and observer depends on A and B. The observer uses A and
# B, in that order.
# This is to store the value from observe()
observed_value1 <- NA
observed_value2 <- NA
values <- reactiveValues(A=1)
funcB <- reactive({
values$A + 5
})
obsC <- observe({
observed_value1 <<- funcB() * values$A
})
obsD <- observe({
observed_value2 <<- funcB() * values$A
})
flushReact()
expect_equal(observed_value1, 6) # Should be 1 * (1 + 5) = 6
expect_equal(observed_value2, 6) # Should be 1 * (1 + 5) = 6
expect_equal(execCount(funcB), 1)
expect_equal(execCount(obsC), 1)
expect_equal(execCount(obsD), 1)
values$A <- 2
flushReact()
expect_equal(observed_value1, 14) # Should be 2 * (2 + 5) = 14
expect_equal(observed_value2, 14) # Should be 2 * (2 + 5) = 14
expect_equal(execCount(funcB), 2)
expect_equal(execCount(obsC), 2)
expect_equal(execCount(obsD), 2)
})
## Test for isolation. funcB depends on funcA depends on valueA. When funcA
## is invalidated, if its new result is not different than its old result,
## then it doesn't invalidate its dependents. This is done by adding an observer
## (valueB) between obsA and funcC.
##
## valueA => obsB => valueC => funcD => obsE
test_that("isolation", {
values <- reactiveValues(A=10, C=NULL)
obsB <- observe({
values$C <- values$A > 0
})
funcD <- reactive({
values$C
})
obsE <- observe({
funcD()
})
flushReact()
countD <- execCount(funcD)
values$A <- 11
flushReact()
expect_equal(execCount(funcD), countD)
})
## Test for laziness. With lazy evaluation, the observers should "pull" values
## from their dependent functions. In contrast, eager evaluation would have
## reactive values and functions "push" their changes down to their descendents.
test_that("laziness", {
values <- reactiveValues(A=10)
funcA <- reactive({
values$A > 0
})
funcB <- reactive({
funcA()
})
obsC <- observe({
if (values$A > 10)
return()
funcB()
})
flushReact()
expect_equal(execCount(funcA), 1)
expect_equal(execCount(funcB), 1)
expect_equal(execCount(obsC), 1)
values$A <- 11
flushReact()
expect_equal(execCount(funcA), 1)
expect_equal(execCount(funcB), 1)
expect_equal(execCount(obsC), 2)
})
## Suppose B depends on A and C depends on A and B. Then when A is changed,
## the evaluation order should be A, B, C. Also, each time A is changed, B and
## C should be run once, if we want to be maximally efficient.
test_that("order of evaluation", {
# ----------------------------------------------
# Test 1
# B depends on A, and observer depends on A and B. The observer uses A and
# B, in that order.
# This is to store the value from observe()
observed_value <- NA
values <- reactiveValues(A=1)
funcB <- reactive({
values$A + 5
})
obsC <- observe({
observed_value <<- values$A * funcB()
})
flushReact()
expect_equal(observed_value, 6) # Should be 1 * (1 + 5) = 6
expect_equal(execCount(funcB), 1)
expect_equal(execCount(obsC), 1)
values$A <- 2
flushReact()
expect_equal(observed_value, 14) # Should be 2 * (2 + 5) = 14
expect_equal(execCount(funcB), 2)
expect_equal(execCount(obsC), 2)
# ----------------------------------------------
# Test 2:
# Same as Test 1, except the observer uses A and B in reversed order.
# Resulting values should be the same.
observed_value <- NA
values <- reactiveValues(A=1)
funcB <- reactive({
values$A + 5
})
obsC <- observe({
observed_value <<- funcB() * values$A
})
flushReact()
# Should be 1 * (1 + 5) = 6
expect_equal(observed_value, 6)
expect_equal(execCount(funcB), 1)
expect_equal(execCount(obsC), 1)
values$A <- 2
flushReact()
# Should be 2 * (2 + 5) = 14
expect_equal(observed_value, 14)
expect_equal(execCount(funcB), 2)
expect_equal(execCount(obsC), 2)
})
## Expressions in isolate() should not invalidate the parent context.
test_that("isolate() blocks invalidations from propagating", {
obsC_value <- NA
obsD_value <- NA
values <- reactiveValues(A=1, B=10)
funcB <- reactive({
values$B + 100
})
# References to valueB and funcB are isolated
obsC <- observe({
obsC_value <<-
values$A + isolate(values$B) + isolate(funcB())
})
# In contrast with obsC, this has a non-isolated reference to funcB
obsD <- observe({
obsD_value <<-
values$A + isolate(values$B) + funcB()
})
flushReact()
expect_equal(obsC_value, 121)
expect_equal(execCount(obsC), 1)
expect_equal(obsD_value, 121)
expect_equal(execCount(obsD), 1)
# Changing A should invalidate obsC and obsD
values$A <- 2
flushReact()
expect_equal(obsC_value, 122)
expect_equal(execCount(obsC), 2)
expect_equal(obsD_value, 122)
expect_equal(execCount(obsD), 2)
# Changing B shouldn't invalidate obsC becuause references to B are in isolate()
# But it should invalidate obsD.
values$B <- 20
flushReact()
expect_equal(obsC_value, 122)
expect_equal(execCount(obsC), 2)
expect_equal(obsD_value, 142)
expect_equal(execCount(obsD), 3)
# Changing A should invalidate obsC and obsD, and they should see updated
# values for valueA, valueB, and funcB
values$A <- 3
flushReact()
expect_equal(obsC_value, 143)
expect_equal(execCount(obsC), 3)
expect_equal(obsD_value, 143)
expect_equal(execCount(obsD), 4)
})
test_that("isolate() evaluates expressions in calling environment", {
outside <- 1
inside <- 1
loc <- 1
outside <- isolate(2) # Assignment outside isolate
isolate(inside <- 2) # Assignment inside isolate
# Should affect vars in the calling environment
expect_equal(outside, 2)
expect_equal(inside, 2)
isolate(local(loc <<- 2)) # <<- inside isolate(local)
isolate(local(loc <- 3)) # <- inside isolate(local) - should have no effect
expect_equal(loc, 2)
})
test_that("Circular refs/reentrancy in reactive functions work", {
values <- reactiveValues(A=3)
funcB <- reactive({
# Each time fB executes, it reads and then writes valueA,
# effectively invalidating itself--until valueA becomes 0.
if (values$A == 0)
return()
values$A <- values$A - 1
return(values$A)
})
obsC <- observe({
funcB()
})
flushReact()
expect_equal(execCount(obsC), 4)
values$A <- 3
flushReact()
expect_equal(execCount(obsC), 8)
})
test_that("Simple recursion", {
values <- reactiveValues(A=5)
funcB <- reactive({
if (values$A == 0)
return(0)
values$A <- values$A - 1
funcB()
})
obsC <- observe({
funcB()
})
flushReact()
expect_equal(execCount(obsC), 2)
expect_equal(execCount(funcB), 6)
})
test_that("Non-reactive recursion", {
nonreactiveA <- 3
outputD <- NULL
funcB <- reactive({
if (nonreactiveA == 0)
return(0)
nonreactiveA <<- nonreactiveA - 1
return(funcB())
})
obsC <- observe({
outputD <<- funcB()
})
flushReact()
expect_equal(execCount(funcB), 4)
expect_equal(outputD, 0)
})
test_that("Circular dep with observer only", {
values <- reactiveValues(A=3)
obsB <- observe({
if (values$A == 0)
return()
values$A <- values$A - 1
})
flushReact()
expect_equal(execCount(obsB), 4)
})
test_that("Writing then reading value is not circular", {
values <- reactiveValues(A=3)
funcB <- reactive({
values$A <- isolate(values$A) - 1
values$A
})
obsC <- observe({
funcB()
})
flushReact()
expect_equal(execCount(obsC), 1)
values$A <- 10
flushReact()
expect_equal(execCount(obsC), 2)
})
test_that("names() and reactiveValuesToList()", {
values <- reactiveValues(A=1, .B=2)
# Dependent on names
depNames <- observe({
names(values)
})
# Dependent on all non-hidden objects
depValues <- observe({
reactiveValuesToList(values)
})
# Dependent on all objects, including hidden
depAllValues <- observe({
reactiveValuesToList(values, all.names = TRUE)
})
# names() returns all names
expect_equal(sort(isolate(names(values))), sort(c(".B", "A")))
# Assigning names fails
expect_error(isolate(names(v) <- c('x', 'y')))
expect_equal(isolate(reactiveValuesToList(values)), list(A=1))
expect_equal(isolate(reactiveValuesToList(values, all.names=TRUE)), list(A=1, .B=2))
flushReact()
expect_equal(execCount(depNames), 1)
expect_equal(execCount(depValues), 1)
expect_equal(execCount(depAllValues), 1)
# Update existing variable
values$A <- 2
flushReact()
expect_equal(execCount(depNames), 1)
expect_equal(execCount(depValues), 2)
expect_equal(execCount(depAllValues), 2)
# Update existing hidden variable
values$.B <- 3
flushReact()
expect_equal(execCount(depNames), 1)
expect_equal(execCount(depValues), 2)
expect_equal(execCount(depAllValues), 3)
# Add new variable
values$C <- 1
flushReact()
expect_equal(execCount(depNames), 2)
expect_equal(execCount(depValues), 3)
expect_equal(execCount(depAllValues), 4)
# Add new hidden variable
values$.D <- 1
flushReact()
expect_equal(execCount(depNames), 3)
expect_equal(execCount(depValues), 3)
expect_equal(execCount(depAllValues), 5)
})
test_that("Observer pausing works", {
values <- reactiveValues(a=1)
funcA <- reactive({
values$a
})
obsB <- observe({
funcA()
})
# Important: suspend() only affects observer at invalidation time
# Observers are invalidated at creation time, so it will run once regardless
# of being suspended
obsB$suspend()
flushReact()
expect_equal(execCount(funcA), 1)
expect_equal(execCount(obsB), 1)
# When resuming, if nothing changed, don't do anything
obsB$resume()
flushReact()
expect_equal(execCount(funcA), 1)
expect_equal(execCount(obsB), 1)
# Make sure suspended observers do not flush, but do invalidate
obsB_invalidated <- FALSE
obsB$onInvalidate(function() {obsB_invalidated <<- TRUE})
obsB$suspend()
values$a <- 2
flushReact()
expect_equal(obsB_invalidated, TRUE)
expect_equal(execCount(funcA), 1)
expect_equal(execCount(obsB), 1)
obsB$resume()
values$a <- 2.5
obsB$suspend()
flushReact()
expect_equal(execCount(funcA), 2)
expect_equal(execCount(obsB), 2)
values$a <- 3
flushReact()
expect_equal(execCount(funcA), 2)
expect_equal(execCount(obsB), 2)
# If onInvalidate() is added _after_ obsB is suspended and the values$a
# changes, then it shouldn't get run (onInvalidate runs on invalidation, not
# on flush)
values$a <- 4
obsB_invalidated2 <- FALSE
obsB$onInvalidate(function() {obsB_invalidated2 <<- TRUE})
obsB$resume()
flushReact()
expect_equal(execCount(funcA), 3)
expect_equal(execCount(obsB), 3)
expect_equal(obsB_invalidated2, FALSE)
})
test_that("suspended/resumed observers run at most once", {
values <- reactiveValues(A=1)
obs <- observe({
values$A
})
expect_equal(execCount(obs), 0)
# First flush should run obs once
flushReact()
expect_equal(execCount(obs), 1)
# Modify the dependency at each stage of suspend/resume/flush should still
# only result in one run of obs()
values$A <- 2
obs$suspend()
values$A <- 3
obs$resume()
values$A <- 4
flushReact()
expect_equal(execCount(obs), 2)
})
test_that("reactive() accepts quoted and unquoted expressions", {
vals <- reactiveValues(A=1)
# Unquoted expression, with curly braces
fun <- reactive({ vals$A + 1 })
expect_equal(isolate(fun()), 2)
# Unquoted expression, no curly braces
fun <- reactive(vals$A + 1)
expect_equal(isolate(fun()), 2)
# Quoted expression
fun <- reactive(quote(vals$A + 1), quoted = TRUE)
expect_equal(isolate(fun()), 2)
# Quoted expression, saved in a variable
q_expr <- quote(vals$A + 1)
fun <- reactive(q_expr, quoted = TRUE)
expect_equal(isolate(fun()), 2)
# Functions being passed to reactives is no longer treated specially
fun <- reactive(function() { vals$A + 1 })
expect_true(is.function(isolate(fun())))
# Check that environment is correct - parent environment should be this one
this_env <- environment()
fun <- reactive(environment())
expect_identical(isolate(parent.env(fun())), this_env)
# Sanity check: environment structure for a reactive() should be the same as for
# a normal function
fun <- function() environment()
expect_identical(parent.env(fun()), this_env)
})
test_that("observe() accepts quoted and unquoted expressions", {
vals <- reactiveValues(A=0)
valB <- 0
# Unquoted expression, with curly braces
observe({ valB <<- vals$A + 1})
flushReact()
expect_equal(valB, 1)
# Unquoted expression, no curly braces
observe({ valB <<- vals$A + 2})
flushReact()
expect_equal(valB, 2)
# Quoted expression
observe(quote(valB <<- vals$A + 3), quoted = TRUE)
flushReact()
expect_equal(valB, 3)
# Quoted expression, saved in a variable
q_expr <- quote(valB <<- vals$A + 4)
fun <- observe(q_expr, quoted = TRUE)
flushReact()
expect_equal(valB, 4)
# Functions are no longer treated specially
observe(function() { valB <<- vals$A + 5 })
flushReact()
expect_equal(valB, 4)
# Check that environment is correct - parent environment should be this one
this_env <- environment()
inside_env <- NULL
fun <- observe(inside_env <<- environment())
flushReact()
expect_identical(parent.env(inside_env), this_env)
})
test_that("Observer priorities are respected", {
results <- c()
observe(results <<- c(results, 10), priority=10)
observe(results <<- c(results, 30), priority=30)
observe(results <<- c(results, 20), priority=20L)
observe(results <<- c(results, 21), priority=20)
observe(results <<- c(results, 22), priority=20L)
flushReact()
expect_identical(results, c(30, 20, 21, 22, 10))
})
test_that("installExprFunction doesn't rely on name being `expr`", {
justExecute <- function(anExpression, envirToUse = parent.frame(), isQuoted = FALSE) {
shiny:::installExprFunction(anExpression, "myFunc", envirToUse, quoted = isQuoted)
myFunc()
}
expect_identical(-1, justExecute({-1}))
})
test_that("reactivePoll and reactiveFileReader", {
path <- tempfile('file')
on.exit(unlink(path))
write.csv(cars, file=path, row.names=FALSE)
rfr <- reactiveFileReader(100, NULL, path, read.csv)
expect_equal(isolate(rfr()), cars)
write.csv(rbind(cars, cars), file=path, row.names=FALSE)
Sys.sleep(0.15)
timerCallbacks$executeElapsed()
expect_equal(isolate(rfr()), cars)
flushReact()
expect_equal(isolate(rfr()), rbind(cars, cars))
})
test_that("classes of reactive object", {
v <- reactiveValues(a = 1)
r <- reactive({ v$a + 1 })
o <- observe({ print(r()) })
expect_false(is.reactivevalues(12))
expect_true(is.reactivevalues(v))
expect_false(is.reactivevalues(r))
expect_false(is.reactivevalues(o))
expect_false(is.reactive(12))
expect_false(is.reactive(v))
expect_true(is.reactive(r))
expect_false(is.reactive(o))
o$destroy()
})
test_that("{} and NULL also work in reactive()", {
reactive({})
reactive(NULL)
})
test_that("shiny.suppressMissingContextError option works", {
options(shiny.suppressMissingContextError=TRUE)
on.exit(options(shiny.suppressMissingContextError=FALSE), add = TRUE)
expect_true(reactive(TRUE)())
})
test_that("reactive domains are inherited", {
domainA <- createMockDomain()
domainB <- createMockDomain()
local({
domainY <- NULL
domainZ <- NULL
x <- observe({
y <- observe({
# Should be domainA (inherited from observer x)
domainY <<- getDefaultReactiveDomain()
})
z <- observe({
# Should be domainB (explicitly passed in)
domainZ <<- getDefaultReactiveDomain()
}, domain = domainB)
}, domain = domainA)
flushReact()
flushReact()
expect_identical(domainY, domainA)
expect_identical(domainZ, domainB)
})
local({
domainY <- 1
x <- NULL
y <- NULL
z <- NULL
r3 <- NULL
domainR3 <- NULL
r1 <- reactive({
y <<- observe({
# Should be NULL (r1 has no domain)
domainY <<- getDefaultReactiveDomain()
})
})
r2 <- reactive({
z <<- observe({
# Should be domainB (r2 has explicit domainB)
domainZ <<- getDefaultReactiveDomain()
})
}, domain = domainB)
observe({
r3 <<- reactive({
# This should be domainA. Doesn't matter where r3 is invoked, it only
# matters where it was created.
domainR3 <<- getDefaultReactiveDomain()
})
r1()
r2()
}, domain = domainA)
flushReact()
flushReact()
isolate(r3())
expect_identical(execCount(y), 1L)
expect_identical(execCount(z), 1L)
expect_identical(domainY, NULL)
expect_identical(domainZ, domainB)
expect_identical(domainR3, domainA)
})
})
test_that("observers autodestroy (or not)", {
domainA <- createMockDomain()
local({
a <- observe(NULL, domain = domainA)
b <- observe(NULL, domain = domainA, autoDestroy = FALSE)
c <- observe(NULL, domain = domainA)
c$setAutoDestroy(FALSE)
d <- observe(NULL, domain = domainA, autoDestroy = FALSE)
d$setAutoDestroy(TRUE)
e <- observe(NULL)
domainA$end()
flushReact()
expect_identical(execCount(a), 0L)
expect_identical(execCount(b), 1L)
expect_identical(execCount(c), 1L)
expect_identical(execCount(d), 0L)
expect_identical(execCount(e), 1L)
})
})
test_that("observers are garbage collected when destroyed", {
domain <- createMockDomain()
rv <- reactiveValues(x = 1)
# Auto-destroy. GC on domain end.
a <- observe(rv$x, domain = domain)
# No auto-destroy. GC with rv.
b <- observe(rv$x, domain = domain, autoDestroy = FALSE)
# No auto-destroy and no reactive dependencies. GC immediately.
c <- observe({}, domain = domain)
c$setAutoDestroy(FALSE)
# Similar to b, but we'll set it to autoDestroy later.
d <- observe(rv$x, domain = domain, autoDestroy = FALSE)
# Like a, but we'll destroy it immediately.
e <- observe(rx$x, domain = domain)
e$destroy()
collected <- new.env(parent = emptyenv())
reg.finalizer(a, function(o) collected$a <- TRUE)
reg.finalizer(b, function(o) collected$b <- TRUE)
reg.finalizer(c, function(o) collected$c <- TRUE)
reg.finalizer(d, function(o) collected$d <- TRUE)
reg.finalizer(e, function(o) collected$e <- TRUE)
rm(list = c("a", "b", "c", "e")) # Not "d"
gc()
# Nothing can be GC'd yet, because all of the observers are
# pending execution (i.e. waiting for flushReact).
expect_equal(ls(collected), character())
flushReact()
# Now "c" can be garbage collected, because it ran and took
# no dependencies (and isn't tied to the session in any way).
# And "e" can also be garbage collected, it's been destroyed.
gc()
expect_equal(ls(collected), c("c", "e"))
domain$end()
# We can GC "a" as well; even though it references rv, it is
# destroyed when the session ends.
gc()
expect_equal(sort(ls(collected)), c("a", "c", "e"))
# It's OK to turn on auto-destroy even after the session was
# destroyed.
d$setAutoDestroy(TRUE)
# This should no-op.
d$setAutoDestroy(FALSE)
rm(d)
gc()
expect_equal(sort(ls(collected)), c("a", "c", "d", "e"))
rm(rv)
# Both rv and "b" can now be collected.
gc()
expect_equal(sort(ls(collected)), c("a", "b", "c", "d", "e"))
})
test_that("maskReactiveContext blocks use of reactives", {
vals <- reactiveValues(x = 123)
# Block reactive contexts (created by isolate)
expect_error(isolate(maskReactiveContext(vals$x)))
expect_error(isolate(isolate(maskReactiveContext(vals$x))))
# Reactive contexts within maskReactiveContext shouldn't be blocked
expect_identical(maskReactiveContext(isolate(vals$x)), 123)
expect_identical(isolate(maskReactiveContext(isolate(vals$x))), 123)
})
test_that("Flush completes even when errors occur", {
vals <- reactiveValues(x = 1)
r <- reactive({
if (vals$x == 0) stop("x is zero!")
else vals$x
})
# Set up counters
n11 <- n12 <- n21 <- n22 <- 0
observe({
n11 <<- n11 + 1
r()
n12 <<- n12 + 1
})
observe({
n21 <<- n21 + 1
r()
n22 <<- n22 + 1
})
flushReact()
expect_true(all(c(n11, n12, n21, n22) == 1))
# Trigger an error
vals$x <- 0
# Errors in reactive are translated to warnings in observers by default
expect_warning(flushReact())
# Both observers should run up until the reactive that errors
expect_true(all(c(n11, n12, n21, n22) == c(2,1,2,1)))
# Nothing should happen on next flush
flushReact()
expect_true(all(c(n11, n12, n21, n22) == c(2,1,2,1)))
})
test_that("event handling helpers take correct dependencies", {
vals <- reactiveValues(action = NULL, x = 1)
o1_count <- 0
o1 <- observeEvent(vals$action, {
vals$x
o1_count <<- o1_count + 1
})
o2_count <- 0
o2 <- observeEvent(ignoreNULL = FALSE, vals$action, {
vals$x
o2_count <<- o2_count + 1
})
r1 <- eventReactive(vals$action, {
vals$x
})
r2 <- eventReactive(ignoreNULL = FALSE, vals$action, {
vals$x
})
flushReact()
expect_error(isolate(r1()))
expect_identical(isolate(r2()), 1)
expect_equal(o1_count, 0)
expect_equal(o2_count, 1)
expect_equal(execCount(o1), 1)
expect_equal(execCount(o2), 1)
vals$x <- 2
flushReact()
expect_error(isolate(r1()))
expect_identical(isolate(r2()), 1)
expect_equal(o1_count, 0)
expect_equal(o2_count, 1)
expect_equal(execCount(o1), 1)
expect_equal(execCount(o2), 1)
vals$action <- 1
flushReact()
expect_identical(isolate(r1()), 2)
expect_identical(isolate(r2()), 2)
expect_equal(o1_count, 1)
expect_equal(o2_count, 2)
expect_equal(execCount(o1), 2)
expect_equal(execCount(o2), 2)
})