tests/testthat/test-wrong.R

context("wrong")

# This test file demonstrates some behavior of R nonstandard evalution
# functions that I think is wrong, and that this package tries to correct.

expect_different <- function(object, expected, ...) {
  lab_act <- deparse(arg_expr(object));
  lab_exp <- deparse(arg_expr(expected));
  comp <- compare(object, expected, ...)
  expect(!comp$equal, sprintf("%s was equal to %s\n%s", lab_act, lab_exp, comp$message))
  invisible(object)
}

`%should_be%` <- expect_different
`%but_is%` <- expect_equal
`%is%` <- expect_equal

test_that("parent_frame returns garbage when called from a promise.", {
  # A tediously explained example.
  # A problem with using parent_frame to determine a function's caller is that
  # parent.frame()'s return value can change when called at different times
  # from the same environment.

  # To illustrate this, here is a NSE function that captures an expression and
  # returns a function that evaluates that expression.

  make_callable <- function(arg, envir=parent.frame()) {
    expr <- substitute(arg)
    function() {
      eval(expr, envir)
    }
  }

  # Say we use "make_callable" to implement a counter factory.
  make_counter <- function() {
    x <- 0 #local x for this invocation
    make_callable(x <- x + 1) # this should refer to the local x
  }

  # Since "make_callable" is invoked from a frame containing a local x,
  count <- make_counter()
  # we expect count() should update that local value, not this global value.

  #x <- 999 # oddly sometimes it goes for global x (depending on how
            # test is invoked?)
  x <<- 999 # wherever x is, it is scoped outside the count!

  # But it does.
  ct <- count()
  ct %should_be% 1 %but_is% 1000
  x %should_be% 999 %but_is% 1000

  # That is, make_callable is not capturing the correct environment of
  # its argument. it's hitting a wrong local or global x.

  # The problem is that callable's argument "envir" is lazily evaluated, so
  # "parent.frame" gets called only when the true caller has fallen off the
  # stack. And when `parent.frame` is called by forcing a promise (and the
  # original caller is no longer on the stack), it can silently do a wrong
  # thing: it returns the stack frame that occasioned the evaluation of "x",
  # and NOT the caller of make_callable.

  # A standard workaround is to force envir to evaluate before its
  # parent frame falls off the stack.
  make_callable <- function(arg, envir=parent.frame()) {
    force(envir)
    expr <- substitute(arg)
    function() eval(expr, envir)
  }
  count <- make_counter()
  x <- 999

  count() %is% 1
  x %is% 999

  # But IMO, this is WRONG! parent.frame should not change its
  # output depending on whether it is called early or late. This
  # package provides "arg_env" and "arg_expr" instead of
  # parent.frame() and substitute(). This way we get the behavior we
  # usually want.
  make_callable <- function(arg, envir=arg_env(arg)) {
    expr <- arg_expr(arg)
    function() eval(expr, envir)
  }

  # And this works as expected.
  count <- make_counter()
  x <- 999
  count() %is% 1
  x %is% 999
})

#The following test cases were borrowed from test_caller. These are all
#things that caller() gets right (or throws) where parent.frame gets
#wrong.

test_that("parent.frame from a lazy argument in a closed environment", {
  where <<- "00"
  where <- "00"
  d <- function() {
    where <- "0"
    e <- function() {
      where <- "e"
      f <- function() {
        where <- "f"
        g <- function(g) {
          where <- "g"
          function(f) g
        }
        g(parent.frame())
      }
      f()
    }
    e()()$where
  }
  d <- compiler::cmpfun(d)
  d() %should_be% "e" %but_is% "00"
})




test_that("parent.frame from eval and do.call", {
  where <- "0"
  x <- y <- z <- NULL
  e <- function() {
    where <- "e"
    x <<- environment()
    f <- function() {
      where <- "f"
      y <<- environment()
      g <- function() {
        where <- "g"
        z <<- environment()
        eval(quote(parent.frame()))$where %should_be% "f" %but_is% NULL
        eval(quote(parent.frame()), y)$where %should_be% "e" %but_is% NULL
      }
      g()
    }
    f()
  }
  e()
})

test_that("parent.frame from eval and do.call in closed environments", {
  where <<- "00"
  x <- y <- z <- NULL
  d <- function() {
    where <- "0"
    function(x) x
    e <- function() {
      where <- "e"
      x <<- environment()
      f <- function() {
        where <- "f"
        y <<- environment()
        g <- function() {
          where <- "g"
          z <<- environment()
        }
        g()
      }
      f()
    }
    e()
  }
  d <- compiler::cmpfun(d)
  d()
  h <- function() {
    eval(quote(parent.frame()))$where %should_be% "0" %but_is% NULL
    do.call("parent.frame", list(), envir=z)$where %should_be% "f" %but_is% "00"
    do.call("parent.frame", list(), envir=y)$where %should_be% "e" %but_is% "00"
  }
  h()
})
crowding/nse documentation built on Jan. 5, 2024, 12:14 a.m.