tests/testthat/test-cpp-gc-safety.R

.gc_stress_enabled <- function() {
  # Never run on CRAN even if the opt-in env var is somehow set on a CRAN
  # machine: `NOT_CRAN` is conventionally "true" only in interactive / CI
  # contexts, so treating a missing / non-"true" value as "this is CRAN"
  # gives belt-and-suspenders protection alongside the skip_on_cran() call
  # inside `.skip_if_no_gc_stress()`.
  if(!identical(Sys.getenv("NOT_CRAN"), "true")) return(FALSE)
  gc_stress <- tolower(Sys.getenv("IMMUTABLES_GC_STRESS", unset = "0"))
  gc_stress %in% c("1", "true", "yes", "on")
}

.skip_if_no_gc_stress <- function() {
  testthat::skip_on_cran()
  if(!isTRUE(.gc_cpp_run$enabled)) {
    testthat::skip("Set IMMUTABLES_GC_STRESS=1 to run GC torture regression.")
  }
}

.run_gc_stress_suite <- function() {
  if(!.gc_stress_enabled()) {
    return(list(enabled = FALSE, setup_error = NULL, results = list()))
  }

  results <- list()
  setup_error <- NULL

  tryCatch({
    old_cpp <- getOption("immutables.use_cpp")
    options(immutables.use_cpp = FALSE)
    on.exit({
      if(is.null(old_cpp)) {
        options(immutables.use_cpp = NULL)
      } else {
        options(immutables.use_cpp = old_cpp)
      }
    }, add = TRUE)

    base_plain <- as_flexseq(as.list(1:8))
    ms <- attr(base_plain, "monoids", exact = TRUE)
    base_names <- paste0("k", seq_len(8))

    x_oms <- as_ordered_sequence(list("aa", "bb", "c", "ddd"), keys = c(2, 2, 1, 3))
    ms_oms <- attr(x_oms, "monoids", exact = TRUE)
    key_type_oms <- attr(x_oms, "oms_key_type", exact = TRUE)
    x_ivx <- as_interval_index(list("a", "b", "c"), start = c(1, 2, 2), end = c(3, 2, 4), default_query_bounds = "[]")
    q_num <- priority_queue("a", "b", "c", priorities = c(2, 1, 3))
    q_date <- priority_queue(
      "d1", "d2", "d3",
      priorities = as.Date(c("2020-01-03", "2020-01-01", "2020-01-02"))
    )
    x_ord_num <- as_ordered_sequence(list("aa", "bb", "cc"), keys = c(2, 1, 2))
    x_ord_date <- as_ordered_sequence(
      list("aa", "bb", "cc"),
      keys = as.Date(c("2020-01-02", "2020-01-01", "2020-01-02"))
    )

    options(immutables.use_cpp = TRUE)

    old_torture <- gctorture2(1, inhibit_release = FALSE)
    on.exit(gctorture2(old_torture, inhibit_release = FALSE), add = TRUE)

    step <- function(label, expr) {
      results[[label]] <<- tryCatch(
        {
          force(expr)
          NULL
        },
        error = function(e) conditionMessage(e)
      )
      invisible(NULL)
    }

    t <- NULL
    step("ft_cpp_tree_from", t <- .ft_cpp_tree_from(as.list(1:8), ms))
    step("ft_cpp_tree_from_prepared", .ft_cpp_tree_from_prepared(as.list(1:8), base_names, ms))
    step("ft_cpp_tree_from_sorted", .ft_cpp_tree_from_sorted(as.list(1:8), ms))

    if(is.null(results[["ft_cpp_tree_from"]])) {
      step("ft_cpp_add_right", t <- .ft_cpp_add_right(t, 9, ms))
      step("ft_cpp_add_left", t <- .ft_cpp_add_left(t, 0, ms))
    } else {
      results[["ft_cpp_add_right"]] <- "skipped because ft_cpp_tree_from failed"
      results[["ft_cpp_add_left"]] <- "skipped because ft_cpp_tree_from failed"
    }

    step("ft_cpp_concat", .ft_cpp_concat(base_plain, base_plain, ms))
    step("ft_cpp_locate", .ft_cpp_locate(base_plain, function(v) v >= 4, ms, ".size", 0))
    step("ft_cpp_split_tree", .ft_cpp_split_tree(base_plain, function(v) v >= 4, ms, ".size", 0))

    step("ft_cpp_oms_insert", .ft_cpp_oms_insert(x_oms, list(value = "new", key = 2), ms_oms, key_type_oms))

    step("insert", insert(x_oms, "newer", key = 2))
    step("insert_interval_index", insert(x_ivx, "newer", start = 2, end = 5))
    step("peek_point_interval_index", peek_all_point(x_ivx, 2))
    step("pop_point_interval_index", pop_all_point(x_ivx, 2))
    step("peek_overlaps_interval_index", peek_all_overlaps(x_ivx, 2, 3))
    step("peek_containing_interval_index", peek_all_containing(x_ivx, 2, 3))
    step("peek_within_interval_index", peek_all_within(x_ivx, 2, 3))

    step("pq_numeric_peek_min_max", {
      invisible(peek_min(q_num))
      invisible(peek_max(q_num))
    })
    step("pq_numeric_pop_min_insert", {
      p1 <- pop_min(q_num)
      q2 <- insert(p1$remaining, "x", priority = 1)
      invisible(peek_min(q2))
      invisible(peek_max(q2))
    })
    step("pq_date_peek_pop_insert", {
      invisible(peek_min(q_date))
      invisible(peek_max(q_date))
      p1 <- pop_min(q_date)
      q2 <- insert(p1$remaining, "d4", priority = as.Date("2020-01-05"))
      invisible(peek_min(q2))
      invisible(peek_max(q2))
    })
    step("ordered_bounds_numeric", {
      invisible(lower_bound(x_ord_num, 2))
      invisible(upper_bound(x_ord_num, 2))
    })
    step("ordered_bounds_date", {
      k <- as.Date("2020-01-02")
      invisible(lower_bound(x_ord_date, k))
      invisible(upper_bound(x_ord_date, k))
    })
  }, error = function(e) {
    setup_error <<- conditionMessage(e)
  })

  list(enabled = TRUE, setup_error = setup_error, results = results)
}

.gc_cpp_run <- .run_gc_stress_suite()

.expect_step_ok <- function(label) {
  .skip_if_no_gc_stress()

  if(!is.null(.gc_cpp_run$setup_error)) {
    testthat::fail(sprintf("GC stress suite setup failed: %s", .gc_cpp_run$setup_error))
  }

  err <- .gc_cpp_run$results[[label]]
  if(is.null(err)) {
    testthat::succeed()
  } else {
    testthat::fail(sprintf("%s failed: %s", label, err))
  }
}

testthat::test_that("C++ tree builders survive GC torture", {
  .expect_step_ok("ft_cpp_tree_from")
  .expect_step_ok("ft_cpp_tree_from_prepared")
  .expect_step_ok("ft_cpp_tree_from_sorted")
})

testthat::test_that("C++ core tree ops survive GC torture", {
  .expect_step_ok("ft_cpp_add_right")
  .expect_step_ok("ft_cpp_add_left")
  .expect_step_ok("ft_cpp_concat")
  .expect_step_ok("ft_cpp_locate")
  .expect_step_ok("ft_cpp_split_tree")
})

testthat::test_that("C++ OMS primitives survive GC torture", {
  .expect_step_ok("ft_cpp_oms_insert")
})

testthat::test_that("OMS public APIs survive GC torture", {
  .expect_step_ok("insert")
})

testthat::test_that("interval_index public APIs survive GC torture", {
  .expect_step_ok("insert_interval_index")
  .expect_step_ok("peek_point_interval_index")
  .expect_step_ok("pop_point_interval_index")
  .expect_step_ok("peek_overlaps_interval_index")
  .expect_step_ok("peek_containing_interval_index")
  .expect_step_ok("peek_within_interval_index")
})

testthat::test_that("priority_queue public APIs survive GC torture", {
  .expect_step_ok("pq_numeric_peek_min_max")
  .expect_step_ok("pq_numeric_pop_min_insert")
  .expect_step_ok("pq_date_peek_pop_insert")
})

testthat::test_that("ordered bounds survive GC torture", {
  .expect_step_ok("ordered_bounds_numeric")
  .expect_step_ok("ordered_bounds_date")
})

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.