tests/testthat/test-cpp-parity.R

with_cpp_mode <- function(flag, expr) {
  old <- getOption("immutables.use_cpp")
  options(immutables.use_cpp = flag)
  on.exit({
    if(is.null(old)) {
      options(immutables.use_cpp = NULL)
    } else {
      options(immutables.use_cpp = old)
    }
  }, add = TRUE)
  force(expr)
}

backend_eval <- function(expr, flag, env = parent.frame()) {
  with_cpp_mode(flag, eval(expr, env))
}

expect_backend_identical <- function(expr, transform = identity, env = parent.frame()) {
  q <- substitute(expr)
  cpp <- transform(backend_eval(q, TRUE, env = env))
  r <- transform(backend_eval(q, FALSE, env = env))
  if(!identical(cpp, r)) {
    diff <- capture.output(all.equal(cpp, r))
    stop(
      paste0(
        "Backend parity mismatch for expression:\n",
        deparse(q),
        "\nDiff:\n",
        paste(diff, collapse = "\n")
      )
    )
  }
  testthat::succeed()
}

expect_wrapper_dispatch <- function(wrapper_name, expr, should_dispatch = TRUE, env = parent.frame()) {
  ns <- asNamespace("Immutables")
  flag <- new.env(parent = emptyenv())
  flag$called <- FALSE
  suppressMessages(base::trace(
    what = wrapper_name,
    tracer = bquote(assign("called", TRUE, envir = .(flag))),
    where = ns,
    print = FALSE
  ))
  on.exit(suppressMessages(base::untrace(what = wrapper_name, where = ns)), add = TRUE)
  eval(substitute(expr), envir = env)
  if(should_dispatch) {
    testthat::expect_true(isTRUE(flag$called), info = paste("Expected wrapper dispatch:", wrapper_name))
  } else {
    testthat::expect_false(isTRUE(flag$called), info = paste("Expected no wrapper dispatch:", wrapper_name))
  }
}

snapshot_tree <- function(t) {
  els <- Immutables:::.ft_to_list(t)
  vals <- lapply(els, Immutables:::.ft_strip_name)
  nms <- vapply(els, function(el) {
    nm <- Immutables:::.ft_get_name(el)
    if(is.null(nm)) "" else nm
  }, character(1))
  list(
    class = class(t),
    values = vals,
    names = nms,
    measures = attr(t, "measures", exact = TRUE),
    monoid_names = names(attr(t, "monoids", exact = TRUE))
  )
}

snapshot_split <- function(s) {
  list(
    left = snapshot_tree(s$left),
    value = Immutables:::.ft_strip_name(s$value),
    right = snapshot_tree(s$right)
  )
}

snapshot_locate <- function(x) {
  list(
    found = x$found,
    value = if(isTRUE(x$found)) Immutables:::.ft_strip_name(x$value) else NULL,
    index = x$index,
    left_measure = x$left_measure,
    hit_measure = x$hit_measure,
    right_measure = x$right_measure
  )
}

parity_scenarios <- c(
  "MeasureMonoid constructor/use",
  "predicate constructor/use",
  "empty_tree",
  "tree_from",
  "tree_from unnamed",
  "add_monoids",
  "append",
  "append named",
  "prepend",
  "prepend named",
  "concat_trees",
  "split_around_by_predicate",
  "split_by_predicate",
  "locate_by_predicate",
  "peek/pop_at helpers",
  "insert_at helper",
  "[ read (integer/logical/name)",
  "[[ read (integer/name)",
  "[<- replacement (integer/logical/name)",
  "[[<- replacement and deletion",
  "$ read",
  "$<- replacement",
  "ordered_sequence insert",
  "interval_index insert and queries",
  "print.FingerTree output",
  "get_graph_df",
  "validate_tree",
  "validate_name_state"
)

cpp_wrapper_coverage <- list(
  .ft_cpp_add_right = c("append"),
  .ft_cpp_add_right_named = c("append named"),
  .ft_cpp_add_left = c("prepend"),
  .ft_cpp_add_left_named = c("prepend named"),
  .ft_cpp_tree_from = c("tree_from unnamed"),
  .ft_cpp_tree_from_prepared = c("tree_from"),
  .ft_cpp_tree_from_sorted = c("ordered_sequence insert"),
  .ft_cpp_concat = c("concat_trees", "interval_index insert and queries"),
  .ft_cpp_oms_insert = c("ordered_sequence insert"),
  .ft_cpp_locate = c("locate_by_predicate"),
  .ft_cpp_split_tree = c("split_around_by_predicate", "split_by_predicate", "interval_index insert and queries"),
  .ft_cpp_split_at_index = c("[[ read (integer/name)", "[[<- replacement and deletion", "peek/pop_at helpers"),
  .ft_cpp_find_name_position = c("$ read", "[ read (integer/logical/name)"),
  .ft_cpp_get_by_index = c("[[ read (integer/name)"),
  .ft_cpp_get_many_by_index = c("[ read (integer/logical/name)"),
  .ft_cpp_name_positions = c("[ read (integer/logical/name)")
)

testthat::test_that("backend parity: coverage map includes all cpp wrappers", {
  ns <- asNamespace("Immutables")
  wrappers <- ls(ns, all.names = TRUE)
  wrappers <- wrappers[startsWith(wrappers, ".ft_cpp_")]
  wrappers <- setdiff(
    wrappers,
    c(".ft_cpp_enabled", ".ft_cpp_eligible_monoids", ".ft_cpp_can_use", ".ft_cpp_can_use_oms_insert")
  )

  testthat::expect_setequal(names(cpp_wrapper_coverage), wrappers)

  for(op in names(cpp_wrapper_coverage)) {
    tags <- cpp_wrapper_coverage[[op]]
    for(tag in tags) {
      testthat::expect_true(tag %in% parity_scenarios, info = paste("Missing parity scenario tag:", tag))
    }
  }
})

testthat::test_that("backend parity: forced backend option gates C++ eligibility", {
  ms <- ensure_size_monoids(list(.size = size_measure_monoid()))
  testthat::expect_true(backend_eval(quote(.ft_cpp_can_use(ms)), TRUE))
  testthat::expect_false(backend_eval(quote(.ft_cpp_can_use(ms)), FALSE))
})

testthat::test_that("backend parity: append dispatch respects forced backend mode", {
  backend_eval(
    quote(expect_wrapper_dispatch(".ft_cpp_add_right", {
      t <- as_flexseq(1:6)
      push_back(t, 7)
    }, should_dispatch = TRUE)),
    TRUE
  )

  backend_eval(
    quote(expect_wrapper_dispatch(".ft_cpp_add_right", {
      t <- as_flexseq(1:6)
      push_back(t, 7)
    }, should_dispatch = FALSE)),
    FALSE
  )
})

testthat::test_that("backend parity: MeasureMonoid constructor/use", {
  expect_backend_identical({
    m <- measure_monoid(function(a, b) a + b, 0, as.numeric)
    t <- add_monoids(as_flexseq(1:5), list(sum = m))
    as.integer(node_measure(t, "sum"))
  })
})

testthat::test_that("backend parity: empty_tree", {
  expect_backend_identical({
    snapshot_tree(flexseq())
  })
})

testthat::test_that("backend parity: tree_from", {
  expect_backend_identical({
    ms <- list(sum = measure_monoid(function(a, b) a + b, 0, as.numeric))
    snapshot_tree(add_monoids(as_flexseq(setNames(as.list(1:8), letters[1:8])), ms))
  })
})

testthat::test_that("backend parity: tree_from unnamed", {
  expect_backend_identical({
    ms <- list(sum = measure_monoid(function(a, b) a + b, 0, as.numeric))
    snapshot_tree(add_monoids(as_flexseq(as.list(1:8)), ms))
  })
})

testthat::test_that("backend parity: tree_from prepared names type check", {
  ms <- ensure_size_monoids(list(.size = size_measure_monoid()))
  testthat::expect_error(
    .ft_cpp_tree_from_prepared(as.list(1:2), list("a", "b"), ms),
    "character vector"
  )
})

testthat::test_that("backend parity: add_monoids", {
  expect_backend_identical({
    t <- as_flexseq(1:8)
    m <- measure_monoid(function(a, b) a + b, 0, as.numeric)
    snapshot_tree(add_monoids(t, list(sum = m)))
  })
})

testthat::test_that("backend parity: append", {
  expect_backend_identical({
    t <- as_flexseq(1:10)
    t <- push_back(t, 11)
    snapshot_tree(t)
  })
})

testthat::test_that("backend parity: append named", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(1:4), LETTERS[1:4]))
    t <- push_back(t, structure(5, names = "E"))
    snapshot_tree(t)
  })
})

testthat::test_that("backend parity: prepend", {
  expect_backend_identical({
    t <- as_flexseq(1:10)
    t <- push_front(t, 0)
    snapshot_tree(t)
  })
})

testthat::test_that("backend parity: prepend named", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(1:4), LETTERS[1:4]))
    t <- push_front(t, structure(0, names = "Z"))
    snapshot_tree(t)
  })
})

testthat::test_that("backend parity: concat_trees", {
  expect_backend_identical({
    t1 <- as_flexseq(1:10)
    t2 <- as_flexseq(11:20)
    snapshot_tree(c(t1, t2))
  })
})

testthat::test_that("backend parity: split_around_by_predicate", {
  expect_backend_identical({
    t <- as_flexseq(letters[1:20])
    snapshot_split(split_around_by_predicate(t, function(v) v >= 8, ".size"))
  })
})

testthat::test_that("backend parity: split_by_predicate", {
  expect_backend_identical({
    t <- as_flexseq(letters[1:20])
    s <- split_by_predicate(t, function(v) v >= 8, ".size")
    list(left = snapshot_tree(s$left), right = snapshot_tree(s$right))
  })
})

testthat::test_that("backend parity: locate_by_predicate", {
  expect_backend_identical({
    t <- as_flexseq(letters[1:20])
    snapshot_locate(locate_by_predicate(t, function(v) v >= 10, ".size", include_metadata = TRUE))
  })
})

testthat::test_that("backend parity: peek/pop_at helpers", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(letters[1:8]), LETTERS[1:8]))
    out <- pop_at(t, 5)
    list(
      peek = peek_at(t, 3),
      popped = out$value,
      remaining = snapshot_tree(out$remaining)
    )
  })
})

testthat::test_that("backend parity: insert_at helper", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(letters[1:8]), LETTERS[1:8]))
    out <- insert_at(t, 4, setNames(as.list(c("x", "y")), c("X1", "X2")))
    snapshot_tree(out)
  })
})

testthat::test_that("backend parity: [ read (integer/logical/name)", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(letters[1:12]), LETTERS[1:12]))
    list(
      int = snapshot_tree(t[c(2, 8, 8, 1)]),
      lgl = snapshot_tree(t[c(TRUE, FALSE, TRUE)]),
      chr = snapshot_tree(t[c("H", "A", "missing", "C")])
    )
  })
})

testthat::test_that("backend parity: [[ read (integer/name)", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(letters[1:12]), LETTERS[1:12]))
    list(int = t[[3]], chr = t[["C"]])
  })
})

testthat::test_that("backend parity: [<- replacement (integer/logical/name)", {
  expect_backend_identical({
    t1 <- as_flexseq(1:12)
    t1[c(2, 5, 5)] <- list(200, 500, 501)
    t1[c(TRUE, FALSE, TRUE)] <- list(9, 8)

    t2 <- as_flexseq(setNames(as.list(letters[1:8]), LETTERS[1:8]))
    t2[c("B", "D")] <- list("bb", "dd")

    list(int_lgl = snapshot_tree(t1), chr = snapshot_tree(t2))
  })
})

testthat::test_that("backend parity: [[<- replacement and deletion", {
  expect_backend_identical({
    t1 <- as_flexseq(1:10)
    t1[[4]] <- 99
    t1[[6]] <- NULL

    t2 <- as_flexseq(setNames(as.list(letters[1:8]), LETTERS[1:8]))
    t2[["C"]] <- structure("cc", names = "Z")
    t2[["E"]] <- NULL

    list(int = snapshot_tree(t1), chr = snapshot_tree(t2))
  })
})

testthat::test_that("backend parity: $ read", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(letters[1:8]), LETTERS[1:8]))
    list(A = t$A, C = t$C)
  })
})

testthat::test_that("backend parity: $<- replacement", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(letters[1:8]), LETTERS[1:8]))
    t$B <- "bbb"
    snapshot_tree(t)
  })
})

testthat::test_that("backend parity: ordered_sequence insert", {
  expect_backend_identical({
    x <- as_ordered_sequence(list("aa", "bb", "c", "ddd"), keys = c(2, 2, 1, 3))
    out <- insert(x, "qq", key = 2)
    as.list(out)
  })
})

testthat::test_that("backend parity: ordered_sequence Date keys use consistent fallback path", {
  expect_backend_identical({
    x <- as_ordered_sequence(
      list("late", "early1", "early2"),
      keys = as.Date(c("2024-01-03", "2024-01-01", "2024-01-01"))
    )
    y <- insert(x, "early3", key = as.Date("2024-01-01"))
    list(
      values = as.list(y),
      lower = lower_bound(y, as.Date("2024-01-01")),
      upper = upper_bound(y, as.Date("2024-01-01"))
    )
  })
})

testthat::test_that("backend parity: interval_index insert and queries", {
  expect_backend_identical({
    x <- as_interval_index(
      list("a", "b", "c"),
      start = c(1, 2, 2),
      end = c(3, 2, 4),
      default_query_bounds = "[]"
    )
    y <- insert(x, "d", start = 2, end = 5)

    p0 <- pop_point(y, 2)
    p1 <- pop_overlaps(y, 2, 3)
    p2 <- pop_all_within(y, 2, 5)
    bounds_tokens <- c("[)", "[]", "()", "(]")
    bounds_matrix <- lapply(bounds_tokens, function(bt) {
      list(
        token = bt,
        point = as.list(peek_all_point(y, 2, bounds = bt)),
        overlaps = as.list(peek_all_overlaps(y, 2, 3, bounds = bt)),
        containing = as.list(peek_all_containing(y, 2, 3, bounds = bt)),
        within = as.list(peek_all_within(y, 2, 3, bounds = bt)),
        pop_point = as.list(pop_all_point(y, 2, bounds = bt)$elements),
        pop_overlaps = as.list(pop_all_overlaps(y, 2, 3, bounds = bt)$elements),
        pop_containing = as.list(pop_all_containing(y, 2, 3, bounds = bt)$elements),
        pop_within = as.list(pop_all_within(y, 2, 3, bounds = bt)$elements)
      )
    })

    match_at_tokens <- c("start", "end", "either")
    match_at_matrix <- lapply(match_at_tokens, function(ma) {
      list(
        mode = ma,
        peek = as.list(peek_all_point(y, 2, match_at = ma)),
        peek_first = peek_point(y, 2, match_at = ma),
        pop = as.list(pop_all_point(y, 2, match_at = ma)$elements),
        pop_remaining = as.list(pop_all_point(y, 2, match_at = ma)$remaining)
      )
    })

    list(
      values = as.list(y),
      intervals = lapply(.ivx_entries(y), function(e) list(start = e$start, end = e$end)),
      bounds_matrix = bounds_matrix,
      match_at_matrix = match_at_matrix,
      point_first = peek_point(y, 2),
      point = as.list(peek_all_point(y, 2)),
      overlaps_first = peek_overlaps(y, 2, 3),
      overlaps = as.list(peek_all_overlaps(y, 2, 3)),
      containing_first = peek_containing(y, 2, 3),
      containing = as.list(peek_all_containing(y, 2, 3)),
      within_first = peek_within(y, 2, 5),
      within = as.list(peek_all_within(y, 2, 5)),
      pop_point_first = list(
        value = p0$value,
        start = p0$start,
        end = p0$end,
        remaining = as.list(p0$remaining)
      ),
      pop_first = list(
        value = p1$value,
        start = p1$start,
        end = p1$end,
        remaining = as.list(p1$remaining)
      ),
      pop_all = list(
        elements = as.list(p2$elements),
        remaining = as.list(p2$remaining)
      )
    )
  })
})

testthat::test_that("backend parity: interval_index user monoid recomputation", {
  expect_backend_identical({
    sum_item <- measure_monoid(function(a, b) a + b, 0, function(entry) as.numeric(entry$value))
    width_sum <- measure_monoid(function(a, b) a + b, 0, function(entry) as.numeric(entry$end - entry$start))

    x <- add_monoids(as_interval_index(
      as.list(c(10, 20, 30)),
      start = c(1, 2, 4),
      end = c(3, 5, 6)
    ), list(sum_item = sum_item, width_sum = width_sum))
    y <- insert(x, 40, start = 3, end = 4)
    z <- fapply(y, function(value, start, end, name) value + 1)
    s <- peek_all_overlaps(z, 2, 3, bounds = "[)")
    p <- pop_all_overlaps(z, 2, 3, bounds = "[)")

    list(
      base = c(sum_item = node_measure(x, "sum_item"), width_sum = node_measure(x, "width_sum")),
      inserted = c(sum_item = node_measure(y, "sum_item"), width_sum = node_measure(y, "width_sum")),
      applied = c(sum_item = node_measure(z, "sum_item"), width_sum = node_measure(z, "width_sum")),
      slice = c(sum_item = node_measure(s, "sum_item"), width_sum = node_measure(s, "width_sum")),
      pop_element = c(
        sum_item = node_measure(p$elements, "sum_item"),
        width_sum = node_measure(p$elements, "width_sum")
      ),
      pop_remaining = c(
        sum_item = node_measure(p$remaining, "sum_item"),
        width_sum = node_measure(p$remaining, "width_sum")
      ),
      slice_values = as.list(s),
      pop_values = as.list(p$elements),
      remaining_values = as.list(p$remaining)
    )
  })
})

testthat::test_that("backend parity: print.FingerTree output", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(1:10), letters[1:10]))
    capture.output(print(t))
  })
})

testthat::test_that("backend parity: get_graph_df", {
  expect_backend_identical({
    t <- as_flexseq(letters[1:12])
    get_graph_df(t)
  })
})

testthat::test_that("backend parity: validate_tree", {
  expect_backend_identical({
    t <- as_flexseq(letters[1:10])
    isTRUE(validate_tree(t))
  })
})

testthat::test_that("backend parity: validate_name_state", {
  expect_backend_identical({
    t <- as_flexseq(setNames(as.list(letters[1:10]), LETTERS[1:10]))
    isTRUE(validate_name_state(t))
  })
})

Try the Immutables package in your browser

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

Immutables documentation built on April 29, 2026, 1:06 a.m.