1

I have some delicate issues with environments that are currently manifesting themselves in my unit tests. My basic structure is this

  • I have a main function main that has many arguments
  • wrapper is a wrapper function (one of many) that pertains only to selected arguments of main
  • helper is an intermediate helper function that is used by all wrapper functions

I use eval and match.call() to move between wrappers and the main function smoothly. My issue now is that my tests work when I run them line by line, but not using test_that().

Here is a MWE that shows the problem. If you step through the lines in the test manually, the test passes. However, evaluating the whole test_that() chunk the test fails because one of the arguments can not be found.

library(testthat)

wrapper <- function(a, b) {
  fun_call <- as.list(match.call())
  ret <- helper(fun_call)
  return(ret)
}

helper <- function(fun_call) {
  fun_call[[1]] <- quote(main)
  fun_call <- as.call(fun_call)
  fun_eval <- eval(as.call(fun_call))
  return(fun_eval)
}

main <- function(a, b, c = 1) {
  ret <- list(a = a, b = b, c = c)
  return(ret)
}

test_that("Test", {
  a <- 1
  b <- 2
  x <- wrapper(a = a, b = b)
  y <- list(a = 1, b = 2, c = 1)
  expect_equal(x, y)
})

With quite some confidence, I suspect I need to modify the default environment used by eval (i.e. parent.frame()), but I am not sure how to do this.

hejseb
  • 2,064
  • 3
  • 18
  • 28

1 Answers1

3

You want to evaluate your call in your parent environment, not the local function environment. Change your helper to

helper <- function(fun_call) {
  fun_call[[1]] <- quote(main)
  fun_call <- as.call(fun_call)
  fun_eval <- eval.parent(fun_call, n=2)
  return(fun_eval)
}

This is assuming that helper is always called within wrapper which is called from somewhere else the parameters are defined.

It's not clear in this case that you really need all this non-standard evaulation. You might also consider a solution like

wrapper <- function(a, b) {
  helper(mget(ls()))
}
helper <- function(params) {
  do.call("main", params)
}

Here wrapper just bundles all it's parameters values into a list. Then you can just pass a list of parameters to helper and do.call will pass that list as parameters to your main function. This will evaluate the parameters of wrapper when you call it do you don't have to worry about the execution evironment.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • This seems to work fine, but I'm puzzled as to why! The default in `eval.parent()` is `n=1`, and `eval.parent(expr, n)` is short for `eval(expr, parent.frame(n))`. So `eval.parent(expr, 1)` is `eval(expr, parent.frame(1))`, but the default environment in `eval` is `parent.frame()` --- and the default for `parent.frame()` is `n=1`! So, tl; dr: isn't `eval.parent(expr)` just the same as `eval(expr)`? (Evidently not, but I can't see why.) – hejseb Apr 29 '21 at 12:58
  • Also, I just realized that this breaks down if in the test you set `a2 <- 1` and then `x <- wrapper(a = a2, b = b)`. So it seems as if this solution requires inputs to be named the same way as arguments? – hejseb Apr 29 '21 at 18:07
  • @hejseb, First, it's important to note that when you pass parameters to functions they are evaluated in the calling environment but when you have a default parameter value, those are executed in the function environment which is different. So `parent.frame` behaves differently when passed to a function vs when it's a default parameter value. If helper is always called from within weapper, than you can do `fun_eval <- eval.parent(fun_call, n=2)` Or it would be even easier just to evaluate the parameters in `wrapper` before passing to `helper`. Is there a reason you are delaying evaluation? – MrFlick Apr 29 '21 at 18:49
  • Thanks, I'm learning a lot here. `wrapper` always calls `helper`, so that can safely be assumed. However, I wouldn't say that I intentionally am delaying evaluation --- that's just sort of what I've been experiencing. Originally, I wanted a bunch of wrappers that would all just modify certain arguments of `main`. Then I ended up with `match.call()` to avoid lots of hard-coded wrappers, and because all wrappers are structured the same way I put that stuff into the `helper`. – hejseb Apr 29 '21 at 18:54
  • I can also add that in my case, `wrapper` also has default values and I need to keep track of those and which have been overridden. That I achieve using `formals()` and passing that too to the `helper`, so I do believe the intermediate function is necessary (although it came with some unexpected complications!). – hejseb Apr 29 '21 at 18:56
  • It doesn't sound like form your use case that you should be using `match.call`. A more direct method might be something like: `wrapper <- function(a, b) {helper(mget(ls()))}; helper <- function(params) {do.call("main", params)}` – MrFlick Apr 29 '21 at 18:58
  • That does admittedly seem easier. I'll see if that fits my case. Thanks a lot! – hejseb Apr 29 '21 at 19:01