Refuse cacheAs files written before the scores were corrected
A cacheAs file holds a finished analysis, association scores included,
and 1.4.0 computes those differently. A file written by 1.3.0 would
therefore be handed back with numbers that would not be arrived at again,
and the comparison of parameters added during this cycle cannot notice
it: the parameters did not change, the formula did. The version that
wrote a file is now recorded in it, and one from before 1.4.0 is
recomputed and overwritten, with a warning saying why.
The machinery moves to a file of its own on the way, since it is no
longer about collocation analysis alone, and verbose drops out of what a
file records: how loud a query is does not change what it returns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: I8af9eac21ae53fe5d583578c3689b37d140a4af4
diff --git a/DESCRIPTION b/DESCRIPTION
index 107d156..2ef1ae5 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -67,6 +67,7 @@
'RKorAPClient-package.R'
'KorAPQuery.R'
'association-scores.R'
+ 'cacheAs.R'
'ci.R'
'collocationAnalysis.R'
'collocationScoreQuery.R'
diff --git a/NEWS.md b/NEWS.md
index 43a233c..a4650ca 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,7 @@
# unpublished dev version 1.3.0.9000
+- **`cacheAs` files record the version that wrote them** and are refused, with a warning, when that is older than 1.4.0. Their contents are finished results including the association scores, which this version computes differently, so an old file would silently hand back numbers that would not be arrived at again - something no comparison of parameters can notice. The file is then recomputed and overwritten; pass a different name to keep it. How loud a query is no longer counts as a parameter either: `verbose` does not change what is returned
+
- **`collocationAnalysis()` now discards collocates that occur less often than expected** by chance. `logDice`, by which it ranks and thresholds, expresses how salient a pair is rather than how surprising, so a frequent word could appear among the top collocates although the node does not attract it at all: for *Grund* in a 5+5 window, *Berlin* reaches a logDice of 3.84, close to *triftiger* at 3.96, while co-occurring 1.74 bits *less* often than expected. The new `minObservedExpectedRatio` parameter defaults to 1 and keeps such pairs out. Raise it to demand a stronger contrast, e.g. 2 for collocates occurring at least twice as often as expected, or set it to 0 for the unfiltered result of earlier versions, e.g. to study repulsion. `collocationScoreQuery()` is unaffected, as there the pairs to score are given explicitly
- **changed `logDice` values**: `logDice()` is now computed as defined by Rychlý (2008), `14 + log2(2 * O / (O1 + O2))`, so that its values are comparable to those of Sketch Engine and other tools. It previously multiplied the node frequency by the window size, `14 + log2(2 * O / (w * O1 + O2))`, which added a count of window positions to a count of word tokens and made the coefficient asymmetric, so that swapping node and collocate changed the score. Because `w * O1` dominated the denominator for a frequent node, rare collocates were penalised: for *triftiger* as a collocate of *Grund* in a 5+5 window, logDice was 0.64, below *Berlin* at 2.03, although *Berlin* co-occurs with *Grund* less often than chance predicts. The values are now 3.96 and 3.84. Scores rise by up to `log2(w)`, that is by up to 3.32 for the default context of 5 left and 5 right, so `collocationAnalysis()` with the default `thresholdScore = "logDice"` and `threshold = 2` is now somewhat more permissive when recursing. Results computed with a total window size of 1, as in the light verb construction example of the Readme, are unaffected. The other association scores are unchanged: they take the window size into account through the expected frequency `E`, which is correct
diff --git a/R/cacheAs.R b/R/cacheAs.R
new file mode 100644
index 0000000..dfb41bb
--- /dev/null
+++ b/R/cacheAs.R
@@ -0,0 +1,175 @@
+#' Keeping results in a file of one's own
+#'
+#' The `cacheAs` parameter of the query functions is a different thing from the
+#' `cache` parameter of [KorAPConnection()]. The latter is a transparent
+#' speed-up: it stores server responses where the package finds them again, and
+#' throwing it away costs nothing but time. The former stores a finished result
+#' in a file the caller names and keeps, which is what makes an analysis
+#' reproducible: KorAP corpora grow, so the same query returns different numbers
+#' next year, and the scores computed from them may change with the package.
+#'
+#' A cache file therefore records what produced it, and is not reused when that
+#' no longer matches what is being asked for.
+#'
+#' @name cacheAs
+#' @keywords internal
+NULL
+
+#' Attribute under which cache files record what produced them
+#' @noRd
+cacheAsAttribute <- "RKorAPClient.cacheAs"
+
+#' First version whose association scores are still computed the same way today
+#'
+#' `logDice()` and `ll()` were corrected in 1.4.0, so a file written by an
+#' earlier version holds numbers that would not be arrived at again, which no
+#' comparison of parameters can notice.
+#'
+#' Deliberately a constant of its own rather than the package version: what a
+#' cache file has to be checked against is the generation of the scores, which
+#' changes far more rarely than the version does, and reading it out of
+#' DESCRIPTION would make the check depend on a release having been prepared.
+#' Raise it whenever a score changes.
+#' @noRd
+cacheAsScoreVersion <- "1.4.0"
+
+#' Append .rds to a cache file name that does not end in it
+#' @noRd
+cacheAsFileName <- function(cacheAs) {
+ if (grepl("\\.rds$", cacheAs, ignore.case = TRUE)) cacheAs else paste0(cacheAs, ".rds")
+}
+
+#' What a cached result was computed with
+#'
+#' Collected from the calling function's frame, so that parameters added in the
+#' future are taken into account automatically. `kco` and `cacheAs` are
+#' excluded: the former is not a parameter of the analysis, the latter only says
+#' where to store it.
+#'
+#' @param frame environment of the calling query function
+#' @param dots its `...` arguments, or `NULL` where it has none
+#' @param kco [KorAPConnection()] object
+#' @return list to store with, and compare against, a cache file
+#' @noRd
+cacheAsRecord <- function(frame, dots, kco) {
+ parameterNames <- setdiff(
+ names(formals(sys.function(sys.parent()))),
+ # verbose says how loud the computation is, not what it computes
+ c("kco", "cacheAs", "verbose", "...")
+ )
+ list(
+ # what has to match on the next call
+ scoreVersion = cacheAsScoreVersion,
+ # recorded so that a file can say where its numbers come from
+ packageVersion = as.character(utils::packageVersion("RKorAPClient")),
+ parameters = mget(parameterNames, envir = frame),
+ dots = dots,
+ # reusing one cache file for two KorAP instances is a mistake worth catching
+ apiUrl = kco@apiUrl,
+ # recorded for reference only, deliberately not compared: corpus updates
+ # should not invalidate a deliberately kept result
+ indexRevision = kco@indexRevision
+ )
+}
+
+#' Why a cache file cannot be used for the current call
+#'
+#' @param stored record read from the cache file, `NULL` if it has none
+#' @param current record of the call at hand
+#' @return a sentence naming the reason, or `NULL` if the file can be used
+#' @noRd
+cacheAsRejectionReason <- function(stored, current) {
+ generation <- if (is.null(stored)) NULL else stored$scoreVersion
+
+ if (is.null(generation) || package_version(generation) < package_version(cacheAsScoreVersion)) {
+ writtenBy <- if (is.null(stored)) NULL else stored$packageVersion
+ return(if (is.null(writtenBy)) {
+ sprintf(
+ "was written before RKorAPClient %s, which corrected logDice and ll",
+ cacheAsScoreVersion
+ )
+ } else {
+ sprintf(
+ "was written by RKorAPClient %s, whose logDice and ll differ from those of %s",
+ writtenBy, cacheAsScoreVersion
+ )
+ })
+ }
+
+ differing <- character(0)
+ for (name in union(names(stored$parameters), names(current$parameters))) {
+ if (!identical(stored$parameters[[name]], current$parameters[[name]])) {
+ differing <- c(differing, name)
+ }
+ }
+ if (!identical(stored$dots, current$dots)) {
+ differing <- c(differing, "...")
+ }
+ if (!identical(stored$apiUrl, current$apiUrl)) {
+ differing <- c(differing, "KorAP instance")
+ }
+
+ if (length(differing) == 0) {
+ NULL
+ } else {
+ sprintf("was created with different parameters (%s)", paste(differing, collapse = ", "))
+ }
+}
+
+#' Read back a result stored in a cache file, if it is the one being asked for
+#'
+#' Warns and returns `NULL` where the file exists but does not match, so that
+#' the caller recomputes and [writeCacheAs()] overwrites it.
+#'
+#' @param cacheAs cache file name, already passed through [cacheAsFileName()]
+#' @param kco [KorAPConnection()] object, for its `verbose` flag
+#' @param record what the call at hand computes, from [cacheAsRecord()]
+#' @param what name of the result, for the log and warning messages
+#' @return the cached result, or `NULL` if there is none to use
+#' @noRd
+readCacheAs <- function(cacheAs, kco, record, what) {
+ if (!file.exists(cacheAs)) {
+ return(NULL)
+ }
+
+ cached <- readRDS(cacheAs)
+ stored <- attr(cached, cacheAsAttribute)
+ attr(cached, cacheAsAttribute) <- NULL
+
+ reason <- cacheAsRejectionReason(stored, record)
+ if (is.null(reason)) {
+ log_info(kco@verbose, sprintf("Loading %s from cache: %s\n", what, cacheAs))
+ return(cached)
+ }
+
+ warning(
+ sprintf(
+ paste0(
+ "Cache file '%s' %s.\n",
+ "It is recomputed and overwritten; pass a different cacheAs file name to keep it."
+ ),
+ cacheAs, reason
+ ),
+ call. = FALSE
+ )
+ NULL
+}
+
+#' Store a result in a cache file, together with what produced it
+#'
+#' @param cacheAs cache file name, already passed through [cacheAsFileName()]
+#' @param kco [KorAPConnection()] object, for its `verbose` flag
+#' @param record what produced the result, from [cacheAsRecord()]
+#' @param what name of the result, for the log message
+#' @param result the result to store
+#' @return `result`, invisibly and unchanged
+#' @noRd
+writeCacheAs <- function(cacheAs, kco, record, what, result) {
+ log_info(kco@verbose, sprintf("Saving %s to cache: %s\n", what, cacheAs))
+ # only the stored copy carries the record, so that the returned value is the
+ # same whether it was cached or not
+ cachedResult <- result
+ attr(cachedResult, cacheAsAttribute) <- record
+ saveRDS(cachedResult, cacheAs)
+ invisible(result)
+}
diff --git a/R/collocationAnalysis.R b/R/collocationAnalysis.R
index c9b8695..c54b46d 100644
--- a/R/collocationAnalysis.R
+++ b/R/collocationAnalysis.R
@@ -20,61 +20,6 @@
result[is.na(result$E) | result$O >= minObservedExpectedRatio * result$E, , drop = FALSE]
}
-#' Name of the attribute under which cache files record their analysis parameters
-#' @noRd
-collocationCacheAttribute <- "RKorAPClient.collocationAnalysis"
-
-#' Parameters that a cached collocation analysis was computed with
-#'
-#' Collected from the calling `collocationAnalysis()` frame, so that parameters
-#' added in the future are taken into account automatically. `kco` and `cacheAs`
-#' are excluded: the former is not a parameter of the analysis, the latter only
-#' says where to store it.
-#'
-#' @param frame environment of the `collocationAnalysis()` call
-#' @param dots arguments passed on to [collocationScoreQuery()]
-#' @param kco [KorAPConnection()] object
-#' @return list of parameters to store with, and compare against, a cache file
-#' @noRd
-collocationCacheParameters <- function(frame, dots, kco) {
- parameterNames <- setdiff(
- names(formals(sys.function(sys.parent()))),
- c("kco", "cacheAs", "...")
- )
- list(
- parameters = mget(parameterNames, envir = frame),
- dots = dots,
- # reusing one cache file for two KorAP instances is a mistake worth catching
- apiUrl = kco@apiUrl,
- # recorded for reference only, deliberately not compared: corpus updates
- # should not invalidate a deliberately kept analysis
- indexRevision = kco@indexRevision
- )
-}
-
-#' Parameters in which a cached collocation analysis differs from the current call
-#'
-#' @param stored parameters recorded in the cache file
-#' @param current parameters of the current call
-#' @return names of the differing parameters, empty if the cache is still valid
-#' @noRd
-differingCollocationCacheParameters <- function(stored, current) {
- differing <- character(0)
-
- for (name in union(names(stored$parameters), names(current$parameters))) {
- if (!identical(stored$parameters[[name]], current$parameters[[name]])) {
- differing <- c(differing, name)
- }
- }
- if (!identical(stored$dots, current$dots)) {
- differing <- c(differing, "...")
- }
- if (!identical(stored$apiUrl, current$apiUrl)) {
- differing <- c(differing, "KorAP instance")
- }
-
- differing
-}
#' Collocation analysis
#'
@@ -135,7 +80,7 @@
#' @param queryMissingScores if TRUE, attempt to retrieve corpus-based association scores for vc/collocate combinations that would otherwise be imputed, by re-querying the KorAP backend without applying the collocate frequency threshold
#' @param missingScoreQuantile lower quantile (evaluated per association measure over the pooled result set) that anchors the adaptive floor used for imputing missing scores between virtual corpora; a robust spread is subtracted from this anchor so the imputed values stay at or below the weakest observed scores. Imputed cells are marked in the `imputed*` columns; see the section on interpreting multi-VC comparisons below
#' @param vcLabel optional label override for the current virtual corpus (used internally when named VC collections are expanded)
-#' @param cacheAs path to an RDS file for caching the result. If the file already exists, the cached result is loaded and returned immediately without contacting the server. Otherwise the analysis is run normally and the result is saved to the file before returning. Defaults to \code{NULL} (no caching).
+#' @param cacheAs path to an RDS file to keep the result in. If the file exists and records the same call, it is read back instead of contacting the server; otherwise the query is run and its result stored there. Unlike the connection's `cache`, this file belongs to the caller, which is what keeps an analysis reproducible once the corpus has grown or the scores have changed. Defaults to \code{NULL} (no file).
#'
#' The analysis parameters are stored alongside the result. If they differ from
#' those of the current call, the cached result would not be the one that was
@@ -261,43 +206,14 @@
...) {
word <- frequency <- O <- NULL
- cacheParameters <- NULL
+ cacheRecord <- NULL
if (!is.null(cacheAs)) {
- if (!grepl("\\.rds$", cacheAs, ignore.case = TRUE)) {
- cacheAs <- paste0(cacheAs, ".rds")
- }
- cacheParameters <- collocationCacheParameters(environment(), list(...), kco)
- }
-
- if (!is.null(cacheAs) && file.exists(cacheAs)) {
- cached <- readRDS(cacheAs)
- storedParameters <- attr(cached, collocationCacheAttribute)
- attr(cached, collocationCacheAttribute) <- NULL
-
- if (is.null(storedParameters)) {
- # written before parameter checking existed, so there is nothing to check
- log_info(kco@verbose, sprintf(
- "Loading collocation analysis from cache (written without parameters): %s\n", cacheAs
- ))
+ cacheAs <- cacheAsFileName(cacheAs)
+ cacheRecord <- cacheAsRecord(environment(), list(...), kco)
+ cached <- readCacheAs(cacheAs, kco, cacheRecord, "collocation analysis")
+ if (!is.null(cached)) {
return(cached)
}
-
- differing <- differingCollocationCacheParameters(storedParameters, cacheParameters)
- if (length(differing) == 0) {
- log_info(kco@verbose, sprintf("Loading collocation analysis from cache: %s\n", cacheAs))
- return(cached)
- }
-
- warning(
- sprintf(
- paste0(
- "Cache file '%s' was created with different parameters (%s) and is recomputed and overwritten.\n",
- "Pass a different cacheAs file name to keep the cached analysis."
- ),
- cacheAs, paste(differing, collapse = ", ")
- ),
- call. = FALSE
- )
}
if (!exactFrequencies && (!is.na(withinSpan) && !is.null(withinSpan) && nzchar(withinSpan))) {
@@ -548,12 +464,7 @@
}
if (!is.null(cacheAs)) {
- log_info(kco@verbose, sprintf("Saving collocation analysis to cache: %s\n", cacheAs))
- # only the stored copy carries the parameters, so that the returned value
- # is the same whether it was cached or not
- cachedResult <- result
- attr(cachedResult, collocationCacheAttribute) <- cacheParameters
- saveRDS(cachedResult, cacheAs)
+ writeCacheAs(cacheAs, kco, cacheRecord, "collocation analysis", result)
}
result
diff --git a/man/cacheAs.Rd b/man/cacheAs.Rd
new file mode 100644
index 0000000..ff270d9
--- /dev/null
+++ b/man/cacheAs.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/cacheAs.R
+\name{cacheAs}
+\alias{cacheAs}
+\title{Keeping results in a file of one's own}
+\description{
+The \code{cacheAs} parameter of the query functions is a different thing from the
+\code{cache} parameter of \code{\link[=KorAPConnection]{KorAPConnection()}}. The latter is a transparent
+speed-up: it stores server responses where the package finds them again, and
+throwing it away costs nothing but time. The former stores a finished result
+in a file the caller names and keeps, which is what makes an analysis
+reproducible: KorAP corpora grow, so the same query returns different numbers
+next year, and the scores computed from them may change with the package.
+}
+\details{
+A cache file therefore records what produced it, and is not reused when that
+no longer matches what is being asked for.
+}
+\keyword{internal}
diff --git a/man/collocationAnalysis-KorAPConnection-method.Rd b/man/collocationAnalysis-KorAPConnection-method.Rd
index ac7e7d9..c91867b 100644
--- a/man/collocationAnalysis-KorAPConnection-method.Rd
+++ b/man/collocationAnalysis-KorAPConnection-method.Rd
@@ -100,7 +100,7 @@
\item{vcLabel}{optional label override for the current virtual corpus (used internally when named VC collections are expanded)}
-\item{cacheAs}{path to an RDS file for caching the result. If the file already exists, the cached result is loaded and returned immediately without contacting the server. Otherwise the analysis is run normally and the result is saved to the file before returning. Defaults to \code{NULL} (no caching).
+\item{cacheAs}{path to an RDS file to keep the result in. If the file exists and records the same call, it is read back instead of contacting the server; otherwise the query is run and its result stored there. Unlike the connection's \code{cache}, this file belongs to the caller, which is what keeps an analysis reproducible once the corpus has grown or the scores have changed. Defaults to \code{NULL} (no file).
The analysis parameters are stored alongside the result. If they differ from
those of the current call, the cached result would not be the one that was
diff --git a/tests/testthat/test-collocation-cache-parameters.R b/tests/testthat/test-collocation-cache-parameters.R
index 06e8ca1..a207a8f 100644
--- a/tests/testthat/test-collocation-cache-parameters.R
+++ b/tests/testthat/test-collocation-cache-parameters.R
@@ -26,8 +26,10 @@
collocationAnalysis(offlineConnection(), "Test", minOccur = 3, cacheAs = cacheFile)
- stored <- attr(readRDS(cacheFile), RKorAPClient:::collocationCacheAttribute)
+ stored <- attr(readRDS(cacheFile), RKorAPClient:::cacheAsAttribute)
expect_false(is.null(stored))
+ expect_equal(stored$scoreVersion, RKorAPClient:::cacheAsScoreVersion)
+ expect_equal(stored$packageVersion, as.character(utils::packageVersion("RKorAPClient")))
expect_equal(stored$parameters$node, "Test")
expect_equal(stored$parameters$minOccur, 3)
expect_equal(stored$apiUrl, "https://example.invalid/")
@@ -52,7 +54,7 @@
expect_equal(fromCache, fresh)
# the parameters live in the file only, not in the returned value
- expect_null(attr(fromCache, RKorAPClient:::collocationCacheAttribute))
+ expect_null(attr(fromCache, RKorAPClient:::cacheAsAttribute))
})
test_that("an unchanged call is served from the cache without contacting the server", {
@@ -124,51 +126,83 @@
)
})
- stored <- attr(readRDS(cacheFile), RKorAPClient:::collocationCacheAttribute)
+ stored <- attr(readRDS(cacheFile), RKorAPClient:::cacheAsAttribute)
expect_equal(stored$parameters$minOccur, 5)
})
-test_that("cache files written without parameters are still used", {
+test_that("cache files written before the scores were corrected are refused", {
+ mockEmptyAnalysis()
cacheFile <- tempfile(fileext = ".rds")
on.exit(unlink(cacheFile), add = TRUE)
kco <- offlineConnection()
- # as written by RKorAPClient 1.3.0
+ # as written by RKorAPClient 1.3.0, whose logDice and ll differ from today's
legacy <- tibble::tibble(node = "Test", collocate = "c", logDice = 7)
saveRDS(legacy, cacheFile)
- testthat::local_mocked_bindings(
- collocatesQuery = function(...) stop("server must not be contacted"),
- .package = "RKorAPClient"
+ expect_warning(
+ result <- collocationAnalysis(kco, "Test", cacheAs = cacheFile),
+ "logDice"
)
- expect_equal(collocationAnalysis(kco, "Test", cacheAs = cacheFile), legacy)
+ expect_false(identical(result, legacy))
+ # and the stale file is replaced by one that records what wrote it
+ expect_false(is.null(attr(readRDS(cacheFile), RKorAPClient:::cacheAsAttribute)))
})
-test_that("differingCollocationCacheParameters reports what changed", {
- differing <- RKorAPClient:::differingCollocationCacheParameters
+test_that("cache files of an older version are refused, naming it", {
+ mockEmptyAnalysis()
+ cacheFile <- tempfile(fileext = ".rds")
+ on.exit(unlink(cacheFile), add = TRUE)
+ kco <- offlineConnection()
+
+ collocationAnalysis(kco, "Test", cacheAs = cacheFile)
+ aged <- readRDS(cacheFile)
+ record <- attr(aged, RKorAPClient:::cacheAsAttribute)
+ record$scoreVersion <- "1.3.0"
+ record$packageVersion <- "1.3.0"
+ attr(aged, RKorAPClient:::cacheAsAttribute) <- record
+ saveRDS(aged, cacheFile)
+
+ expect_warning(collocationAnalysis(kco, "Test", cacheAs = cacheFile), "1\\.3\\.0")
+})
+
+test_that("cacheAsRejectionReason says why a cache file cannot be used", {
+ reason <- RKorAPClient:::cacheAsRejectionReason
stored <- list(
+ scoreVersion = RKorAPClient:::cacheAsScoreVersion,
parameters = list(node = "Test", minOccur = 3, vc = ""),
dots = list(),
apiUrl = "https://korap.ids-mannheim.de/api/v1.0/"
)
- expect_equal(differing(stored, stored), character(0))
+ expect_null(reason(stored, stored))
+ expect_match(reason(NULL, stored), "before RKorAPClient")
+
+ # a file from before the corrections: an older score generation, and the
+ # version that wrote it, which the reason names
+ aged <- stored
+ aged$scoreVersion <- "1.3.0"
+ aged$packageVersion <- "1.3.0"
+ expect_match(reason(aged, stored), "written by RKorAPClient 1\\.3\\.0")
+
+ # one that does not even say what wrote it
+ anonymous <- stored
+ anonymous$scoreVersion <- NULL
+ expect_match(reason(anonymous, stored), "written before RKorAPClient")
changed <- stored
changed$parameters$minOccur <- 5
- expect_equal(differing(stored, changed), "minOccur")
+ expect_match(reason(stored, changed), "minOccur")
- changed <- stored
- changed$parameters$minOccur <- 5
changed$parameters$vc <- "textType=/Zeit.*/"
- expect_setequal(differing(stored, changed), c("minOccur", "vc"))
+ expect_match(reason(stored, changed), "minOccur, vc")
changed <- stored
changed$dots <- list(smoothingConstant = 1)
- expect_equal(differing(stored, changed), "...")
+ expect_match(reason(stored, changed), "\\.\\.\\.")
changed <- stored
changed$apiUrl <- "https://korap.dnb.de/api/v1.0/"
- expect_equal(differing(stored, changed), "KorAP instance")
+ expect_match(reason(stored, changed), "KorAP instance")
})
diff --git a/tests/testthat/test-collocations.R b/tests/testthat/test-collocations.R
index 866ecc0..5b4db98 100644
--- a/tests/testthat/test-collocations.R
+++ b/tests/testthat/test-collocations.R
@@ -920,16 +920,26 @@
"KorAPConnection",
apiUrl = "https://example.invalid/",
KorAPUrl = "https://example.invalid/",
+ authorizationSupported = FALSE,
verbose = FALSE
)
- cached <- tibble::tibble(node = "n", collocate = "c", logDice = 7)
cache_file <- tempfile(fileext = ".rds")
on.exit(unlink(cache_file), add = TRUE)
- saveRDS(cached, cache_file)
+
+ # written by the package itself, so that it carries the record saying what
+ # produced it - a file without one is refused, as one from an older version
+ cached <- local({
+ testthat::local_mocked_bindings(
+ collocatesQuery = function(...) tibble::tibble(),
+ .package = "RKorAPClient"
+ )
+ collocationAnalysis(kco, "n", cacheAs = cache_file)
+ })
testthat::local_mocked_bindings(
corpusQuery = function(...) stop("server must not be contacted"),
+ collocatesQuery = function(...) stop("server must not be contacted"),
.package = "RKorAPClient"
)
@@ -947,19 +957,27 @@
verbose = FALSE
)
- cached <- tibble::tibble(node = "n", collocate = "c", logDice = 7)
base_path <- tempfile()
on.exit(unlink(paste0(base_path, ".rds")), add = TRUE)
- saveRDS(cached, paste0(base_path, ".rds"))
+
+ # given without extension, .rds is appended when writing
+ cached <- local({
+ testthat::local_mocked_bindings(
+ collocatesQuery = function(...) tibble::tibble(),
+ .package = "RKorAPClient"
+ )
+ collocationAnalysis(kco, "n", cacheAs = base_path)
+ })
+ expect_true(file.exists(paste0(base_path, ".rds")))
testthat::local_mocked_bindings(
corpusQuery = function(...) stop("server must not be contacted"),
+ collocatesQuery = function(...) stop("server must not be contacted"),
.package = "RKorAPClient"
)
- # given without extension, the .rds file is found
+ # and the same file is found again, given with or without the extension
expect_equal(collocationAnalysis(kco, "n", cacheAs = base_path), cached)
- # given with extension, it is not doubled
expect_equal(collocationAnalysis(kco, "n", cacheAs = paste0(base_path, ".rds")), cached)
})
@@ -985,8 +1003,8 @@
expect_true(file.exists(cache_file))
# the file records the analysis parameters, the returned value does not
cached <- readRDS(cache_file)
- expect_false(is.null(attr(cached, RKorAPClient:::collocationCacheAttribute)))
- attr(cached, RKorAPClient:::collocationCacheAttribute) <- NULL
+ expect_false(is.null(attr(cached, RKorAPClient:::cacheAsAttribute)))
+ attr(cached, RKorAPClient:::cacheAsAttribute) <- NULL
expect_equal(cached, first)
# the second call is served from the cache and therefore issues no token warning