tests/testthat/test-cache-disk.R

cache_disk_deterministic <- function(...) {
  d <- cache_disk(...)

  # Normally the throttle counter starts with a random value, but for these
  # tests we need to make it deterministic.
  environment(d$set)$prune_throttle_counter_ <- 0

  d
}


test_that("cache_disk: handling missing values", {
  d <- cache_disk()
  expect_true(is.key_missing(d$get("abcd")))
  d$set("a", 100)
  expect_identical(d$get("a"), 100)
  expect_identical(d$get("y", missing = NULL), NULL)
  expect_error(
    d$get("y", missing = stop("Missing key")),
    "^Missing key$",
  )

  d <- cache_disk(missing = NULL)
  expect_true(is.null(d$get("abcd")))
  d$set("a", 100)
  expect_identical(d$get("a"), 100)
  expect_identical(d$get("y", missing = -1), -1)
  expect_error(
    d$get("y", missing = stop("Missing key")),
    "^Missing key$",
  )

  d <- cache_disk(missing = stop("Missing key"))
  expect_error(d$get("abcd"), "^Missing key$")
  d$set("x", NULL)
  d$set("a", 100)
  expect_identical(d$get("a"), 100)
  expect_error(d$get("y"), "^Missing key$")
  expect_identical(d$get("y", missing = NULL), NULL)
  expect_true(is.key_missing(d$get("y", missing = key_missing())))
  expect_error(
    d$get("y", missing = stop("Missing key 2")),
    "^Missing key 2$",
  )

  # Pass in a quosure
  expr <- rlang::quo(stop("Missing key"))
  d <- cache_disk(missing = !!expr)
  expect_error(d$get("y"), "^Missing key$")
  expect_error(d$get("y"), "^Missing key$") # Make sure a second time also throws
})


test_that("cache_disk: pruning respects max_n", {
  # Timing is apparently unreliable on CRAN, so skip tests there. It's possible
  # that a heavily loaded system will have issues with these tests because of
  # the time resolution.
  skip_on_cran()
  delay <- 0.01

  d <- cache_disk_deterministic(max_n = 3)
  # NOTE: The short delays after each item are meant to tests more reliable on
  # CI systems.
  d$set("a", rnorm(100)); Sys.sleep(delay)
  d$set("b", rnorm(100)); Sys.sleep(delay)
  d$set("c", rnorm(100)); Sys.sleep(delay)
  d$set("d", rnorm(100)); Sys.sleep(delay)
  d$set("e", rnorm(100)); Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("c", "d", "e"))
})

test_that("cache_disk: pruning respects max_size", {
  skip_on_cran()
  delay <- 0.01

  d <- cache_disk_deterministic(max_size = 200)
  d$set("a", rnorm(100)); Sys.sleep(delay)
  d$set("b", rnorm(100)); Sys.sleep(delay)
  d$set("c", 1);          Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("c"))
  d$set("d", rnorm(100)); Sys.sleep(delay)
  # Objects are pruned with oldest first, so even though "c" would fit in the
  # cache, it is removed after adding "d" (and "d" is removed as well because it
  # doesn't fit).
  d$prune()
  expect_length(d$keys(), 0)
  d$set("e", 2);          Sys.sleep(delay)
  d$set("f", 3);          Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("e", "f"))
})

# Issue shiny#3033
test_that("cache_disk: pruning respects both max_n and max_size", {
  skip_on_cran()
  d <- cache_disk_deterministic(max_n = 3, max_size = 200)

  # Set some values. Use rnorm so that object size is large; a simple vector
  # like 1:100 will be stored very efficiently by R's ALTREP, and won't exceed
  # the max_size. We want each of these objects to exceed max_size so that
  # they'll be pruned.
  d$set("a", rnorm(100))
  d$set("b", rnorm(100))
  d$set("c", rnorm(100))
  d$set("d", rnorm(100))
  d$set("e", rnorm(100))
  Sys.sleep(0.1)  # For systems that have low mtime resolution.
  d$set("f", 1)   # This object is small and shouldn't be pruned.
  d$prune()
  expect_identical(d$keys(), "f")
})

# Return TRUE if the Sys.setFileTime() has subsecond resolution, FALSE
# otherwise.
setfiletime_has_subsecond_resolution <- function() {
  tmp <- tempfile()
  file.create(tmp)
  Sys.setFileTime(tmp, Sys.time())
  time <- as.numeric(file.info(tmp)[['mtime']])
  if (time == floor(time)) {
    return(FALSE)
  } else {
    return(TRUE)
  }
}

test_that('cache_disk: pruning with evict="lru"', {
  skip_on_cran()
  delay <- 0.01
  # For lru tests, make sure there's sub-second resolution for
  # Sys.setFileTime(), because that's what the lru code uses to update times.
  skip_if_not(
    setfiletime_has_subsecond_resolution(),
    "Sys.setFileTime() does not have subsecond resolution on this platform."
  )

  d <- cache_disk_deterministic(max_n = 2)
  d$set("a", 1); Sys.sleep(delay)
  d$set("b", 1); Sys.sleep(delay)
  d$set("c", 1); Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("b", "c"))
  d$get("b");    Sys.sleep(delay)
  d$set("d", 1); Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("b", "d"))
  d$get("b");    Sys.sleep(delay)
  d$set("e", 2); Sys.sleep(delay)
  d$get("b");    Sys.sleep(delay)
  d$set("f", 3); Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("b", "f"))
})

test_that('cache_disk: pruning with evict="fifo"', {
  skip_on_cran()
  delay <- 0.01

  d <- cache_disk_deterministic(max_n = 2, evict = "fifo")
  d$set("a", 1); Sys.sleep(delay)
  d$set("b", 1); Sys.sleep(delay)
  d$set("c", 1); Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("b", "c"))
  d$get("b")
  d$set("d", 1); Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("c", "d"))
  d$get("b")
  d$set("e", 2); Sys.sleep(delay)
  d$get("b")
  d$set("f", 3); Sys.sleep(delay)
  d$prune()
  expect_identical(sort(d$keys()), c("e", "f"))
})


test_that("cache_disk: pruning throttling", {
  skip_on_cran()
  delay <- 0.01

  # Pruning won't happen when the number of items is less than prune_rate AND
  # the set() calls happen within 5 seconds.
  d <- cache_disk_deterministic(max_n = 2, prune_rate = 20)
  d$set("a", 1); Sys.sleep(delay)
  d$set("b", 1); Sys.sleep(delay)
  d$set("c", 1); Sys.sleep(delay)
  d$set("d", 1); Sys.sleep(delay)
  expect_identical(sort(d$keys()), c("a", "b", "c", "d"))

  # Pruning will happen with a lower prune_rate value.
  d <- cache_disk_deterministic(max_n = 2, prune_rate = 3)
  d$set("a", 1); Sys.sleep(delay)
  d$set("b", 1); Sys.sleep(delay)
  d$set("c", 1); Sys.sleep(delay)
  expect_identical(sort(d$keys()), c("b", "c"))
  d$set("d", 1); Sys.sleep(delay)
  expect_identical(sort(d$keys()), c("b", "c", "d"))
  d$set("e", 1); Sys.sleep(delay)
  expect_identical(sort(d$keys()), c("b", "c", "d", "e"))
  d$set("f", 1); Sys.sleep(delay)
  expect_identical(sort(d$keys()), c("e", "f"))

  Sys.sleep(5)
  d$set("f", 1); Sys.sleep(delay)
  expect_identical(sort(d$keys()), c("e", "f"))
})

test_that("destroy_on_finalize works", {
  d <- cache_disk(destroy_on_finalize = TRUE)
  cache_dir <- d$info()$dir

  expect_true(dir.exists(cache_dir))
  rm(d)
  gc()
  expect_false(dir.exists(cache_dir))
})


test_that("Warnings for caching reference objects", {
  d <- cache_disk(warn_ref_objects = TRUE)
  expect_warning(d$set("a", new.env()))
  expect_warning(d$set("a", function() NULL))
  expect_warning(d$set("a", fastmap()))  # fastmap objects contain an external pointer

  # Default is to not warn on ref objects
  d <- cache_disk()
  expect_silent(d$set("a", new.env()))
  expect_silent(d$set("a", function() NULL))
  expect_silent(d$set("a", fastmap()))
})

test_that("Cache disk can use different formts", {
  my_write <- function(...) write.csv(..., row.names = FALSE)

  d <- cache_disk(read_fn = read.csv, write_fn = my_write, extension = ".csv")

  mt <- mtcars
  rownames(mt) <- NULL
  d$set("mt", mt)
  expect_equal(d$get("mt"), mt)
})
wch/cache documentation built on Feb. 2, 2024, 8:06 a.m.