tests/testthat/test-cnd-message.R

test_that("format_error_bullets() formats bullets depending on names", {
  expect_equal(format_error_bullets(c("foo", "bar")), "* foo\n* bar")
  expect_equal(format_error_bullets(c(i = "foo", "*" = "baz", x = "bar", v = "bam")), "i foo\n* baz\nx bar\nv bam")
  expect_equal(format_error_bullets(c(i = "foo", u = "bar")), "i foo\nbar")
  expect_equal(format_error_bullets(chr()), chr())
})

test_that("default conditionMessage() method for rlang errors calls cnd_message()", {
  # Fallback
  out <- conditionMessage(error_cnd("rlang_foobar", message = "embedded"))
  expect_identical(out, "embedded")

  # Only `cnd_header()` method
  out <- with_methods(
    cnd_header.rlang_foobar = function(cnd, ...) "dispatched!",
    conditionMessage(error_cnd("rlang_foobar", message = "embedded"))
  )
  expect_identical(out, "dispatched!")

  # Both `cnd_header()` and `cnd_body()` methods
  out <- with_methods(
    cnd_header.rlang_foobar = function(cnd, ...) "dispatched!",
    cnd_body.rlang_foobar = function(cnd, ...) c("one", "two", "three"),
    conditionMessage(error_cnd("rlang_foobar", message = "embedded"))
  )
  exp <- paste0("dispatched!\n", paste_line(c("one", "two", "three")))
  expect_identical(out, exp)

  # All three methods defined
  out <- with_methods(
    cnd_header.rlang_foobar = function(cnd, ...) "dispatched!",
    cnd_body.rlang_foobar = function(cnd, ...) c("one", "two", "three"),
    cnd_footer.rlang_foobar = function(cnd, ...) c("foo", "bar"),
    conditionMessage(error_cnd("rlang_foobar", message = "embedded"))
  )
  exp <- paste0(exp, "\nfoo\nbar")
  expect_identical(out, exp)
})

test_that("can override header, body, and footer methods with fields", {
  local_methods(cnd_body.rlang_foobar = function(...) "wrong")

  expect_error(
    stop(error_cnd(
      "rlang_foobar",
      message = "header",
      body = "body"
    )),
    "header\nbody",
    class = "rlang_foobar"
  )
  expect_error(
    stop(error_cnd(
      "rlang_foobar",
      header = "header",
      body = "body",
      footer = "footer"
    )),
    "header\nbody\nfooter",
    class = "rlang_foobar"
  )

  # Also messages and warnings
  expect_message(
    message(message_cnd(
      "rlang_foobar",
      header = ~ "header",
      body = ~ "body",
      footer = ~ "footer"
    )),
    "header\nbody\nfooter",
    class = "rlang_foobar"
  )
  expect_warning(
    warning(warning_cnd(
      "rlang_foobar",
      header = function(...) "header",
      body = function(...) "body",
      footer = function(...) "footer"
    )),
    "header\nbody\nfooter",
    class = "rlang_foobar"
  )

  expect_error(
    stop(error_cnd("rlang_foobar", message = "header", body = ~ format_error_bullets("body"))),
    "header\n* body",
    fixed = TRUE,
    class = "rlang_foobar"
  )
})

test_that("`body` must be a character vector or a function", {
  expect_snapshot({
    (expect_error(
      stop(error_cnd("foo", body = 1:3)),
      "must be"
    ))
  })
})

test_that("can request a line break in error bullets (#1130)", {
  expect_snapshot({
    (expect_error(abort(c(
      "Main header.",
      "Header 1",
      x = "Bullet 1",
      x = "Bullet 2",
      "Header 2",
      x = "Bullet 3",
      x = "Bullet 4"
    ))))

    (expect_error(abort(c(
      "Main header.",
      "Header 1",
      "x" = "Bullet 1",
      " " = "Break line",
      "x" = "Bullet 2",
      "",
      "Header 2",
      "x" = "Bullet 3",
      " " = "Break line",
      "x" = "Bullet 4"
    ))))
  })
})

test_that("fully unnamed bullet vectors are treated as bullets", {
  expect_equal(
    format_error_bullets("foo"),
    "* foo"
  )
  expect_equal(
    format_error_bullets(c("foo", "bar")),
    "* foo\n* bar"
  )

  non_bullets <- set_names(c("foo", "bar"), c("", ""))
  expect_equal(
    format_error_bullets(non_bullets),
    "foo\nbar"
  )
})

test_that("empty names in partially named bullet vectors are treated as line breaks", {
  expect_equal(
    format_error_bullets(c("foo", i = "bar", "baz")),
    "foo\ni bar\nbaz"
  )
  expect_equal(
    format_error_bullets(c(i = "bar", "baz")),
    "i bar\nbaz"
  )
})

test_that("! and > symbols create warning and alert bullets", {
  expect_equal(
    format_error_bullets(c("Header", "!" = "Attention", ">" = "Alert")),
    "Header\n! Attention\n> Alert"
  )
})

test_that("cli is not used when message is escaped with `I()`", {
  local_use_cli(inline = TRUE)

  x <- "foo"

  expect_equal(
    conditionMessage(expect_error(abort("{x}"))),
    "foo"
  )

  return("no longer the case")

  expect_equal(
    conditionMessage(expect_error(abort(I("{x}")))),
    "{x}"
  )
})

test_that(".rlang_cli_str_restore() deals with attributes", {
  msg <- structure("foo", attr = TRUE)

  expect_equal(
    .rlang_cli_str_restore("bar", msg),
    structure("bar", attr = TRUE)
  )

  msg_oo <- structure("foo", attr = TRUE, class = "foo")
  expect_equal(
    .rlang_cli_str_restore("bar", msg_oo),
    "bar"
  )
})

skip_if_not_installed("cli", "2.5.0")
skip_if_not_installed("glue")

cli::test_that_cli("format_error_bullets() generates bullets", {
  expect_snapshot({
    format_error_bullets(c("Header.", i = "Bullet."))
  })
})

cli::test_that_cli(configs = c("plain", "fancy"), "can use cli syntax in `cnd_message()` methods", {
  local_methods(
    cnd_header.rlang_foobar = function(cnd, ...) {
      cli::format_inline("Header: {.emph {cnd$field}}")
    },
    cnd_body.rlang_foobar = function(cnd, ...) {
      c("i" = cli::format_inline("Bullet: {.emph {cnd$field}}"))
    },
    cnd_footer.rlang_foobar = function(cnd, ...) {
      c("_" = cli::format_inline("i" = "Footer: {.emph {cnd$field}}"))
    }
  )
  cnd <- error_cnd(
    "rlang_foobar",
    field = "User { {field}.",
    use_cli_format = TRUE
  )
  expect_snapshot(cnd_message(cnd))
})

test_that("prefix takes call into account", {
  expect_snapshot({
    err <- error_cnd(message = "msg", call = quote(foo(bar = TRUE)))
    writeLines(cnd_message_format_prefixed(err))

    # Inlined objects disable context deparsing
    err1 <- error_cnd(message = "msg", call = expr(foo(bar = !!(1:3))))
    err2 <- error_cnd(message = "msg", call = quote(foo$bar()))
    err3 <- error_cnd(message = "msg", call = call2(identity))
    writeLines(cnd_message_format_prefixed(err1))
    writeLines(cnd_message_format_prefixed(err2))
    writeLines(cnd_message_format_prefixed(err3))
  })
})

test_that("long prefixes cause a line break", {
  very_very_very_very_very_long_function_name <- function() {
    abort("My somewhat longish and verbose error message.")
  }

  expect_snapshot((expect_error(very_very_very_very_very_long_function_name())))
})

test_that("prefixes include srcrefs", {
  withr::local_envvar("TESTTHAT" = "")

  eval_parse("{
    f <- function() g()
    g <- function() abort('Foo.')
  }")

  src_file <- g %@% srcref %@% srcfile
  src_file$filename <- "/foo/bar/baz/myfile.R"

  expect_snapshot((expect_error(f())))
})

test_that("inform() and warn() use fallback bullets formatting", {
  msg <- c("foo", i = "bar")

  expect_snapshot({
    local_use_cli(format = FALSE)
    warn(msg)
    warn(msg, .frequency = "once", .frequency_id = as.character(runif(1)))
  })

  expect_snapshot({
    local_use_cli(format = TRUE)
    warn(msg)
    warn(msg, .frequency = "once", .frequency_id = as.character(runif(1)))
  })

  expect_snapshot({
    local_use_cli(format = FALSE)
    inform(msg)
    inform(msg, .frequency = "once", .frequency_id = as.character(runif(1)))
  })

  expect_snapshot({
    local_use_cli(format = TRUE)
    inform(msg)
    inform(msg, .frequency = "once", .frequency_id = as.character(runif(1)))
  })
})

test_that("cnd_message() uses `body` and `footer` fields by default", {
  expect_equal(
    cnd_message(cnd("foo", message = "foo", footer = "baz")),
    "foo\nbaz"
  )
  expect_equal(
    cnd_message(cnd("foo", message = "foo", body = "bar", footer = "baz")),
    "foo\nbar\nbaz"
  )
})

test_that("can supply bullet without header", {
  expect_snapshot({
    (catch_cnd(inform(c(i = "foo")), "message"))
    (catch_cnd(warn(c(i = "foo")), "warning"))
  })
})

test_that("parent errors prints with bullets in all cases", {
  f <- function(use_cli = TRUE) {
    local_use_cli(format = use_cli)

    try_fetch(
      abort(c(
        "Header",
        i = "Bullet"
      )),
      error = function(cnd) {
        abort("Wrapper", parent = cnd)
      }
    )
  }

  expect_snapshot(error = TRUE, cnd_class = TRUE, {
    f(TRUE)
    f(FALSE)
  })
})

test_that("qualified calls are included in error prefix (#1315)", {
  expect_equal(
    error_call_as_string(quote(foo::bar())),
    "foo::bar()"
  )
})

test_that("special syntax calls handle edge cases", {
  expect_snapshot({
    error_call_as_string(quote(`+`()))
    error_call_as_string(quote(base::`+`(1, 2)))
  })
})

test_that("can print message with and without prefix", {
  expect_snapshot(cran = TRUE, {
    foo <- error_cnd(
      "foo",
      message = "Parent message.",
      body = c("*" = "Bullet 1.", "*" = "Bullet 2."),
      use_cli_format = TRUE
    )
    bar <- error_cnd(
      "bar",
      message = "Message.",
      body = c("*" = "Bullet A.", "*" = "Bullet B."),
      parent = foo,
      use_cli_format = TRUE
    )

    writeLines(cnd_message(foo, prefix = TRUE))
    writeLines(cnd_message(bar, prefix = TRUE))

    writeLines(cnd_message(foo, prefix = FALSE))
    writeLines(cnd_message(bar, prefix = FALSE))
  })
})

test_that("can print message without inheritance", {
  expect_snapshot(cran = TRUE, {
    foo <- error_cnd(
      "foo",
      message = "Parent message.",
      body = c("*" = "Bullet 1.", "*" = "Bullet 2."),
      use_cli_format = TRUE
    )
    bar <- error_cnd(
      "bar",
      message = "Message.",
      body = c("*" = "Bullet A.", "*" = "Bullet B."),
      parent = foo,
      use_cli_format = TRUE
    )

    writeLines(cnd_message(foo, inherit = FALSE, prefix = TRUE))
    writeLines(cnd_message(bar, inherit = FALSE, prefix = TRUE))

    writeLines(cnd_message(foo, inherit = FALSE, prefix = FALSE))
    writeLines(cnd_message(bar, inherit = FALSE, prefix = FALSE))
  })
})

test_that("ANSI escapes are supported in `conditionMessage()`", {
  skip_if_not_installed("cli")

  foo <- error_cnd(
    "foo",
    message = "Parent message.",
    use_cli_format = TRUE
  )
  bar <- error_cnd(
    "bar",
    message = "Message.",
    parent = foo,
    use_cli_format = TRUE
  )

  testthat::local_reproducible_output(
    unicode = FALSE,
    crayon = FALSE
  )
  out_bare <- conditionMessage(bar)

  testthat::local_reproducible_output(
    unicode = TRUE,
    crayon = TRUE
  )
  out_ansi <- conditionMessage(bar)

  expect_equal(out_bare, cli::ansi_strip(out_ansi))
})

test_that("as.character() and conditionMessage() methods for errors, warnings, and messages", {
  parent_cnd <- error_cnd(
    "foo",
    message = "Parent message.",
    body = c("*" = "Bullet 1.", "*" = "Bullet 2."),
    call = call("foo"),
    use_cli_format = TRUE
  )

  cnd_with <- function(ctor, parent = FALSE) {
    ctor(
      "bar",
      message = "Message.",
      body = c("*" = "Bullet A.", "*" = "Bullet B."),
      call = call("bar"),
      parent = if (parent) parent_cnd,
      use_cli_format = TRUE
    )
  }

  expect_snapshot(cran = TRUE, {
    cat(as.character(cnd_with(error_cnd)))
    cat(as.character(cnd_with(warning_cnd)))
    cat(as.character(cnd_with(message_cnd)))

    cat(as.character(cnd_with(error_cnd, parent = TRUE)))
    cat(as.character(cnd_with(warning_cnd, parent = TRUE)))
    cat(as.character(cnd_with(message_cnd, parent = TRUE)))

    cat(conditionMessage(cnd_with(error_cnd)))
    cat(conditionMessage(cnd_with(warning_cnd)))
    cat(conditionMessage(cnd_with(message_cnd)))

    cat(conditionMessage(cnd_with(error_cnd, parent = TRUE)))
    cat(conditionMessage(cnd_with(warning_cnd, parent = TRUE)))
    cat(conditionMessage(cnd_with(message_cnd, parent = TRUE)))
  })
})

test_that("multiline operator calls are preserved", {
  err <- function(expr) error_cnd(message = "This is the error message.", call = enexpr(expr))

  expect_snapshot_output(err(1 + ("veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery_long" + "veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery_long")))
  expect_snapshot_output(err({ 1; 2 } + { 2; 3 }))
  expect_snapshot_output(err(x[{ 1; 2 }]))
})

test_that("eval_tidy() is not mentioned in calls", {
  expect_null(format_error_call(quote(eval_tidy(expr))))
})

test_that("header, body, and footer don't partial-match", {
  expect_equal(
    cnd_header(error_cnd("foo", headers = 1)),
    ""
  )
  expect_equal(
    cnd_body(error_cnd("foo", bodyy = 1)),
    chr()
  )
  expect_equal(
    cnd_footer(error_cnd("foo", footers = 1)),
    chr()
  )
})

test_that("can disable srcrefs in call formatting", {
  withr::local_envvar(c("TESTTHAT" = "false"))
  local_options(rlang_call_format_srcrefs = FALSE)

  with_srcref("{
    f <- function() { g() }
    g <- function() abort('foo')
  }")

  expect_snapshot(err(f()))
})

test_that("fallback method supports unknown bullets (#1364)", {
  local_use_cli(format = FALSE)
  expect_snapshot({
    "With fallback"
    (expect_error(abort(c("foo", i2 = "bar"))))
    (expect_error(abort(c(i1 = "foo", i2 = "bar"))))
  })

  local_use_cli(format = TRUE)
  expect_snapshot({
    "With cli"
    (expect_error(abort(c("foo", i2 = "bar"))))
    (expect_error(abort(c(i1 = "foo", i2 = "bar"))))
  })
})

test_that("`cnd_message(prefix = TRUE)` propagates warning style across parent errors (#1387)", {
  local_options(cli.num_colors = 8)

  hnd_message <- function(cnd) cnd_message(cnd, prefix = TRUE)

  msg_warning <- try_fetch(
    error = function(cnd) warn("foo", parent = cnd),
    condition = hnd_message,
    abort("bar")
  )
  msg_error <- try_fetch(
    error = function(cnd) abort("foo", parent = cnd),
    condition = hnd_message,
    abort("bar")
  )

  expect_false(grepl("\033\\[1mCaused by error", msg_warning))
  expect_true(grepl("\033\\[1mCaused by error", msg_error))
})

test_that("arguments are highlighted but code spans are not", {
  local_options("rlang:::trace_test_highlight" = TRUE)

  err <- error_cnd(header = function(cnd) sprintf(
    "%s - %s - %s",
    format_arg("arg1"),
    format_code("code"),
    format_arg("arg2")
  ))

  expect_snapshot({
    with_error_arg_highlight(
      print(err)
    )
  })
})

test_that("chained errors may have empty messages", {
  parent <- error_cnd(message = "Tilt.")
  child <- error_cnd(parent = parent)
  expect_snapshot({
    print(child)
    cat_line(cnd_message(child, prefix = TRUE))
  })

  # This is the intended usage
  child <- error_cnd(call = call("foo"), parent = parent)
  expect_snapshot({
    print(child)
    cat_line(cnd_message(child, prefix = TRUE))
  })

  # Irrelevant calls are considered as NULL
  child <- error_cnd(call = call("eval"), parent = parent)
  expect_snapshot({
    print(child)
    cat_line(cnd_message(child, prefix = TRUE))
  })
})

test_that("`cnd_message()` returns a single string", {
  local_interactive(TRUE)

  f <- function(do) g(do)
  g <- function(do) h(do)
  h <- function(do) do("foo")

  cnd <- catch_cnd(f(abort))
  cnd <- cnd_set_backtrace_on_error(cnd, "reminder")
  expect_length(cnd_message(cnd), 1)

  class(cnd) <- c("rlang_warning", "warning", "condition")
  expect_length(cnd_message(cnd), 1)
})
tidyverse/rlang documentation built on Oct. 31, 2024, 5:35 p.m.