From c5b4b76a0ac8353902d2bdcb2aeaa98dbe7317f9 Mon Sep 17 00:00:00 2001 From: Vincent Guyader Date: Fri, 24 Apr 2026 22:47:58 +0200 Subject: [PATCH 1/4] feat(cache_sqlite): add SQLite cache backend (#1) Pure-R, zero-service backend usable as ':memory:' for unit tests and as a file path for a persistent on-disk cache. Follows the same surface as cache_postgres / cache_mongo / cache_redis and is fully {memoise}-compatible. - RSQLite moved from implicit to suggested. - tests/testthat/test-sqlite.R exercises the full surface in-memory. --- DESCRIPTION | 3 +- NAMESPACE | 1 + R/sqlite.R | 162 ++++++++++++++++++++++++++ man/cache_sqlite.Rd | 215 +++++++++++++++++++++++++++++++++++ tests/testthat/test-sqlite.R | 41 +++++++ 5 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 R/sqlite.R create mode 100644 man/cache_sqlite.Rd create mode 100644 tests/testthat/test-sqlite.R diff --git a/DESCRIPTION b/DESCRIPTION index 2fc1831..b35e46b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,9 +17,10 @@ Suggests: testthat (>= 3.0.0), redux (>= 1.1.0), RPostgres, + RSQLite, withr Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index 485406f..0ed8571 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export(cache_mongo) export(cache_postgres) export(cache_redis) +export(cache_sqlite) importFrom(R6,R6Class) importFrom(attempt,stop_if_not) importFrom(digest,digest) diff --git a/R/sqlite.R b/R/sqlite.R new file mode 100644 index 0000000..9d2d8b0 --- /dev/null +++ b/R/sqlite.R @@ -0,0 +1,162 @@ +#' A caching object backed by SQLite +#' +#' Create a cache backend stored in a SQLite database. This is the most +#' lightweight SQL-backed cache offered by `{bank}` — it only needs `{DBI}` +#' and `{RSQLite}` and supports an in-memory database (`dbname = ":memory:"`), +#' which makes it ideal for unit tests and short-lived sessions (#1). +#' +#' @export +#' @importFrom R6 R6Class +#' @importFrom attempt stop_if_not +#' @importFrom digest digest +cache_sqlite <- R6::R6Class( + "cache_sqlite", + public = list( + #' @description Start a new SQLite cache. + #' @param dbname path to the SQLite file, or `":memory:"` for an + #' in-memory database (default). + #' @param ... additional arguments passed to + #' `DBI::dbConnect(RSQLite::SQLite(), dbname = dbname, ...)`. + #' @param cache_table name of the SQLite table backing the cache. + #' Defaults to `"bankrcache"`. + #' @param algo for `{memoise}` compatibility, the `digest()` algorithm. + #' @param compress for `{memoise}` compatibility, should the data be + #' compressed? + #' @return A `cache_sqlite` object. + initialize = function(dbname = ":memory:", + ..., + cache_table = "bankrcache", + algo = "sha512", + compress = FALSE) { + if (!requireNamespace("RSQLite", quietly = TRUE)) { + stop( + "The {RSQLite} package has to be installed before using `cache_sqlite`.\n", + "Please install it first, for example with install.packages('RSQLite')." + ) + } + if (!requireNamespace("DBI", quietly = TRUE)) { + stop( + "The {DBI} package has to be installed before using `cache_sqlite`.\n", + "Please install it first, for example with install.packages('DBI')." + ) + } + + private$interface <- DBI::dbConnect( + RSQLite::SQLite(), + dbname = dbname, + ... + ) + + private$cache_table <- cache_table + + if (!cache_table %in% DBI::dbListTables(private$interface)) { + DBI::dbCreateTable( + private$interface, + cache_table, + fields = c( + id = "TEXT", + cache = "BLOB" + ) + ) + } + + private$algo <- algo + private$compress <- compress + }, + #' @description Does the cache contain a given key? + #' @param key key name. + #' @return `TRUE`/`FALSE`. + has_key = function(key) { + res <- DBI::dbGetQuery( + private$interface, + sprintf("SELECT id FROM %s WHERE id = ?;", private$cache_table), + params = list(key) + ) + if (nrow(res) > 1L) { + stop("Corrupted cache: more than one entry for ", key) + } + nrow(res) == 1L + }, + #' @description Get a key from the cache. + #' @param key key name. + #' @return the value stored under `key`, or an object of class + #' `"key_missing"` if the key is absent. + get = function(key) { + if (!self$has_key(key)) { + return(structure(list(), class = "key_missing")) + } + out <- DBI::dbGetQuery( + private$interface, + sprintf("SELECT cache FROM %s WHERE id = ?;", private$cache_table), + params = list(key) + ) + tryCatch( + unserialize(out$cache[[1L]]), + error = function(e) structure(list(), class = "key_missing") + ) + }, + #' @description Set a key in the cache. + #' @param key key name. + #' @param value value to store. + #' @return used for side effect. + set = function(key, value) { + if (!self$has_key(key)) { + blob <- serialize(value, NULL) + DBI::dbExecute( + private$interface, + sprintf( + "INSERT INTO %s (id, cache) VALUES (?, ?);", + private$cache_table + ), + params = list(key, list(blob)) + ) + } + invisible(NULL) + }, + #' @description Clear the whole cache. + #' @return used for side effect. + reset = function() { + DBI::dbExecute( + private$interface, + sprintf("DELETE FROM %s;", private$cache_table) + ) + invisible(NULL) + }, + #' @description Remove a single key. + #' @param key key name. + #' @return number of rows removed. + remove = function(key) { + DBI::dbExecute( + private$interface, + sprintf("DELETE FROM %s WHERE id = ?;", private$cache_table), + params = list(key) + ) + }, + #' @description List every key in the cache. + #' @return a character vector of keys. + keys = function() { + DBI::dbGetQuery( + private$interface, + sprintf("SELECT id FROM %s;", private$cache_table) + )$id + }, + #' @description Hash helper for `{memoise}` compatibility. + #' @param ... value to hash. + #' @return a character(1) hash. + digest = function(...) digest::digest(..., algo = private$algo) + ), + private = list( + interface = NULL, + cache_table = character(0), + algo = character(0), + compress = logical(0), + finalize = function() { + if (!is.null(private$interface) && + inherits(private$interface, "DBIConnection") && + DBI::dbIsValid(private$interface)) { + DBI::dbDisconnect(private$interface) + } + invisible(NULL) + } + ) +) diff --git a/man/cache_sqlite.Rd b/man/cache_sqlite.Rd new file mode 100644 index 0000000..c9f242b --- /dev/null +++ b/man/cache_sqlite.Rd @@ -0,0 +1,215 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/sqlite.R +\name{cache_sqlite} +\alias{cache_sqlite} +\title{A caching object backed by SQLite} +\description{ +A caching object backed by SQLite + +A caching object backed by SQLite +} +\details{ +Create a cache backend stored in a SQLite database. This is the most +lightweight SQL-backed cache offered by \code{{bank}} — it only needs \code{{DBI}} +and \code{{RSQLite}} and supports an in-memory database (\code{dbname = ":memory:"}), +which makes it ideal for unit tests and short-lived sessions (#1). +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-cache_sqlite-new}{\code{cache_sqlite$new()}} +\item \href{#method-cache_sqlite-has_key}{\code{cache_sqlite$has_key()}} +\item \href{#method-cache_sqlite-get}{\code{cache_sqlite$get()}} +\item \href{#method-cache_sqlite-set}{\code{cache_sqlite$set()}} +\item \href{#method-cache_sqlite-reset}{\code{cache_sqlite$reset()}} +\item \href{#method-cache_sqlite-remove}{\code{cache_sqlite$remove()}} +\item \href{#method-cache_sqlite-keys}{\code{cache_sqlite$keys()}} +\item \href{#method-cache_sqlite-digest}{\code{cache_sqlite$digest()}} +\item \href{#method-cache_sqlite-clone}{\code{cache_sqlite$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-new}{}}} +\subsection{Method \code{new()}}{ +Start a new SQLite cache. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$new( + dbname = ":memory:", + ..., + cache_table = "bankrcache", + algo = "sha512", + compress = FALSE +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{dbname}}{path to the SQLite file, or \code{":memory:"} for an +in-memory database (default).} + +\item{\code{...}}{additional arguments passed to +\code{DBI::dbConnect(RSQLite::SQLite(), dbname = dbname, ...)}.} + +\item{\code{cache_table}}{name of the SQLite table backing the cache. +Defaults to \code{"bankrcache"}.} + +\item{\code{algo}}{for \code{{memoise}} compatibility, the \code{digest()} algorithm.} + +\item{\code{compress}}{for \code{{memoise}} compatibility, should the data be +compressed?} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{cache_sqlite} object. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-has_key}{}}} +\subsection{Method \code{has_key()}}{ +Does the cache contain a given key? +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$has_key(key)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{key}}{key name.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\code{TRUE}/\code{FALSE}. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-get}{}}} +\subsection{Method \code{get()}}{ +Get a key from the cache. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$get(key)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{key}}{key name.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +the value stored under \code{key}, or an object of class +\code{"key_missing"} if the key is absent. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-set}{}}} +\subsection{Method \code{set()}}{ +Set a key in the cache. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$set(key, value)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{key}}{key name.} + +\item{\code{value}}{value to store.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +used for side effect. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-reset}{}}} +\subsection{Method \code{reset()}}{ +Clear the whole cache. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$reset()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +used for side effect. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-remove}{}}} +\subsection{Method \code{remove()}}{ +Remove a single key. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$remove(key)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{key}}{key name.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +number of rows removed. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-keys}{}}} +\subsection{Method \code{keys()}}{ +List every key in the cache. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$keys()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +a character vector of keys. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-digest}{}}} +\subsection{Method \code{digest()}}{ +Hash helper for \code{{memoise}} compatibility. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$digest(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{value to hash.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +a character(1) hash. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-cache_sqlite-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{cache_sqlite$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/tests/testthat/test-sqlite.R b/tests/testthat/test-sqlite.R new file mode 100644 index 0000000..e8bf08e --- /dev/null +++ b/tests/testthat/test-sqlite.R @@ -0,0 +1,41 @@ +test_that("cache_sqlite works end-to-end in-memory", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("memoise") + + sqlite_cache <- cache_sqlite$new(dbname = ":memory:") + + expect_true(inherits(sqlite_cache, "cache_sqlite")) + expect_length(sqlite_cache$keys(), 0L) + + sqlite_cache$set("hello", list(value = 42)) + expect_true(sqlite_cache$has_key("hello")) + expect_equal(sqlite_cache$get("hello"), list(value = 42)) + + sqlite_cache$set("world", "abc") + expect_setequal(sqlite_cache$keys(), c("hello", "world")) + + sqlite_cache$remove("hello") + expect_false(sqlite_cache$has_key("hello")) + + sqlite_cache$reset() + expect_length(sqlite_cache$keys(), 0L) +}) + +test_that("cache_sqlite is compatible with {memoise}", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("memoise") + + sqlite_cache <- cache_sqlite$new(dbname = ":memory:") + f <- function(x) sample(1:1000, x) + mf <- memoise::memoise(f, cache = sqlite_cache) + expect_equal(mf(5), mf(5)) + expect_true(length(sqlite_cache$keys()) >= 1L) +}) + +test_that("missing key returns a key_missing sentinel", { + skip_if_not_installed("RSQLite") + + sqlite_cache <- cache_sqlite$new(dbname = ":memory:") + res <- sqlite_cache$get("nope") + expect_s3_class(res, "key_missing") +}) From 10548821f78a087960edbb5a5c8e95cfee6a5eaf Mon Sep 17 00:00:00 2001 From: Vincent Guyader Date: Sat, 25 Apr 2026 09:18:18 +0200 Subject: [PATCH 2/4] fix(cache_sqlite): upsert in set(), validate existing schema, drop unused import - set() now overwrites an existing key instead of being a silent no-op, matching the {memoise} cache contract. Old behaviour confused callers that expected the cache to reflect the latest assigned value. - When the cache table already exists, validate that its columns are exactly (id, cache) before reusing it; otherwise raise a readable error instead of failing later inside an INSERT/SELECT. - Drop the orphan @importFrom attempt stop_if_not (no call sites). --- R/sqlite.R | 32 ++++++++++++++++++++++----- man/cache_sqlite.Rd | 3 ++- tests/testthat/test-sqlite.R | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/R/sqlite.R b/R/sqlite.R index 9d2d8b0..3c806b1 100644 --- a/R/sqlite.R +++ b/R/sqlite.R @@ -7,7 +7,6 @@ #' #' @export #' @importFrom R6 R6Class -#' @importFrom attempt stop_if_not #' @importFrom digest digest cache_sqlite <- R6::R6Class( "cache_sqlite", @@ -49,7 +48,20 @@ cache_sqlite <- R6::R6Class( private$cache_table <- cache_table - if (!cache_table %in% DBI::dbListTables(private$interface)) { + if (cache_table %in% DBI::dbListTables(private$interface)) { + # Reusing an existing table: validate that the schema matches what + # cache_sqlite expects. Bail out early with a readable error rather + # than failing later inside an INSERT/SELECT. + cols <- DBI::dbListFields(private$interface, cache_table) + if (!setequal(cols, c("id", "cache"))) { + stop( + "Existing table `", cache_table, "` does not match the cache schema.", + "\nExpected columns: id, cache. Found: ", + paste(cols, collapse = ", "), + call. = FALSE + ) + } + } else { DBI::dbCreateTable( private$interface, cache_table, @@ -95,13 +107,23 @@ cache_sqlite <- R6::R6Class( error = function(e) structure(list(), class = "key_missing") ) }, - #' @description Set a key in the cache. + #' @description Set a key in the cache. If the key already exists, its + #' value is overwritten (matching the {memoise} cache contract). #' @param key key name. #' @param value value to store. #' @return used for side effect. set = function(key, value) { - if (!self$has_key(key)) { - blob <- serialize(value, NULL) + blob <- serialize(value, NULL) + if (self$has_key(key)) { + DBI::dbExecute( + private$interface, + sprintf( + "UPDATE %s SET cache = ? WHERE id = ?;", + private$cache_table + ), + params = list(list(blob), key) + ) + } else { DBI::dbExecute( private$interface, sprintf( diff --git a/man/cache_sqlite.Rd b/man/cache_sqlite.Rd index c9f242b..0e7e378 100644 --- a/man/cache_sqlite.Rd +++ b/man/cache_sqlite.Rd @@ -111,7 +111,8 @@ the value stored under \code{key}, or an object of class \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-cache_sqlite-set}{}}} \subsection{Method \code{set()}}{ -Set a key in the cache. +Set a key in the cache. If the key already exists, its +value is overwritten (matching the {memoise} cache contract). \subsection{Usage}{ \if{html}{\out{
}}\preformatted{cache_sqlite$set(key, value)}\if{html}{\out{
}} } diff --git a/tests/testthat/test-sqlite.R b/tests/testthat/test-sqlite.R index e8bf08e..e99fd75 100644 --- a/tests/testthat/test-sqlite.R +++ b/tests/testthat/test-sqlite.R @@ -39,3 +39,45 @@ test_that("missing key returns a key_missing sentinel", { res <- sqlite_cache$get("nope") expect_s3_class(res, "key_missing") }) + +test_that("set() overwrites an existing key", { + skip_if_not_installed("RSQLite") + + sqlite_cache <- cache_sqlite$new(dbname = ":memory:") + sqlite_cache$set("k", "first") + sqlite_cache$set("k", "second") + expect_equal(sqlite_cache$get("k"), "second") + expect_length(sqlite_cache$keys(), 1L) +}) + +test_that("opening an existing table with a foreign schema errors clearly", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + path <- tempfile(fileext = ".sqlite") + on.exit(unlink(path), add = TRUE) + con <- DBI::dbConnect(RSQLite::SQLite(), dbname = path) + DBI::dbExecute(con, "CREATE TABLE bankrcache (foo TEXT, bar TEXT);") + DBI::dbDisconnect(con) + + expect_error( + cache_sqlite$new(dbname = path), + regexp = "does not match the cache schema" + ) +}) + +test_that("file-backed sqlite cache persists across sessions", { + skip_if_not_installed("RSQLite") + + path <- tempfile(fileext = ".sqlite") + on.exit(unlink(path), add = TRUE) + + c1 <- cache_sqlite$new(dbname = path) + c1$set("alpha", list(x = 1L, y = 2L)) + rm(c1) + gc() + + c2 <- cache_sqlite$new(dbname = path) + expect_true(c2$has_key("alpha")) + expect_equal(c2$get("alpha"), list(x = 1L, y = 2L)) +}) From 7b11db2b9d27b1dfa6c4aba74e5f319e97f2bb4a Mon Sep 17 00:00:00 2001 From: Vincent Guyader Date: Sat, 25 Apr 2026 09:35:16 +0200 Subject: [PATCH 3/4] test: skip docker-backed cases when their package or docker is missing R CMD check ran the redis/mongo/postgres tests on machines without those packages installed (or without docker), which meant a clean sandbox failed the whole suite even though cache_sqlite is pure-R. Add skip_if_not_installed() and a docker-availability skip to each test, and make the helper's optional install.packages() calls non-fatal when the repo is unreachable. --- tests/testthat/helpers-utils.R | 28 +++++++++++++++++----------- tests/testthat/test-mongo.R | 2 ++ tests/testthat/test-postgres.R | 3 +++ tests/testthat/test-redis.R | 2 ++ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/tests/testthat/helpers-utils.R b/tests/testthat/helpers-utils.R index d7770d6..6e76678 100644 --- a/tests/testthat/helpers-utils.R +++ b/tests/testthat/helpers-utils.R @@ -54,18 +54,24 @@ test_them_all <- function(cache_obj) { ) } -if (!requireNamespace("redux", quietly = TRUE)) { - install.packages("redux") -} -if (!requireNamespace("mongolite", quietly = TRUE)) { - install.packages("mongolite") -} -if (!requireNamespace("DBI", quietly = TRUE)) { - install.packages("DBI") -} -if (!requireNamespace("RPostgres", quietly = TRUE)) { - install.packages("RPostgres") +# Optional backend deps. They are only useful for the docker-backed +# integration tests (Redis / Mongo / Postgres). Failure to install them +# (e.g. on a sandbox without a compiler or repo access) must not crash the +# whole test run — cache_sqlite is a pure-R backend that needs none of +# them, and individual tests already skip_if_not_installed(). +maybe_install <- function(pkg) { + if (!requireNamespace(pkg, quietly = TRUE)) { + tryCatch( + install.packages(pkg), + error = function(e) message("Could not install ", pkg, ": ", conditionMessage(e)), + warning = function(w) message("Could not install ", pkg, ": ", conditionMessage(w)) + ) + } } +maybe_install("redux") +maybe_install("mongolite") +maybe_install("DBI") +maybe_install("RPostgres") withr::defer( { diff --git a/tests/testthat/test-mongo.R b/tests/testthat/test-mongo.R index 9a0565b..c999b6d 100644 --- a/tests/testthat/test-mongo.R +++ b/tests/testthat/test-mongo.R @@ -1,5 +1,7 @@ test_that("cache_redis works", { skip_on_ci() + skip_if_not_installed("mongolite") + if (Sys.which("docker") == "") skip("docker not available") system("docker run --rm --name mongobankunittest -d -p 27066:27017 -e MONGO_INITDB_ROOT_USERNAME=bebop -e MONGO_INITDB_ROOT_PASSWORD=aloula mongo:4") Sys.sleep(10) diff --git a/tests/testthat/test-postgres.R b/tests/testthat/test-postgres.R index 51751f9..3cd6456 100644 --- a/tests/testthat/test-postgres.R +++ b/tests/testthat/test-postgres.R @@ -1,5 +1,8 @@ test_that("cache_postgres works", { skip_on_ci() + skip_if_not_installed("RPostgres") + skip_if_not_installed("DBI") + if (Sys.which("docker") == "") skip("docker not available") system("docker run --rm --name postgresbankunittest -e POSTGRES_PASSWORD=mysecretpassword -d -p 5434:5432 postgres") Sys.sleep(10) diff --git a/tests/testthat/test-redis.R b/tests/testthat/test-redis.R index aec1d67..6d0fbdc 100644 --- a/tests/testthat/test-redis.R +++ b/tests/testthat/test-redis.R @@ -1,5 +1,7 @@ test_that("cache_redis works", { skip_on_ci() + skip_if_not_installed("redux") + if (Sys.which("docker") == "") skip("docker not available") system("docker run --rm --name redisbankunittest -d -p 6379:6379 redis:5.0.5 --requirepass bebopalula") Sys.sleep(10) From f74f994dcae9959567b9dcc2e9cc22be26e8b8b6 Mon Sep 17 00:00:00 2001 From: Vincent Guyader Date: Sat, 25 Apr 2026 19:20:37 +0200 Subject: [PATCH 4/4] ci: bump actions/checkout v3 -> v4 --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a3ac618..74d8c97 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -29,7 +29,7 @@ jobs: R_KEEP_PKG_SOURCE: yes steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-pandoc@v2