context("exceptions")


test_that("py_last_error() returns R strings", {
  skip_if_no_python()

  tryCatch(py_eval("range(3)[3]"), error = identity)

  er <- py_last_error()
  expect_identical(er$type, "IndexError")
  expect_type(er$value, "character")
  expect_type(er$traceback, "character")
  expect_type(er$message, "character")

})


test_that("py_last_error() returns the R error condition object", {
    skip_if_no_python()

    signal_simple_error <- function() {
      stop("Hello signal_simple_error")
    }
    raise_py_exception <- function() {
      py_run_string("raise RuntimeError('Hello raise_py_exception')")
    }
    signal_py_exception <- function() {
      stop(import_builtins()$RuntimeError("Hello signal_py_exception"))
    }

    f1 <- py_func(signal_simple_error)
    f2 <- py_func(function() f1())
    f3 <- py_func(function() f2())
    f4 <- py_func(function() f3())

    g1 <- py_func(raise_py_exception)
    g2 <- py_func(function() g1())
    g3 <- py_func(function() g2())
    g4 <- py_func(function() g3())

    h1 <- py_func(signal_py_exception)
    h2 <- py_func(function() h1())
    h3 <- py_func(function() h2())
    h4 <- py_func(function() h3())

    for (fn in list(f1, f2, f3, f4,
                    g1, g2, g3, g4,
                    h1, h2, h3, h4)) {

      e <- tryCatch( fn(), error = function(e) e )

      for (cls in  c("python.builtin.Exception",
                     "python.builtin.BaseException",
                     "python.builtin.object",
                     "error", "condition"))
        expect_s3_class(e, cls)

      expect_identical(conditionMessage(e), e$message)
      expect_identical(conditionCall(e), e$call)

      expect_match(conditionMessage(e), "Hello")
      expect_type(conditionCall(e), "language")

      expect_s3_class(e$trace, "data.frame")
      expect_s3_class(e$trace, "rlang_trace")
    }
    # test that py_last_error() reports full r_trace
    # even if python discards the exception object

    catch_and_replace_exception <- py_run_string("
def catch_and_replace_exception(fn):
    try:
      fn()
    except:
      raise RuntimeError('''It's a mystery!''')
")$catch_and_replace_exception

    catch_clear_errstatus_then_raise_new_exception <- py_run_string("
def catch_clear_errstatus_then_raise_new_exception(fn):
    failed = False
    try:
      res = fn()
    except:
      failed = True
    if failed:
      raise RuntimeError('''It's a mystery!''')
    return res
")$catch_clear_errstatus_then_raise_new_exception

    expect_match2 <- expect_match
    formals(expect_match2)$fixed <- TRUE
    formals(expect_match2)$all <- FALSE

    for (erroring_fn in list(signal_simple_error,
                             raise_py_exception,
                             signal_py_exception)) {
      f1 <- py_func(erroring_fn)
      f2 <- py_func(function() f1())
      f3 <- py_func(function() catch_and_replace_exception(f2))
      f4 <- py_func(function() f3())
      f5 <- py_func(function() f4())

      e <- tryCatch(f5(), error = identity)
      expect_s3_class(e$trace, "data.frame")
      expect_s3_class(e$trace, "rlang_trace")

      output <- suppressMessages(capture.output(print(reticulate::py_last_error())))

      expect_match2(output, "Hello")
      expect_match2(output, "It's a mystery!")
      expect_match2(output, "f1()")
      expect_match2(output, "catch_and_replace_exception(f2)")
      expect_match2(output, "f3()")
      expect_match2(output, "f4()")
      expect_match2(output, "f5()")

      f1 <- py_func(erroring_fn)
      f2 <- py_func(function() f1())
      f3 <- py_func(function() catch_clear_errstatus_then_raise_new_exception(f2))
      f4 <- py_func(function() f3())
      f5 <- py_func(function() f4())

      e <- tryCatch(f5(), error = identity)
      expect_s3_class(e$trace, "data.frame")
      expect_s3_class(e$trace, "rlang_trace")

      output <- suppressMessages(capture.output(print(reticulate::py_last_error())))

      expect_match2(output, "It's a mystery!") # python code made it a mystery
      expect_match2(output, "Hello") # we make it not a mystery by providing the R trace
      expect_match2(output, "f1()")
      expect_match2(output, "catch_clear_errstatus_then_raise_new_exception(f2)")
      expect_match2(output, "f3()")
      expect_match2(output, "f4()")
      expect_match2(output, "f5()")

    }


})


test_that("R abort restarts become Python exceptions", {
  skip_if_no_python()

  abort_state <- new.env(parent = emptyenv())

  abort_without_condition <- function() {
    invokeRestart("abort")
  }

  abort_with_condition <- function() {
    cnd <- simpleError("cancelled from R")
    cnd$trace <- rlang::trace_back()
    abort_state$trace <- cnd$trace
    invokeRestart("abort", cnd)
  }

  ordinary_error <- function() {
    stop("ordinary R error")
  }

  py_run_string("
try:
    r.abort_without_condition()
except RuntimeError as exc:
    abort_exception = exc
else:
    raise AssertionError('expected an exception')

assert str(abort_exception) == 'R evaluation aborted'
survived_first_abort = True
")

  expect_true(py$survived_first_abort)
  expect_false(py_has_attr(py$abort_exception, "trace"))

  py_run_string("
try:
    r.abort_with_condition()
except RuntimeError as exc:
    condition_exception = exc
else:
    raise AssertionError('expected an exception')

assert str(condition_exception) == 'cancelled from R'
assert r.identity(42) == 42
survived_second_abort = True
")

  expect_true(py$survived_second_abort)
  expect_identical(py$condition_exception$trace, abort_state$trace)

  py_run_string("
try:
    r.ordinary_error()
except RuntimeError as exc:
    ordinary_exception = exc
else:
    raise AssertionError('expected an exception')

assert str(ordinary_exception) == 'ordinary R error'
")

  expect_s3_class(py$ordinary_exception$trace, "rlang_trace")
})


test_that("confirm rlang/purrr can catch the exception", {
  skip_if_no_python()

  cnd <- tryCatch(
    py_run_string("raise ZeroDivisionError"),
    error = identity
  )

  # we already Suggests on rlang
  expect_equal(rlang::cnd_type(cnd), "error")

  # don't want to Suggests on purrr
  requireNamespace_ <- requireNamespace
  if (requireNamespace_("purrr", quietly = TRUE)) {
    expect_error({
      map <- yoink("purrr", "map")
      map(1:3, function(i)
        if (i == 2)
          reticulate::py_run_string("raise Exception('fooobaar')"))
    }, regexp = "In index: 2.*fooobaar")
  }

})
