tests/testthat/test-r-session.R

test_that("regular use", {
  rs <- r_session$new()
  on.exit(rs$kill())

  expect_equal(rs$get_state(), "idle")

  ## Clean session
  expect_identical(rs$run(function() ls(.GlobalEnv)), character())

  ## Start a command
  rs$call(function() 42)

  ## Get result
  res <- read_next(rs)
  expect_equal(res$result, 42)
  expect_null(res$error)
  expect_equal(rs$get_state(), "idle")

  ## Run another command, with arguments
  rs$call(function(x, y)  x + y, list(x = 42, y = 42))

  ## Get result
  res <- read_next(rs)
  expect_equal(res$result, 84)
  expect_equal(rs$get_state(), "idle")

  ## While session is idle the current call running time is NA
  rs$call(function() 42)
  expect_equal(
    is.na(rs$get_running_time()),
    c(total = FALSE, current = FALSE)
  )
  res <- read_next(rs)
  expect_equal(
    is.na(rs$get_running_time()),
    c(total = FALSE, current = TRUE)
  )
  expect_s3_class(rs$get_running_time(), "difftime")

  ## Close
  rs$close()
  expect_equal(rs$get_state(), "finished")
  expect_false(rs$is_alive())
})

test_that("run", {
  rs <- r_session$new()
  on.exit(rs$kill())

  expect_equal(rs$run_with_output(function() 42)$result, 42)
  expect_equal(rs$run_with_output(function() 42)$result, 42)
  expect_equal(
    rs$run_with_output(function(x, y) { x + y },
                       list(x = 42, y = 42))$result,
    84)

  ## Finish
  rs$close()
  expect_equal(rs$get_state(), "finished")
  expect_false(rs$is_alive())
})


test_that("get stdout/stderr from file", {
  rs <- r_session$new()
  on.exit(rs$kill())

  eol <- function(x) {
    paste0(x, if (.Platform$OS.type == "windows") "\r\n" else "\n")
  }

  for (i in 1:10) {
    res <- rs$run_with_output(function() {
      cat("foo\n"); message("bar"); 42 })
    expect_equal(
      res[c("result", "stdout", "stderr")],
      list(result = 42, stdout = eol("foo"), stderr = eol("bar")))

    res <- rs$run_with_output(function() {
      cat("bar\n"); message("foo"); 43 })
    expect_equal(
      res[c("result", "stdout", "stderr")],
      list(result = 43, stdout = eol("bar"), stderr = eol("foo")))
  }
  rs$close()
})

test_that("stdout/stderr from pipe", {
  skip_on_cran()
  opt <- r_session_options(stdout = "|", stderr = "|")
  rs <- r_session$new(opt)
  on.exit(rs$kill())

  ## Sometimes a NULL slips through....  this is bug to be fixed
  rs$read_output_lines()

  res <- rs$run_with_output(function() {
    cat("foo\n"); message("bar"); 42 })
  expect_equal(
    res[c("result", "stdout", "stderr")],
    list(result = 42, stdout = NULL, stderr = NULL))

  res <- rs$run_with_output(function() {
    cat("bar\n"); message("foo"); 43 })
  expect_equal(
    res[c("result", "stdout", "stderr")],
    list(result = 43, stdout = NULL, stderr = NULL))

  processx::poll(list(rs$get_output_connection()), 1000)
  expect_equal(rs$read_output_lines(n = 1), "foo")
  processx::poll(list(rs$get_output_connection()), 1000)
  expect_equal(rs$read_output_lines(n = 1), "bar")
  processx::poll(list(rs$get_error_connection()), 1000)
  expect_equal(rs$read_error_lines(n = 1), "bar")
  processx::poll(list(rs$get_error_connection()), 1000)
  expect_equal(rs$read_error_lines(n = 1), "foo")
  rs$close()
})

test_that("interrupt", {
  rs <- r_session$new()
  on.exit(rs$kill())

  rs$call(function() Sys.sleep(5))
  Sys.sleep(0.5)
  rs$interrupt()
  rs$poll_process(1000)
  res <- rs$read()
  expect_s3_class(res$error, "callr_timeout_error")
  rs$close()
})

test_that("messages", {
  f <- function() {
    x <- structure(
      list(code = 301, message = "ahoj"),
      class = c("callr_message", "condition"))
    signalCondition(x)
    22
  }

  msg <- NULL
  cb <- function(x) msg <<- x

  rs <- r_session$new()
  on.exit(rs$kill())

  withCallingHandlers(
    expect_silent(res <- rs$run_with_output(f)),
    callr_message = function(e) msg <<- e
  )
  expect_equal(res$result, 22)
  expect_equal(
    msg,
    structure(
      list(code = 301, message = "ahoj", muffle = "callr_r_session_muffle"),
      class = c("callr_message", "condition")))
  rs$close()
})

test_that("messages with R objects", {
  obj <- list(a = 1, b = 2)
  f <- function(obj) {
    x <- structure(
      c(list(code = 301), obj),
      class = c("foobar_class", "callr_message", "condition"))
    signalCondition(x)
    22
  }

  msg <- NULL
  cb <- function(x) msg <<- x

  rs <- r_session$new()
  on.exit(rs$kill())

  withCallingHandlers(
    expect_silent(res <- rs$run_with_output(f, args = list(obj))),
    foobar_class = function(e) msg <<- e
  )
  expect_equal(res$result, 22)
  exp <- exp2 <- structure(
    list(code = 301, a = 1, b = 2),
    class = c("foobar_class", "callr_message", "condition"))
  exp2$muffle <- "callr_r_session_muffle"
  expect_equal(msg, exp2)
  rs$call(f, args = list(obj))
  rs$poll_process(2000)
  expect_equal(
    rs$read(),
    structure(
      list(code = 301, message = exp),
      class = "callr_session_result"))
  rs$poll_process(2000)
  expect_equal(rs$read()$result, 22)
  rs$close()
})

test_that("run throws", {
  rs <- r_session$new()
  on.exit(rs$kill())
  expect_error(rs$run(function() stop("foobar")), "foobar")
  rs$close()
})

test_that("exit", {
  rs <- r_session$new()
  on.exit(rs$kill(), add = TRUE)
  err <- tryCatch(
    res <- rs$run(function() q()),
    error = function(x) x)

  deadline <- Sys.time() + 3
  while (rs$is_alive() && Sys.time() < deadline) Sys.sleep(0.05)
  expect_true(Sys.time() < deadline)

  expect_false(rs$is_alive())
  expect_equal(rs$get_state(), "finished")
  rs$close()
})

test_that("crash", {
  skip_on_cran()
  rs <- r_session$new()
  on.exit(rs$kill(), add = TRUE)
  err <- tryCatch(
    rs$run(function() get("crash", asNamespace("callr"))()),
    error = function(e) e)
  expect_true(
    grepl("crashed with exit code", conditionMessage(err)) ||
    grepl("R session closed the process connection", conditionMessage(err)) ||
    grepl("Invalid (uninitialized or closed?) connection object",
          conditionMessage(err), fixed = TRUE)
  )
  expect_false(rs$is_alive())
  expect_equal(rs$get_state(), "finished")
  rs$close()

  rs <- r_session$new()
  on.exit(rs$kill(), add = TRUE)
  res <- rs$run_with_output(function() {
    cat("o\n"); message("e");
    get("crash", asNamespace("callr"))()
  })
  expect_null(res$result)
  expect_s3_class(res$error, "error")

  ## This is a race, and sometimes we don't get the stdout/stderr
  ## on Windows
  if (os_platform() != "windows") expect_equal(res$stdout, "o\n")
  if (os_platform() != "windows") expect_equal(substr(res$stderr, 1, 2), "e\n")
  expect_false(rs$is_alive())
  expect_equal(rs$get_state(), "finished")
  rs$close()
})

test_that("custom load hook", {
  opts <- r_session_options(load_hook = quote(options(foobar = "baz")))
  rs <- r_session$new(opts)
  on.exit(rs$kill(), add = TRUE)

  res <- rs$run_with_output(function() getOption("foobar"))
  expect_null(res$error)
  expect_identical(res$result, "baz")
  expect_equal(res$stdout, "")
  expect_equal(res$stderr, "")
  rs$close()
})

test_that("traceback", {
  skip_if_not_installed("withr")
  withr::local_options(callr.traceback = TRUE)
  rs <- r_session$new()
  on.exit(rs$kill(), add = TRUE)

  do <- function() {
    f <- function() g()
    g <- function() stop("oops")
    f()
  }

  expect_error(rs$run(do), "oops")
  expect_output(tb <- rs$traceback(), "1: \"?f()\"?")
  expect_match(c(tb[[3]]), "f()", fixed = TRUE)
})

test_that("error in the load hook", {
  opts <- r_session_options(load_hook = quote(stop("oops")))
  expect_error({
    rs <- r_session$new(opts)
    on.exit(rs$kill(), add = TRUE)
  })

  rs2 <- r_session$new(opts, wait = FALSE)
  on.exit(rs2$kill(), add = TRUE)

  processx::poll(list(rs2$get_poll_connection()), 3000)
  msg <- rs2$read()
  expect_true(msg$code %in% c(501L, 502L))
  expect_match(msg$stderr, "oops")
})

test_that("fds are not leaked", {
  skip_if_not_installed("ps")
  rs <- r_session$new()
  on.exit(rs$kill(), add = TRUE)

  old <- rs$run(function() ps::ps_num_fds())
  for (i in 1:10) {
    rs$run(function() 1 + 1)
  }
  new <- rs$run(function() ps::ps_num_fds())

  # Allow 2 new fds, just in case
  expect_true(new - old <= 2)
})
r-pkgs/callr documentation built on March 29, 2024, 4:35 a.m.