tests/testthat/test-method-dispatch.R

describe("single dispatch", {
  foo <- new_generic("foo", "x")

  it("works for specials", {
    method(foo, class_any) <- function(x) "fallback"
    expect_equal(foo(), "fallback")
    expect_equal(foo(1), "fallback")

    method(foo, class_missing) <- function(x) "missing"
    expect_equal(foo(), "missing")
  })

  it("works for base types", {
    method(foo, class_character) <- function(x) "base"

    expect_equal(foo("bar"), "base")
  })

  it("works for S7 objects", {
    text <- new_class("text", class_character)
    method(foo, text) <- function(x) "S7"

    expect_equal(foo(text("bar")), "S7")
  })

  it("works for S3 objects", {
    obj <- structure("hi", class = "my_S3")
    method(foo, new_S3_class("my_S3")) <- function(x) "S3"

    expect_equal(foo(obj), "S3")
  })

  it("works for S4 objects", {
    my_S4 <- setClass("my_S4", contains = "numeric")
    method(foo, my_S4) <- function(x) "S4"

    expect_equal(foo(my_S4(1)), "S4")
  })

  it("works for unions", {
    method(foo, new_union(class_integer, class_logical)) <- function(x) "union"

    expect_equal(foo(TRUE), "union")
    expect_equal(foo(1L), "union")
  })
})

describe("multiple dispatch", {
  it("works", {
    foo1 <- new_class("foo1")
    foo2 <- new_class("foo2", foo1)

    bar <- new_generic("bar", c("x", "y"))
    method(bar, list(foo1, foo1)) <- function(x, y) c(1, 1)
    method(bar, list(foo2, foo2)) <- function(x, y) c(2, 2)

    expect_equal(bar(foo1(), foo1()), c(1, 1))
    expect_equal(bar(foo1(), foo2()), c(1, 1))
    expect_equal(bar(foo2(), foo1()), c(1, 1))
    expect_equal(bar(foo2(), foo2()), c(2, 2))
  })
})


test_that("can substitute() args", {
  foo <- new_generic("foo", "x", function(x, ..., z = 1) S7_dispatch())
  method(foo, class_character) <- function(x, ..., z = 1) substitute(x)
  expect_equal(foo(letters), quote(letters))

  suppressMessages(
    method(foo, class_character) <- function(x, ..., z = 1, y) substitute(y)
  )
  expect_equal(foo("x", y = letters), quote(letters))

  suppressMessages(
    method(foo, class_character) <- function(x, ..., z = 1) substitute(z)
  )
  expect_equal(foo("x", z = letters), quote(letters))

  suppressMessages(
    method(foo, class_character) <- function(x, ..., z = 1) substitute(list(...))
  )
  expect_equal(foo("x", abc = xyz), quote(list(abc = xyz)))

  suppressMessages(
    method(foo, class_character) <- function(x, ..., z = 1, y) missing(y)
  )
  expect_true(foo("x"), TRUE)
  expect_true(foo("x", y =), TRUE)
    expect_true(foo("x", y =), TRUE)

  suppressMessages(
    method(foo, class_character) <- function(x, ..., z = 1, y) ...length()
  )

  expect_equal(foo("x"), 0)
  expect_equal(foo("x", y =), 0)
  expect_equal(foo("x", y =, abc), 1)
  expect_equal(foo("x", y =, abc = xyz), 1)
  expect_equal(foo("x", y =, abc, xyz), 2)
})

test_that("methods get values modified in the generic", {
  foo <- new_generic("foo", "x", function(x, y = 1) {
    y <- 10
    S7_dispatch()
  })
  method(foo, class_character) <- function(x, y = 1) y
  expect_equal(foo("x", 1), 10)
})

test_that("dispatched arguments are evaluated once", {
  counter <- local({
    i <- 0
    function() {
      i <<- i + 1
      i
    }
  })

  f <- new_generic("f", "x")
  method(f, class_double) <- function(x) x
  expect_equal(f(counter()), 1)
})

test_that("generics pass ... to methods", {
  foo <- new_generic("foo", "x")

  method(foo, class_character) <- function(x, y = 1) y
  expect_equal(foo("x"), 1)
  expect_equal(foo("x", y = 2), 2)
  expect_snapshot_error(foo("x", z = 2))
})

test_that("generics pass extra args to methods", {
  foo <- new_generic("foo", "x", function(x, ..., z = 1) S7_dispatch())
  method(foo, class_character) <- function(x, ..., z = 1) z
  expect_equal(foo("x", z = 3), 3)
})

test_that("can dispatch on base 'union' types", {
  foo <- new_generic("foo", "x")
  suppressMessages({
    method(foo, class_vector) <- function(x) "v"
    method(foo, class_atomic) <- function(x) "a"
    method(foo, class_numeric) <- function(x) "n"
    method(foo, class_integer) <- function(x) "i"
  })

  expect_equal(foo(list()), "v")
  expect_equal(foo(character()), "a")
  expect_equal(foo(double()), "n")
  expect_equal(foo(integer()), "i")
})

test_that("single dispatch fails with informative messages", {
  fail <- new_generic("fail", "x")

  foo <- new_class("foo", package = NULL)
  Foo <- setClass("Foo", slots = list("x" = "numeric"))
  on.exit(S4_remove_classes("Foo"))

  expect_snapshot(error = TRUE, {
    fail(TRUE)
    fail(tibble::tibble())
    fail(foo())
    fail(Foo(x = 1))
  })

  expect_error(fail(TRUE), class = "S7_error_method_not_found")
})

test_that("multiple dispatch fails with informative messages", {
  fail <- new_generic("fail", c("x", "y"))

  foo <- new_class("foo")
  Foo <- setClass("Foo", slots = list("x" = "numeric"))
  on.exit(S4_remove_classes("Foo"))

  expect_snapshot(error = TRUE, {
    fail(TRUE)
    fail(, TRUE)
    fail(TRUE, TRUE)
  })

  expect_error(fail(TRUE, TRUE), class = "S7_error_method_not_found")
})


test_that("method dispatch preserves method return visibility", {
  foo <- new_generic("foo", "x")
  method(foo, class_integer) <- function(x) invisible("bar")
  expect_invisible(foo(1L))

  method(foo, class_character) <- function(x) {
    if (x == "nope") return(invisible("bar"))
    "bar"
  }

  expect_visible(foo("yep"))
  expect_invisible(foo("nope"))
})

test_that("can dispatch on evaluated arguments", {
  my_generic <- new_generic("my_generic", "x", function(x) {
    x <- 10
    S7_dispatch()
  })
  method(my_generic, class_numeric) <- function(x) 100
  expect_equal(my_generic("x"), 100)
})


test_that("method dispatch works for class_missing", {

  foo <- new_generic("foo", "x")
  method(foo, class_missing) <- function(x) missing(x)

  expect_true(foo())

  # dispatch on class_missing only works directly in the generic call
  foo_wrapper <- function(xx) foo(xx)
  expect_snapshot(
    error = TRUE,
    variant = if (getRversion() < "4.3") "R-lt-4-3",
    foo_wrapper()
  )
})

test_that("errors from dispatched methods have reasonable tracebacks", {

  get_call_stack <- function(n = 3) {
    x <- sys.calls()
    x <- x[-length(x)] # remove get_call_stack()
    x <- tail(x, n)
    lapply(x, utils::removeSource)
  }

  my_generic <- new_generic("my_generic", "x")
  method(my_generic, class_numeric) <- function(x) get_call_stack()
  expect_snapshot(my_generic(10))

  my_generic <- new_generic("my_generic", c("x", "y"))
  method(my_generic, list(class_numeric, class_numeric)) <-
    function(x, y) get_call_stack()
  expect_snapshot(my_generic(3, 4))
})

Try the S7 package in your browser

Any scripts or data that you put into this service are public.

S7 documentation built on April 3, 2025, 10:50 p.m.